From e1be4e96899bfb03193c83b0c979c45e462e0436 Mon Sep 17 00:00:00 2001 From: Bernhard Schussek Date: Sun, 10 Oct 2010 23:15:28 +0200 Subject: [PATCH] [Form] Refactored logic to read and set values from Field to PropertyPath --- src/Symfony/Component/Form/Field.php | 159 +--------- src/Symfony/Component/Form/FieldGroup.php | 26 +- src/Symfony/Component/Form/FieldInterface.php | 4 +- src/Symfony/Component/Form/Form.php | 8 +- src/Symfony/Component/Form/PropertyPath.php | 299 +++++++++++++++--- .../Component/Form/PropertyPathIterator.php | 71 +++++ .../Tests/Component/Form/FieldGroupTest.php | 47 ++- .../Tests/Component/Form/FieldTest.php | 203 ------------ .../Tests/Component/Form/PropertyPathTest.php | 227 +++++++++++-- 9 files changed, 581 insertions(+), 463 deletions(-) create mode 100644 src/Symfony/Component/Form/PropertyPathIterator.php diff --git a/src/Symfony/Component/Form/Field.php b/src/Symfony/Component/Form/Field.php index 60fb18bf0f75..3be61e665755 100644 --- a/src/Symfony/Component/Form/Field.php +++ b/src/Symfony/Component/Form/Field.php @@ -11,8 +11,6 @@ * with this source code in the file LICENSE. */ -use Symfony\Component\Form\Exception\InvalidPropertyException; -use Symfony\Component\Form\Exception\PropertyAccessDeniedException; use Symfony\Component\Form\ValueTransformer\ValueTransformerInterface; use Symfony\Component\Form\ValueTransformer\TransformationFailedException; @@ -274,7 +272,7 @@ public function getData() * * @see FieldInterface */ - public function addError($messageTemplate, array $messageParameters = array(), PropertyPath $path = null, $type = null) + public function addError($messageTemplate, array $messageParameters = array(), PropertyPathIterator $pathIterator = null, $type = null) { $this->errors[] = array($messageTemplate, $messageParameters); } @@ -410,8 +408,7 @@ public function updateFromObject(&$objectOrArray) { // TODO throw exception if not object or array if ($this->propertyPath !== null) { - $this->propertyPath->rewind(); - $this->setData($this->readPropertyPath($objectOrArray, $this->propertyPath)); + $this->setData($this->propertyPath->getValue($objectOrArray)); } else { // pass object through if the property path is empty $this->setData($objectOrArray); @@ -426,157 +423,7 @@ public function updateObject(&$objectOrArray) // TODO throw exception if not object or array if ($this->propertyPath !== null) { - $this->propertyPath->rewind(); - $this->updatePropertyPath($objectOrArray, $this->propertyPath); - } - } - - /** - * Recursively reads the value of the property path in the data - * - * @param array|object $objectOrArray An object or array - * @param PropertyPath $propertyPath A property path pointing to a property - * in the object/array. - */ - protected function readPropertyPath(&$objectOrArray, PropertyPath $propertyPath) - { - if (is_object($objectOrArray)) { - $value = $this->readProperty($objectOrArray, $propertyPath); - } - // arrays need to be treated separately (due to PHP bug?) - // http://bugs.php.net/bug.php?id=52133 - else { - if (!array_key_exists($propertyPath->getCurrent(), $objectOrArray)) { - $objectOrArray[$propertyPath->getCurrent()] = array(); - } - - $value =& $objectOrArray[$propertyPath->getCurrent()]; - } - - if ($propertyPath->hasNext()) { - $propertyPath->next(); - - return $this->readPropertyPath($value, $propertyPath); - } else { - return $value; - } - } - - protected function updatePropertyPath(&$objectOrArray, PropertyPath $propertyPath) - { - if ($propertyPath->hasNext()) { - if (is_object($objectOrArray)) { - $value = $this->readProperty($objectOrArray, $propertyPath); - } - // arrays need to be treated separately (due to PHP bug?) - // http://bugs.php.net/bug.php?id=52133 - else { - if (!array_key_exists($propertyPath->getCurrent(), $objectOrArray)) { - $objectOrArray[$propertyPath->getCurrent()] = array(); - } - - $value =& $objectOrArray[$propertyPath->getCurrent()]; - } - - $propertyPath->next(); - - $this->updatePropertyPath($value, $propertyPath); - } else { - $this->updateProperty($objectOrArray, $propertyPath); - } - } - - /** - * Reads a specific element of the given data - * - * If the data is an array, the value at index $element is returned. - * - * If the data is an object, either the result of get{$element}(), - * is{$element}() or the property $element is returned. If none of these - * is publicly available, an exception is thrown - * - * @param object $object The data to read - * @param string $element The element to read from the data - * @return mixed The value of the element - */ - protected function readProperty($object, PropertyPath $propertyPath) - { - $camelizer = function ($path) { - return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $path); - }; - - if ($propertyPath->isIndex()) { - if (!$object instanceof \ArrayAccess) { - throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $propertyPath->getCurrent(), get_class($object))); - } - - return $object[$propertyPath->getCurrent()]; - } else { - $reflClass = new \ReflectionClass($object); - $getter = 'get'.$camelizer($propertyPath->getCurrent()); - $isser = 'is'.$camelizer($propertyPath->getCurrent()); - $property = $propertyPath->getCurrent(); - - if ($reflClass->hasMethod($getter)) { - if (!$reflClass->getMethod($getter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName())); - } - - return $object->$getter(); - } else if ($reflClass->hasMethod($isser)) { - if (!$reflClass->getMethod($isser)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName())); - } - - return $object->$isser(); - } else if ($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 "get%s()" or "is%s()"?', $property, $reflClass->getName(), ucfirst($property), ucfirst($property))); - } - - return $object->$property; - } else if (property_exists($object, $property)) { - // needed to support \stdClass instances - return $object->$property; - } else { - throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName())); - } - } - } - - protected function updateProperty(&$objectOrArray, PropertyPath $propertyPath) - { - if (is_object($objectOrArray) && $propertyPath->isIndex()) { - if (!$objectOrArray instanceof \ArrayAccess) { - throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $propertyPath->getCurrent(), get_class($objectOrArray))); - } - - $objectOrArray[$propertyPath->getCurrent()] = $this->getData(); - } else if (is_object($objectOrArray)) { - $reflClass = new \ReflectionClass($objectOrArray); - $setter = 'set'.ucfirst($propertyPath->getCurrent()); - $property = $propertyPath->getCurrent(); - - if ($reflClass->hasMethod($setter)) { - if (!$reflClass->getMethod($setter)->isPublic()) { - throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName())); - } - - $objectOrArray->$setter($this->getData()); - } else if ($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 "set%s()"?', $property, $reflClass->getName(), ucfirst($property))); - } - - $objectOrArray->$property = $this->getData(); - } else if (property_exists($objectOrArray, $property)) { - // needed to support \stdClass instances - $objectOrArray->$property = $this->getData(); - } else { - throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName())); - } - } else { - $objectOrArray[$propertyPath->getCurrent()] = $this->getData(); + $this->propertyPath->setValue($objectOrArray, $this->getData()); } } } diff --git a/src/Symfony/Component/Form/FieldGroup.php b/src/Symfony/Component/Form/FieldGroup.php index 8318f3ac35cc..5b0160382b1c 100644 --- a/src/Symfony/Component/Form/FieldGroup.php +++ b/src/Symfony/Component/Form/FieldGroup.php @@ -393,18 +393,18 @@ public function isValid() /** * {@inheritDoc} */ - public function addError($messageTemplate, array $messageParameters = array(), PropertyPath $path = null, $type = null) + public function addError($messageTemplate, array $messageParameters = array(), PropertyPathIterator $pathIterator = null, $type = null) { - if ($path !== null) { - if ($type === self::FIELD_ERROR && $path->hasNext()) { - $path->next(); + if ($pathIterator !== null) { + if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) { + $pathIterator->next(); - if ($path->isProperty() && $path->getCurrent() === 'fields') { - $path->next(); + if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') { + $pathIterator->next(); } - if ($this->has($path->getCurrent()) && !$this->get($path->getCurrent())->isHidden()) { - $this->get($path->getCurrent())->addError($messageTemplate, $messageParameters, $path, $type); + if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) { + $this->get($pathIterator->current())->addError($messageTemplate, $messageParameters, $pathIterator, $type); return; } @@ -414,14 +414,12 @@ public function addError($messageTemplate, array $messageParameters = array(), P foreach ($iterator as $field) { if (null !== ($fieldPath = $field->getPropertyPath())) { - $fieldPath->rewind(); - - if ($fieldPath->getCurrent() === $path->getCurrent() && !$field->isHidden()) { - if ($path->hasNext()) { - $path->next(); + if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) { + if ($pathIterator->hasNext()) { + $pathIterator->next(); } - $field->addError($messageTemplate, $messageParameters, $path, $type); + $field->addError($messageTemplate, $messageParameters, $pathIterator, $type); return; } diff --git a/src/Symfony/Component/Form/FieldInterface.php b/src/Symfony/Component/Form/FieldInterface.php index e6c3ed4e614b..7ae03a903436 100644 --- a/src/Symfony/Component/Form/FieldInterface.php +++ b/src/Symfony/Component/Form/FieldInterface.php @@ -182,10 +182,10 @@ public function bind($taintedData); * * * @param FieldInterface $field - * @param PropertyPath $path + * @param PropertyPathIterator $pathIterator * @param ConstraintViolation$violation */ - function addError($messageTemplate, array $messageParameters = array(), PropertyPath $path = null, $type = null); + function addError($messageTemplate, array $messageParameters = array(), PropertyPathIterator $pathIterator = null, $type = null); /** * Returns whether the field is bound. diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index f46678f3799c..cdeaecf50304 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -142,17 +142,19 @@ final public function bind($taintedValues, array $taintedFiles = null) if ($this->getParent() === null) { if ($violations = $this->validator->validate($this, $this->getValidationGroups())) { + // TODO: test me foreach ($violations as $violation) { $propertyPath = new PropertyPath($violation->getPropertyPath()); + $iterator = $propertyPath->getIterator(); - if ($propertyPath->getCurrent() == 'data') { + if ($iterator->current() == 'data') { $type = self::DATA_ERROR; - $propertyPath->next(); // point at the first data element + $iterator->next(); // point at the first data element } else { $type = self::FIELD_ERROR; } - $this->addError($violation->getMessageTemplate(), $violation->getMessageParameters(), $propertyPath, $type); + $this->addError($violation->getMessageTemplate(), $violation->getMessageParameters(), $iterator, $type); } } } diff --git a/src/Symfony/Component/Form/PropertyPath.php b/src/Symfony/Component/Form/PropertyPath.php index e0039541d6a9..db3b91db2f6d 100644 --- a/src/Symfony/Component/Form/PropertyPath.php +++ b/src/Symfony/Component/Form/PropertyPath.php @@ -12,32 +12,34 @@ */ use Symfony\Component\Form\Exception\InvalidPropertyPathException; +use Symfony\Component\Form\Exception\InvalidPropertyException; +use Symfony\Component\Form\Exception\PropertyAccessDeniedException; /** * Allows easy traversing of a property path * * @author Bernhard Schussek */ -class PropertyPath +class PropertyPath implements \IteratorAggregate { - /** - * The current index of the traversal - * @var integer - */ - protected $currentIndex = 0; - /** * The elements of the property path * @var array */ protected $elements = array(); + /** + * The number of elements in the property path + * @var integer + */ + protected $length; + /** * Contains a boolean for each property in $elements denoting whether this - * element is a property. It is an index otherwise. + * element is an index. It is a property otherwise. * @var array */ - protected $isProperty = array(); + protected $isIndex = array(); /** * String representation of the path @@ -48,7 +50,7 @@ class PropertyPath /** * Parses the given property path * - * @param string $propertyPath + * @param string $this */ public function __construct($propertyPath) { @@ -66,10 +68,10 @@ public function __construct($propertyPath) while (preg_match($pattern, $remaining, $matches)) { if ($matches[2] !== '') { $this->elements[] = $matches[2]; - $this->isProperty[] = true; + $this->isIndex[] = false; } else { $this->elements[] = $matches[3]; - $this->isProperty[] = false; + $this->isIndex[] = true; } $position += strlen($matches[1]); @@ -85,6 +87,8 @@ public function __construct($propertyPath) $position )); } + + $this->length = count($this->elements); } /** @@ -98,67 +102,284 @@ public function __toString() } /** - * Returns the current element of the path + * Returns a new iterator for this path * - * @return string + * @return PropertyPathIterator + */ + public function getIterator() + { + return new PropertyPathIterator($this); + } + + /** + * Returns the elements of the property path as array + * + * @return array An array of property/index names + */ + public function getElements() + { + return $this->elements; + } + + /** + * Returns the element at the given index in the property path + * + * @return string A property or index name */ - public function getCurrent() + public function getElement($index) { - return $this->elements[$this->currentIndex]; + return $this->elements[$index]; } /** - * Returns whether the current element is a property + * Returns whether the element at the given index is a property * - * @return boolean + * @param integer $index The index in the property path + * @return boolean Whether the element at this index is a property */ - public function isProperty() + public function isProperty($index) { - return $this->isProperty[$this->currentIndex]; + return !$this->isIndex($index); } /** - * Returns whether the currente element is an array index + * Returns whether the element at the given index is an array index * - * @return boolean + * @param integer $index The index in the property path + * @return boolean Whether the element at this index is an array index */ - public function isIndex() + public function isIndex($index) { - return !$this->isProperty(); + return $this->isIndex[$index]; } /** - * Returns whether there is a next element in the path + * 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" or "is". + * + * If the getter does not exist, this method tries to find a public + * property. The value of the property is then returned. + * + * If neither is found, an exception is thrown. * - * @return boolean + * @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 */ - public function hasNext() + public function getValue($objectOrArray) { - return isset($this->elements[$this->currentIndex + 1]); + return $this->readPropertyPath($objectOrArray, 0); } /** - * Sets the internal cursor to the next element in the path + * 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. * - * Use hasNext() to verify whether there is a next element before calling this - * method, otherwise an exception will be 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/setter does not + * exist + * @throws PropertyAccessDeniedException If the property/setter exists but + * is not public + */ + public function setValue(&$objectOrArray, $value) + { + $this->updatePropertyPath($objectOrArray, 0, $value); + } + + /** + * Recursive implementation of getValue() + * + * @param object|array $objectOrArray The object or array to traverse + * @param integer $currentIndex The current index in the property path + * @return mixed The value at the end of the path + */ + protected function readPropertyPath(&$objectOrArray, $currentIndex) + { + $property = $this->elements[$currentIndex]; + + if (is_object($objectOrArray)) { + $value = $this->readProperty($objectOrArray, $currentIndex); + } + // arrays need to be treated separately (due to PHP bug?) + // http://bugs.php.net/bug.php?id=52133 + else { + if (!array_key_exists($property, $objectOrArray)) { + $objectOrArray[$property] = array(); + } + + $value =& $objectOrArray[$property]; + } + + ++$currentIndex; + + if ($currentIndex < $this->length) { + return $this->readPropertyPath($value, $currentIndex); + } else { + return $value; + } + } + + /** + * Recursive implementation of setValue() * - * @throws OutOfBoundsException If there is no next element + * @param object|array $objectOrArray The object or array to traverse + * @param integer $currentIndex The current index in the property path + * @param mixed $value The value to set at the end of the + * property path */ - public function next() + protected function updatePropertyPath(&$objectOrArray, $currentIndex, $value) { - if (!$this->hasNext()) { - throw new \OutOfBoundsException('There is no next element in the path'); + $property = $this->elements[$currentIndex]; + + if ($currentIndex + 1 < $this->length) { + if (is_object($objectOrArray)) { + $nestedObject = $this->readProperty($objectOrArray, $currentIndex); + } + // arrays need to be treated separately (due to PHP bug?) + // http://bugs.php.net/bug.php?id=52133 + else { + if (!array_key_exists($property, $objectOrArray)) { + $objectOrArray[$property] = array(); + } + + $nestedObject =& $objectOrArray[$property]; + } + + $this->updatePropertyPath($nestedObject, $currentIndex + 1, $value); + } else { + $this->updateProperty($objectOrArray, $currentIndex, $value); } + } + + /** + * Reads the value of the property at the given index in the path + * + * @param object $object The object to read from + * @param integer $currentIndex The index of the read property in the path + * @return mixed The value of the property + */ + protected function readProperty($object, $currentIndex) + { + $property = $this->elements[$currentIndex]; + + if ($this->isIndex[$currentIndex]) { + if (!$object instanceof \ArrayAccess) { + throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object))); + } + + return $object[$property]; + } else { + $reflClass = new \ReflectionClass($object); + $getter = 'get'.$this->camelize($property); + $isser = 'is'.$this->camelize($property); + + if ($reflClass->hasMethod($getter)) { + if (!$reflClass->getMethod($getter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName())); + } - ++$this->currentIndex; + return $object->$getter(); + } else if ($reflClass->hasMethod($isser)) { + if (!$reflClass->getMethod($isser)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName())); + } + + return $object->$isser(); + } else if ($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 "get%s()" or "is%s()"?', $property, $reflClass->getName(), ucfirst($property), ucfirst($property))); + } + + return $object->$property; + } else if (property_exists($object, $property)) { + // needed to support \stdClass instances + return $object->$property; + } else { + throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName())); + } + } } /** - * Sets the internal cursor to the first element in the path + * Sets the value of the property at the given index in the path + * + * @param object $object The object or array to traverse + * @param integer $currentIndex The index of the modified property in the + * path + * @param mixed $value The value to set */ - public function rewind() + protected function updateProperty(&$objectOrArray, $currentIndex, $value) + { + $property = $this->elements[$currentIndex]; + + if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) { + if (!$objectOrArray instanceof \ArrayAccess) { + 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; + } else if (is_object($objectOrArray)) { + $reflClass = new \ReflectionClass($objectOrArray); + $setter = 'set'.ucfirst($property); // TODO camelize correctly + + if ($reflClass->hasMethod($setter)) { + if (!$reflClass->getMethod($setter)->isPublic()) { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName())); + } + + $objectOrArray->$setter($value); + } else if ($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 "set%s()"?', $property, $reflClass->getName(), ucfirst($property))); + } + + $objectOrArray->$property = $value; + } else if (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"', $property, $setter, $reflClass->getName())); + } + } else { + $objectOrArray[$property] = $value; + } + } + + protected function camelize($property) { - $this->currentIndex = 0; + return preg_replace(array('/(^|_)+(.)/e', '/\.(.)/e'), array("strtoupper('\\2')", "'_'.strtoupper('\\1')"), $property); } } diff --git a/src/Symfony/Component/Form/PropertyPathIterator.php b/src/Symfony/Component/Form/PropertyPathIterator.php new file mode 100644 index 000000000000..01ee5ddbb0fd --- /dev/null +++ b/src/Symfony/Component/Form/PropertyPathIterator.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Traverses a property path and provides additional methods to find out + * information about the current element + * + * @author Bernhard Schussek + */ +class PropertyPathIterator extends \ArrayIterator +{ + /** + * The traversed property path + * @var PropertyPath + */ + protected $path; + + /** + * Constructor. + * + * @param PropertyPath $path The property path to traverse + */ + public function __construct(PropertyPath $path) + { + parent::__construct($path->getElements()); + + $this->path = $path; + } + + /** + * Returns whether next() can be called without making the iterator invalid + * + * @return boolean + */ + public function hasNext() + { + return $this->offsetExists($this->key() + 1); + } + + /** + * Returns whether the current element in the property path is an array + * index + * + * @return boolean + */ + public function isIndex() + { + return $this->path->isIndex($this->key()); + } + + /** + * Returns whether the current element in the property path is a property + * names + * + * @return boolean + */ + public function isProperty() + { + return $this->path->isProperty($this->key()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/FieldGroupTest.php b/tests/Symfony/Tests/Component/Form/FieldGroupTest.php index d3729904e612..7d45531f3e9f 100644 --- a/tests/Symfony/Tests/Component/Form/FieldGroupTest.php +++ b/tests/Symfony/Tests/Component/Form/FieldGroupTest.php @@ -153,7 +153,9 @@ public function testAddErrorMapsFieldValidationErrorsOntoFields() $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('fields[firstName].data'), FieldGroup::FIELD_ERROR); + $path = new PropertyPath('fields[firstName].data'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::FIELD_ERROR); } public function testAddErrorMapsFieldValidationErrorsOntoFieldsWithinNestedFieldGroups() @@ -168,7 +170,9 @@ public function testAddErrorMapsFieldValidationErrorsOntoFieldsWithinNestedField $innerGroup->add($field); $group->add($innerGroup); - $group->addError('Message', array(), new PropertyPath('fields[names].fields[firstName].data'), FieldGroup::FIELD_ERROR); + $path = new PropertyPath('fields[names].fields[firstName].data'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::FIELD_ERROR); } public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound() @@ -180,7 +184,9 @@ public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound() $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('fields[bar].data'), FieldGroup::FIELD_ERROR); + $path = new PropertyPath('fields[bar].data'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::FIELD_ERROR); $this->assertEquals(array(array('Message', array())), $group->getErrors()); } @@ -197,7 +203,9 @@ public function testAddErrorKeepsFieldValidationErrorsIfFieldIsHidden() $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('fields[firstName].data'), FieldGroup::FIELD_ERROR); + $path = new PropertyPath('fields[firstName].data'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::FIELD_ERROR); $this->assertEquals(array(array('Message', array())), $group->getErrors()); } @@ -206,6 +214,7 @@ public function testAddErrorMapsDataValidationErrorsOntoFields() { // path is expected to point at "firstName" $expectedPath = new PropertyPath('firstName'); + $expectedPathIterator = $expectedPath->getIterator(); $field = $this->createMockField('firstName'); $field->expects($this->any()) @@ -213,12 +222,14 @@ public function testAddErrorMapsDataValidationErrorsOntoFields() ->will($this->returnValue(new PropertyPath('firstName'))); $field->expects($this->once()) ->method('addError') - ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('firstName'), FieldGroup::DATA_ERROR); + $path = new PropertyPath('firstName'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::DATA_ERROR); } public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound() @@ -233,7 +244,9 @@ public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound() $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('bar'), FieldGroup::DATA_ERROR); + $path = new PropertyPath('bar'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::DATA_ERROR); } public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden() @@ -251,14 +264,17 @@ public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden() $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('firstName'), FieldGroup::DATA_ERROR); + $path = new PropertyPath('firstName'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::DATA_ERROR); } public function testAddErrorMapsDataValidationErrorsOntoNestedFields() { // path is expected to point at "street" $expectedPath = new PropertyPath('address.street'); - $expectedPath->next(); + $expectedPathIterator = $expectedPath->getIterator(); + $expectedPathIterator->next(); $field = $this->createMockField('address'); $field->expects($this->any()) @@ -266,18 +282,21 @@ public function testAddErrorMapsDataValidationErrorsOntoNestedFields() ->will($this->returnValue(new PropertyPath('address'))); $field->expects($this->once()) ->method('addError') - ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); $group = new FieldGroup('author'); $group->add($field); - $group->addError('Message', array(), new PropertyPath('address.street'), FieldGroup::DATA_ERROR); + $path = new PropertyPath('address.street'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::DATA_ERROR); } public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups() { // path is expected to point at "address" $expectedPath = new PropertyPath('address'); + $expectedPathIterator = $expectedPath->getIterator(); $field = $this->createMockField('address'); $field->expects($this->any()) @@ -285,14 +304,16 @@ public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups() ->will($this->returnValue(new PropertyPath('address'))); $field->expects($this->once()) ->method('addError') - ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + ->with($this->equalTo('Message'), array(), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); $group = new FieldGroup('author'); $group2 = new FieldGroup('anonymous', array('property_path' => null)); $group2->add($field); $group->add($group2); - $group->addError('Message', array(), new PropertyPath('address'), FieldGroup::DATA_ERROR); + $path = new PropertyPath('address'); + + $group->addError('Message', array(), $path->getIterator(), FieldGroup::DATA_ERROR); } public function testAddThrowsExceptionIfAlreadyBound() diff --git a/tests/Symfony/Tests/Component/Form/FieldTest.php b/tests/Symfony/Tests/Component/Form/FieldTest.php index 8b52cd158da8..6783bf82f81d 100644 --- a/tests/Symfony/Tests/Component/Form/FieldTest.php +++ b/tests/Symfony/Tests/Component/Form/FieldTest.php @@ -401,69 +401,6 @@ public function testBoundValuesAreNotTrimmedBeforeTransformingIfDisabled() $this->assertEquals('reverse[ a ]', $field->getData()); } - public function testUpdateFromObjectReadsArray() - { - $array = array('firstName' => 'Bernhard'); - - $field = new TestField('firstName'); - $field->updateFromObject($array); - - $this->assertEquals('Bernhard', $field->getData()); - } - - public function testUpdateFromObjectReadsArrayWithCustomPropertyPath() - { - $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); - - $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); - $field->updateFromObject($array); - - $this->assertEquals('Bernhard', $field->getData()); - } - - public function testUpdateFromObjectReadsProperty() - { - $object = new Author(); - $object->firstName = 'Bernhard'; - - $field = new TestField('firstName'); - $field->updateFromObject($object); - - $this->assertEquals('Bernhard', $field->getData()); - } - - public function testUpdateFromObjectReadsPropertyWithCustomPropertyPath() - { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - $object->child['index']->firstName = 'Bernhard'; - - $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); - $field->updateFromObject($object); - - $this->assertEquals('Bernhard', $field->getData()); - } - - public function testUpdateFromObjectReadsArrayAccess() - { - $object = new \ArrayObject(); - $object['firstName'] = 'Bernhard'; - - $field = new TestField('firstName', array('property_path' => '[firstName]')); - $field->updateFromObject($object); - - $this->assertEquals('Bernhard', $field->getData()); - } - - public function testUpdateFromObjectThrowsExceptionIfArrayAccessExpected() - { - $field = new TestField('firstName', array('property_path' => '[firstName]')); - - $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); - $field->updateFromObject(new Author()); - } - /* * The use case of this test is a field group with an empty property path. * Even if the field group itself is not associated to a specific property, @@ -480,82 +417,6 @@ public function testUpdateFromObjectPassesObjectThroughIfPropertyPathIsEmpty() $this->assertEquals($object, $field->getData()); } - public function testUpdateFromObjectThrowsExceptionIfPropertyIsNotPublic() - { - $field = new TestField('privateProperty'); - - $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); - $field->updateFromObject(new Author()); - } - - public function testUpdateFromObjectReadsGetters() - { - $object = new Author(); - $object->setLastName('Schussek'); - - $field = new TestField('lastName'); - $field->updateFromObject($object); - - $this->assertEquals('Schussek', $field->getData()); - } - - public function testUpdateFromObjectThrowsExceptionIfGetterIsNotPublic() - { - $field = new TestField('privateGetter'); - - $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); - $field->updateFromObject(new Author()); - } - - public function testUpdateFromObjectReadsIssers() - { - $object = new Author(); - $object->setAustralian(false); - - $field = new TestField('australian'); - $field->updateFromObject($object); - - $this->assertSame(false, $field->getData()); - } - - public function testUpdateFromObjectThrowsExceptionIfIsserIsNotPublic() - { - $field = new TestField('privateIsser'); - - $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); - $field->updateFromObject(new Author()); - } - - public function testUpdateFromObjectThrowsExceptionIfPropertyDoesNotExist() - { - $field = new TestField('foobar'); - - $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); - $field->updateFromObject(new Author()); - } - - public function testUpdateObjectUpdatesArrays() - { - $array = array(); - - $field = new TestField('firstName'); - $field->bind('Bernhard'); - $field->updateObject($array); - - $this->assertEquals(array('firstName' => 'Bernhard'), $array); - } - - public function testUpdateObjectUpdatesArraysWithCustomPropertyPath() - { - $array = array(); - - $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); - $field->bind('Bernhard'); - $field->updateObject($array); - - $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); - } - /* * This is important so that bind() can work even if setData() was not called * before @@ -571,50 +432,6 @@ public function testUpdateObjectTreatsEmptyValuesAsArrays() $this->assertEquals(array('firstName' => 'Bernhard'), $array); } - public function testUpdateObjectUpdatesProperties() - { - $object = new Author(); - - $field = new TestField('firstName'); - $field->bind('Bernhard'); - $field->updateObject($object); - - $this->assertEquals('Bernhard', $object->firstName); - } - - public function testUpdateObjectUpdatesPropertiesWithCustomPropertyPath() - { - $object = new Author(); - $object->child = array(); - $object->child['index'] = new Author(); - - $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); - $field->bind('Bernhard'); - $field->updateObject($object); - - $this->assertEquals('Bernhard', $object->child['index']->firstName); - } - - public function testUpdateObjectUpdatesArrayAccess() - { - $object = new \ArrayObject(); - - $field = new TestField('firstName', array('property_path' => '[firstName]')); - $field->bind('Bernhard'); - $field->updateObject($object); - - $this->assertEquals('Bernhard', $object['firstName']); - } - - public function testUpdateObjectThrowsExceptionIfArrayAccessExpected() - { - $field = new TestField('firstName', array('property_path' => '[firstName]')); - $field->bind('Bernhard'); - - $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); - $field->updateObject(new Author()); - } - public function testUpdateObjectDoesNotUpdatePropertyIfPropertyPathIsEmpty() { $object = new Author(); @@ -626,26 +443,6 @@ public function testUpdateObjectDoesNotUpdatePropertyIfPropertyPathIsEmpty() $this->assertEquals(null, $object->firstName); } - public function testUpdateObjectUpdatesSetters() - { - $object = new Author(); - - $field = new TestField('lastName'); - $field->bind('Schussek'); - $field->updateObject($object); - - $this->assertEquals('Schussek', $object->getLastName()); - } - - public function testUpdateObjectThrowsExceptionIfGetterIsNotPublic() - { - $field = new TestField('privateSetter'); - $field->bind('foobar'); - - $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); - $field->updateObject(new Author()); - } - protected function createMockTransformer() { return $this->getMock('Symfony\Component\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false); diff --git a/tests/Symfony/Tests/Component/Form/PropertyPathTest.php b/tests/Symfony/Tests/Component/Form/PropertyPathTest.php index e12296ac400d..3995bd599ae3 100644 --- a/tests/Symfony/Tests/Component/Form/PropertyPathTest.php +++ b/tests/Symfony/Tests/Component/Form/PropertyPathTest.php @@ -2,46 +2,216 @@ namespace Symfony\Tests\Component\Form; +require_once __DIR__ . '/Fixtures/Author.php'; + use Symfony\Component\Form\PropertyPath; +use Symfony\Tests\Component\Form\Fixtures\Author; class PropertyPathTest extends \PHPUnit_Framework_TestCase { - public function testValidPropertyPath() + + public function testGetValueReadsArray() { - $path = new PropertyPath('reference.traversable[index].property'); + $array = array('firstName' => 'Bernhard'); - $this->assertEquals('reference', $path->getCurrent()); - $this->assertTrue($path->hasNext()); - $this->assertTrue($path->isProperty()); - $this->assertFalse($path->isIndex()); + $path = new PropertyPath('firstName'); - $path->next(); + $this->assertEquals('Bernhard', $path->getValue($array)); + } - $this->assertEquals('traversable', $path->getCurrent()); - $this->assertTrue($path->hasNext()); - $this->assertTrue($path->isProperty()); - $this->assertFalse($path->isIndex()); + public function testGetValueReadsZeroIndex() + { + $array = array('Bernhard'); - $path->next(); + $path = new PropertyPath('0'); - $this->assertEquals('index', $path->getCurrent()); - $this->assertTrue($path->hasNext()); - $this->assertFalse($path->isProperty()); - $this->assertTrue($path->isIndex()); + $this->assertEquals('Bernhard', $path->getValue($array)); + } - $path->next(); + public function testGetValueReadsArrayWithCustomPropertyPath() + { + $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); + + $path = new PropertyPath('child[index].firstName'); - $this->assertEquals('property', $path->getCurrent()); - $this->assertFalse($path->hasNext()); - $this->assertTrue($path->isProperty()); - $this->assertFalse($path->isIndex()); + $this->assertEquals('Bernhard', $path->getValue($array)); } - public function testValidPropertyPath_zero() + public function testGetValueReadsProperty() { - $path = new PropertyPath('0'); + $object = new Author(); + $object->firstName = 'Bernhard'; + + $path = new PropertyPath('firstName'); + + $this->assertEquals('Bernhard', $path->getValue($object)); + } + + 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)); + } + + public function testGetValueReadsArrayAccess() + { + $object = new \ArrayObject(); + $object['firstName'] = 'Bernhard'; + + $path = new PropertyPath('[firstName]'); + + $this->assertEquals('Bernhard', $path->getValue($object)); + } + + public function testGetValueThrowsExceptionIfArrayAccessExpected() + { + $path = new PropertyPath('[firstName]'); + + $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); + + $path->getValue(new Author()); + } + + public function testGetValueThrowsExceptionIfPropertyIsNotPublic() + { + $path = new PropertyPath('privateProperty'); + + $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); + + $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 testGetValueThrowsExceptionIfGetterIsNotPublic() + { + $path = new PropertyPath('privateGetter'); + + $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); + + $path->getValue(new Author()); + } + + public function testGetValueReadsIssers() + { + $path = new PropertyPath('australian'); + + $object = new Author(); + $object->setAustralian(false); + + $this->assertSame(false, $path->getValue($object)); + } + + public function testGetValueThrowsExceptionIfIsserIsNotPublic() + { + $path = new PropertyPath('privateIsser'); + + $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); - $this->assertEquals('0', $path->getCurrent()); + $path->getValue(new Author()); + } + + public function testGetValueThrowsExceptionIfPropertyDoesNotExist() + { + $path = new PropertyPath('foobar'); + + $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); + + $path->getValue(new Author()); + } + + public function testSetValueUpdatesArrays() + { + $array = array(); + + $path = new PropertyPath('firstName'); + $path->setValue($array, 'Bernhard'); + + $this->assertEquals(array('firstName' => 'Bernhard'), $array); + } + + 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 testSetValueUpdatesArrayAccess() + { + $object = new \ArrayObject(); + + $path = new PropertyPath('[firstName]'); + $path->setValue($object, 'Bernhard'); + + $this->assertEquals('Bernhard', $object['firstName']); + } + + public function testSetValueThrowsExceptionIfArrayAccessExpected() + { + $path = new PropertyPath('[firstName]'); + + $this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException'); + + $path->setValue(new Author(), 'Bernhard'); + } + + public function testSetValueUpdatesSetters() + { + $object = new Author(); + + $path = new PropertyPath('lastName'); + $path->setValue($object, 'Schussek'); + + $this->assertEquals('Schussek', $object->getLastName()); + } + + public function testSetValueThrowsExceptionIfGetterIsNotPublic() + { + $path = new PropertyPath('privateSetter'); + + $this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException'); + + $path->setValue(new Author(), 'foobar'); } public function testToString() @@ -85,13 +255,4 @@ public function testInvalidPropertyPath_null() new PropertyPath(null); } - - public function testNextThrowsExceptionIfNoNextElement() - { - $path = new PropertyPath('property'); - - $this->setExpectedException('OutOfBoundsException'); - - $path->next(); - } } \ No newline at end of file