From 1bae7b242c37760b809b653a177ad61fc5b47984 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Mon, 7 Jan 2013 09:19:31 +0100 Subject: [PATCH 1/2] [PropertyAccess] Extracted PropertyAccess component out of Form --- UPGRADE-2.2.md | 128 ++++ src/Symfony/Bridge/Doctrine/CHANGELOG.md | 6 + .../Form/ChoiceList/EntityChoiceList.php | 24 +- .../Doctrine/Form/DoctrineOrmExtension.php | 3 +- .../Doctrine/Form/Type/DoctrineType.php | 16 +- .../Tests/Form/Type/EntityTypeTest.php | 1 + src/Symfony/Bridge/Propel1/CHANGELOG.md | 3 + .../Form/ChoiceList/ModelChoiceList.php | 22 +- .../Bridge/Propel1/Form/PropelExtension.php | 3 +- .../Bridge/Propel1/Form/Type/ModelType.php | 19 +- .../Form/ChoiceList/ModelChoiceListTest.php | 4 + .../FrameworkBundle/Resources/config/form.xml | 5 + src/Symfony/Component/Form/CHANGELOG.md | 9 + .../Core/ChoiceList/ObjectChoiceList.php | 61 +- .../Form/Extension/Core/CoreExtension.php | 3 +- .../Core/DataMapper/PropertyPathMapper.php | 28 +- .../Form/Extension/Core/Type/FormType.php | 14 +- .../ViolationMapper/RelativePath.php | 2 +- .../ViolationMapper/ViolationMapper.php | 8 +- .../ViolationMapper/ViolationPath.php | 4 +- .../ViolationMapper/ViolationPathIterator.php | 2 +- src/Symfony/Component/Form/Form.php | 2 +- .../Component/Form/FormConfigBuilder.php | 4 +- .../Form/FormConfigBuilderInterface.php | 24 +- .../Component/Form/FormConfigInterface.php | 2 +- src/Symfony/Component/Form/FormInterface.php | 2 +- .../Core/ChoiceList/ObjectChoiceListTest.php | 3 + .../DataMapper/PropertyPathMapperTest.php | 66 +- .../Extension/Core/Type/ChoiceTypeTest.php | 2 - .../Extension/Core/Type/FormTypeTest.php | 2 +- .../EventListener/ValidationListenerTest.php | 2 +- .../ViolationMapper/ViolationMapperTest.php | 2 +- .../Component/Form/Tests/SimpleFormTest.php | 2 +- .../Form/Tests/Util/PropertyPathTest.php | 557 ---------------- src/Symfony/Component/Form/Util/FormUtil.php | 166 +---- .../Component/Form/Util/PropertyPath.php | 608 +----------------- .../Form/Util/PropertyPathBuilder.php | 284 +------- .../Form/Util/PropertyPathInterface.php | 75 +-- .../Form/Util/PropertyPathIterator.php | 41 +- .../Util/PropertyPathIteratorInterface.php | 31 +- src/Symfony/Component/Form/composer.json | 3 +- .../Component/PropertyAccess/.gitattributes | 2 + .../Component/PropertyAccess/.gitignore | 4 + .../Exception/ExceptionInterface.php} | 9 +- .../InvalidPropertyPathException.php} | 9 +- .../Exception/NoSuchPropertyException.php} | 9 +- .../Exception/OutOfBoundsException.php | 21 + .../PropertyAccessDeniedException.php | 21 + .../Exception/RuntimeException.php | 21 + .../Exception/UnexpectedTypeException.php | 25 + src/Symfony/Component/PropertyAccess/LICENSE | 19 + .../PropertyAccess/PropertyAccess.php | 37 ++ .../PropertyAccess/PropertyAccessor.php | 399 ++++++++++++ .../PropertyAccessorInterface.php | 84 +++ .../Component/PropertyAccess/PropertyPath.php | 225 +++++++ .../PropertyAccess/PropertyPathBuilder.php | 296 +++++++++ .../PropertyAccess/PropertyPathInterface.php | 86 +++ .../PropertyAccess/PropertyPathIterator.php | 55 ++ .../PropertyPathIteratorInterface.php | 34 + .../Component/PropertyAccess/README.md | 14 + .../Component/PropertyAccess/StringUtil.php | 192 ++++++ .../PropertyAccess/Tests/Fixtures/Author.php | 71 ++ .../Tests/Fixtures/Magician.php | 2 +- .../PropertyAccessorArrayObjectTest.php} | 4 +- .../Tests/PropertyAccessorArrayTest.php} | 4 +- .../Tests/PropertyAccessorCollectionTest.php} | 107 ++- ...PropertyAccessorCustomArrayObjectTest.php} | 4 +- .../Tests/PropertyAccessorTest.php | 334 ++++++++++ .../Tests}/PropertyPathBuilderTest.php | 6 +- .../PropertyAccess/Tests/PropertyPathTest.php | 191 ++++++ .../Tests/StringUtilTest.php} | 8 +- .../Component/PropertyAccess/composer.json | 31 + .../Component/PropertyAccess/phpunit.xml.dist | 29 + 73 files changed, 2706 insertions(+), 1890 deletions(-) delete mode 100644 src/Symfony/Component/Form/Tests/Util/PropertyPathTest.php create mode 100644 src/Symfony/Component/PropertyAccess/.gitattributes create mode 100644 src/Symfony/Component/PropertyAccess/.gitignore rename src/Symfony/Component/{Form/Exception/InvalidPropertyException.php => PropertyAccess/Exception/ExceptionInterface.php} (54%) rename src/Symfony/Component/{Form/Exception/PropertyAccessDeniedException.php => PropertyAccess/Exception/InvalidPropertyPathException.php} (52%) rename src/Symfony/Component/{Form/Exception/InvalidPropertyPathException.php => PropertyAccess/Exception/NoSuchPropertyException.php} (53%) create mode 100644 src/Symfony/Component/PropertyAccess/Exception/OutOfBoundsException.php create mode 100644 src/Symfony/Component/PropertyAccess/Exception/PropertyAccessDeniedException.php create mode 100644 src/Symfony/Component/PropertyAccess/Exception/RuntimeException.php create mode 100644 src/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.php create mode 100644 src/Symfony/Component/PropertyAccess/LICENSE create mode 100644 src/Symfony/Component/PropertyAccess/PropertyAccess.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyAccessor.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyPath.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyPathBuilder.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyPathInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyPathIterator.php create mode 100644 src/Symfony/Component/PropertyAccess/PropertyPathIteratorInterface.php create mode 100644 src/Symfony/Component/PropertyAccess/README.md create mode 100644 src/Symfony/Component/PropertyAccess/StringUtil.php create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php rename src/Symfony/Component/{Form => PropertyAccess}/Tests/Fixtures/Magician.php (89%) rename src/Symfony/Component/{Form/Tests/Util/PropertyPathArrayObjectTest.php => PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php} (73%) rename src/Symfony/Component/{Form/Tests/Util/PropertyPathArrayTest.php => PropertyAccess/Tests/PropertyAccessorArrayTest.php} (73%) rename src/Symfony/Component/{Form/Tests/Util/PropertyPathCollectionTest.php => PropertyAccess/Tests/PropertyAccessorCollectionTest.php} (73%) rename src/Symfony/Component/{Form/Tests/Util/PropertyPathCustomArrayObjectTest.php => PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php} (75%) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php rename src/Symfony/Component/{Form/Tests/Util => PropertyAccess/Tests}/PropertyPathBuilderTest.php (97%) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php rename src/Symfony/Component/{Form/Tests/Util/FormUtilTest.php => PropertyAccess/Tests/StringUtilTest.php} (95%) create mode 100644 src/Symfony/Component/PropertyAccess/composer.json create mode 100644 src/Symfony/Component/PropertyAccess/phpunit.xml.dist diff --git a/UPGRADE-2.2.md b/UPGRADE-2.2.md index c485bf6112..33ed8c5700 100644 --- a/UPGRADE-2.2.md +++ b/UPGRADE-2.2.md @@ -84,6 +84,46 @@ {{ error.message }} ``` + * FormType, ModelType and PropertyPathMapper now have constructors. If you + extended these classes, you should call the parent constructor now. + Note that you are not recommended to extend FormType nor ModelType. You should + extend AbstractType instead and use the Form component's own inheritance + mechanism (`AbstractType::getParent()`). + + Before: + + ``` + use Symfony\Component\Form\Extensions\Core\DataMapper\PropertyPathMapper; + + class CustomMapper extends PropertyPathMapper + { + public function __construct() + { + // ... + } + + // ... + } + ``` + + After: + + ``` + use Symfony\Component\Form\Extensions\Core\DataMapper\PropertyPathMapper; + + class CustomMapper extends PropertyPathMapper + { + public function __construct() + { + parent::__construct(); + + // ... + } + + // ... + } + ``` + #### Deprecations * The methods `getParent()`, `setParent()` and `hasParent()` in @@ -91,6 +131,94 @@ You should not rely on these methods in your form type because the parent of a form can change after building it. + * The class PropertyPath and related classes were deprecated and moved to a + dedicated component PropertyAccess. If you used any of these classes or + interfaces, you should adapt the namespaces now. During the move, + InvalidPropertyException was renamed to NoSuchPropertyException. + + Before: + + ``` + use Symfony\Component\Form\Util\PropertyPath; + use Symfony\Component\Form\Util\PropertyPathBuilder; + use Symfony\Component\Form\Util\PropertyPathInterface; + use Symfony\Component\Form\Util\PropertyPathIterator; + use Symfony\Component\Form\Util\PropertyPathIteratorInterface; + use Symfony\Component\Form\Exception\InvalidPropertyException; + use Symfony\Component\Form\Exception\InvalidPropertyPathException; + use Symfony\Component\Form\Exception\PropertyAccessDeniedException; + ``` + + After: + + ``` + use Symfony\Component\PropertyAccess\PropertyPath; + use Symfony\Component\PropertyAccess\PropertyPathBuilder; + use Symfony\Component\PropertyAccess\PropertyPathInterface; + use Symfony\Component\PropertyAccess\PropertyPathIterator; + use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface; + use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; + use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; + use Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException; + ``` + + Also, `FormUtil::singularify()` was split away into a class StringUtil + in the new component. + + Before: + + ``` + use Symfony\Component\Form\Util\FormUtil; + + $singular = FormUtil::singularify($plural); + ``` + + After: + + ``` + use Symfony\Component\PropertyAccess\StringUtil; + + $singular = StringUtil::singularify($plural); + ``` + + The methods `getValue()` and `setValue()` were moved to a new class + PropertyAccessor. + + Before: + + ``` + use Symfony\Component\Form\Util\PropertyPath; + + $propertyPath = new PropertyPath('some.path'); + + $value = $propertyPath->getValue($object); + $propertyPath->setValue($object, 'new value'); + ``` + + After (alternative 1): + + ``` + use Symfony\Component\PropertyAccess\PropertyAccess; + + $accessor = PropertyAccess::getPropertyAccessor(); + + $value = $propertyAccessor->getValue($object, 'some.path'); + $accessor->setValue($object, 'some.path', 'new value'); + ``` + + After (alternative 2): + + ``` + use Symfony\Component\PropertyAccess\PropertyAccess; + use Symfony\Component\PropertyAccess\PropertyPath; + + $accessor = PropertyAccess::getPropertyAccessor(); + $propertyPath = new PropertyPath('some.path'); + + $value = $propertyAccessor->getValue($object, $propertyPath); + $accessor->setValue($object, $propertyPath, 'new value'); + ``` + ### Routing * RouteCollection does not behave like a tree structure anymore but as a flat diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 6369ea3634..9c747b8abc 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.2.0 +----- + + * added an optional PropertyAccessorInterface parameter to DoctrineType, + EntityType and EntityChoiceList + 2.1.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php index 9cf636df95..16a65ebf40 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/EntityChoiceList.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\Exception\Exception; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; use Doctrine\Common\Persistence\ObjectManager; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * A choice list presenting a list of Doctrine entities as choices @@ -86,17 +87,18 @@ class EntityChoiceList extends ObjectChoiceList /** * Creates a new entity choice list. * - * @param ObjectManager $manager An EntityManager instance - * @param string $class The class name - * @param string $labelPath The property path used for the label - * @param EntityLoaderInterface $entityLoader An optional query builder - * @param array $entities An array of choices - * @param array $preferredEntities An array of preferred choices - * @param string $groupPath A property path pointing to the property used - * to group the choices. Only allowed if - * the choices are given as flat array. + * @param ObjectManager $manager An EntityManager instance + * @param string $class The class name + * @param string $labelPath The property path used for the label + * @param EntityLoaderInterface $entityLoader An optional query builder + * @param array $entities An array of choices + * @param array $preferredEntities An array of preferred choices + * @param string $groupPath A property path pointing to the property used + * to group the choices. Only allowed if + * the choices are given as flat array. + * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. */ - public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null) + public function __construct(ObjectManager $manager, $class, $labelPath = null, EntityLoaderInterface $entityLoader = null, $entities = null, array $preferredEntities = array(), $groupPath = null, PropertyAccessorInterface $propertyAccessor = null) { $this->em = $manager; $this->entityLoader = $entityLoader; @@ -122,7 +124,7 @@ class EntityChoiceList extends ObjectChoiceList $entities = array(); } - parent::__construct($entities, $labelPath, $preferredEntities, $groupPath); + parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 7c9941c29a..a98b2d2a24 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Form; use Doctrine\Common\Persistence\ManagerRegistry; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\PropertyAccess\PropertyAccess; class DoctrineOrmExtension extends AbstractExtension { @@ -26,7 +27,7 @@ class DoctrineOrmExtension extends AbstractExtension protected function loadTypes() { return array( - new Type\EntityType($this->registry), + new Type\EntityType($this->registry, PropertyAccess::getPropertyAccessor()), ); } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 8180d1a67c..df6f2077f8 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -22,6 +22,8 @@ use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; abstract class DoctrineType extends AbstractType { @@ -35,9 +37,15 @@ abstract class DoctrineType extends AbstractType */ private $choiceListCache = array(); - public function __construct(ManagerRegistry $registry) + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null) { $this->registry = $registry; + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); } public function buildForm(FormBuilderInterface $builder, array $options) @@ -54,6 +62,7 @@ abstract class DoctrineType extends AbstractType { $choiceListCache =& $this->choiceListCache; $registry = $this->registry; + $propertyAccessor = $this->propertyAccessor; $type = $this; $loader = function (Options $options) use ($type) { @@ -64,7 +73,7 @@ abstract class DoctrineType extends AbstractType return null; }; - $choiceList = function (Options $options) use (&$choiceListCache, &$time) { + $choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) { // Support for closures $propertyHash = is_object($options['property']) ? spl_object_hash($options['property']) @@ -118,7 +127,8 @@ abstract class DoctrineType extends AbstractType $options['loader'], $options['choices'], $options['preferred_choices'], - $options['group_by'] + $options['group_by'], + $propertyAccessor ); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index cf4606f9d4..6fdcf66f24 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -90,6 +90,7 @@ class EntityTypeTest extends TypeTestCase parent::tearDown(); $this->em = null; + $this->emRegistry = null; } protected function getExtensions() diff --git a/src/Symfony/Bridge/Propel1/CHANGELOG.md b/src/Symfony/Bridge/Propel1/CHANGELOG.md index b549e027c3..242d576c33 100644 --- a/src/Symfony/Bridge/Propel1/CHANGELOG.md +++ b/src/Symfony/Bridge/Propel1/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG ----- * added a collection type for the I18n behavior + * added an optional PropertyAccessorInterface parameter to ModelType and + ModelChoiceList + * [BC BREAK] ModelType now has a constructor 2.1.0 ----- diff --git a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php index 04ec7a8c62..e33934fbff 100644 --- a/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php +++ b/src/Symfony/Bridge/Propel1/Form/ChoiceList/ModelChoiceList.php @@ -18,6 +18,7 @@ use \Persistent; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Exception\StringCastException; use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Widely inspired by the EntityChoiceList. @@ -69,16 +70,17 @@ class ModelChoiceList extends ObjectChoiceList * * @see Symfony\Bridge\Propel1\Form\Type\ModelType How to use the preferred choices. * - * @param string $class The FQCN of the model class to be loaded. - * @param string $labelPath A property path pointing to the property used for the choice labels. - * @param array $choices An optional array to use, rather than fetching the models. - * @param ModelCriteria $queryObject The query to use retrieving model data from database. - * @param string $groupPath A property path pointing to the property used to group the choices. - * @param array|ModelCriteria $preferred The preferred items of this choice. - * Either an array if $choices is given, - * or a ModelCriteria to be merged with the $queryObject. + * @param string $class The FQCN of the model class to be loaded. + * @param string $labelPath A property path pointing to the property used for the choice labels. + * @param array $choices An optional array to use, rather than fetching the models. + * @param ModelCriteria $queryObject The query to use retrieving model data from database. + * @param string $groupPath A property path pointing to the property used to group the choices. + * @param array|ModelCriteria $preferred The preferred items of this choice. + * Either an array if $choices is given, + * or a ModelCriteria to be merged with the $queryObject. + * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. */ - public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array()) + public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null) { $this->class = $class; @@ -104,7 +106,7 @@ class ModelChoiceList extends ObjectChoiceList $this->identifierAsIndex = true; } - parent::__construct($choices, $labelPath, $preferred, $groupPath); + parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor); } /** diff --git a/src/Symfony/Bridge/Propel1/Form/PropelExtension.php b/src/Symfony/Bridge/Propel1/Form/PropelExtension.php index 408fc7f008..b238a5e6ff 100644 --- a/src/Symfony/Bridge/Propel1/Form/PropelExtension.php +++ b/src/Symfony/Bridge/Propel1/Form/PropelExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Propel1\Form; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\PropertyAccess\PropertyAccess; /** * Represents the Propel form extension, which loads the Propel functionality. @@ -23,7 +24,7 @@ class PropelExtension extends AbstractExtension protected function loadTypes() { return array( - new Type\ModelType(), + new Type\ModelType(PropertyAccess::getPropertyAccessor()), new Type\TranslationCollectionType(), new Type\TranslationType() ); diff --git a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php b/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php index faf5f25f1c..16d633cda2 100644 --- a/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php +++ b/src/Symfony/Bridge/Propel1/Form/Type/ModelType.php @@ -17,6 +17,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * ModelType class. @@ -48,6 +50,16 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; */ class ModelType extends AbstractType { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); + } + public function buildForm(FormBuilderInterface $builder, array $options) { if ($options['multiple']) { @@ -57,14 +69,17 @@ class ModelType extends AbstractType public function setDefaultOptions(OptionsResolverInterface $resolver) { - $choiceList = function (Options $options) { + $propertyAccessor = $this->propertyAccessor; + + $choiceList = function (Options $options) use ($propertyAccessor) { return new ModelChoiceList( $options['class'], $options['property'], $options['choices'], $options['query'], $options['group_by'], - $options['preferred_choices'] + $options['preferred_choices'], + $propertyAccessor ); }; diff --git a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php b/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php index 6675eac727..748b1cd9b1 100644 --- a/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php +++ b/src/Symfony/Bridge/Propel1/Tests/Form/ChoiceList/ModelChoiceListTest.php @@ -26,6 +26,10 @@ class ModelChoiceListTest extends Propel1TestCase if (!class_exists('Symfony\Component\Form\Form')) { $this->markTestSkipped('The "Form" component is not available'); } + + if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) { + $this->markTestSkipped('The "PropertyAccessor" component is not available'); + } } public function testEmptyChoicesReturnsEmpty() diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index d2df3eb60e..d614e4dc50 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -10,6 +10,7 @@ Symfony\Component\Form\FormFactory Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser + Symfony\Component\PropertyAccess\PropertyAccessor @@ -53,11 +54,15 @@ + + + + diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index d8e309637f..67f8e3c18e 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -13,6 +13,15 @@ CHANGELOG * [BC BREAK] FormException is now an interface * protected FormBuilder methods from being called when it is turned into a FormConfigInterface with getFormConfig() * [BC BREAK] inserted argument `$message` in the constructor of `FormError` + * the PropertyPath class and related classes were moved to a dedicated + PropertyAccess component. During the move, InvalidPropertyException was + renamed to NoSuchPropertyException. FormUtil was split: FormUtil::singularify() + can now be found in Symfony\Component\PropertyAccess\StringUtil. The methods + getValue() and setValue() from PropertyPath were extracted into a new class + PropertyAccessor. + * added an optional PropertyAccessorInterface parameter to FormType, + ObjectChoiceList and PropertyPathMapper + * [BC BREAK] PropertyPathMapper and FormType now have a constructor 2.1.0 ----- diff --git a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php index eeb6b646c3..04be48ace4 100644 --- a/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php +++ b/src/Symfony/Component/Form/Extension/Core/ChoiceList/ObjectChoiceList.php @@ -11,9 +11,11 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList; -use Symfony\Component\Form\Util\PropertyPath; use Symfony\Component\Form\Exception\StringCastException; -use Symfony\Component\Form\Exception\InvalidPropertyException; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * A choice list for object choices. @@ -32,6 +34,11 @@ use Symfony\Component\Form\Exception\InvalidPropertyException; */ class ObjectChoiceList extends ChoiceList { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + /** * The property path used to obtain the choice label. * @@ -56,28 +63,30 @@ class ObjectChoiceList extends ChoiceList /** * Creates a new object choice list. * - * @param array|\Traversable $choices The array of choices. Choices may also be given - * as hierarchy of unlimited depth by creating nested - * arrays. The title of the sub-hierarchy can be - * stored in the array key pointing to the nested - * array. The topmost level of the hierarchy may also - * be a \Traversable. - * @param string $labelPath A property path pointing to the property used - * for the choice labels. The value is obtained - * by calling the getter on the object. If the - * path is NULL, the object's __toString() method - * is used instead. - * @param array $preferredChoices A flat array of choices that should be - * presented to the user with priority. - * @param string $groupPath A property path pointing to the property used - * to group the choices. Only allowed if - * the choices are given as flat array. - * @param string $valuePath A property path pointing to the property used - * for the choice values. If not given, integers - * are generated instead. + * @param array|\Traversable $choices The array of choices. Choices may also be given + * as hierarchy of unlimited depth by creating nested + * arrays. The title of the sub-hierarchy can be + * stored in the array key pointing to the nested + * array. The topmost level of the hierarchy may also + * be a \Traversable. + * @param string $labelPath A property path pointing to the property used + * for the choice labels. The value is obtained + * by calling the getter on the object. If the + * path is NULL, the object's __toString() method + * is used instead. + * @param array $preferredChoices A flat array of choices that should be + * presented to the user with priority. + * @param string $groupPath A property path pointing to the property used + * to group the choices. Only allowed if + * the choices are given as flat array. + * @param string $valuePath A property path pointing to the property used + * for the choice values. If not given, integers + * are generated instead. + * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. */ - public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null) + public function __construct($choices, $labelPath = null, array $preferredChoices = array(), $groupPath = null, $valuePath = null, PropertyAccessorInterface $propertyAccessor = null) { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); $this->labelPath = null !== $labelPath ? new PropertyPath($labelPath) : null; $this->groupPath = null !== $groupPath ? new PropertyPath($groupPath) : null; $this->valuePath = null !== $valuePath ? new PropertyPath($valuePath) : null; @@ -108,8 +117,8 @@ class ObjectChoiceList extends ChoiceList } try { - $group = $this->groupPath->getValue($choice); - } catch (InvalidPropertyException $e) { + $group = $this->propertyAccessor->getValue($choice, $this->groupPath); + } catch (NoSuchPropertyException $e) { // Don't group items whose group property does not exist // see https://github.com/symfony/symfony/commit/d9b7abb7c7a0f28e0ce970afc5e305dce5dccddf $group = null; @@ -150,7 +159,7 @@ class ObjectChoiceList extends ChoiceList protected function createValue($choice) { if ($this->valuePath) { - return (string) $this->valuePath->getValue($choice); + return (string) $this->propertyAccessor->getValue($choice, $this->valuePath); } return parent::createValue($choice); @@ -163,7 +172,7 @@ class ObjectChoiceList extends ChoiceList $labels[$i] = array(); $this->extractLabels($choice, $labels[$i]); } elseif ($this->labelPath) { - $labels[$i] = $this->labelPath->getValue($choice); + $labels[$i] = $this->propertyAccessor->getValue($choice, $this->labelPath); } elseif (method_exists($choice, '__toString')) { $labels[$i] = (string) $choice; } else { diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index f0f0d181da..ab6529d6dc 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Extension\Core; use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\PropertyAccess\PropertyAccess; /** * Represents the main form extension, which loads the core functionality. @@ -24,7 +25,7 @@ class CoreExtension extends AbstractExtension { return array( new Type\FieldType(), - new Type\FormType(), + new Type\FormType(PropertyAccess::getPropertyAccessor()), new Type\BirthdayType(), new Type\CheckboxType(), new Type\ChoiceType(), diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index ce850eaac7..f691ecca21 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -14,9 +14,31 @@ namespace Symfony\Component\Form\Extension\Core\DataMapper; use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\Util\VirtualFormAwareIterator; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +/** + * A data mapper using property paths to read/write data. + * + * @author Bernhard Schussek + */ class PropertyPathMapper implements DataMapperInterface { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * Creates a new property path mapper. + * + * @param PropertyAccessorInterface $propertyAccessor + */ + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); + } + /** * {@inheritdoc} */ @@ -39,7 +61,7 @@ class PropertyPathMapper implements DataMapperInterface $config = $form->getConfig(); if (null !== $propertyPath && $config->getMapped()) { - $form->setData($propertyPath->getValue($data)); + $form->setData($this->propertyAccessor->getValue($data, $propertyPath)); } } } @@ -70,8 +92,8 @@ class PropertyPathMapper implements DataMapperInterface if (null !== $propertyPath && $config->getMapped() && $form->isSynchronized() && !$form->isDisabled()) { // If the data is identical to the value in $data, we are // dealing with a reference - if (!is_object($data) || !$config->getByReference() || $form->getData() !== $propertyPath->getValue($data)) { - $propertyPath->setValue($data, $form->getData()); + if (!is_object($data) || !$config->getByReference() || $form->getData() !== $this->propertyAccessor->getValue($data, $propertyPath)) { + $this->propertyAccessor->setValue($data, $propertyPath, $form->getData()); } } } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 0b9c38df90..44b60aaf86 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -20,9 +20,21 @@ use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Exception\Exception; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; class FormType extends AbstractType { + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::getPropertyAccessor(); + } + /** * {@inheritdoc} */ @@ -41,7 +53,7 @@ class FormType extends AbstractType ->setCompound($options['compound']) ->setData(isset($options['data']) ? $options['data'] : null) ->setDataLocked(isset($options['data'])) - ->setDataMapper($options['compound'] ? new PropertyPathMapper() : null) + ->setDataMapper($options['compound'] ? new PropertyPathMapper($this->propertyAccessor) : null) ; if ($options['trim']) { diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php index 8f487bcc15..ef5c9fad05 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/RelativePath.php @@ -12,7 +12,7 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPath; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index cece98804e..61d08c3e5d 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -13,9 +13,9 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Util\VirtualFormAwareIterator; -use Symfony\Component\Form\Util\PropertyPathIterator; -use Symfony\Component\Form\Util\PropertyPathBuilder; -use Symfony\Component\Form\Util\PropertyPathIteratorInterface; +use Symfony\Component\PropertyAccess\PropertyPathIterator; +use Symfony\Component\PropertyAccess\PropertyPathBuilder; +use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator; use Symfony\Component\Form\FormError; use Symfony\Component\Validator\ConstraintViolation; @@ -265,7 +265,7 @@ class ViolationMapper implements ViolationMapperInterface $propertyPathBuilder->remove(0, $i + 1); $i = 0; } else { - /* @var \Symfony\Component\Form\Util\PropertyPathInterface $propertyPath */ + /* @var \Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath */ $propertyPath = $scope->getPropertyPath(); if (null === $propertyPath) { diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php index 65cbda6b18..9c99e8618f 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Util\PropertyPathInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php index 531da861eb..50baa4533e 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPathIterator.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; -use Symfony\Component\Form\Util\PropertyPathIterator; +use Symfony\Component\PropertyAccess\PropertyPathIterator; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index eb93637877..7a49822425 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -16,8 +16,8 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Exception\AlreadyBoundException; use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Util\FormUtil; -use Symfony\Component\Form\Util\PropertyPath; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyPath; /** * Form represents a form. diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 1b428194f8..5d1f3a2331 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -14,8 +14,8 @@ namespace Symfony\Component\Form; use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\Exception; use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Util\PropertyPathInterface; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\ImmutableEventDispatcher; diff --git a/src/Symfony/Component/Form/FormConfigBuilderInterface.php b/src/Symfony/Component/Form/FormConfigBuilderInterface.php index 496fc38c66..c63f0e8aaa 100644 --- a/src/Symfony/Component/Form/FormConfigBuilderInterface.php +++ b/src/Symfony/Component/Form/FormConfigBuilderInterface.php @@ -117,7 +117,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets the data mapper used by the form. * - * @param DataMapperInterface $dataMapper + * @param DataMapperInterface $dataMapper * * @return self The configuration object. */ @@ -126,7 +126,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Set whether the form is disabled. * - * @param Boolean $disabled Whether the form is disabled + * @param Boolean $disabled Whether the form is disabled * * @return self The configuration object. */ @@ -135,7 +135,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets the data used for the client data when no value is bound. * - * @param mixed $emptyData The empty data. + * @param mixed $emptyData The empty data. * * @return self The configuration object. */ @@ -144,7 +144,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets whether errors bubble up to the parent. * - * @param Boolean $errorBubbling + * @param Boolean $errorBubbling * * @return self The configuration object. */ @@ -162,9 +162,9 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets the property path that the form should be mapped to. * - * @param null|string|PropertyPathInterface $propertyPath The property path or null if the path - * should be set automatically based on - * the form's name. + * @param null|string|\Symfony\Component\PropertyAccess\PropertyPathInterface $propertyPath + * The property path or null if the path should be set + * automatically based on the form's name. * * @return self The configuration object. */ @@ -174,7 +174,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface * Sets whether the form should be mapped to an element of its * parent's data. * - * @param Boolean $mapped Whether the form should be mapped. + * @param Boolean $mapped Whether the form should be mapped. * * @return self The configuration object. */ @@ -183,7 +183,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets whether the form's data should be modified by reference. * - * @param Boolean $byReference Whether the data should be + * @param Boolean $byReference Whether the data should be * modified by reference. * * @return self The configuration object. @@ -193,7 +193,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets whether the form should be virtual. * - * @param Boolean $virtual Whether the form should be virtual. + * @param Boolean $virtual Whether the form should be virtual. * * @return self The configuration object. */ @@ -202,7 +202,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface /** * Sets whether the form should be compound. * - * @param Boolean $compound Whether the form should be compound. + * @param Boolean $compound Whether the form should be compound. * * @return self The configuration object. * @@ -235,7 +235,7 @@ interface FormConfigBuilderInterface extends FormConfigInterface * this configuration. The data can only be modified then by * binding the form. * - * @param Boolean $locked Whether to lock the default data. + * @param Boolean $locked Whether to lock the default data. * * @return self The configuration object. */ diff --git a/src/Symfony/Component/Form/FormConfigInterface.php b/src/Symfony/Component/Form/FormConfigInterface.php index b5d914f6d3..0a27c2a7fb 100644 --- a/src/Symfony/Component/Form/FormConfigInterface.php +++ b/src/Symfony/Component/Form/FormConfigInterface.php @@ -35,7 +35,7 @@ interface FormConfigInterface /** * Returns the property path that the form should be mapped to. * - * @return null|Util\PropertyPathInterface The property path. + * @return null|\Symfony\Component\PropertyAccess\PropertyPathInterface The property path. */ public function getPropertyPath(); diff --git a/src/Symfony/Component/Form/FormInterface.php b/src/Symfony/Component/Form/FormInterface.php index d217a07dd1..94f6b3e29d 100644 --- a/src/Symfony/Component/Form/FormInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -166,7 +166,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable /** * Returns the property path that the form is mapped to. * - * @return Util\PropertyPathInterface The property path. + * @return \Symfony\Component\PropertyAccess\PropertyPathInterface The property path. */ public function getPropertyPath(); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php index ff449610a5..b7487377e6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/ChoiceList/ObjectChoiceListTest.php @@ -39,6 +39,9 @@ class ObjectChoiceListTest extends \PHPUnit_Framework_TestCase private $obj4; + /** + * @var ObjectChoiceList + */ private $list; protected function setUp() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index 05d2fe1577..8af2fd5f07 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -11,10 +11,8 @@ namespace Symfony\Component\Form\Tests\Extension\Core\DataMapper; -use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormConfigInterface; -use Symfony\Component\Form\Util\PropertyPath; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase @@ -29,14 +27,24 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase */ private $dispatcher; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $propertyAccessor; + protected function setUp() { if (!class_exists('Symfony\Component\EventDispatcher\Event')) { $this->markTestSkipped('The "EventDispatcher" component is not available'); } + if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccess')) { + $this->markTestSkipped('The "PropertyAccess" component is not available'); + } + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $this->mapper = new PropertyPathMapper(); + $this->propertyAccessor = $this->getMock('Symfony\Component\PropertyAccess\PropertyAccessorInterface'); + $this->mapper = new PropertyPathMapper($this->propertyAccessor); } /** @@ -45,7 +53,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase */ private function getPropertyPath($path) { - return $this->getMockBuilder('Symfony\Component\Form\Util\PropertyPath') + return $this->getMockBuilder('Symfony\Component\PropertyAccess\PropertyPath') ->setConstructorArgs(array($path)) ->setMethods(array('getValue', 'setValue')) ->getMock(); @@ -84,9 +92,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('getValue') - ->with($car) + ->with($car, $propertyPath) ->will($this->returnValue($engine)); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -107,9 +115,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('getValue') - ->with($car) + ->with($car, $propertyPath) ->will($this->returnValue($engine)); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -143,7 +151,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $car = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('getValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -161,7 +169,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase { $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('getValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -180,9 +188,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('getValue') - ->with($car) + ->with($car, $propertyPath) ->will($this->returnValue($engine)); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -211,9 +219,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('setValue') - ->with($car, $engine); + ->with($car, $propertyPath, $engine); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); $config->setByReference(false); @@ -230,9 +238,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('setValue') - ->with($car, $engine); + ->with($car, $propertyPath, $engine); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); $config->setByReference(true); @@ -250,12 +258,12 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $propertyPath = $this->getPropertyPath('engine'); // $car already contains the reference of $engine - $propertyPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) ->method('getValue') - ->with($car) + ->with($car, $propertyPath) ->will($this->returnValue($engine)); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('setValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -273,7 +281,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('setValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -291,7 +299,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $car = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('setValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -309,7 +317,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('setValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -327,7 +335,7 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $engine = new \stdClass(); $propertyPath = $this->getPropertyPath('engine'); - $propertyPath->expects($this->never()) + $this->propertyAccessor->expects($this->never()) ->method('setValue'); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); @@ -347,14 +355,14 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase $parentPath = $this->getPropertyPath('name'); $childPath = $this->getPropertyPath('engine'); - $parentPath->expects($this->never()) - ->method('getValue'); - $parentPath->expects($this->never()) - ->method('setValue'); + // getValue() and setValue() must never be invoked for $parentPath - $childPath->expects($this->once()) + $this->propertyAccessor->expects($this->once()) + ->method('getValue') + ->with($car, $childPath); + $this->propertyAccessor->expects($this->once()) ->method('setValue') - ->with($car, $engine); + ->with($car, $childPath, $engine); $config = new FormConfigBuilder('name', '\stdClass', $this->dispatcher); $config->setPropertyPath($parentPath); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 61ce5e7c58..3b32ab38e8 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList; - use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; use Symfony\Component\Form\Extension\Core\View\ChoiceView; diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php index 65c7b68693..fae4ae3e5b 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FormTypeTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; -use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Form\Form; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Tests\Fixtures\Author; diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php index d9555e13e1..77420ebb9f 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php @@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests\Extension\Validator\EventListener; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormError; -use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Form\Extension\Validator\Constraints\Form; use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; use Symfony\Component\Validator\ConstraintViolation; diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php index 2f41a99101..ac529b213f 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormError; -use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Validator\ConstraintViolation; /** diff --git a/src/Symfony/Component/Form/Tests/SimpleFormTest.php b/src/Symfony/Component/Form/Tests/SimpleFormTest.php index 0d72df3acb..102c973fd4 100644 --- a/src/Symfony/Component/Form/Tests/SimpleFormTest.php +++ b/src/Symfony/Component/Form/Tests/SimpleFormTest.php @@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Tests; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Exception\TransformationFailedException; diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathTest.php b/src/Symfony/Component/Form/Tests/Util/PropertyPathTest.php deleted file mode 100644 index e9be1445e6..0000000000 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathTest.php +++ /dev/null @@ -1,557 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Util; - -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Tests\Fixtures\Author; -use Symfony\Component\Form\Tests\Fixtures\Magician; - -class PropertyPathTest extends \PHPUnit_Framework_TestCase -{ - public function testGetValueReadsArray() - { - $array = array('firstName' => 'Bernhard'); - - $path = new PropertyPath('[firstName]'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException - */ - public function testGetValueThrowsExceptionIfIndexNotationExpected() - { - $array = array('firstName' => 'Bernhard'); - - $path = new PropertyPath('firstName'); - - $path->getValue($array); - } - - public function testGetValueReadsZeroIndex() - { - $array = array('Bernhard'); - - $path = new PropertyPath('[0]'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - public function testGetValueReadsIndexWithSpecialChars() - { - $array = array('%!@$§.' => 'Bernhard'); - - $path = new PropertyPath('[%!@$§.]'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - public function testGetValueReadsNestedIndexWithSpecialChars() - { - $array = array('root' => array('%!@$§.' => 'Bernhard')); - - $path = new PropertyPath('[root][%!@$§.]'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - public function testGetValueReadsArrayWithCustomPropertyPath() - { - $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); - - $path = new PropertyPath('[child][index][firstName]'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() - { - $array = array('child' => array('index' => array())); - - $path = new PropertyPath('[child][index][firstName]'); - - $this->assertNull($path->getValue($array)); - } - - public function testGetValueReadsProperty() - { - $object = new Author(); - $object->firstName = 'Bernhard'; - - $path = new PropertyPath('firstName'); - - $this->assertEquals('Bernhard', $path->getValue($object)); - } - - public function testGetValueIgnoresSingular() - { - $this->markTestSkipped('This feature is temporarily disabled as of 2.1'); - - $object = (object) array('children' => 'Many'); - - $path = new PropertyPath('children|child'); - - $this->assertEquals('Many', $path->getValue($object)); - } - - public function testGetValueReadsPropertyWithSpecialCharsExceptDot() - { - $array = (object) array('%!@$§' => 'Bernhard'); - - $path = new PropertyPath('%!@$§'); - - $this->assertEquals('Bernhard', $path->getValue($array)); - } - - public function testGetValueReadsPropertyWithCustomPropertyPath() - { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - $object->child['index']->firstName = 'Bernhard'; - - $path = new PropertyPath('child[index].firstName'); - - $this->assertEquals('Bernhard', $path->getValue($object)); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException - */ - public function testGetValueThrowsExceptionIfPropertyIsNotPublic() - { - $path = new PropertyPath('privateProperty'); - - $path->getValue(new Author()); - } - - public function testGetValueReadsGetters() - { - $path = new PropertyPath('lastName'); - - $object = new Author(); - $object->setLastName('Schussek'); - - $this->assertEquals('Schussek', $path->getValue($object)); - } - - public function testGetValueCamelizesGetterNames() - { - $path = new PropertyPath('last_name'); - - $object = new Author(); - $object->setLastName('Schussek'); - - $this->assertEquals('Schussek', $path->getValue($object)); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException - */ - public function testGetValueThrowsExceptionIfGetterIsNotPublic() - { - $path = new PropertyPath('privateGetter'); - - $path->getValue(new Author()); - } - - public function testGetValueReadsIssers() - { - $path = new PropertyPath('australian'); - - $object = new Author(); - $object->setAustralian(false); - - $this->assertFalse($path->getValue($object)); - } - - public function testGetValueReadHassers() - { - $path = new PropertyPath('read_permissions'); - - $object = new Author(); - $object->setReadPermissions(true); - - $this->assertTrue($path->getValue($object)); - } - - public function testGetValueReadsMagicGet() - { - $path = new PropertyPath('magicProperty'); - - $object = new Magician(); - $object->__set('magicProperty', 'foobar'); - - $this->assertSame('foobar', $path->getValue($object)); - } - - /* - * https://github.com/symfony/symfony/pull/4450 - */ - public function testGetValueReadsMagicGetThatReturnsConstant() - { - $path = new PropertyPath('magicProperty'); - - $object = new Magician(); - - $this->assertNull($path->getValue($object)); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException - */ - public function testGetValueThrowsExceptionIfIsserIsNotPublic() - { - $path = new PropertyPath('privateIsser'); - - $path->getValue(new Author()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException - */ - public function testGetValueThrowsExceptionIfPropertyDoesNotExist() - { - $path = new PropertyPath('foobar'); - - $path->getValue(new Author()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testGetValueThrowsExceptionIfNotObjectOrArray() - { - $path = new PropertyPath('foobar'); - - $path->getValue('baz'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testGetValueThrowsExceptionIfNull() - { - $path = new PropertyPath('foobar'); - - $path->getValue(null); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testGetValueThrowsExceptionIfEmpty() - { - $path = new PropertyPath('foobar'); - - $path->getValue(''); - } - - public function testSetValueUpdatesArrays() - { - $array = array(); - - $path = new PropertyPath('[firstName]'); - $path->setValue($array, 'Bernhard'); - - $this->assertEquals(array('firstName' => 'Bernhard'), $array); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException - */ - public function testSetValueThrowsExceptionIfIndexNotationExpected() - { - $array = array(); - - $path = new PropertyPath('firstName'); - $path->setValue($array, 'Bernhard'); - } - - public function testSetValueUpdatesArraysWithCustomPropertyPath() - { - $array = array(); - - $path = new PropertyPath('[child][index][firstName]'); - $path->setValue($array, 'Bernhard'); - - $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); - } - - public function testSetValueUpdatesProperties() - { - $object = new Author(); - - $path = new PropertyPath('firstName'); - $path->setValue($object, 'Bernhard'); - - $this->assertEquals('Bernhard', $object->firstName); - } - - public function testSetValueUpdatesPropertiesWithCustomPropertyPath() - { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - - $path = new PropertyPath('child[index].firstName'); - $path->setValue($object, 'Bernhard'); - - $this->assertEquals('Bernhard', $object->child['index']->firstName); - } - - public function testSetValueUpdateMagicSet() - { - $object = new Magician(); - - $path = new PropertyPath('magicProperty'); - $path->setValue($object, 'foobar'); - - $this->assertEquals('foobar', $object->__get('magicProperty')); - } - - public function testSetValueUpdatesSetters() - { - $object = new Author(); - - $path = new PropertyPath('lastName'); - $path->setValue($object, 'Schussek'); - - $this->assertEquals('Schussek', $object->getLastName()); - } - - public function testSetValueCamelizesSetterNames() - { - $object = new Author(); - - $path = new PropertyPath('last_name'); - $path->setValue($object, 'Schussek'); - - $this->assertEquals('Schussek', $object->getLastName()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\PropertyAccessDeniedException - */ - public function testSetValueThrowsExceptionIfGetterIsNotPublic() - { - $path = new PropertyPath('privateSetter'); - - $path->setValue(new Author(), 'foobar'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testSetValueThrowsExceptionIfNotObjectOrArray() - { - $path = new PropertyPath('foobar'); - $value = 'baz'; - - $path->setValue($value, 'bam'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testSetValueThrowsExceptionIfNull() - { - $path = new PropertyPath('foobar'); - $value = null; - - $path->setValue($value, 'bam'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testSetValueThrowsExceptionIfEmpty() - { - $path = new PropertyPath('foobar'); - $value = ''; - - $path->setValue($value, 'bam'); - } - - public function testToString() - { - $path = new PropertyPath('reference.traversable[index].property'); - - $this->assertEquals('reference.traversable[index].property', $path->__toString()); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException - */ - public function testInvalidPropertyPath_noDotBeforeProperty() - { - new PropertyPath('[index]property'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException - */ - public function testInvalidPropertyPath_dotAtTheBeginning() - { - new PropertyPath('.property'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException - */ - public function testInvalidPropertyPath_unexpectedCharacters() - { - new PropertyPath('property.$form'); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyPathException - */ - public function testInvalidPropertyPath_empty() - { - new PropertyPath(''); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testInvalidPropertyPath_null() - { - new PropertyPath(null); - } - - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testInvalidPropertyPath_false() - { - new PropertyPath(false); - } - - public function testValidPropertyPath_zero() - { - new PropertyPath('0'); - } - - public function testGetParent_dot() - { - $propertyPath = new PropertyPath('grandpa.parent.child'); - - $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); - } - - public function testGetParent_index() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); - } - - public function testGetParent_noParent() - { - $propertyPath = new PropertyPath('path'); - - $this->assertNull($propertyPath->getParent()); - } - - public function testCopyConstructor() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - $copy = new PropertyPath($propertyPath); - - $this->assertEquals($propertyPath, $copy); - } - - public function testGetElement() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $this->assertEquals('child', $propertyPath->getElement(2)); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testGetElementDoesNotAcceptInvalidIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->getElement(3); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testGetElementDoesNotAcceptNegativeIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->getElement(-1); - } - - public function testIsProperty() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $this->assertTrue($propertyPath->isProperty(1)); - $this->assertFalse($propertyPath->isProperty(2)); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testIsPropertyDoesNotAcceptInvalidIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->isProperty(3); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testIsPropertyDoesNotAcceptNegativeIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->isProperty(-1); - } - - public function testIsIndex() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $this->assertFalse($propertyPath->isIndex(1)); - $this->assertTrue($propertyPath->isIndex(2)); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testIsIndexDoesNotAcceptInvalidIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->isIndex(3); - } - - /** - * @expectedException \OutOfBoundsException - */ - public function testIsIndexDoesNotAcceptNegativeIndices() - { - $propertyPath = new PropertyPath('grandpa.parent[child]'); - - $propertyPath->isIndex(-1); - } -} diff --git a/src/Symfony/Component/Form/Util/FormUtil.php b/src/Symfony/Component/Form/Util/FormUtil.php index 797028bd19..481f6a5080 100644 --- a/src/Symfony/Component/Form/Util/FormUtil.php +++ b/src/Symfony/Component/Form/Util/FormUtil.php @@ -11,181 +11,29 @@ namespace Symfony\Component\Form\Util; +use Symfony\Component\PropertyAccess\StringUtil; + /** * @author Bernhard Schussek */ class FormUtil { - /** - * Map english plural to singular suffixes - * - * @var array - * - * @see http://english-zone.com/spelling/plurals.html - * @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English - */ - private static $pluralMap = array( - // First entry: plural suffix, reversed - // Second entry: length of plural suffix - // Third entry: Whether the suffix may succeed a vocal - // Fourth entry: Whether the suffix may succeed a consonant - // Fifth entry: singular suffix, normal - - // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) - array('a', 1, true, true, array('on', 'um')), - - // nebulae (nebula) - array('ea', 2, true, true, 'a'), - - // mice (mouse), lice (louse) - array('eci', 3, false, true, 'ouse'), - - // geese (goose) - array('esee', 4, false, true, 'oose'), - - // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) - array('i', 1, true, true, 'us'), - - // men (man), women (woman) - array('nem', 3, true, true, 'man'), - - // children (child) - array('nerdlihc', 8, true, true, 'child'), - - // oxen (ox) - array('nexo', 4, false, false, 'ox'), - - // indices (index), appendices (appendix), prices (price) - array('seci', 4, false, true, array('ex', 'ix', 'ice')), - - // babies (baby) - array('sei', 3, false, true, 'y'), - - // analyses (analysis), ellipses (ellipsis), funguses (fungus), - // neuroses (neurosis), theses (thesis), emphases (emphasis), - // oases (oasis), crises (crisis), houses (house), bases (base), - // atlases (atlas), kisses (kiss) - array('ses', 3, true, true, array('s', 'se', 'sis')), - - // lives (life), wives (wife) - array('sevi', 4, false, true, 'ife'), - - // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) - array('sev', 3, true, true, 'f'), - - // axes (axis), axes (ax), axes (axe) - array('sexa', 4, false, false, array('ax', 'axe', 'axis')), - - // indexes (index), matrixes (matrix) - array('sex', 3, true, false, 'x'), - - // quizzes (quiz) - array('sezz', 4, true, false, 'z'), - - // bureaus (bureau) - array('suae', 4, false, true, 'eau'), - - // roses (rose), garages (garage), cassettes (cassette), - // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), - // shoes (shoe) - array('se', 2, true, true, array('', 'e')), - - // tags (tag) - array('s', 1, true, true, ''), - - // chateaux (chateau) - array('xuae', 4, false, true, 'eau'), - ); - /** * This class should not be instantiated */ private function __construct() {} /** - * Returns the singular form of a word + * Alias for {@link StringUtil::singularify()} * - * If the method can't determine the form with certainty, an array of the - * possible singulars is returned. - * - * @param string $plural A word in plural form - * @return string|array The singular form or an array of possible singular - * forms + * @deprecated Deprecated since version 2.2, to be removed in 2.3. Use + * {@link StringUtil::singularify()} instead. */ public static function singularify($plural) { - $pluralRev = strrev($plural); - $lowerPluralRev = strtolower($pluralRev); - $pluralLength = strlen($lowerPluralRev); + trigger_error('\Symfony\Component\Form\Util\FormUtil::singularify() is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\StringUtil::singularify() in the PropertyAccess component instead.', E_USER_DEPRECATED); - // The outer loop iterates over the entries of the plural table - // The inner loop $j iterates over the characters of the plural suffix - // in the plural table to compare them with the characters of the actual - // given plural suffix - foreach (self::$pluralMap as $map) { - $suffix = $map[0]; - $suffixLength = $map[1]; - $j = 0; - - // Compare characters in the plural table and of the suffix of the - // given plural one by one - while ($suffix[$j] === $lowerPluralRev[$j]) { - // Let $j point to the next character - ++$j; - - // Successfully compared the last character - // Add an entry with the singular suffix to the singular array - if ($j === $suffixLength) { - // Is there any character preceding the suffix in the plural string? - if ($j < $pluralLength) { - $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]); - - if (!$map[2] && $nextIsVocal) { - // suffix may not succeed a vocal but next char is one - break; - } - - if (!$map[3] && !$nextIsVocal) { - // suffix may not succeed a consonant but next char is one - break; - } - } - - $newBase = substr($plural, 0, $pluralLength - $suffixLength); - $newSuffix = $map[4]; - - // Check whether the first character in the plural suffix - // is uppercased. If yes, uppercase the first character in - // the singular suffix too - $firstUpper = ctype_upper($pluralRev[$j - 1]); - - if (is_array($newSuffix)) { - $singulars = array(); - - foreach ($newSuffix as $newSuffixEntry) { - $singulars[] = $newBase . ($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); - } - - return $singulars; - } - - return $newBase . ($firstUpper ? ucFirst($newSuffix) : $newSuffix); - } - - // Suffix is longer than word - if ($j === $pluralLength) { - break; - } - } - } - - // Convert teeth to tooth, feet to foot - if (false !== ($pos = strpos($plural, 'ee'))) { - return substr_replace($plural, 'oo', $pos, 2); - } - - // Assume that plural and singular is identical - return $plural; + return StringUtil::singularify($plural); } /** diff --git a/src/Symfony/Component/Form/Util/PropertyPath.php b/src/Symfony/Component/Form/Util/PropertyPath.php index a5cf13befe..445fae81d5 100644 --- a/src/Symfony/Component/Form/Util/PropertyPath.php +++ b/src/Symfony/Component/Form/Util/PropertyPath.php @@ -11,619 +11,47 @@ namespace Symfony\Component\Form\Util; -use Traversable; -use ReflectionClass; -use Symfony\Component\Form\Exception\InvalidPropertyPathException; -use Symfony\Component\Form\Exception\InvalidPropertyException; -use Symfony\Component\Form\Exception\PropertyAccessDeniedException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\PropertyAccess\PropertyPath as BasePropertyPath; +use Symfony\Component\PropertyAccess\PropertyAccess; /** - * Allows easy traversing of a property path + * Alias for {@link \Symfony\Component\PropertyAccess\PropertyPath}. * * @author Bernhard Schussek + * + * @deprecated deprecated since version 2.2, to be removed in 2.3. Use + * {@link \Symfony\Component\PropertyAccess\PropertyPath} + * instead. */ -class PropertyPath implements \IteratorAggregate, PropertyPathInterface +class PropertyPath extends BasePropertyPath { /** - * Character used for separating between plural and singular of an element. - * @var string - */ - const SINGULAR_SEPARATOR = '|'; - - const VALUE = 0; - const IS_REF = 1; - - /** - * The elements of the property path - * @var array - */ - private $elements = array(); - - /** - * The singular forms of the elements in the property path. - * @var array - */ - private $singulars = array(); - - /** - * The number of elements in the property path - * @var integer - */ - private $length; - - /** - * Contains a Boolean for each property in $elements denoting whether this - * element is an index. It is a property otherwise. - * @var array - */ - private $isIndex = array(); - - /** - * String representation of the path - * @var string - */ - private $pathAsString; - - /** - * Constructs a property path from a string. - * - * @param PropertyPath|string $propertyPath The property path as string or instance. - * - * @throws UnexpectedTypeException If the given path is not a string. - * @throws InvalidPropertyPathException If the syntax of the property path is not valid. + * {@inheritdoc} */ public function __construct($propertyPath) { - // Can be used as copy constructor - if ($propertyPath instanceof PropertyPath) { - /* @var PropertyPath $propertyPath */ - $this->elements = $propertyPath->elements; - $this->singulars = $propertyPath->singulars; - $this->length = $propertyPath->length; - $this->isIndex = $propertyPath->isIndex; - $this->pathAsString = $propertyPath->pathAsString; + parent::__construct($propertyPath); - return; - } - if (!is_string($propertyPath)) { - throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\Form\Util\PropertyPath'); - } - - if ('' === $propertyPath) { - throw new InvalidPropertyPathException('The property path should not be empty.'); - } - - $this->pathAsString = $propertyPath; - $position = 0; - $remaining = $propertyPath; - - // first element is evaluated differently - no leading dot for properties - $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; - - while (preg_match($pattern, $remaining, $matches)) { - if ('' !== $matches[2]) { - $element = $matches[2]; - $this->isIndex[] = false; - } else { - $element = $matches[3]; - $this->isIndex[] = true; - } - // Disabled this behaviour as the syntax is not yet final - //$pos = strpos($element, self::SINGULAR_SEPARATOR); - $pos = false; - $singular = null; - - if (false !== $pos) { - $singular = substr($element, $pos + 1); - $element = substr($element, 0, $pos); - } - - $this->elements[] = $element; - $this->singulars[] = $singular; - - $position += strlen($matches[1]); - $remaining = $matches[4]; - $pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/'; - } - - if ('' !== $remaining) { - throw new InvalidPropertyPathException(sprintf( - 'Could not parse property path "%s". Unexpected token "%s" at position %d', - $propertyPath, - $remaining{0}, - $position - )); - } - - $this->length = count($this->elements); + trigger_error('\Symfony\Component\Form\Util\PropertyPath is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPath instead.', E_USER_DEPRECATED); } /** - * {@inheritdoc} - */ - public function __toString() - { - return $this->pathAsString; - } - - /** - * {@inheritdoc} - */ - public function getLength() - { - return $this->length; - } - - /** - * {@inheritdoc} - */ - public function getParent() - { - if ($this->length <= 1) { - return null; - } - - $parent = clone $this; - - --$parent->length; - $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '['))); - array_pop($parent->elements); - array_pop($parent->singulars); - array_pop($parent->isIndex); - - return $parent; - } - - /** - * Returns a new iterator for this path - * - * @return PropertyPathIteratorInterface - */ - public function getIterator() - { - return new PropertyPathIterator($this); - } - - /** - * {@inheritdoc} - */ - public function getElements() - { - return $this->elements; - } - - /** - * {@inheritdoc} - */ - public function getElement($index) - { - if (!isset($this->elements[$index])) { - throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path'); - } - - return $this->elements[$index]; - } - - /** - * {@inheritdoc} - */ - public function isProperty($index) - { - if (!isset($this->isIndex[$index])) { - throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path'); - } - - return !$this->isIndex[$index]; - } - - /** - * {@inheritdoc} - */ - public function isIndex($index) - { - if (!isset($this->isIndex[$index])) { - throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path'); - } - - return $this->isIndex[$index]; - } - - /** - * Returns the value at the end of the property path of the object - * - * Example: - * - * $path = new PropertyPath('child.name'); - * - * echo $path->getValue($object); - * // equals echo $object->getChild()->getName(); - * - * - * This method first tries to find a public getter for each property in the - * path. The name of the getter must be the camel-cased property name - * prefixed with "get", "is", or "has". - * - * If the getter does not exist, this method tries to find a public - * property. The value of the property is then returned. - * - * If none of them are found, an exception is thrown. - * - * @param object|array $objectOrArray The object or array to traverse - * - * @return mixed The value at the end of the property path - * - * @throws InvalidPropertyException If the property/getter does not exist - * @throws PropertyAccessDeniedException If the property/getter exists but is not public + * Alias for {@link PropertyAccessor::getValue()} */ public function getValue($objectOrArray) { - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $this->length - 1); + $propertyAccessor = PropertyAccess::getPropertyAccessor(); - return $propertyValues[count($propertyValues) - 1][self::VALUE]; + return $propertyAccessor->getValue($objectOrArray, $this); } /** - * Sets the value at the end of the property path of the object - * - * Example: - * - * $path = new PropertyPath('child.name'); - * - * echo $path->setValue($object, 'Fabien'); - * // equals echo $object->getChild()->setName('Fabien'); - * - * - * This method first tries to find a public setter for each property in the - * path. The name of the setter must be the camel-cased property name - * prefixed with "set". - * - * If the setter does not exist, this method tries to find a public - * property. The value of the property is then changed. - * - * If neither is found, an exception is thrown. - * - * @param object|array $objectOrArray The object or array to modify. - * @param mixed $value The value to set at the end of the property path. - * - * @throws InvalidPropertyException If a property does not exist. - * @throws PropertyAccessDeniedException If a property cannot be accessed due to - * access restrictions (private or protected). - * @throws UnexpectedTypeException If a value within the path is neither object - * nor array. + * Alias for {@link PropertyAccessor::setValue()} */ - public function setValue(&$objectOrArray, $value) + public function setValue($objectOrArray, $value) { - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $this->length - 2); - $overwrite = true; + $propertyAccessor = PropertyAccess::getPropertyAccessor(); - // Add the root object to the list - array_unshift($propertyValues, array( - self::VALUE => &$objectOrArray, - self::IS_REF => true, - )); - - for ($i = count($propertyValues) - 1; $i >= 0; --$i) { - $objectOrArray =& $propertyValues[$i][self::VALUE]; - - if ($overwrite) { - if (!is_object($objectOrArray) && !is_array($objectOrArray)) { - throw new UnexpectedTypeException($objectOrArray, 'object or array'); - } - - $property = $this->elements[$i]; - $singular = $this->singulars[$i]; - $isIndex = $this->isIndex[$i]; - - $this->writeProperty($objectOrArray, $property, $singular, $isIndex, $value); - } - - $value =& $objectOrArray; - $overwrite = !$propertyValues[$i][self::IS_REF]; - } - } - - /** - * Reads the path from an object up to a given path index. - * - * @param object|array $objectOrArray The object or array to read from. - * @param integer $lastIndex The integer up to which should be read. - * - * @return array The values read in the path. - * - * @throws UnexpectedTypeException If a value within the path is neither object nor array. - */ - private function &readPropertiesUntil(&$objectOrArray, $lastIndex) - { - $propertyValues = array(); - - for ($i = 0; $i <= $lastIndex; ++$i) { - if (!is_object($objectOrArray) && !is_array($objectOrArray)) { - throw new UnexpectedTypeException($objectOrArray, 'object or array'); - } - - $property = $this->elements[$i]; - $isIndex = $this->isIndex[$i]; - $isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess; - - // Create missing nested arrays on demand - if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { - $objectOrArray[$property] = $i + 1 < $this->length ? array() : null; - } - - $propertyValue =& $this->readProperty($objectOrArray, $property, $isIndex); - $objectOrArray =& $propertyValue[self::VALUE]; - - $propertyValues[] =& $propertyValue; - } - - return $propertyValues; - } - - /** - * Reads the a property from an object or array. - * - * @param object|array $objectOrArray The object or array to read from. - * @param string $property The property to read. - * @param Boolean $isIndex Whether to interpret the property as index. - * - * @return mixed The value of the read property - * - * @throws InvalidPropertyException If the property does not exist. - * @throws PropertyAccessDeniedException If the property cannot be accessed due to - * access restrictions (private or protected). - */ - private function &readProperty(&$objectOrArray, $property, $isIndex) - { - // Use an array instead of an object since performance is - // very crucial here - $result = array( - self::VALUE => null, - self::IS_REF => false - ); - - if ($isIndex) { - if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { - throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); - } - - if (isset($objectOrArray[$property])) { - if (is_array($objectOrArray)) { - $result[self::VALUE] =& $objectOrArray[$property]; - $result[self::IS_REF] = true; - } else { - $result[self::VALUE] = $objectOrArray[$property]; - } - } - } elseif (is_object($objectOrArray)) { - $camelProp = $this->camelize($property); - $reflClass = new ReflectionClass($objectOrArray); - $getter = 'get'.$camelProp; - $isser = 'is'.$camelProp; - $hasser = 'has'.$camelProp; - - if ($reflClass->hasMethod($getter)) { - if (!$reflClass->getMethod($getter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$getter(); - } elseif ($reflClass->hasMethod($isser)) { - if (!$reflClass->getMethod($isser)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$isser(); - } elseif ($reflClass->hasMethod($hasser)) { - if (!$reflClass->getMethod($hasser)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$hasser(); - } elseif ($reflClass->hasMethod('__get')) { - // needed to support magic method __get - $result[self::VALUE] = $objectOrArray->$property; - } elseif ($reflClass->hasProperty($property)) { - if (!$reflClass->getProperty($property)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser)); - } - - $result[self::VALUE] =& $objectOrArray->$property; - $result[self::IS_REF] = true; - } elseif (property_exists($objectOrArray, $property)) { - // needed to support \stdClass instances - $result[self::VALUE] =& $objectOrArray->$property; - $result[self::IS_REF] = true; - } else { - throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name)); - } - } else { - throw new InvalidPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); - } - - // Objects are always passed around by reference - if (is_object($result[self::VALUE])) { - $result[self::IS_REF] = true; - } - - return $result; - } - - /** - * Sets the value of the property at the given index in the path - * - * @param object|array $objectOrArray The object or array to write to. - * @param string $property The property to write. - * @param string|null $singular The singular form of the property name or null. - * @param Boolean $isIndex Whether to interpret the property as index. - * @param mixed $value The value to write. - * - * @throws InvalidPropertyException If the property does not exist. - * @throws PropertyAccessDeniedException If the property cannot be accessed due to - * access restrictions (private or protected). - */ - private function writeProperty(&$objectOrArray, $property, $singular, $isIndex, $value) - { - $adderRemoverError = null; - - if ($isIndex) { - if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { - throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); - } - - $objectOrArray[$property] = $value; - } elseif (is_object($objectOrArray)) { - $reflClass = new ReflectionClass($objectOrArray); - - // The plural form is the last element of the property path - $plural = $this->camelize($this->elements[$this->length - 1]); - - // Any of the two methods is required, but not yet known - $singulars = null !== $singular ? array($singular) : (array) FormUtil::singularify($plural); - - if (is_array($value) || $value instanceof Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); - if (null !== $methods) { - // At this point the add and remove methods have been found - // Use iterator_to_array() instead of clone in order to prevent side effects - // see https://github.com/symfony/symfony/issues/4670 - $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; - $itemToRemove = array(); - $propertyValue = $this->readProperty($objectOrArray, $property, $isIndex); - $previousValue = $propertyValue[self::VALUE]; - - if (is_array($previousValue) || $previousValue instanceof Traversable) { - foreach ($previousValue as $previousItem) { - foreach ($value as $key => $item) { - if ($item === $previousItem) { - // Item found, don't add - unset($itemsToAdd[$key]); - - // Next $previousItem - continue 2; - } - } - - // Item not found, add to remove list - $itemToRemove[] = $previousItem; - } - } - - foreach ($itemToRemove as $item) { - call_user_func(array($objectOrArray, $methods[1]), $item); - } - - foreach ($itemsToAdd as $item) { - call_user_func(array($objectOrArray, $methods[0]), $item); - } - - return; - } else { - $adderRemoverError = ', nor could adders and removers be found based on the '; - if (null === $singular) { - // $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)'; - $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars); - } else { - $adderRemoverError .= 'passed singular: '.$singular; - } - } - } - - $setter = 'set'.$this->camelize($property); - if ($reflClass->hasMethod($setter)) { - if (!$reflClass->getMethod($setter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name)); - } - - $objectOrArray->$setter($value); - } elseif ($reflClass->hasMethod('__set')) { - // needed to support magic method __set - $objectOrArray->$property = $value; - } elseif ($reflClass->hasProperty($property)) { - if (!$reflClass->getProperty($property)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter)); - } - - $objectOrArray->$property = $value; - } elseif (property_exists($objectOrArray, $property)) { - // needed to support \stdClass instances - $objectOrArray->$property = $value; - } else { - throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError)); - } - } else { - throw new InvalidPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); - } - } - - /** - * Camelizes a given string. - * - * @param string $string Some string. - * - * @return string The camelized version of the string. - */ - private function camelize($string) - { - return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string); - } - - /** - * Searches for add and remove methods. - * - * @param \ReflectionClass $reflClass The reflection class for the given object - * @param array $singulars The singular form of the property name or null. - * - * @return array|null An array containing the adder and remover when found, null otherwise. - * - * @throws InvalidPropertyException If the property does not exist. - */ - private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) - { - foreach ($singulars as $singular) { - $addMethod = 'add' . $singular; - $removeMethod = 'remove' . $singular; - - $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1); - $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1); - - if ($addMethodFound && $removeMethodFound) { - return array($addMethod, $removeMethod); - } - - if ($addMethodFound xor $removeMethodFound) { - throw new InvalidPropertyException(sprintf( - 'Found the public method "%s", but did not find a public "%s" on class %s', - $addMethodFound ? $addMethod : $removeMethod, - $addMethodFound ? $removeMethod : $addMethod, - $reflClass->name - )); - } - } - - return null; - } - - /** - * Returns whether a method is public and has a specific number of required parameters. - * - * @param \ReflectionClass $class The class of the method. - * @param string $methodName The method name. - * @param integer $parameters The number of parameters. - * - * @return Boolean Whether the method is public and has $parameters - * required parameters. - */ - private function isAccessible(ReflectionClass $class, $methodName, $parameters) - { - if ($class->hasMethod($methodName)) { - $method = $class->getMethod($methodName); - - if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) { - return true; - } - } - - return false; + return $propertyAccessor->getValue($objectOrArray, $this, $value); } } diff --git a/src/Symfony/Component/Form/Util/PropertyPathBuilder.php b/src/Symfony/Component/Form/Util/PropertyPathBuilder.php index f17c8b2258..31836b63bd 100644 --- a/src/Symfony/Component/Form/Util/PropertyPathBuilder.php +++ b/src/Symfony/Component/Form/Util/PropertyPathBuilder.php @@ -11,284 +11,26 @@ namespace Symfony\Component\Form\Util; +use Symfony\Component\PropertyAccess\PropertyPathBuilder as BasePropertyPathBuilder; + /** + * Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathBuilder}. + * * @author Bernhard Schussek + * + * @deprecated deprecated since version 2.2, to be removed in 2.3. Use + * {@link \Symfony\Component\PropertyAccess\PropertyPathBuilder} + * instead. */ -class PropertyPathBuilder +class PropertyPathBuilder extends BasePropertyPathBuilder { /** - * @var array + * {@inheritdoc} */ - private $elements = array(); - - /** - * @var array - */ - private $isIndex = array(); - - /** - * Creates a new property path builder. - * - * @param null|PropertyPathInterface $path The path to initially store - * in the builder. Optional. - */ - public function __construct(PropertyPathInterface $path = null) + public function __construct($propertyPath) { - if (null !== $path) { - $this->append($path); - } - } + parent::__construct($propertyPath); - /** - * Appends a (sub-) path to the current path. - * - * @param PropertyPathInterface $path The path to append. - * @param integer $offset The offset where the appended piece - * starts in $path. - * @param integer $length The length of the appended piece. - * If 0, the full path is appended. - */ - public function append(PropertyPathInterface $path, $offset = 0, $length = 0) - { - if (0 === $length) { - $end = $path->getLength(); - } else { - $end = $offset + $length; - } - - for (; $offset < $end; ++$offset) { - $this->elements[] = $path->getElement($offset); - $this->isIndex[] = $path->isIndex($offset); - } - } - - /** - * Appends an index element to the current path. - * - * @param string $name The name of the appended index. - */ - public function appendIndex($name) - { - $this->elements[] = $name; - $this->isIndex[] = true; - } - - /** - * Appends a property element to the current path. - * - * @param string $name The name of the appended property. - */ - public function appendProperty($name) - { - $this->elements[] = $name; - $this->isIndex[] = false; - } - - /** - * Removes elements from the current path. - * - * @param integer $offset The offset at which to remove. - * @param integer $length The length of the removed piece. - * - * @throws \OutOfBoundsException if offset is invalid - */ - public function remove($offset, $length = 1) - { - if (!isset($this->elements[$offset])) { - throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); - } - - $this->resize($offset, $length, 0); - } - - /** - * Replaces a sub-path by a different (sub-) path. - * - * @param integer $offset The offset at which to replace. - * @param integer $length The length of the piece to replace. - * @param PropertyPathInterface $path The path to insert. - * @param integer $pathOffset The offset where the inserted piece - * starts in $path. - * @param integer $pathLength The length of the inserted piece. - * If 0, the full path is inserted. - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function replace($offset, $length, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0) - { - if (!isset($this->elements[$offset])) { - throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); - } - - if (0 === $pathLength) { - $pathLength = $path->getLength() - $pathOffset; - } - - $this->resize($offset, $length, $pathLength); - - for ($i = 0; $i < $pathLength; ++$i) { - $this->elements[$offset + $i] = $path->getElement($pathOffset + $i); - $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i); - } - } - - /** - * Replaces a property element by an index element. - * - * @param integer $offset The offset at which to replace. - * @param string $name The new name of the element. Optional. - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function replaceByIndex($offset, $name = null) - { - if (!isset($this->elements[$offset])) { - throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); - } - - if (null !== $name) { - $this->elements[$offset] = $name; - } - - $this->isIndex[$offset] = true; - } - - /** - * Replaces an index element by a property element. - * - * @param integer $offset The offset at which to replace. - * @param string $name The new name of the element. Optional. - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function replaceByProperty($offset, $name = null) - { - if (!isset($this->elements[$offset])) { - throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); - } - - if (null !== $name) { - $this->elements[$offset] = $name; - } - - $this->isIndex[$offset] = false; - } - - /** - * Returns the length of the current path. - * - * @return integer The path length. - */ - public function getLength() - { - return count($this->elements); - } - - /** - * Returns the current property path. - * - * @return PropertyPathInterface The constructed property path. - */ - public function getPropertyPath() - { - $pathAsString = $this->__toString(); - - return '' !== $pathAsString ? new PropertyPath($pathAsString) : null; - } - - /** - * Returns the current property path as string. - * - * @return string The property path as string. - */ - public function __toString() - { - $string = ''; - - foreach ($this->elements as $offset => $element) { - if ($this->isIndex[$offset]) { - $element = '[' . $element . ']'; - } elseif ('' !== $string) { - $string .= '.'; - } - - $string .= $element; - } - - return $string; - } - - /** - * Resizes the path so that a chunk of length $cutLength is - * removed at $offset and another chunk of length $insertionLength - * can be inserted. - * - * @param integer $offset The offset where the removed chunk starts. - * @param integer $cutLength The length of the removed chunk. - * @param integer $insertionLength The length of the inserted chunk. - */ - private function resize($offset, $cutLength, $insertionLength) - { - // Nothing else to do in this case - if ($insertionLength === $cutLength) { - return; - } - - $length = count($this->elements); - - if ($cutLength > $insertionLength) { - // More elements should be removed than inserted - $diff = $cutLength - $insertionLength; - $newLength = $length - $diff; - - // Shift elements to the left (left-to-right until the new end) - // Max allowed offset to be shifted is such that - // $offset + $diff < $length (otherwise invalid index access) - // i.e. $offset < $length - $diff = $newLength - for ($i = $offset; $i < $newLength; ++$i) { - $this->elements[$i] = $this->elements[$i + $diff]; - $this->isIndex[$i] = $this->isIndex[$i + $diff]; - } - - // All remaining elements should be removed - for (; $i < $length; ++$i) { - unset($this->elements[$i]); - unset($this->isIndex[$i]); - } - } else { - $diff = $insertionLength - $cutLength; - - $newLength = $length + $diff; - $indexAfterInsertion = $offset + $insertionLength; - - // $diff <= $insertionLength - // $indexAfterInsertion >= $insertionLength - // => $diff <= $indexAfterInsertion - - // In each of the following loops, $i >= $diff must hold, - // otherwise ($i - $diff) becomes negative. - - // Shift old elements to the right to make up space for the - // inserted elements. This needs to be done left-to-right in - // order to preserve an ascending array index order - // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff, - // $i >= $diff is guaranteed. - for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) { - $this->elements[$i] = $this->elements[$i - $diff]; - $this->isIndex[$i] = $this->isIndex[$i - $diff]; - } - - // Shift remaining elements to the right. Do this right-to-left - // so we don't overwrite elements before copying them - // The last written index is the immediate index after the inserted - // string, because the indices before that will be overwritten - // anyway. - // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff, - // $i >= $diff is guaranteed. - for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) { - $this->elements[$i] = $this->elements[$i - $diff]; - $this->isIndex[$i] = $this->isIndex[$i - $diff]; - } - } + trigger_error('\Symfony\Component\Form\Util\PropertyPathBuilder is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPathBuilder instead.', E_USER_DEPRECATED); } } diff --git a/src/Symfony/Component/Form/Util/PropertyPathInterface.php b/src/Symfony/Component/Form/Util/PropertyPathInterface.php index bceca29d90..ec2d5e9efb 100644 --- a/src/Symfony/Component/Form/Util/PropertyPathInterface.php +++ b/src/Symfony/Component/Form/Util/PropertyPathInterface.php @@ -11,74 +11,17 @@ namespace Symfony\Component\Form\Util; +use Symfony\Component\PropertyAccess\PropertyPathInterface as BasePropertyPathInterface; + /** + * Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathInterface}. + * * @author Bernhard Schussek + * + * @deprecated deprecated since version 2.2, to be removed in 2.3. Use + * {@link \Symfony\Component\PropertyAccess\PropertyPathInterface} + * instead. */ -interface PropertyPathInterface extends \Traversable +interface PropertyPathInterface extends BasePropertyPathInterface { - /** - * Returns the string representation of the property path - * - * @return string The path as string. - */ - public function __toString(); - - /** - * Returns the length of the property path, i.e. the number of elements. - * - * @return integer The path length. - */ - public function getLength(); - - /** - * Returns the parent property path. - * - * The parent property path is the one that contains the same items as - * this one except for the last one. - * - * If this property path only contains one item, null is returned. - * - * @return PropertyPath The parent path or null. - */ - public function getParent(); - - /** - * Returns the elements of the property path as array - * - * @return array An array of property/index names - */ - public function getElements(); - - /** - * Returns the element at the given index in the property path - * - * @param integer $index The index key - * - * @return string A property or index name - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function getElement($index); - - /** - * Returns whether the element at the given index is a property - * - * @param integer $index The index in the property path - * - * @return Boolean Whether the element at this index is a property - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function isProperty($index); - - /** - * Returns whether the element at the given index is an array index - * - * @param integer $index The index in the property path - * - * @return Boolean Whether the element at this index is an array index - * - * @throws \OutOfBoundsException If the offset is invalid. - */ - public function isIndex($index); } diff --git a/src/Symfony/Component/Form/Util/PropertyPathIterator.php b/src/Symfony/Component/Form/Util/PropertyPathIterator.php index c165d0854e..024ed6a653 100644 --- a/src/Symfony/Component/Form/Util/PropertyPathIterator.php +++ b/src/Symfony/Component/Form/Util/PropertyPathIterator.php @@ -11,45 +11,26 @@ namespace Symfony\Component\Form\Util; +use Symfony\Component\PropertyAccess\PropertyPathIterator as BasePropertyPathIterator; + /** - * Traverses a property path and provides additional methods to find out - * information about the current element + * Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathIterator}. * * @author Bernhard Schussek + * + * @deprecated deprecated since version 2.2, to be removed in 2.3. Use + * {@link \Symfony\Component\PropertyAccess\PropertyPathIterator} + * instead. */ -class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface +class PropertyPathIterator extends BasePropertyPathIterator { - /** - * The traversed property path - * @var PropertyPathInterface - */ - protected $path; - - /** - * Constructor. - * - * @param PropertyPathInterface $path The property path to traverse - */ - public function __construct(PropertyPathInterface $path) - { - parent::__construct($path->getElements()); - - $this->path = $path; - } - /** * {@inheritdoc} */ - public function isIndex() + public function __construct($propertyPath) { - return $this->path->isIndex($this->key()); - } + parent::__construct($propertyPath); - /** - * {@inheritdoc} - */ - public function isProperty() - { - return $this->path->isProperty($this->key()); + trigger_error('\Symfony\Component\Form\Util\PropertyPathIterator is deprecated since version 2.2 and will be removed in 2.3. Use \Symfony\Component\PropertyAccess\PropertyPathIterator instead.', E_USER_DEPRECATED); } } diff --git a/src/Symfony/Component/Form/Util/PropertyPathIteratorInterface.php b/src/Symfony/Component/Form/Util/PropertyPathIteratorInterface.php index 35fb11ad73..3540639530 100644 --- a/src/Symfony/Component/Form/Util/PropertyPathIteratorInterface.php +++ b/src/Symfony/Component/Form/Util/PropertyPathIteratorInterface.php @@ -11,24 +11,17 @@ namespace Symfony\Component\Form\Util; -/** - * @author Bernhard Schussek - */ -interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator -{ - /** - * Returns whether the current element in the property path is an array - * index. - * - * @return Boolean - */ - public function isIndex(); +use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface as BasePropertyPathIteratorInterface; - /** - * Returns whether the current element in the property path is a property - * name. - * - * @return Boolean - */ - public function isProperty(); +/** + * Alias for {@link \Symfony\Component\PropertyAccess\PropertyPathIteratorInterface}. + * + * @author Bernhard Schussek + * + * @deprecated deprecated since version 2.2, to be removed in 2.3. Use + * {@link \Symfony\Component\PropertyAccess\PropertyPathIterator} + * instead. + */ +interface PropertyPathIteratorInterface extends BasePropertyPathIteratorInterface +{ } diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 1e69cb140d..51afa67c3a 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -19,7 +19,8 @@ "php": ">=5.3.3", "symfony/event-dispatcher": "2.2.*", "symfony/locale": "2.2.*", - "symfony/options-resolver": "2.2.*" + "symfony/options-resolver": "2.2.*", + "symfony/property-access": "2.2.*" }, "require-dev": { "symfony/validator": "2.2.*", diff --git a/src/Symfony/Component/PropertyAccess/.gitattributes b/src/Symfony/Component/PropertyAccess/.gitattributes new file mode 100644 index 0000000000..80481513cf --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/.gitattributes @@ -0,0 +1,2 @@ +/Tests export-ignore +phpunit.xml.dist export-ignore diff --git a/src/Symfony/Component/PropertyAccess/.gitignore b/src/Symfony/Component/PropertyAccess/.gitignore new file mode 100644 index 0000000000..44de97a36a --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/.gitignore @@ -0,0 +1,4 @@ +vendor/ +composer.lock +phpunit.xml + diff --git a/src/Symfony/Component/Form/Exception/InvalidPropertyException.php b/src/Symfony/Component/PropertyAccess/Exception/ExceptionInterface.php similarity index 54% rename from src/Symfony/Component/Form/Exception/InvalidPropertyException.php rename to src/Symfony/Component/PropertyAccess/Exception/ExceptionInterface.php index 48185d55fd..d1fcdac942 100644 --- a/src/Symfony/Component/Form/Exception/InvalidPropertyException.php +++ b/src/Symfony/Component/PropertyAccess/Exception/ExceptionInterface.php @@ -9,8 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Exception; +namespace Symfony\Component\PropertyAccess\Exception; -class InvalidPropertyException extends Exception +/** + * Marker interface for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +interface ExceptionInterface { } diff --git a/src/Symfony/Component/Form/Exception/PropertyAccessDeniedException.php b/src/Symfony/Component/PropertyAccess/Exception/InvalidPropertyPathException.php similarity index 52% rename from src/Symfony/Component/Form/Exception/PropertyAccessDeniedException.php rename to src/Symfony/Component/PropertyAccess/Exception/InvalidPropertyPathException.php index 4905da9ea2..69de31cee4 100644 --- a/src/Symfony/Component/Form/Exception/PropertyAccessDeniedException.php +++ b/src/Symfony/Component/PropertyAccess/Exception/InvalidPropertyPathException.php @@ -9,8 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Exception; +namespace Symfony\Component\PropertyAccess\Exception; -class PropertyAccessDeniedException extends Exception +/** + * Thrown when a property path is malformed. + * + * @author Bernhard Schussek + */ +class InvalidPropertyPathException extends RuntimeException { } diff --git a/src/Symfony/Component/Form/Exception/InvalidPropertyPathException.php b/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php similarity index 53% rename from src/Symfony/Component/Form/Exception/InvalidPropertyPathException.php rename to src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php index c8bd3ad71b..ebaa5a3079 100644 --- a/src/Symfony/Component/Form/Exception/InvalidPropertyPathException.php +++ b/src/Symfony/Component/PropertyAccess/Exception/NoSuchPropertyException.php @@ -9,8 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Exception; +namespace Symfony\Component\PropertyAccess\Exception; -class InvalidPropertyPathException extends Exception +/** + * Thrown when a property cannot be found. + * + * @author Bernhard Schussek + */ +class NoSuchPropertyException extends RuntimeException { } diff --git a/src/Symfony/Component/PropertyAccess/Exception/OutOfBoundsException.php b/src/Symfony/Component/PropertyAccess/Exception/OutOfBoundsException.php new file mode 100644 index 0000000000..a3c45597da --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/OutOfBoundsException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base OutOfBoundsException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class OutOfBoundsException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/PropertyAccessDeniedException.php b/src/Symfony/Component/PropertyAccess/Exception/PropertyAccessDeniedException.php new file mode 100644 index 0000000000..b36e5f088d --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/PropertyAccessDeniedException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a property cannot be accessed because it is not public. + * + * @author Bernhard Schussek + */ +class PropertyAccessDeniedException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/RuntimeException.php b/src/Symfony/Component/PropertyAccess/Exception/RuntimeException.php new file mode 100644 index 0000000000..9fe843e309 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Base RuntimeException for the PropertyAccess component. + * + * @author Bernhard Schussek + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.php b/src/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000000..029d48c22a --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Exception/UnexpectedTypeException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Exception; + +/** + * Thrown when a value does not match an expected type. + * + * @author Bernhard Schussek + */ +class UnexpectedTypeException extends RuntimeException +{ + public function __construct($value, $expectedType) + { + parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value))); + } +} diff --git a/src/Symfony/Component/PropertyAccess/LICENSE b/src/Symfony/Component/PropertyAccess/LICENSE new file mode 100644 index 0000000000..88a57f8d8d --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccess.php b/src/Symfony/Component/PropertyAccess/PropertyAccess.php new file mode 100644 index 0000000000..6fb747578f --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyAccess.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Entry point of the PropertyAccess component. + * + * @author Bernhard Schussek + */ +final class PropertyAccess +{ + /** + * Creates a property accessor with the default configuration. + * + * @return PropertyAccessor The new property accessor. + */ + public static function getPropertyAccessor() + { + return new PropertyAccessor(); + } + + /** + * This class cannot be instantiated. + */ + private function __construct() + { + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php new file mode 100644 index 0000000000..229627aa53 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -0,0 +1,399 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; + +/** + * Default implementation of {@link PropertyAccessorInterface}. + * + * @author Bernhard Schussek + */ +class PropertyAccessor implements PropertyAccessorInterface +{ + const VALUE = 0; + const IS_REF = 1; + + /** + * Should not be used by application code. Use + * {@link PropertyAccess::getPropertyAccessor()} instead. + */ + public function __construct() + { + } + + /** + * {@inheritdoc} + */ + public function getValue($objectOrArray, $propertyPath) + { + if (is_string($propertyPath)) { + $propertyPath = new PropertyPath($propertyPath); + } elseif (!$propertyPath instanceof PropertyPathInterface) { + throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + } + + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); + + return $propertyValues[count($propertyValues) - 1][self::VALUE]; + } + + /** + * {@inheritdoc} + */ + public function setValue(&$objectOrArray, $propertyPath, $value) + { + if (is_string($propertyPath)) { + $propertyPath = new PropertyPath($propertyPath); + } elseif (!$propertyPath instanceof PropertyPathInterface) { + throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); + } + + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 2); + $overwrite = true; + + // Add the root object to the list + array_unshift($propertyValues, array( + self::VALUE => &$objectOrArray, + self::IS_REF => true, + )); + + for ($i = count($propertyValues) - 1; $i >= 0; --$i) { + $objectOrArray =& $propertyValues[$i][self::VALUE]; + + if ($overwrite) { + if (!is_object($objectOrArray) && !is_array($objectOrArray)) { + throw new UnexpectedTypeException($objectOrArray, 'object or array'); + } + + $property = $propertyPath->getElement($i); + //$singular = $propertyPath->singulars[$i]; + $singular = null; + $isIndex = $propertyPath->isIndex($i); + + $this->writeProperty($objectOrArray, $propertyPath, $property, $singular, $isIndex, $value); + } + + $value =& $objectOrArray; + $overwrite = !$propertyValues[$i][self::IS_REF]; + } + } + + /** + * Reads the path from an object up to a given path index. + * + * @param object|array $objectOrArray The object or array to read from. + * @param PropertyPathInterface $propertyPath The property path to read. + * @param integer $lastIndex The integer up to which should be read. + * + * @return array The values read in the path. + * + * @throws UnexpectedTypeException If a value within the path is neither object nor array. + */ + private function &readPropertiesUntil(&$objectOrArray, PropertyPathInterface $propertyPath, $lastIndex) + { + $propertyValues = array(); + + for ($i = 0; $i <= $lastIndex; ++$i) { + if (!is_object($objectOrArray) && !is_array($objectOrArray)) { + throw new UnexpectedTypeException($objectOrArray, 'object or array'); + } + + $property = $propertyPath->getElement($i); + $isIndex = $propertyPath->isIndex($i); + $isArrayAccess = is_array($objectOrArray) || $objectOrArray instanceof \ArrayAccess; + + // Create missing nested arrays on demand + if ($isIndex && $isArrayAccess && !isset($objectOrArray[$property])) { + $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; + } + + $propertyValue =& $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex); + $objectOrArray =& $propertyValue[self::VALUE]; + + $propertyValues[] =& $propertyValue; + } + + return $propertyValues; + } + + /** + * Reads the a property from an object or array. + * + * @param object|array $objectOrArray The object or array to read from. + * @param PropertyPathInterface $propertyPath The property path to read. + * @param string $property The property to read. + * @param Boolean $isIndex Whether to interpret the property as index. + * + * @return mixed The value of the read property + * + * @throws NoSuchPropertyException If the property does not exist. + * @throws PropertyAccessDeniedException If the property cannot be accessed due to + * access restrictions (private or protected). + */ + private function &readProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $isIndex) + { + // Use an array instead of an object since performance is + // very crucial here + $result = array( + self::VALUE => null, + self::IS_REF => false + ); + + if ($isIndex) { + if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { + throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); + } + + if (isset($objectOrArray[$property])) { + if (is_array($objectOrArray)) { + $result[self::VALUE] =& $objectOrArray[$property]; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $objectOrArray[$property]; + } + } + } elseif (is_object($objectOrArray)) { + $camelProp = $this->camelize($property); + $reflClass = new \ReflectionClass($objectOrArray); + $getter = 'get'.$camelProp; + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; + + if ($reflClass->hasMethod($getter)) { + if (!$reflClass->getMethod($getter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name)); + } + + $result[self::VALUE] = $objectOrArray->$getter(); + } elseif ($reflClass->hasMethod($isser)) { + if (!$reflClass->getMethod($isser)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name)); + } + + $result[self::VALUE] = $objectOrArray->$isser(); + } elseif ($reflClass->hasMethod($hasser)) { + if (!$reflClass->getMethod($hasser)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name)); + } + + $result[self::VALUE] = $objectOrArray->$hasser(); + } elseif ($reflClass->hasMethod('__get')) { + // needed to support magic method __get + $result[self::VALUE] = $objectOrArray->$property; + } elseif ($reflClass->hasProperty($property)) { + if (!$reflClass->getProperty($property)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser)); + } + + $result[self::VALUE] =& $objectOrArray->$property; + $result[self::IS_REF] = true; + } elseif (property_exists($objectOrArray, $property)) { + // needed to support \stdClass instances + $result[self::VALUE] =& $objectOrArray->$property; + $result[self::IS_REF] = true; + } else { + throw new NoSuchPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name)); + } + } else { + throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + } + + // Objects are always passed around by reference + if (is_object($result[self::VALUE])) { + $result[self::IS_REF] = true; + } + + return $result; + } + + /** + * Sets the value of the property at the given index in the path + * + * @param object|array $objectOrArray The object or array to write to. + * @param PropertyPathInterface $propertyPath The property path to write. + * @param string $property The property to write. + * @param string|null $singular The singular form of the property name or null. + * @param Boolean $isIndex Whether to interpret the property as index. + * @param mixed $value The value to write. + * + * @throws NoSuchPropertyException If the property does not exist. + * @throws PropertyAccessDeniedException If the property cannot be accessed due to + * access restrictions (private or protected). + */ + private function writeProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $singular, $isIndex, $value) + { + $adderRemoverError = null; + + if ($isIndex) { + if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { + throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); + } + + $objectOrArray[$property] = $value; + } elseif (is_object($objectOrArray)) { + $reflClass = new \ReflectionClass($objectOrArray); + + // The plural form is the last element of the property path + $plural = $this->camelize($propertyPath->getElement($propertyPath->getLength() - 1)); + + // Any of the two methods is required, but not yet known + $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); + + if (is_array($value) || $value instanceof \Traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + if (null !== $methods) { + // At this point the add and remove methods have been found + // Use iterator_to_array() instead of clone in order to prevent side effects + // see https://github.com/symfony/symfony/issues/4670 + $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; + $itemToRemove = array(); + $propertyValue = $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex); + $previousValue = $propertyValue[self::VALUE]; + + if (is_array($previousValue) || $previousValue instanceof \Traversable) { + foreach ($previousValue as $previousItem) { + foreach ($value as $key => $item) { + if ($item === $previousItem) { + // Item found, don't add + unset($itemsToAdd[$key]); + + // Next $previousItem + continue 2; + } + } + + // Item not found, add to remove list + $itemToRemove[] = $previousItem; + } + } + + foreach ($itemToRemove as $item) { + call_user_func(array($objectOrArray, $methods[1]), $item); + } + + foreach ($itemsToAdd as $item) { + call_user_func(array($objectOrArray, $methods[0]), $item); + } + + return; + } else { + $adderRemoverError = ', nor could adders and removers be found based on the '; + if (null === $singular) { + // $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)'; + $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars); + } else { + $adderRemoverError .= 'passed singular: '.$singular; + } + } + } + + $setter = 'set'.$this->camelize($property); + if ($reflClass->hasMethod($setter)) { + if (!$reflClass->getMethod($setter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name)); + } + + $objectOrArray->$setter($value); + } elseif ($reflClass->hasMethod('__set')) { + // needed to support magic method __set + $objectOrArray->$property = $value; + } elseif ($reflClass->hasProperty($property)) { + if (!$reflClass->getProperty($property)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter)); + } + + $objectOrArray->$property = $value; + } elseif (property_exists($objectOrArray, $property)) { + // needed to support \stdClass instances + $objectOrArray->$property = $value; + } else { + throw new NoSuchPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError)); + } + } else { + throw new NoSuchPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + } + } + + /** + * Camelizes a given string. + * + * @param string $string Some string. + * + * @return string The camelized version of the string. + */ + private function camelize($string) + { + return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string); + } + + /** + * Searches for add and remove methods. + * + * @param \ReflectionClass $reflClass The reflection class for the given object + * @param array $singulars The singular form of the property name or null. + * + * @return array|null An array containing the adder and remover when found, null otherwise. + * + * @throws NoSuchPropertyException If the property does not exist. + */ + private function findAdderAndRemover(\ReflectionClass $reflClass, array $singulars) + { + foreach ($singulars as $singular) { + $addMethod = 'add' . $singular; + $removeMethod = 'remove' . $singular; + + $addMethodFound = $this->isAccessible($reflClass, $addMethod, 1); + $removeMethodFound = $this->isAccessible($reflClass, $removeMethod, 1); + + if ($addMethodFound && $removeMethodFound) { + return array($addMethod, $removeMethod); + } + + if ($addMethodFound xor $removeMethodFound) { + throw new NoSuchPropertyException(sprintf( + 'Found the public method "%s", but did not find a public "%s" on class %s', + $addMethodFound ? $addMethod : $removeMethod, + $addMethodFound ? $removeMethod : $addMethod, + $reflClass->name + )); + } + } + + return null; + } + + /** + * Returns whether a method is public and has a specific number of required parameters. + * + * @param \ReflectionClass $class The class of the method. + * @param string $methodName The method name. + * @param integer $parameters The number of parameters. + * + * @return Boolean Whether the method is public and has $parameters + * required parameters. + */ + private function isAccessible(\ReflectionClass $class, $methodName, $parameters) + { + if ($class->hasMethod($methodName)) { + $method = $class->getMethod($methodName); + + if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) { + return true; + } + } + + return false; + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php new file mode 100644 index 0000000000..058806c6ae --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessorInterface.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Writes and reads values to/from an object/array graph. + * + * @author Bernhard Schussek + */ +interface PropertyAccessorInterface +{ + /** + * Sets the value at the end of the property path of the object + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::getPropertyAccessor(); + * + * echo $propertyAccessor->setValue($object, 'child.name', 'Fabien'); + * // equals echo $object->getChild()->setName('Fabien'); + * + * This method first tries to find a public setter for each property in the + * path. The name of the setter must be the camel-cased property name + * prefixed with "set". + * + * If the setter does not exist, this method tries to find a public + * property. The value of the property is then changed. + * + * If neither is found, an exception is thrown. + * + * @param object|array $objectOrArray The object or array to modify. + * @param string|PropertyPathInterface $propertyPath The property path to modify. + * @param mixed $value The value to set at the end of the property path. + * + * @throws Exception\NoSuchPropertyException If a property does not exist. + * @throws Exception\PropertyAccessDeniedException If a property cannot be accessed due to + * access restrictions (private or protected). + * @throws Exception\UnexpectedTypeException If a value within the path is neither object + * nor array. + */ + public function setValue(&$objectOrArray, $propertyPath, $value); + + /** + * Returns the value at the end of the property path of the object + * + * Example: + * + * use Symfony\Component\PropertyAccess\PropertyAccess; + * + * $propertyAccessor = PropertyAccess::getPropertyAccessor(); + * + * echo $propertyAccessor->getValue($object, 'child.name); + * // equals echo $object->getChild()->getName(); + * + * This method first tries to find a public getter for each property in the + * path. The name of the getter must be the camel-cased property name + * prefixed with "get", "is", or "has". + * + * If the getter does not exist, this method tries to find a public + * property. The value of the property is then returned. + * + * If none of them are found, an exception is thrown. + * + * @param object|array $objectOrArray The object or array to traverse + * @param string|PropertyPathInterface $propertyPath The property path to modify. + * + * @return mixed The value at the end of the property path + * + * @throws Exception\NoSuchPropertyException If the property/getter does not exist + * @throws Exception\PropertyAccessDeniedException If the property/getter exists but is not public + */ + public function getValue($objectOrArray, $propertyPath); +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyPath.php b/src/Symfony/Component/PropertyAccess/PropertyPath.php new file mode 100644 index 0000000000..5fc4ac635b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyPath.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; +use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; + +/** + * Default implementation of {@link PropertyPathInterface}. + * + * @author Bernhard Schussek + */ +class PropertyPath implements \IteratorAggregate, PropertyPathInterface +{ + /** + * Character used for separating between plural and singular of an element. + * @var string + */ + const SINGULAR_SEPARATOR = '|'; + + /** + * The elements of the property path + * @var array + */ + private $elements = array(); + + /** + * The singular forms of the elements in the property path. + * @var array + */ + private $singulars = array(); + + /** + * The number of elements in the property path + * @var integer + */ + private $length; + + /** + * Contains a Boolean for each property in $elements denoting whether this + * element is an index. It is a property otherwise. + * @var array + */ + private $isIndex = array(); + + /** + * String representation of the path + * @var string + */ + private $pathAsString; + + /** + * Constructs a property path from a string. + * + * @param PropertyPath|string $propertyPath The property path as string or instance. + * + * @throws UnexpectedTypeException If the given path is not a string. + * @throws InvalidPropertyPathException If the syntax of the property path is not valid. + */ + public function __construct($propertyPath) + { + // Can be used as copy constructor + if ($propertyPath instanceof PropertyPath) { + /* @var PropertyPath $propertyPath */ + $this->elements = $propertyPath->elements; + $this->singulars = $propertyPath->singulars; + $this->length = $propertyPath->length; + $this->isIndex = $propertyPath->isIndex; + $this->pathAsString = $propertyPath->pathAsString; + + return; + } + if (!is_string($propertyPath)) { + throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPath'); + } + + if ('' === $propertyPath) { + throw new InvalidPropertyPathException('The property path should not be empty.'); + } + + $this->pathAsString = $propertyPath; + $position = 0; + $remaining = $propertyPath; + + // first element is evaluated differently - no leading dot for properties + $pattern = '/^(([^\.\[]+)|\[([^\]]+)\])(.*)/'; + + while (preg_match($pattern, $remaining, $matches)) { + if ('' !== $matches[2]) { + $element = $matches[2]; + $this->isIndex[] = false; + } else { + $element = $matches[3]; + $this->isIndex[] = true; + } + // Disabled this behaviour as the syntax is not yet final + //$pos = strpos($element, self::SINGULAR_SEPARATOR); + $pos = false; + $singular = null; + + if (false !== $pos) { + $singular = substr($element, $pos + 1); + $element = substr($element, 0, $pos); + } + + $this->elements[] = $element; + $this->singulars[] = $singular; + + $position += strlen($matches[1]); + $remaining = $matches[4]; + $pattern = '/^(\.(\w+)|\[([^\]]+)\])(.*)/'; + } + + if ('' !== $remaining) { + throw new InvalidPropertyPathException(sprintf( + 'Could not parse property path "%s". Unexpected token "%s" at position %d', + $propertyPath, + $remaining{0}, + $position + )); + } + + $this->length = count($this->elements); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->pathAsString; + } + + /** + * {@inheritdoc} + */ + public function getLength() + { + return $this->length; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + if ($this->length <= 1) { + return null; + } + + $parent = clone $this; + + --$parent->length; + $parent->pathAsString = substr($parent->pathAsString, 0, max(strrpos($parent->pathAsString, '.'), strrpos($parent->pathAsString, '['))); + array_pop($parent->elements); + array_pop($parent->singulars); + array_pop($parent->isIndex); + + return $parent; + } + + /** + * Returns a new iterator for this path + * + * @return PropertyPathIteratorInterface + */ + public function getIterator() + { + return new PropertyPathIterator($this); + } + + /** + * {@inheritdoc} + */ + public function getElements() + { + return $this->elements; + } + + /** + * {@inheritdoc} + */ + public function getElement($index) + { + if (!isset($this->elements[$index])) { + throw new OutOfBoundsException('The index ' . $index . ' is not within the property path'); + } + + return $this->elements[$index]; + } + + /** + * {@inheritdoc} + */ + public function isProperty($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException('The index ' . $index . ' is not within the property path'); + } + + return !$this->isIndex[$index]; + } + + /** + * {@inheritdoc} + */ + public function isIndex($index) + { + if (!isset($this->isIndex[$index])) { + throw new OutOfBoundsException('The index ' . $index . ' is not within the property path'); + } + + return $this->isIndex[$index]; + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyPathBuilder.php b/src/Symfony/Component/PropertyAccess/PropertyPathBuilder.php new file mode 100644 index 0000000000..f169f2835b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyPathBuilder.php @@ -0,0 +1,296 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +use Symfony\Component\PropertyAccess\Exception\OutOfBoundsException; + +/** + * @author Bernhard Schussek + */ +class PropertyPathBuilder +{ + /** + * @var array + */ + private $elements = array(); + + /** + * @var array + */ + private $isIndex = array(); + + /** + * Creates a new property path builder. + * + * @param null|PropertyPathInterface $path The path to initially store + * in the builder. Optional. + */ + public function __construct(PropertyPathInterface $path = null) + { + if (null !== $path) { + $this->append($path); + } + } + + /** + * Appends a (sub-) path to the current path. + * + * @param PropertyPathInterface $path The path to append. + * @param integer $offset The offset where the appended piece + * starts in $path. + * @param integer $length The length of the appended piece. + * If 0, the full path is appended. + */ + public function append(PropertyPathInterface $path, $offset = 0, $length = 0) + { + if (0 === $length) { + $end = $path->getLength(); + } else { + $end = $offset + $length; + } + + for (; $offset < $end; ++$offset) { + $this->elements[] = $path->getElement($offset); + $this->isIndex[] = $path->isIndex($offset); + } + } + + /** + * Appends an index element to the current path. + * + * @param string $name The name of the appended index. + */ + public function appendIndex($name) + { + $this->elements[] = $name; + $this->isIndex[] = true; + } + + /** + * Appends a property element to the current path. + * + * @param string $name The name of the appended property. + */ + public function appendProperty($name) + { + $this->elements[] = $name; + $this->isIndex[] = false; + } + + /** + * Removes elements from the current path. + * + * @param integer $offset The offset at which to remove. + * @param integer $length The length of the removed piece. + * + * @throws OutOfBoundsException if offset is invalid + */ + public function remove($offset, $length = 1) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); + } + + $this->resize($offset, $length, 0); + } + + /** + * Replaces a sub-path by a different (sub-) path. + * + * @param integer $offset The offset at which to replace. + * @param integer $length The length of the piece to replace. + * @param PropertyPathInterface $path The path to insert. + * @param integer $pathOffset The offset where the inserted piece + * starts in $path. + * @param integer $pathLength The length of the inserted piece. + * If 0, the full path is inserted. + * + * @throws OutOfBoundsException If the offset is invalid. + */ + public function replace($offset, $length, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); + } + + if (0 === $pathLength) { + $pathLength = $path->getLength() - $pathOffset; + } + + $this->resize($offset, $length, $pathLength); + + for ($i = 0; $i < $pathLength; ++$i) { + $this->elements[$offset + $i] = $path->getElement($pathOffset + $i); + $this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i); + } + } + + /** + * Replaces a property element by an index element. + * + * @param integer $offset The offset at which to replace. + * @param string $name The new name of the element. Optional. + * + * @throws OutOfBoundsException If the offset is invalid. + */ + public function replaceByIndex($offset, $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = true; + } + + /** + * Replaces an index element by a property element. + * + * @param integer $offset The offset at which to replace. + * @param string $name The new name of the element. Optional. + * + * @throws OutOfBoundsException If the offset is invalid. + */ + public function replaceByProperty($offset, $name = null) + { + if (!isset($this->elements[$offset])) { + throw new OutOfBoundsException('The offset ' . $offset . ' is not within the property path'); + } + + if (null !== $name) { + $this->elements[$offset] = $name; + } + + $this->isIndex[$offset] = false; + } + + /** + * Returns the length of the current path. + * + * @return integer The path length. + */ + public function getLength() + { + return count($this->elements); + } + + /** + * Returns the current property path. + * + * @return PropertyPathInterface The constructed property path. + */ + public function getPropertyPath() + { + $pathAsString = $this->__toString(); + + return '' !== $pathAsString ? new PropertyPath($pathAsString) : null; + } + + /** + * Returns the current property path as string. + * + * @return string The property path as string. + */ + public function __toString() + { + $string = ''; + + foreach ($this->elements as $offset => $element) { + if ($this->isIndex[$offset]) { + $element = '[' . $element . ']'; + } elseif ('' !== $string) { + $string .= '.'; + } + + $string .= $element; + } + + return $string; + } + + /** + * Resizes the path so that a chunk of length $cutLength is + * removed at $offset and another chunk of length $insertionLength + * can be inserted. + * + * @param integer $offset The offset where the removed chunk starts. + * @param integer $cutLength The length of the removed chunk. + * @param integer $insertionLength The length of the inserted chunk. + */ + private function resize($offset, $cutLength, $insertionLength) + { + // Nothing else to do in this case + if ($insertionLength === $cutLength) { + return; + } + + $length = count($this->elements); + + if ($cutLength > $insertionLength) { + // More elements should be removed than inserted + $diff = $cutLength - $insertionLength; + $newLength = $length - $diff; + + // Shift elements to the left (left-to-right until the new end) + // Max allowed offset to be shifted is such that + // $offset + $diff < $length (otherwise invalid index access) + // i.e. $offset < $length - $diff = $newLength + for ($i = $offset; $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i + $diff]; + $this->isIndex[$i] = $this->isIndex[$i + $diff]; + } + + // All remaining elements should be removed + for (; $i < $length; ++$i) { + unset($this->elements[$i]); + unset($this->isIndex[$i]); + } + } else { + $diff = $insertionLength - $cutLength; + + $newLength = $length + $diff; + $indexAfterInsertion = $offset + $insertionLength; + + // $diff <= $insertionLength + // $indexAfterInsertion >= $insertionLength + // => $diff <= $indexAfterInsertion + + // In each of the following loops, $i >= $diff must hold, + // otherwise ($i - $diff) becomes negative. + + // Shift old elements to the right to make up space for the + // inserted elements. This needs to be done left-to-right in + // order to preserve an ascending array index order + // Since $i = max($length, $indexAfterInsertion) and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = max($length, $indexAfterInsertion); $i < $newLength; ++$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + + // Shift remaining elements to the right. Do this right-to-left + // so we don't overwrite elements before copying them + // The last written index is the immediate index after the inserted + // string, because the indices before that will be overwritten + // anyway. + // Since $i >= $indexAfterInsertion and $indexAfterInsertion >= $diff, + // $i >= $diff is guaranteed. + for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) { + $this->elements[$i] = $this->elements[$i - $diff]; + $this->isIndex[$i] = $this->isIndex[$i - $diff]; + } + } + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php b/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php new file mode 100644 index 0000000000..fb95f1464d --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyPathInterface.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * A sequence of property names or array indices. + * + * @author Bernhard Schussek + */ +interface PropertyPathInterface extends \Traversable +{ + /** + * Returns the string representation of the property path + * + * @return string The path as string. + */ + public function __toString(); + + /** + * Returns the length of the property path, i.e. the number of elements. + * + * @return integer The path length. + */ + public function getLength(); + + /** + * Returns the parent property path. + * + * The parent property path is the one that contains the same items as + * this one except for the last one. + * + * If this property path only contains one item, null is returned. + * + * @return PropertyPath The parent path or null. + */ + public function getParent(); + + /** + * Returns the elements of the property path as array + * + * @return array An array of property/index names + */ + public function getElements(); + + /** + * Returns the element at the given index in the property path + * + * @param integer $index The index key + * + * @return string A property or index name + * + * @throws Exception\OutOfBoundsException If the offset is invalid. + */ + public function getElement($index); + + /** + * Returns whether the element at the given index is a property + * + * @param integer $index The index in the property path + * + * @return Boolean Whether the element at this index is a property + * + * @throws Exception\OutOfBoundsException If the offset is invalid. + */ + public function isProperty($index); + + /** + * Returns whether the element at the given index is an array index + * + * @param integer $index The index in the property path + * + * @return Boolean Whether the element at this index is an array index + * + * @throws Exception\OutOfBoundsException If the offset is invalid. + */ + public function isIndex($index); +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyPathIterator.php b/src/Symfony/Component/PropertyAccess/PropertyPathIterator.php new file mode 100644 index 0000000000..d6cd49caa0 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyPathIterator.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Traverses a property path and provides additional methods to find out + * information about the current element + * + * @author Bernhard Schussek + */ +class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface +{ + /** + * The traversed property path + * @var PropertyPathInterface + */ + protected $path; + + /** + * Constructor. + * + * @param PropertyPathInterface $path The property path to traverse + */ + public function __construct(PropertyPathInterface $path) + { + parent::__construct($path->getElements()); + + $this->path = $path; + } + + /** + * {@inheritdoc} + */ + public function isIndex() + { + return $this->path->isIndex($this->key()); + } + + /** + * {@inheritdoc} + */ + public function isProperty() + { + return $this->path->isProperty($this->key()); + } +} diff --git a/src/Symfony/Component/PropertyAccess/PropertyPathIteratorInterface.php b/src/Symfony/Component/PropertyAccess/PropertyPathIteratorInterface.php new file mode 100644 index 0000000000..cb43f8d7ea --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/PropertyPathIteratorInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * @author Bernhard Schussek + */ +interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator +{ + /** + * Returns whether the current element in the property path is an array + * index. + * + * @return Boolean + */ + public function isIndex(); + + /** + * Returns whether the current element in the property path is a property + * name. + * + * @return Boolean + */ + public function isProperty(); +} diff --git a/src/Symfony/Component/PropertyAccess/README.md b/src/Symfony/Component/PropertyAccess/README.md new file mode 100644 index 0000000000..0ae94e0879 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/README.md @@ -0,0 +1,14 @@ +PropertyAccess Component +======================== + +PropertyAccess reads/writes values from/to object/array graphs using a simple +string notation. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/PropertyAccess/ + $ composer.phar install --dev + $ phpunit diff --git a/src/Symfony/Component/PropertyAccess/StringUtil.php b/src/Symfony/Component/PropertyAccess/StringUtil.php new file mode 100644 index 0000000000..012ed8846b --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/StringUtil.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess; + +/** + * Creates singulars from plurals. + * + * @author Bernhard Schussek + */ +class StringUtil +{ + /** + * Map english plural to singular suffixes + * + * @var array + * + * @see http://english-zone.com/spelling/plurals.html + * @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English + */ + private static $pluralMap = array( + // First entry: plural suffix, reversed + // Second entry: length of plural suffix + // Third entry: Whether the suffix may succeed a vocal + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: singular suffix, normal + + // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) + array('a', 1, true, true, array('on', 'um')), + + // nebulae (nebula) + array('ea', 2, true, true, 'a'), + + // mice (mouse), lice (louse) + array('eci', 3, false, true, 'ouse'), + + // geese (goose) + array('esee', 4, false, true, 'oose'), + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + array('i', 1, true, true, 'us'), + + // men (man), women (woman) + array('nem', 3, true, true, 'man'), + + // children (child) + array('nerdlihc', 8, true, true, 'child'), + + // oxen (ox) + array('nexo', 4, false, false, 'ox'), + + // indices (index), appendices (appendix), prices (price) + array('seci', 4, false, true, array('ex', 'ix', 'ice')), + + // babies (baby) + array('sei', 3, false, true, 'y'), + + // analyses (analysis), ellipses (ellipsis), funguses (fungus), + // neuroses (neurosis), theses (thesis), emphases (emphasis), + // oases (oasis), crises (crisis), houses (house), bases (base), + // atlases (atlas), kisses (kiss) + array('ses', 3, true, true, array('s', 'se', 'sis')), + + // lives (life), wives (wife) + array('sevi', 4, false, true, 'ife'), + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) + array('sev', 3, true, true, 'f'), + + // axes (axis), axes (ax), axes (axe) + array('sexa', 4, false, false, array('ax', 'axe', 'axis')), + + // indexes (index), matrixes (matrix) + array('sex', 3, true, false, 'x'), + + // quizzes (quiz) + array('sezz', 4, true, false, 'z'), + + // bureaus (bureau) + array('suae', 4, false, true, 'eau'), + + // roses (rose), garages (garage), cassettes (cassette), + // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), + // shoes (shoe) + array('se', 2, true, true, array('', 'e')), + + // tags (tag) + array('s', 1, true, true, ''), + + // chateaux (chateau) + array('xuae', 4, false, true, 'eau'), + ); + + /** + * This class should not be instantiated + */ + private function __construct() {} + + /** + * Returns the singular form of a word + * + * If the method can't determine the form with certainty, an array of the + * possible singulars is returned. + * + * @param string $plural A word in plural form + * @return string|array The singular form or an array of possible singular + * forms + */ + public static function singularify($plural) + { + $pluralRev = strrev($plural); + $lowerPluralRev = strtolower($pluralRev); + $pluralLength = strlen($lowerPluralRev); + + // The outer loop iterates over the entries of the plural table + // The inner loop $j iterates over the characters of the plural suffix + // in the plural table to compare them with the characters of the actual + // given plural suffix + foreach (self::$pluralMap as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the plural table and of the suffix of the + // given plural one by one + while ($suffix[$j] === $lowerPluralRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the singular suffix to the singular array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $pluralLength) { + $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]); + + if (!$map[2] && $nextIsVocal) { + // suffix may not succeed a vocal but next char is one + break; + } + + if (!$map[3] && !$nextIsVocal) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($plural, 0, $pluralLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the plural suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($pluralRev[$j - 1]); + + if (is_array($newSuffix)) { + $singulars = array(); + + foreach ($newSuffix as $newSuffixEntry) { + $singulars[] = $newBase . ($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $singulars; + } + + return $newBase . ($firstUpper ? ucFirst($newSuffix) : $newSuffix); + } + + // Suffix is longer than word + if ($j === $pluralLength) { + break; + } + } + } + + // Convert teeth to tooth, feet to foot + if (false !== ($pos = strpos($plural, 'ee'))) { + return substr_replace($plural, 'oo', $pos, 2); + } + + // Assume that plural and singular is identical + return $plural; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php new file mode 100644 index 0000000000..ed2331bab0 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Author.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +class Author +{ + public $firstName; + private $lastName; + private $australian; + public $child; + private $readPermissions; + + private $privateProperty; + + public function setLastName($lastName) + { + $this->lastName = $lastName; + } + + public function getLastName() + { + return $this->lastName; + } + + private function getPrivateGetter() + { + return 'foobar'; + } + + public function setAustralian($australian) + { + $this->australian = $australian; + } + + public function isAustralian() + { + return $this->australian; + } + + public function setReadPermissions($bool) + { + $this->readPermissions = $bool; + } + + public function hasReadPermissions() + { + return $this->readPermissions; + } + + private function isPrivateIsser() + { + return true; + } + + public function getPrivateSetter() + { + } + + private function setPrivateSetter($data) + { + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/Magician.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php similarity index 89% rename from src/Symfony/Component/Form/Tests/Fixtures/Magician.php rename to src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php index cd66f29d07..6faa5dbf7b 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/Magician.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/Magician.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Fixtures; +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; class Magician { diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathArrayObjectTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php similarity index 73% rename from src/Symfony/Component/Form/Tests/Util/PropertyPathArrayObjectTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php index 96ddc6d6f0..aaa86b3c25 100644 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathArrayObjectTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayObjectTest.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; -class PropertyPathArrayObjectTest extends PropertyPathCollectionTest +class PropertyAccessorArrayObjectTest extends PropertyAccessorCollectionTest { protected function getCollection(array $array) { diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathArrayTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php similarity index 73% rename from src/Symfony/Component/Form/Tests/Util/PropertyPathArrayTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php index ce726c80f8..5ab63c67cb 100644 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathArrayTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorArrayTest.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; -class PropertyPathArrayTest extends PropertyPathCollectionTest +class PropertyAccessorArrayTest extends PropertyAccessorCollectionTest { protected function getCollection(array $array) { diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathCollectionTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php similarity index 73% rename from src/Symfony/Component/Form/Tests/Util/PropertyPathCollectionTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php index 2f7589663e..a91f1ddf5a 100644 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathCollectionTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCollectionTest.php @@ -9,12 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\PropertyAccess\Exception\ExceptionInterface; +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\StringUtil; -class PropertyPathCollectionTest_Car +class PropertyAccessorCollectionTest_Car { private $axes; @@ -23,7 +24,7 @@ class PropertyPathCollectionTest_Car $this->axes = $axes; } - // In the test, use a name that FormUtil can't uniquely singularify + // In the test, use a name that StringUtil can't uniquely singularify public function addAxis($axis) { $this->axes[] = $axis; @@ -46,7 +47,7 @@ class PropertyPathCollectionTest_Car } } -class PropertyPathCollectionTest_CarCustomSingular +class PropertyAccessorCollectionTest_CarCustomSingular { public function addFoo($axis) {} @@ -55,44 +56,44 @@ class PropertyPathCollectionTest_CarCustomSingular public function getAxes() {} } -class PropertyPathCollectionTest_Engine +class PropertyAccessorCollectionTest_Engine { } -class PropertyPathCollectionTest_CarOnlyAdder +class PropertyAccessorCollectionTest_CarOnlyAdder { public function addAxis($axis) {} public function getAxes() {} } -class PropertyPathCollectionTest_CarOnlyRemover +class PropertyAccessorCollectionTest_CarOnlyRemover { public function removeAxis($axis) {} public function getAxes() {} } -class PropertyPathCollectionTest_CarNoAdderAndRemover +class PropertyAccessorCollectionTest_CarNoAdderAndRemover { public function getAxes() {} } -class PropertyPathCollectionTest_CarNoAdderAndRemoverWithProperty +class PropertyAccessorCollectionTest_CarNoAdderAndRemoverWithProperty { protected $axes = array(); public function getAxes() {} } -class PropertyPathCollectionTest_CompositeCar +class PropertyAccessorCollectionTest_CompositeCar { public function getStructure() {} public function setStructure($structure) {} } -class PropertyPathCollectionTest_CarStructure +class PropertyAccessorCollectionTest_CarStructure { public function addAxis($axis) {} @@ -101,44 +102,47 @@ class PropertyPathCollectionTest_CarStructure public function getAxes() {} } -abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase +abstract class PropertyAccessorCollectionTest extends \PHPUnit_Framework_TestCase { + /** + * @var PropertyAccessor + */ + private $propertyAccessor; + + protected function setUp() + { + $this->propertyAccessor = new PropertyAccessor(); + } + abstract protected function getCollection(array $array); public function testGetValueReadsArrayAccess() { $object = $this->getCollection(array('firstName' => 'Bernhard')); - $path = new PropertyPath('[firstName]'); - - $this->assertEquals('Bernhard', $path->getValue($object)); + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[firstName]')); } public function testGetValueReadsNestedArrayAccess() { $object = $this->getCollection(array('person' => array('firstName' => 'Bernhard'))); - $path = new PropertyPath('[person][firstName]'); - - $this->assertEquals('Bernhard', $path->getValue($object)); + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, '[person][firstName]')); } /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ public function testGetValueThrowsExceptionIfArrayAccessExpected() { - $path = new PropertyPath('[firstName]'); - - $path->getValue(new \stdClass()); + $this->propertyAccessor->getValue(new \stdClass(), '[firstName]'); } public function testSetValueUpdatesArrayAccess() { $object = $this->getCollection(array()); - $path = new PropertyPath('[firstName]'); - $path->setValue($object, 'Bernhard'); + $this->propertyAccessor->setValue($object, '[firstName]', 'Bernhard'); $this->assertEquals('Bernhard', $object['firstName']); } @@ -147,20 +151,17 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase { $object = $this->getCollection(array()); - $path = new PropertyPath('[person][firstName]'); - $path->setValue($object, 'Bernhard'); + $this->propertyAccessor->setValue($object, '[person][firstName]', 'Bernhard'); $this->assertEquals('Bernhard', $object['person']['firstName']); } /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ public function testSetValueThrowsExceptionIfArrayAccessExpected() { - $path = new PropertyPath('[firstName]'); - - $path->setValue(new \stdClass(), 'Bernhard'); + $this->propertyAccessor->setValue(new \stdClass(), '[firstName]', 'Bernhard'); } public function testSetValueCallsAdderAndRemoverForCollections() @@ -172,11 +173,9 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase // Don't use a mock in order to test whether the collections are // modified while iterating them - $car = new PropertyPathCollectionTest_Car($axesBefore); + $car = new PropertyAccessorCollectionTest_Car($axesBefore); - $path = new PropertyPath('axes'); - - $path->setValue($car, $axesMerged); + $this->propertyAccessor->setValue($car, 'axes', $axesMerged); $this->assertEquals($axesAfter, $car->getAxes()); @@ -191,8 +190,6 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - $path = new PropertyPath('structure.axes'); - $car->expects($this->any()) ->method('getStructure') ->will($this->returnValue($structure)); @@ -210,7 +207,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase ->method('addAxis') ->with('third'); - $path->setValue($car, $axesAfter); + $this->propertyAccessor->setValue($car, 'structure.axes', $axesAfter); } public function testSetValueCallsCustomAdderAndRemover() @@ -221,8 +218,6 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - $path = new PropertyPath('axes|foo'); - $car->expects($this->at(0)) ->method('getAxes') ->will($this->returnValue($axesBefore)); @@ -236,43 +231,39 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase ->method('addFoo') ->with('third'); - $path->setValue($car, $axesAfter); + $this->propertyAccessor->setValue($car, 'axes|foo', $axesAfter); } /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ - public function testMapFormToDataFailsIfOnlyAdderFound() + public function testSetValueFailsIfOnlyAdderFound() { $car = $this->getMock(__CLASS__ . '_CarOnlyAdder'); $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - $path = new PropertyPath('axes'); - $car->expects($this->any()) ->method('getAxes') ->will($this->returnValue($axesBefore)); - $path->setValue($car, $axesAfter); + $this->propertyAccessor->setValue($car, 'axes', $axesAfter); } /** - * @expectedException \Symfony\Component\Form\Exception\InvalidPropertyException + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException */ - public function testMapFormToDataFailsIfOnlyRemoverFound() + public function testSetValueFailsIfOnlyRemoverFound() { $car = $this->getMock(__CLASS__ . '_CarOnlyRemover'); $axesBefore = $this->getCollection(array(1 => 'second', 3 => 'fourth')); $axesAfter = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); - $path = new PropertyPath('axes'); - $car->expects($this->any()) ->method('getAxes') ->will($this->returnValue($axesBefore)); - $path->setValue($car, $axesAfter); + $this->propertyAccessor->setValue($car, 'axes', $axesAfter); } /** @@ -283,9 +274,9 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase $axes = $this->getCollection(array(0 => 'first', 1 => 'second', 2 => 'third')); try { - $path->setValue($car, $axes); + $this->propertyAccessor->setValue($car, $path, $axes); $this->fail('An expected exception was not thrown!'); - } catch (\Symfony\Component\Form\Exception\Exception $e) { + } catch (ExceptionInterface $e) { $this->assertEquals($message, $e->getMessage()); } } @@ -295,7 +286,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase $data = array(); $car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemover'); - $propertyPath = new PropertyPath('axes'); + $propertyPath = 'axes'; $expectedMessage = sprintf( 'Neither element "axes" nor method "setAxes()" exists in class ' .'"%s", nor could adders and removers be found based on the ' @@ -304,7 +295,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase // .'property path with "|{singular}" to override the guesser)' , get_class($car), - implode(', ', (array) $singulars = FormUtil::singularify('Axes')) + implode(', ', (array) $singulars = StringUtil::singularify('Axes')) ); $data[] = array($car, $propertyPath, $expectedMessage); @@ -323,7 +314,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase */ $car = $this->getMock(__CLASS__ . '_CarNoAdderAndRemoverWithProperty'); - $propertyPath = new PropertyPath('axes'); + $propertyPath = 'axes'; $expectedMessage = sprintf( 'Property "axes" is not public in class "%s", nor could adders and ' .'removers be found based on the guessed singulars: %s' @@ -332,7 +323,7 @@ abstract class PropertyPathCollectionTest extends \PHPUnit_Framework_TestCase . '. Maybe you should ' .'create the method "setAxes()"?', get_class($car), - implode(', ', (array) $singulars = FormUtil::singularify('Axes')) + implode(', ', (array) $singulars = StringUtil::singularify('Axes')) ); $data[] = array($car, $propertyPath, $expectedMessage); diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathCustomArrayObjectTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php similarity index 75% rename from src/Symfony/Component/Form/Tests/Util/PropertyPathCustomArrayObjectTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php index a8c6476372..369614c70f 100644 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathCustomArrayObjectTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorCustomArrayObjectTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; use Symfony\Component\Form\Tests\Fixtures\CustomArrayObject; -class PropertyPathCustomArrayObjectTest extends PropertyPathCollectionTest +class PropertyAccessorCustomArrayObjectTest extends PropertyAccessorCollectionTest { protected function getCollection(array $array) { diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php new file mode 100644 index 0000000000..f2d4bd7552 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -0,0 +1,334 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; +use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; + +class PropertyAccessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var PropertyAccessor + */ + private $propertyAccessor; + + protected function setUp() + { + $this->propertyAccessor = new PropertyAccessor(); + } + + public function testGetValueReadsArray() + { + $array = array('firstName' => 'Bernhard'); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[firstName]')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetValueThrowsExceptionIfIndexNotationExpected() + { + $array = array('firstName' => 'Bernhard'); + + $this->propertyAccessor->getValue($array, 'firstName'); + } + + public function testGetValueReadsZeroIndex() + { + $array = array('Bernhard'); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[0]')); + } + + public function testGetValueReadsIndexWithSpecialChars() + { + $array = array('%!@$§.' => 'Bernhard'); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[%!@$§.]')); + } + + public function testGetValueReadsNestedIndexWithSpecialChars() + { + $array = array('root' => array('%!@$§.' => 'Bernhard')); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[root][%!@$§.]')); + } + + public function testGetValueReadsArrayWithCustomPropertyPath() + { + $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '[child][index][firstName]')); + } + + public function testGetValueReadsArrayWithMissingIndexForCustomPropertyPath() + { + $array = array('child' => array('index' => array())); + + $this->assertNull($this->propertyAccessor->getValue($array, '[child][index][firstName]')); + } + + public function testGetValueReadsProperty() + { + $object = new Author(); + $object->firstName = 'Bernhard'; + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'firstName')); + } + + public function testGetValueIgnoresSingular() + { + $this->markTestSkipped('This feature is temporarily disabled as of 2.1'); + + $object = (object) array('children' => 'Many'); + + $this->assertEquals('Many', $this->propertyAccessor->getValue($object, 'children|child')); + } + + public function testGetValueReadsPropertyWithSpecialCharsExceptDot() + { + $array = (object) array('%!@$§' => 'Bernhard'); + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($array, '%!@$§')); + } + + public function testGetValueReadsPropertyWithCustomPropertyPath() + { + $object = new Author(); + $object->child = array(); + $object->child['index'] = new Author(); + $object->child['index']->firstName = 'Bernhard'; + + $this->assertEquals('Bernhard', $this->propertyAccessor->getValue($object, 'child[index].firstName')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException + */ + public function testGetValueThrowsExceptionIfPropertyIsNotPublic() + { + $this->propertyAccessor->getValue(new Author(), 'privateProperty'); + } + + public function testGetValueReadsGetters() + { + $object = new Author(); + $object->setLastName('Schussek'); + + $this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'lastName')); + } + + public function testGetValueCamelizesGetterNames() + { + $object = new Author(); + $object->setLastName('Schussek'); + + $this->assertEquals('Schussek', $this->propertyAccessor->getValue($object, 'last_name')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException + */ + public function testGetValueThrowsExceptionIfGetterIsNotPublic() + { + $this->propertyAccessor->getValue(new Author(), 'privateGetter'); + } + + public function testGetValueReadsIssers() + { + $object = new Author(); + $object->setAustralian(false); + + $this->assertFalse($this->propertyAccessor->getValue($object, 'australian')); + } + + public function testGetValueReadHassers() + { + $object = new Author(); + $object->setReadPermissions(true); + + $this->assertTrue($this->propertyAccessor->getValue($object, 'read_permissions')); + } + + public function testGetValueReadsMagicGet() + { + $object = new Magician(); + $object->__set('magicProperty', 'foobar'); + + $this->assertSame('foobar', $this->propertyAccessor->getValue($object, 'magicProperty')); + } + + /* + * https://github.com/symfony/symfony/pull/4450 + */ + public function testGetValueReadsMagicGetThatReturnsConstant() + { + $object = new Magician(); + + $this->assertNull($this->propertyAccessor->getValue($object, 'magicProperty')); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException + */ + public function testGetValueThrowsExceptionIfIsserIsNotPublic() + { + $this->propertyAccessor->getValue(new Author(), 'privateIsser'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testGetValueThrowsExceptionIfPropertyDoesNotExist() + { + $this->propertyAccessor->getValue(new Author(), 'foobar'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testGetValueThrowsExceptionIfNotObjectOrArray() + { + $this->propertyAccessor->getValue('baz', 'foobar'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testGetValueThrowsExceptionIfNull() + { + $this->propertyAccessor->getValue(null, 'foobar'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testGetValueThrowsExceptionIfEmpty() + { + $this->propertyAccessor->getValue('', 'foobar'); + } + + public function testSetValueUpdatesArrays() + { + $array = array(); + + $this->propertyAccessor->setValue($array, '[firstName]', 'Bernhard'); + + $this->assertEquals(array('firstName' => 'Bernhard'), $array); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException + */ + public function testSetValueThrowsExceptionIfIndexNotationExpected() + { + $array = array(); + + $this->propertyAccessor->setValue($array, 'firstName', 'Bernhard'); + } + + public function testSetValueUpdatesArraysWithCustomPropertyPath() + { + $array = array(); + + $this->propertyAccessor->setValue($array, '[child][index][firstName]', 'Bernhard'); + + $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); + } + + public function testSetValueUpdatesProperties() + { + $object = new Author(); + + $this->propertyAccessor->setValue($object, 'firstName', 'Bernhard'); + + $this->assertEquals('Bernhard', $object->firstName); + } + + public function testSetValueUpdatesPropertiesWithCustomPropertyPath() + { + $object = new Author(); + $object->child = array(); + $object->child['index'] = new Author(); + + $this->propertyAccessor->setValue($object, 'child[index].firstName', 'Bernhard'); + + $this->assertEquals('Bernhard', $object->child['index']->firstName); + } + + public function testSetValueUpdateMagicSet() + { + $object = new Magician(); + + $this->propertyAccessor->setValue($object, 'magicProperty', 'foobar'); + + $this->assertEquals('foobar', $object->__get('magicProperty')); + } + + public function testSetValueUpdatesSetters() + { + $object = new Author(); + + $this->propertyAccessor->setValue($object, 'lastName', 'Schussek'); + + $this->assertEquals('Schussek', $object->getLastName()); + } + + public function testSetValueCamelizesSetterNames() + { + $object = new Author(); + + $this->propertyAccessor->setValue($object, 'last_name', 'Schussek'); + + $this->assertEquals('Schussek', $object->getLastName()); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\PropertyAccessDeniedException + */ + public function testSetValueThrowsExceptionIfGetterIsNotPublic() + { + $this->propertyAccessor->setValue(new Author(), 'privateSetter', 'foobar'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testSetValueThrowsExceptionIfNotObjectOrArray() + { + $value = 'baz'; + + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testSetValueThrowsExceptionIfNull() + { + $value = null; + + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testSetValueThrowsExceptionIfEmpty() + { + $value = ''; + + $this->propertyAccessor->setValue($value, 'foobar', 'bam'); + } +} diff --git a/src/Symfony/Component/Form/Tests/Util/PropertyPathBuilderTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php similarity index 97% rename from src/Symfony/Component/Form/Tests/Util/PropertyPathBuilderTest.php rename to src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php index 6649bdcdc9..ce8951daf0 100644 --- a/src/Symfony/Component/Form/Tests/Util/PropertyPathBuilderTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathBuilderTest.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Util\PropertyPathBuilder; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\PropertyPathBuilder; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php new file mode 100644 index 0000000000..3548ad6ff0 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\PropertyAccess\Tests\Fixtures\Author; +use Symfony\Component\PropertyAccess\Tests\Fixtures\Magician; + +class PropertyPathTest extends \PHPUnit_Framework_TestCase +{ + public function testToString() + { + $path = new PropertyPath('reference.traversable[index].property'); + + $this->assertEquals('reference.traversable[index].property', $path->__toString()); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testInvalidPropertyPath_noDotBeforeProperty() + { + new PropertyPath('[index]property'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testInvalidPropertyPath_dotAtTheBeginning() + { + new PropertyPath('.property'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testInvalidPropertyPath_unexpectedCharacters() + { + new PropertyPath('property.$foo'); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException + */ + public function testInvalidPropertyPath_empty() + { + new PropertyPath(''); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testInvalidPropertyPath_null() + { + new PropertyPath(null); + } + + /** + * @expectedException \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException + */ + public function testInvalidPropertyPath_false() + { + new PropertyPath(false); + } + + public function testValidPropertyPath_zero() + { + new PropertyPath('0'); + } + + public function testGetParent_dot() + { + $propertyPath = new PropertyPath('grandpa.parent.child'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParent_index() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertEquals(new PropertyPath('grandpa.parent'), $propertyPath->getParent()); + } + + public function testGetParent_noParent() + { + $propertyPath = new PropertyPath('path'); + + $this->assertNull($propertyPath->getParent()); + } + + public function testCopyConstructor() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + $copy = new PropertyPath($propertyPath); + + $this->assertEquals($propertyPath, $copy); + } + + public function testGetElement() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertEquals('child', $propertyPath->getElement(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetElementDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->getElement(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testGetElementDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->getElement(-1); + } + + public function testIsProperty() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertTrue($propertyPath->isProperty(1)); + $this->assertFalse($propertyPath->isProperty(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsPropertyDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isProperty(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsPropertyDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isProperty(-1); + } + + public function testIsIndex() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $this->assertFalse($propertyPath->isIndex(1)); + $this->assertTrue($propertyPath->isIndex(2)); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsIndexDoesNotAcceptInvalidIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isIndex(3); + } + + /** + * @expectedException \OutOfBoundsException + */ + public function testIsIndexDoesNotAcceptNegativeIndices() + { + $propertyPath = new PropertyPath('grandpa.parent[child]'); + + $propertyPath->isIndex(-1); + } +} diff --git a/src/Symfony/Component/Form/Tests/Util/FormUtilTest.php b/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php similarity index 95% rename from src/Symfony/Component/Form/Tests/Util/FormUtilTest.php rename to src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php index c70c0d6fcc..3c36f91a9b 100644 --- a/src/Symfony/Component/Form/Tests/Util/FormUtilTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php @@ -9,11 +9,11 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Form\Tests\Util; +namespace Symfony\Component\PropertyAccess\Tests; -use Symfony\Component\Form\Util\FormUtil; +use Symfony\Component\PropertyAccess\StringUtil; -class FormUtilTest extends \PHPUnit_Framework_TestCase +class StringUtilTest extends \PHPUnit_Framework_TestCase { public function singularifyProvider() { @@ -130,6 +130,6 @@ class FormUtilTest extends \PHPUnit_Framework_TestCase */ public function testSingularify($plural, $singular) { - $this->assertEquals($singular, FormUtil::singularify($plural)); + $this->assertEquals($singular, StringUtil::singularify($plural)); } } diff --git a/src/Symfony/Component/PropertyAccess/composer.json b/src/Symfony/Component/PropertyAccess/composer.json new file mode 100644 index 0000000000..3b85c0308e --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/composer.json @@ -0,0 +1,31 @@ +{ + "name": "symfony/property-access", + "type": "library", + "description": "Symfony PropertyAccess Component", + "keywords": ["property", "index", "access", "object", "array", "extraction", "injection", "reflection", "property path"], + "homepage": "http://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "require": { + "php": ">=5.3.3" + }, + "autoload": { + "psr-0": { "Symfony\\Component\\PropertyAccess\\": "" } + }, + "target-dir": "Symfony/Component/PropertyAccess", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + } +} diff --git a/src/Symfony/Component/PropertyAccess/phpunit.xml.dist b/src/Symfony/Component/PropertyAccess/phpunit.xml.dist new file mode 100644 index 0000000000..8799eaccf5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./Tests/ + + + + + + ./ + + ./Resources + ./Tests + + + + From 6b1652e5eec9e714f4da46436551f877801ff101 Mon Sep 17 00:00:00 2001 From: Yaroslav Kiliba Date: Tue, 31 Jul 2012 14:15:57 +0300 Subject: [PATCH 2/2] [PropertyAccess] Property path, small refactoring, read/writeProperty to read/write Property/Index. --- .../PropertyAccess/PropertyAccessor.php | 323 ++++++++++-------- 1 file changed, 181 insertions(+), 142 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 229627aa53..d318f1c0c5 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -44,7 +44,7 @@ class PropertyAccessor implements PropertyAccessorInterface throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); } - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength()); return $propertyValues[count($propertyValues) - 1][self::VALUE]; } @@ -60,7 +60,7 @@ class PropertyAccessor implements PropertyAccessorInterface throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\PropertyAccess\PropertyPathInterface'); } - $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 2); + $propertyValues =& $this->readPropertiesUntil($objectOrArray, $propertyPath, $propertyPath->getLength() - 1); $overwrite = true; // Add the root object to the list @@ -80,9 +80,12 @@ class PropertyAccessor implements PropertyAccessorInterface $property = $propertyPath->getElement($i); //$singular = $propertyPath->singulars[$i]; $singular = null; - $isIndex = $propertyPath->isIndex($i); - $this->writeProperty($objectOrArray, $propertyPath, $property, $singular, $isIndex, $value); + if ($propertyPath->isIndex($i)) { + $this->writeIndex($objectOrArray, $property, $value); + } else { + $this->writeProperty($objectOrArray, $property, $singular, $value); + } } $value =& $objectOrArray; @@ -105,7 +108,7 @@ class PropertyAccessor implements PropertyAccessorInterface { $propertyValues = array(); - for ($i = 0; $i <= $lastIndex; ++$i) { + for ($i = 0; $i < $lastIndex; ++$i) { if (!is_object($objectOrArray) && !is_array($objectOrArray)) { throw new UnexpectedTypeException($objectOrArray, 'object or array'); } @@ -119,7 +122,12 @@ class PropertyAccessor implements PropertyAccessorInterface $objectOrArray[$property] = $i + 1 < $propertyPath->getLength() ? array() : null; } - $propertyValue =& $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex); + if ($isIndex) { + $propertyValue =& $this->readIndex($objectOrArray, $property); + } else { + $propertyValue =& $this->readProperty($objectOrArray, $property); + } + $objectOrArray =& $propertyValue[self::VALUE]; $propertyValues[] =& $propertyValue; @@ -128,13 +136,47 @@ class PropertyAccessor implements PropertyAccessorInterface return $propertyValues; } + /** + * Reads a key from an array-like structure. + * + * @param \ArrayAccess|array $array The array or \ArrayAccess object to read from. + * @param string|integer $index The key to read. + * + * @return mixed The value of the key + * + * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array. + */ + private function &readIndex(&$array, $index) + { + if (!$array instanceof \ArrayAccess && !is_array($array)) { + throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); + } + + // Use an array instead of an object since performance is very crucial here + $result = array( + self::VALUE => null, + self::IS_REF => false + ); + + if (isset($array[$index])) { + if (is_array($array)) { + $result[self::VALUE] =& $array[$index]; + $result[self::IS_REF] = true; + } else { + $result[self::VALUE] = $array[$index]; + // Objects are always passed around by reference + $result[self::IS_REF] = is_object($array[$index]) ? true : false; + } + } + + return $result; + } + /** * Reads the a property from an object or array. * - * @param object|array $objectOrArray The object or array to read from. - * @param PropertyPathInterface $propertyPath The property path to read. - * @param string $property The property to read. - * @param Boolean $isIndex Whether to interpret the property as index. + * @param object $object The object to read from. + * @param string $property The property to read. * * @return mixed The value of the read property * @@ -142,7 +184,7 @@ class PropertyAccessor implements PropertyAccessorInterface * @throws PropertyAccessDeniedException If the property cannot be accessed due to * access restrictions (private or protected). */ - private function &readProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $isIndex) + private function &readProperty(&$object, $property) { // Use an array instead of an object since performance is // very crucial here @@ -151,65 +193,52 @@ class PropertyAccessor implements PropertyAccessorInterface self::IS_REF => false ); - if ($isIndex) { - if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { - throw new NoSuchPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); - } - - if (isset($objectOrArray[$property])) { - if (is_array($objectOrArray)) { - $result[self::VALUE] =& $objectOrArray[$property]; - $result[self::IS_REF] = true; - } else { - $result[self::VALUE] = $objectOrArray[$property]; - } - } - } elseif (is_object($objectOrArray)) { - $camelProp = $this->camelize($property); - $reflClass = new \ReflectionClass($objectOrArray); - $getter = 'get'.$camelProp; - $isser = 'is'.$camelProp; - $hasser = 'has'.$camelProp; - - if ($reflClass->hasMethod($getter)) { - if (!$reflClass->getMethod($getter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$getter(); - } elseif ($reflClass->hasMethod($isser)) { - if (!$reflClass->getMethod($isser)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$isser(); - } elseif ($reflClass->hasMethod($hasser)) { - if (!$reflClass->getMethod($hasser)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name)); - } - - $result[self::VALUE] = $objectOrArray->$hasser(); - } elseif ($reflClass->hasMethod('__get')) { - // needed to support magic method __get - $result[self::VALUE] = $objectOrArray->$property; - } elseif ($reflClass->hasProperty($property)) { - if (!$reflClass->getProperty($property)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser)); - } - - $result[self::VALUE] =& $objectOrArray->$property; - $result[self::IS_REF] = true; - } elseif (property_exists($objectOrArray, $property)) { - // needed to support \stdClass instances - $result[self::VALUE] =& $objectOrArray->$property; - $result[self::IS_REF] = true; - } else { - throw new NoSuchPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name)); - } - } else { + if (!is_object($object)) { throw new NoSuchPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); } + $camelProp = $this->camelize($property); + $reflClass = new \ReflectionClass($object); + $getter = 'get'.$camelProp; + $isser = 'is'.$camelProp; + $hasser = 'has'.$camelProp; + + if ($reflClass->hasMethod($getter)) { + if (!$reflClass->getMethod($getter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->name)); + } + + $result[self::VALUE] = $object->$getter(); + } elseif ($reflClass->hasMethod($isser)) { + if (!$reflClass->getMethod($isser)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->name)); + } + + $result[self::VALUE] = $object->$isser(); + } elseif ($reflClass->hasMethod($hasser)) { + if (!$reflClass->getMethod($hasser)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->name)); + } + + $result[self::VALUE] = $object->$hasser(); + } elseif ($reflClass->hasMethod('__get')) { + // needed to support magic method __get + $result[self::VALUE] = $object->$property; + } elseif ($reflClass->hasProperty($property)) { + if (!$reflClass->getProperty($property)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()" or "%s()"?', $property, $reflClass->name, $getter, $isser, $hasser)); + } + + $result[self::VALUE] =& $object->$property; + $result[self::IS_REF] = true; + } elseif (property_exists($object, $property)) { + // needed to support \stdClass instances + $result[self::VALUE] =& $object->$property; + $result[self::IS_REF] = true; + } else { + throw new NoSuchPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->name)); + } + // Objects are always passed around by reference if (is_object($result[self::VALUE])) { $result[self::IS_REF] = true; @@ -221,108 +250,118 @@ class PropertyAccessor implements PropertyAccessorInterface /** * Sets the value of the property at the given index in the path * - * @param object|array $objectOrArray The object or array to write to. - * @param PropertyPathInterface $propertyPath The property path to write. - * @param string $property The property to write. - * @param string|null $singular The singular form of the property name or null. - * @param Boolean $isIndex Whether to interpret the property as index. - * @param mixed $value The value to write. + * @param \ArrayAccess|array $array An array or \ArrayAccess object to write to. + * @param string|integer $index The index to write at. + * @param mixed $value The value to write. + * + * @throws NoSuchPropertyException If the array does not implement \ArrayAccess or it is not an array. + */ + private function writeIndex(&$array, $index, $value) + { + if (!$array instanceof \ArrayAccess && !is_array($array)) { + throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $index, get_class($array))); + } + + $array[$index] = $value; + } + + /** + * Sets the value of the property at the given index in the path + * + * @param object|array $object The object or array to write to. + * @param string $property The property to write. + * @param string|null $singular The singular form of the property name or null. + * @param mixed $value The value to write. * * @throws NoSuchPropertyException If the property does not exist. * @throws PropertyAccessDeniedException If the property cannot be accessed due to * access restrictions (private or protected). */ - private function writeProperty(&$objectOrArray, PropertyPathInterface $propertyPath, $property, $singular, $isIndex, $value) + private function writeProperty(&$object, $property, $singular, $value) { $adderRemoverError = null; - if ($isIndex) { - if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) { - throw new NoSuchPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray))); - } + if (!is_object($object)) { + throw new NoSuchPropertyException(sprintf('Cannot write property "%s" to an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + } - $objectOrArray[$property] = $value; - } elseif (is_object($objectOrArray)) { - $reflClass = new \ReflectionClass($objectOrArray); + $reflClass = new \ReflectionClass($object); + $plural = $this->camelize($property); - // The plural form is the last element of the property path - $plural = $this->camelize($propertyPath->getElement($propertyPath->getLength() - 1)); + // Any of the two methods is required, but not yet known + $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); - // Any of the two methods is required, but not yet known - $singulars = null !== $singular ? array($singular) : (array) StringUtil::singularify($plural); + if (is_array($value) || $value instanceof \Traversable) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); - if (is_array($value) || $value instanceof \Traversable) { - $methods = $this->findAdderAndRemover($reflClass, $singulars); - if (null !== $methods) { - // At this point the add and remove methods have been found - // Use iterator_to_array() instead of clone in order to prevent side effects - // see https://github.com/symfony/symfony/issues/4670 - $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; - $itemToRemove = array(); - $propertyValue = $this->readProperty($objectOrArray, $propertyPath, $property, $isIndex); - $previousValue = $propertyValue[self::VALUE]; + if (null !== $methods) { + // At this point the add and remove methods have been found + // Use iterator_to_array() instead of clone in order to prevent side effects + // see https://github.com/symfony/symfony/issues/4670 + $itemsToAdd = is_object($value) ? iterator_to_array($value) : $value; + $itemToRemove = array(); + $propertyValue = $this->readProperty($object, $property); + $previousValue = $propertyValue[self::VALUE]; - if (is_array($previousValue) || $previousValue instanceof \Traversable) { - foreach ($previousValue as $previousItem) { - foreach ($value as $key => $item) { - if ($item === $previousItem) { - // Item found, don't add - unset($itemsToAdd[$key]); + if (is_array($previousValue) || $previousValue instanceof \Traversable) { + foreach ($previousValue as $previousItem) { + foreach ($value as $key => $item) { + if ($item === $previousItem) { + // Item found, don't add + unset($itemsToAdd[$key]); - // Next $previousItem - continue 2; - } + // Next $previousItem + continue 2; } - - // Item not found, add to remove list - $itemToRemove[] = $previousItem; } - } - foreach ($itemToRemove as $item) { - call_user_func(array($objectOrArray, $methods[1]), $item); - } - - foreach ($itemsToAdd as $item) { - call_user_func(array($objectOrArray, $methods[0]), $item); - } - - return; - } else { - $adderRemoverError = ', nor could adders and removers be found based on the '; - if (null === $singular) { - // $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)'; - $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars); - } else { - $adderRemoverError .= 'passed singular: '.$singular; + // Item not found, add to remove list + $itemToRemove[] = $previousItem; } } - } - $setter = 'set'.$this->camelize($property); - if ($reflClass->hasMethod($setter)) { - if (!$reflClass->getMethod($setter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name)); + foreach ($itemToRemove as $item) { + call_user_func(array($object, $methods[1]), $item); } - $objectOrArray->$setter($value); - } elseif ($reflClass->hasMethod('__set')) { - // needed to support magic method __set - $objectOrArray->$property = $value; - } elseif ($reflClass->hasProperty($property)) { - if (!$reflClass->getProperty($property)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter)); + foreach ($itemsToAdd as $item) { + call_user_func(array($object, $methods[0]), $item); } - $objectOrArray->$property = $value; - } elseif (property_exists($objectOrArray, $property)) { - // needed to support \stdClass instances - $objectOrArray->$property = $value; + return; } else { - throw new NoSuchPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError)); + $adderRemoverError = ', nor could adders and removers be found based on the '; + if (null === $singular) { + // $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars).' (provide a singular by suffixing the property path with "|{singular}" to override the guesser)'; + $adderRemoverError .= 'guessed singulars: '.implode(', ', $singulars); + } else { + $adderRemoverError .= 'passed singular: '.$singular; + } } + } + + $setter = 'set'.$this->camelize($property); + + if ($reflClass->hasMethod($setter)) { + if (!$reflClass->getMethod($setter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->name)); + } + + $object->$setter($value); + } elseif ($reflClass->hasMethod('__set')) { + // needed to support magic method __set + $object->$property = $value; + } elseif ($reflClass->hasProperty($property)) { + if (!$reflClass->getProperty($property)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s"%s. Maybe you should create the method "%s()"?', $property, $reflClass->name, $adderRemoverError, $setter)); + } + + $object->$property = $value; + } elseif (property_exists($object, $property)) { + // needed to support \stdClass instances + $object->$property = $value; } else { - throw new NoSuchPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property)); + throw new NoSuchPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"%s', $property, $setter, $reflClass->name, $adderRemoverError)); } }