diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_group.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/field_group.html.php rename to src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form.html.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index cb41862a84..92cb882861 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -13,7 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\Form\FieldInterface; -use Symfony\Component\Form\FieldGroupInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; /** @@ -148,14 +148,14 @@ class FormHelper extends Helper )); } - public function hidden(/*FieldGroupInterface */$group, array $parameters = array(), $template = null) + public function hidden(/*FormInterface */$form, array $parameters = array(), $template = null) { if (null === $template) { $template = 'FrameworkBundle:Form:hidden.html.php'; } return $this->engine->render($template, array( - 'field' => $group, + 'field' => $form, 'params' => $parameters, )); } @@ -185,8 +185,8 @@ class FormHelper extends Helper $currentFqClassName = get_parent_class($currentFqClassName); } while (null === $template && false !== $currentFqClassName); - if (null === $template && $field instanceof FieldGroupInterface) { - $template = 'FrameworkBundle:Form:field_group.html.php'; + if (null === $template && $field instanceof FormInterface) { + $template = 'FrameworkBundle:Form:form.html.php'; } self::$cache[$fqClassName] = $template; diff --git a/src/Symfony/Bundle/TwigBundle/Extension/FormExtension.php b/src/Symfony/Bundle/TwigBundle/Extension/FormExtension.php index 12b99e396a..9012aa21a0 100644 --- a/src/Symfony/Bundle/TwigBundle/Extension/FormExtension.php +++ b/src/Symfony/Bundle/TwigBundle/Extension/FormExtension.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\TwigBundle\Extension; use Symfony\Component\Form\Form; -use Symfony\Component\Form\FieldGroupInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FieldInterface; use Symfony\Component\Form\CollectionField; use Symfony\Component\Form\HybridField; @@ -47,7 +47,7 @@ class FormExtension extends \Twig_Extension $this->environment = $environment; } - public function setTheme(FieldGroupInterface $group, array $resources) + public function setTheme(FormInterface $group, array $resources) { $this->themes->attach($group, $resources); } @@ -159,11 +159,11 @@ class FormExtension extends \Twig_Extension /** * Renders all hidden fields of the given field group * - * @param FieldGroupInterface $group The field group + * @param FormInterface $group The field group * @param array $params Additional variables passed to the * template */ - public function renderHidden(FieldGroupInterface $group, array $parameters = array()) + public function renderHidden(FormInterface $group, array $parameters = array()) { if (null === $this->templates) { $this->templates = $this->resolveResources($this->resources); diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/form.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/form.html.twig index bd776d0d4f..acbc076f00 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/form.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/form.html.twig @@ -9,7 +9,7 @@ {% endspaceless %} {% endblock field_row %} -{% block field_group %} +{% block form %} {% spaceless %} {{ form_errors(field) }} {% for child in field.visibleFields %} @@ -17,7 +17,7 @@ {% endfor %} {{ form_hidden(field) }} {% endspaceless %} -{% endblock field_group %} +{% endblock form %} {% block errors %} {% spaceless %} diff --git a/src/Symfony/Component/Form/CollectionField.php b/src/Symfony/Component/Form/CollectionField.php index c528bc08ef..9422418c6f 100644 --- a/src/Symfony/Component/Form/CollectionField.php +++ b/src/Symfony/Component/Form/CollectionField.php @@ -24,7 +24,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException; * * @author Bernhard Schussek */ -class CollectionField extends FieldGroup +class CollectionField extends Form { /** * The prototype for the inner fields diff --git a/src/Symfony/Component/Form/DateTimeField.php b/src/Symfony/Component/Form/DateTimeField.php index 416c59c711..3002b7f2bf 100644 --- a/src/Symfony/Component/Form/DateTimeField.php +++ b/src/Symfony/Component/Form/DateTimeField.php @@ -46,7 +46,7 @@ use Symfony\Component\Form\ValueTransformer\ValueTransformerChain; * * @author Bernhard Schussek */ -class DateTimeField extends FieldGroup +class DateTimeField extends Form { const DATETIME = 'datetime'; const STRING = 'string'; diff --git a/src/Symfony/Component/Form/FieldGroup.php b/src/Symfony/Component/Form/FieldGroup.php deleted file mode 100644 index 11c178a634..0000000000 --- a/src/Symfony/Component/Form/FieldGroup.php +++ /dev/null @@ -1,561 +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; - -use Symfony\Component\Form\Exception\AlreadyBoundException; -use Symfony\Component\Form\Exception\UnexpectedTypeException; -use Symfony\Component\Form\Exception\DanglingFieldException; -use Symfony\Component\Form\Exception\FieldDefinitionException; - -/** - * FieldGroup represents an array of widgets bind to names and values. - * - * @author Fabien Potencier - */ -class FieldGroup extends Field implements \IteratorAggregate, FieldGroupInterface -{ - /** - * Contains all the fields of this group - * @var array - */ - protected $fields = array(); - - /** - * Contains the names of bound values who don't belong to any fields - * @var array - */ - protected $extraFields = array(); - - /** - * @inheritDoc - */ - public function __construct($key = null, array $options = array()) - { - $this->addOption('virtual', false); - - parent::__construct($key, $options); - } - - /** - * Clones this group - */ - public function __clone() - { - foreach ($this->fields as $name => $field) { - $field = clone $field; - // this condition is only to "bypass" a PHPUnit bug with mocks - if (null !== $field->getParent()) { - $field->setParent($this); - } - $this->fields[$name] = $field; - } - } - - /** - * Adds a new field to this group. A field must have a unique name within - * the group. Otherwise the existing field is overwritten. - * - * If you add a nested group, this group should also be represented in the - * object hierarchy. If you want to add a group that operates on the same - * hierarchy level, use merge(). - * - * - * class Entity - * { - * public $location; - * } - * - * class Location - * { - * public $longitude; - * public $latitude; - * } - * - * $entity = new Entity(); - * $entity->location = new Location(); - * - * $form = new Form('entity', $entity, $validator); - * - * $locationGroup = new FieldGroup('location'); - * $locationGroup->add(new TextField('longitude')); - * $locationGroup->add(new TextField('latitude')); - * - * $form->add($locationGroup); - * - * - * @param FieldInterface|string $field - * @return FieldInterface - */ - public function add($field) - { - if ($this->isBound()) { - throw new AlreadyBoundException('You cannot add fields after binding a form'); - } - - // if the field is given as string, ask the field factory of the form - // to create a field - if (!$field instanceof FieldInterface) { - if (!is_string($field)) { - throw new UnexpectedTypeException($field, 'FieldInterface or string'); - } - - if (!$this->getRoot() instanceof Form) { - throw new DanglingFieldException('Field groups must be added to a form before fields can be created automatically'); - } - - $factory = $this->getRoot()->getFieldFactory(); - - if (!$factory) { - throw new \LogicException('A field factory must be available to automatically create fields'); - } - - $options = func_num_args() > 1 ? func_get_arg(1) : array(); - $field = $factory->getInstance($this->getData(), $field, $options); - } - - if ('' === $field->getKey() || null === $field->getKey()) { - throw new FieldDefinitionException('You cannot add anonymous fields'); - } - - $this->fields[$field->getKey()] = $field; - - $field->setParent($this); - - $data = $this->getTransformedData(); - - // if the property "data" is NULL, getTransformedData() returns an empty - // string - if (!empty($data)) { - $field->updateFromProperty($data); - } - - return $field; - } - - /** - * Removes the field with the given key. - * - * @param string $key - */ - public function remove($key) - { - $this->fields[$key]->setParent(null); - - unset($this->fields[$key]); - } - - /** - * Returns whether a field with the given key exists. - * - * @param string $key - * @return Boolean - */ - public function has($key) - { - return isset($this->fields[$key]); - } - - /** - * Returns the field with the given key. - * - * @param string $key - * @return FieldInterface - */ - public function get($key) - { - if (isset($this->fields[$key])) { - return $this->fields[$key]; - } - - throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key)); - } - - /** - * Returns all fields in this group - * - * @return array - */ - public function getFields() - { - return $this->fields; - } - - /** - * Returns an array of visible fields from the current schema. - * - * @return array - */ - public function getVisibleFields() - { - return $this->getFieldsByVisibility(false, false); - } - - /** - * Returns an array of visible fields from the current schema. - * - * This variant of the method will recursively get all the - * fields from the nested forms or field groups - * - * @return array - */ - public function getAllVisibleFields() - { - return $this->getFieldsByVisibility(false, true); - } - - /** - * Returns an array of hidden fields from the current schema. - * - * @return array - */ - public function getHiddenFields() - { - return $this->getFieldsByVisibility(true, false); - } - - /** - * Returns an array of hidden fields from the current schema. - * - * This variant of the method will recursively get all the - * fields from the nested forms or field groups - * - * @return array - */ - public function getAllHiddenFields() - { - return $this->getFieldsByVisibility(true, true); - } - - /** - * Returns a filtered array of fields from the current schema. - * - * @param Boolean $hidden Whether to return hidden fields only or visible fields only - * @param Boolean $recursive Whether to recur through embedded schemas - * - * @return array - */ - protected function getFieldsByVisibility($hidden, $recursive) - { - $fields = array(); - $hidden = (Boolean)$hidden; - - foreach ($this->fields as $field) { - if ($field instanceof FieldGroup && $recursive) { - $fields = array_merge($fields, $field->getFieldsByVisibility($hidden, $recursive)); - } else if ($hidden === $field->isHidden()) { - $fields[] = $field; - } - } - - return $fields; - } - - /** - * Initializes the field group with an object to operate on - * - * @see FieldInterface - */ - public function setData($data) - { - parent::setData($data); - - // get transformed data and pass its values to child fields - $data = $this->getTransformedData(); - - if (!empty($data) && !is_array($data) && !is_object($data)) { - throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data))); - } - - if (!empty($data)) { - $this->updateFromObject($data); - } - } - - /** - * Returns the data of the field as it is displayed to the user. - * - * @see FieldInterface - * @return array of field name => value - */ - public function getDisplayedData() - { - $values = array(); - - foreach ($this->fields as $key => $field) { - $values[$key] = $field->getDisplayedData(); - } - - return $values; - } - - /** - * Binds POST data to the field, transforms and validates it. - * - * @param string|array $taintedData The POST data - */ - public function bind($taintedData) - { - if (null === $taintedData) { - $taintedData = array(); - } - - if (!is_array($taintedData)) { - throw new UnexpectedTypeException($taintedData, 'array'); - } - - foreach ($this->fields as $key => $field) { - if (!isset($taintedData[$key])) { - $taintedData[$key] = null; - } - } - - $taintedData = $this->preprocessData($taintedData); - - foreach ($taintedData as $key => $value) { - if ($this->has($key)) { - $this->fields[$key]->bind($value); - } - } - - $data = $this->getTransformedData(); - - $this->updateObject($data); - - // bind and reverse transform the data - parent::bind($data); - - $this->extraFields = array(); - - foreach ($taintedData as $key => $value) { - if (!$this->has($key)) { - $this->extraFields[] = $key; - } - } - } - - /** - * Updates the child fields from the properties of the given data - * - * This method calls updateFromProperty() on all child fields that have a - * property path set. If a child field has no property path set but - * implements FieldGroupInterface, updateProperty() is called on its - * children instead. - * - * @param array|object $objectOrArray - */ - protected function updateFromObject(&$objectOrArray) - { - $iterator = new RecursiveFieldIterator($this); - $iterator = new \RecursiveIteratorIterator($iterator); - - foreach ($iterator as $field) { - $field->updateFromProperty($objectOrArray); - } - } - - /** - * Updates all properties of the given data from the child fields - * - * This method calls updateProperty() on all child fields that have a property - * path set. If a child field has no property path set but implements - * FieldGroupInterface, updateProperty() is called on its children instead. - * - * @param array|object $objectOrArray - */ - protected function updateObject(&$objectOrArray) - { - $iterator = new RecursiveFieldIterator($this); - $iterator = new \RecursiveIteratorIterator($iterator); - - foreach ($iterator as $field) { - $field->updateProperty($objectOrArray); - } - } - - /** - * Processes the bound data before it is passed to the individual fields - * - * The data is in the user format. - * - * @param array $data - * @return array - */ - protected function preprocessData(array $data) - { - return $data; - } - - /** - * @inheritDoc - */ - public function isVirtual() - { - return $this->getOption('virtual'); - } - - /** - * Returns whether this form was bound with extra fields - * - * @return Boolean - */ - public function isBoundWithExtraFields() - { - // TODO: integrate the field names in the error message - return count($this->extraFields) > 0; - } - - /** - * Returns whether the field is valid. - * - * @return Boolean - */ - public function isValid() - { - if (!parent::isValid()) { - return false; - } - - foreach ($this->fields as $field) { - if (!$field->isValid()) { - return false; - } - } - - return true; - } - - /** - * {@inheritDoc} - */ - public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null) - { - if (null !== $pathIterator) { - if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) { - $pathIterator->next(); - - if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') { - $pathIterator->next(); - } - - if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) { - $this->get($pathIterator->current())->addError($error, $pathIterator, $type); - - return; - } - } else if ($type === self::DATA_ERROR) { - $iterator = new RecursiveFieldIterator($this); - $iterator = new \RecursiveIteratorIterator($iterator); - - foreach ($iterator as $field) { - if (null !== ($fieldPath = $field->getPropertyPath())) { - if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) { - if ($pathIterator->hasNext()) { - $pathIterator->next(); - } - - $field->addError($error, $pathIterator, $type); - - return; - } - } - } - } - } - - parent::addError($error); - } - - /** - * Returns whether the field requires a multipart form. - * - * @return Boolean - */ - public function isMultipart() - { - foreach ($this->fields as $field) { - if ($field->isMultipart()) { - return true; - } - } - - return false; - } - - /** - * Returns true if the bound field exists (implements the \ArrayAccess interface). - * - * @param string $key The key of the bound field - * - * @return Boolean true if the widget exists, false otherwise - */ - public function offsetExists($key) - { - return $this->has($key); - } - - /** - * Returns the form field associated with the name (implements the \ArrayAccess interface). - * - * @param string $key The offset of the value to get - * - * @return Field A form field instance - */ - public function offsetGet($key) - { - return $this->get($key); - } - - /** - * Throws an exception saying that values cannot be set (implements the \ArrayAccess interface). - * - * @param string $offset (ignored) - * @param string $value (ignored) - * - * @throws \LogicException - */ - public function offsetSet($key, $field) - { - throw new \LogicException('Use the method add() to add fields'); - } - - /** - * Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface). - * - * @param string $key - * - * @throws \LogicException - */ - public function offsetUnset($key) - { - return $this->remove($key); - } - - /** - * Returns the iterator for this group. - * - * @return \ArrayIterator - */ - public function getIterator() - { - return new \ArrayIterator($this->fields); - } - - /** - * Returns the number of form fields (implements the \Countable interface). - * - * @return integer The number of embedded form fields - */ - public function count() - { - return count($this->fields); - } -} diff --git a/src/Symfony/Component/Form/FileField.php b/src/Symfony/Component/Form/FileField.php index addd1c387b..6e6ddc5839 100644 --- a/src/Symfony/Component/Form/FileField.php +++ b/src/Symfony/Component/Form/FileField.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\Exception\FormException; /** * A file field to upload files. */ -class FileField extends FieldGroup +class FileField extends Form { /** * Whether the size of the uploaded file exceeds the upload_max_filesize @@ -61,7 +61,7 @@ class FileField extends FieldGroup * This way the file can survive if the form does not validate and is * resubmitted. * - * @see Symfony\Component\Form\FieldGroup::preprocessData() + * @see Symfony\Component\Form\Form::preprocessData() */ protected function preprocessData(array $data) { diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 0e18a7f495..d373482d93 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -15,6 +15,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\FileBag; use Symfony\Component\Validator\ValidatorInterface; use Symfony\Component\Form\Exception\FormException; +use Symfony\Component\Form\Exception\AlreadyBoundException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\DanglingFieldException; +use Symfony\Component\Form\Exception\FieldDefinitionException; use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface; /** @@ -32,13 +36,19 @@ use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface; * @author Fabien Potencier * @author Bernhard Schussek */ -class Form extends FieldGroup +class Form extends Field implements \IteratorAggregate, FormInterface { /** - * The validator to validate form values - * @var ValidatorInterface + * Contains all the fields of this group + * @var array */ - protected $validator = null; + protected $fields = array(); + + /** + * Contains the names of bound values who don't belong to any fields + * @var array + */ + protected $extraFields = array(); /** * Constructor. @@ -48,19 +58,14 @@ class Form extends FieldGroup * @param ValidatorInterface $validator * @param array $options */ - public function __construct($name = null, $data = null, ValidatorInterface $validator = null, array $options = array()) + public function __construct($name = null, array $options = array()) { - $this->validator = $validator; - - // Prefill the form with the given data - if (null !== $data) { - $this->setData($data); - } - $this->addOption('csrf_field_name', '_token'); $this->addOption('csrf_provider'); $this->addOption('field_factory'); $this->addOption('validation_groups'); + $this->addOption('virtual', false); + $this->addOption('validator'); if (isset($options['validation_groups'])) { $options['validation_groups'] = (array)$options['validation_groups']; @@ -70,11 +75,12 @@ class Form extends FieldGroup // If data is passed to this constructor, objects from parent forms // should be ignored - if (null !== $data) { - $this->setPropertyPath(null); - } +// if (null !== $data) { +// $this->setPropertyPath(null); +// } // Enable CSRF protection, if necessary + // TODO only in root form if ($this->getOption('csrf_provider')) { if (!$this->getOption('csrf_provider') instanceof CsrfProviderInterface) { throw new FormException('The object passed to the "csrf_provider" option must implement CsrfProviderInterface'); @@ -91,6 +97,539 @@ class Form extends FieldGroup } } + /** + * Clones this group + */ + public function __clone() + { + foreach ($this->fields as $name => $field) { + $field = clone $field; + // this condition is only to "bypass" a PHPUnit bug with mocks + if (null !== $field->getParent()) { + $field->setParent($this); + } + $this->fields[$name] = $field; + } + } + + /** + * Adds a new field to this group. A field must have a unique name within + * the group. Otherwise the existing field is overwritten. + * + * If you add a nested group, this group should also be represented in the + * object hierarchy. If you want to add a group that operates on the same + * hierarchy level, use merge(). + * + * + * class Entity + * { + * public $location; + * } + * + * class Location + * { + * public $longitude; + * public $latitude; + * } + * + * $entity = new Entity(); + * $entity->location = new Location(); + * + * $form = new Form('entity', $entity, $validator); + * + * $locationGroup = new Form('location'); + * $locationGroup->add(new TextField('longitude')); + * $locationGroup->add(new TextField('latitude')); + * + * $form->add($locationGroup); + * + * + * @param FieldInterface|string $field + * @return FieldInterface + */ + public function add($field) + { + if ($this->isBound()) { + throw new AlreadyBoundException('You cannot add fields after binding a form'); + } + + // if the field is given as string, ask the field factory of the form + // to create a field + if (!$field instanceof FieldInterface) { + if (!is_string($field)) { + throw new UnexpectedTypeException($field, 'FieldInterface or string'); + } + + $factory = $this->getRoot()->getFieldFactory(); + + if (!$factory) { + throw new \LogicException('A field factory must be available to automatically create fields'); + } + + $options = func_num_args() > 1 ? func_get_arg(1) : array(); + $field = $factory->getInstance($this->getData(), $field, $options); + } + + if ('' === $field->getKey() || null === $field->getKey()) { + throw new FieldDefinitionException('You cannot add anonymous fields'); + } + + $this->fields[$field->getKey()] = $field; + + $field->setParent($this); + + $data = $this->getTransformedData(); + + // if the property "data" is NULL, getTransformedData() returns an empty + // string + if (!empty($data)) { + $field->updateFromProperty($data); + } + + return $field; + } + + /** + * Removes the field with the given key. + * + * @param string $key + */ + public function remove($key) + { + $this->fields[$key]->setParent(null); + + unset($this->fields[$key]); + } + + /** + * Returns whether a field with the given key exists. + * + * @param string $key + * @return Boolean + */ + public function has($key) + { + return isset($this->fields[$key]); + } + + /** + * Returns the field with the given key. + * + * @param string $key + * @return FieldInterface + */ + public function get($key) + { + if (isset($this->fields[$key])) { + return $this->fields[$key]; + } + + throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key)); + } + + /** + * Returns all fields in this group + * + * @return array + */ + public function getFields() + { + return $this->fields; + } + + /** + * Returns an array of visible fields from the current schema. + * + * @return array + */ + public function getVisibleFields() + { + return $this->getFieldsByVisibility(false, false); + } + + /** + * Returns an array of visible fields from the current schema. + * + * This variant of the method will recursively get all the + * fields from the nested forms or field groups + * + * @return array + */ + public function getAllVisibleFields() + { + return $this->getFieldsByVisibility(false, true); + } + + /** + * Returns an array of hidden fields from the current schema. + * + * @return array + */ + public function getHiddenFields() + { + return $this->getFieldsByVisibility(true, false); + } + + /** + * Returns an array of hidden fields from the current schema. + * + * This variant of the method will recursively get all the + * fields from the nested forms or field groups + * + * @return array + */ + public function getAllHiddenFields() + { + return $this->getFieldsByVisibility(true, true); + } + + /** + * Returns a filtered array of fields from the current schema. + * + * @param Boolean $hidden Whether to return hidden fields only or visible fields only + * @param Boolean $recursive Whether to recur through embedded schemas + * + * @return array + */ + protected function getFieldsByVisibility($hidden, $recursive) + { + $fields = array(); + $hidden = (Boolean)$hidden; + + foreach ($this->fields as $field) { + if ($field instanceof Form && $recursive) { + $fields = array_merge($fields, $field->getFieldsByVisibility($hidden, $recursive)); + } else if ($hidden === $field->isHidden()) { + $fields[] = $field; + } + } + + return $fields; + } + + /** + * Initializes the field group with an object to operate on + * + * @see FieldInterface + */ + public function setData($data) + { + parent::setData($data); + + // get transformed data and pass its values to child fields + $data = $this->getTransformedData(); + + if (!empty($data) && !is_array($data) && !is_object($data)) { + throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data))); + } + + if (!empty($data)) { + $this->updateFromObject($data); + } + } + + /** + * Returns the data of the field as it is displayed to the user. + * + * @see FieldInterface + * @return array of field name => value + */ + public function getDisplayedData() + { + $values = array(); + + foreach ($this->fields as $key => $field) { + $values[$key] = $field->getDisplayedData(); + } + + return $values; + } + + /** + * Binds POST data to the field, transforms and validates it. + * + * @param string|array $taintedData The POST data + */ + public function bind($taintedData) + { + if (null === $taintedData) { + $taintedData = array(); + } + + if (!is_array($taintedData)) { + throw new UnexpectedTypeException($taintedData, 'array'); + } + + foreach ($this->fields as $key => $field) { + if (!isset($taintedData[$key])) { + $taintedData[$key] = null; + } + } + + $taintedData = $this->preprocessData($taintedData); + + foreach ($taintedData as $key => $value) { + if ($this->has($key)) { + $this->fields[$key]->bind($value); + } + } + + $data = $this->getTransformedData(); + + $this->updateObject($data); + + // bind and reverse transform the data + parent::bind($data); + + $this->extraFields = array(); + + foreach ($taintedData as $key => $value) { + if (!$this->has($key)) { + $this->extraFields[] = $key; + } + } + + if ($this->isRoot() && null !== $this->getOption('validator')) { +// if () { +// throw new FormException('A validator is required for binding. Forgot to pass it to the constructor of the form?'); +// } + + if ($violations = $this->getOption('validator')->validate($this, $this->getOption('validation_groups'))) { + // TODO: test me + foreach ($violations as $violation) { + $propertyPath = new PropertyPath($violation->getPropertyPath()); + $iterator = $propertyPath->getIterator(); + + if ($iterator->current() == 'data') { + $type = self::DATA_ERROR; + $iterator->next(); // point at the first data element + } else { + $type = self::FIELD_ERROR; + } + + $this->addError(new FieldError($violation->getMessageTemplate(), $violation->getMessageParameters()), $iterator, $type); + } + } + } + } + + /** + * Updates the child fields from the properties of the given data + * + * This method calls updateFromProperty() on all child fields that have a + * property path set. If a child field has no property path set but + * implements FormInterface, updateProperty() is called on its + * children instead. + * + * @param array|object $objectOrArray + */ + protected function updateFromObject(&$objectOrArray) + { + $iterator = new RecursiveFieldIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) { + $field->updateFromProperty($objectOrArray); + } + } + + /** + * Updates all properties of the given data from the child fields + * + * This method calls updateProperty() on all child fields that have a property + * path set. If a child field has no property path set but implements + * FormInterface, updateProperty() is called on its children instead. + * + * @param array|object $objectOrArray + */ + protected function updateObject(&$objectOrArray) + { + $iterator = new RecursiveFieldIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) { + $field->updateProperty($objectOrArray); + } + } + + /** + * Processes the bound data before it is passed to the individual fields + * + * The data is in the user format. + * + * @param array $data + * @return array + */ + protected function preprocessData(array $data) + { + return $data; + } + + /** + * @inheritDoc + */ + public function isVirtual() + { + return $this->getOption('virtual'); + } + + /** + * Returns whether this form was bound with extra fields + * + * @return Boolean + */ + public function isBoundWithExtraFields() + { + // TODO: integrate the field names in the error message + return count($this->extraFields) > 0; + } + + /** + * Returns whether the field is valid. + * + * @return Boolean + */ + public function isValid() + { + if (!parent::isValid()) { + return false; + } + + foreach ($this->fields as $field) { + if (!$field->isValid()) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null) + { + if (null !== $pathIterator) { + if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) { + $pathIterator->next(); + + if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') { + $pathIterator->next(); + } + + if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) { + $this->get($pathIterator->current())->addError($error, $pathIterator, $type); + + return; + } + } else if ($type === self::DATA_ERROR) { + $iterator = new RecursiveFieldIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) { + if (null !== ($fieldPath = $field->getPropertyPath())) { + if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) { + if ($pathIterator->hasNext()) { + $pathIterator->next(); + } + + $field->addError($error, $pathIterator, $type); + + return; + } + } + } + } + } + + parent::addError($error); + } + + /** + * Returns whether the field requires a multipart form. + * + * @return Boolean + */ + public function isMultipart() + { + foreach ($this->fields as $field) { + if ($field->isMultipart()) { + return true; + } + } + + return false; + } + + /** + * Returns true if the bound field exists (implements the \ArrayAccess interface). + * + * @param string $key The key of the bound field + * + * @return Boolean true if the widget exists, false otherwise + */ + public function offsetExists($key) + { + return $this->has($key); + } + + /** + * Returns the form field associated with the name (implements the \ArrayAccess interface). + * + * @param string $key The offset of the value to get + * + * @return Field A form field instance + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Throws an exception saying that values cannot be set (implements the \ArrayAccess interface). + * + * @param string $offset (ignored) + * @param string $value (ignored) + * + * @throws \LogicException + */ + public function offsetSet($key, $field) + { + throw new \LogicException('Use the method add() to add fields'); + } + + /** + * Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface). + * + * @param string $key + * + * @throws \LogicException + */ + public function offsetUnset($key) + { + return $this->remove($key); + } + + /** + * Returns the iterator for this group. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->fields); + } + + /** + * Returns the number of form fields (implements the \Countable interface). + * + * @return integer The number of embedded form fields + */ + public function count() + { + return count($this->fields); + } + /** * Returns a factory for automatically creating fields based on metadata * available for a form's object @@ -173,37 +712,6 @@ class Form extends FieldGroup $this->bind(self::deepArrayUnion($values, $files)); } - /** - * {@inheritDoc} - */ - public function bind($values) - { - parent::bind($values); - - if ($this->getParent() === null) { - if ($this->validator === null) { - throw new FormException('A validator is required for binding. Forgot to pass it to the constructor of the form?'); - } - - if ($violations = $this->validator->validate($this, $this->getOption('validation_groups'))) { - // TODO: test me - foreach ($violations as $violation) { - $propertyPath = new PropertyPath($violation->getPropertyPath()); - $iterator = $propertyPath->getIterator(); - - if ($iterator->current() == 'data') { - $type = self::DATA_ERROR; - $iterator->next(); // point at the first data element - } else { - $type = self::FIELD_ERROR; - } - - $this->addError(new FieldError($violation->getMessageTemplate(), $violation->getMessageParameters()), $iterator, $type); - } - } - } - } - /** * @return true if this form is CSRF protected */ diff --git a/src/Symfony/Component/Form/FormContext.php b/src/Symfony/Component/Form/FormContext.php index 9a4dc7b0d1..4ba1ae8636 100644 --- a/src/Symfony/Component/Form/FormContext.php +++ b/src/Symfony/Component/Form/FormContext.php @@ -150,17 +150,19 @@ class FormContext implements FormContextInterface */ public function getForm($name, $data = null) { - return new Form( + $form = new Form( $name, - $data, - $this->validator, array( + 'validator' => $this->validator, 'csrf_field_name' => $this->csrfFieldName, 'csrf_provider' => $this->csrfProtection ? $this->csrfProvider : null, 'validation_groups' => $this->validationGroups, 'field_factory' => $this->fieldFactory, ) ); + $form->setData($data); + + return $form; } /** diff --git a/src/Symfony/Component/Form/FieldGroupInterface.php b/src/Symfony/Component/Form/FormInterface.php similarity index 89% rename from src/Symfony/Component/Form/FieldGroupInterface.php rename to src/Symfony/Component/Form/FormInterface.php index 62f03073df..d36430782c 100644 --- a/src/Symfony/Component/Form/FieldGroupInterface.php +++ b/src/Symfony/Component/Form/FormInterface.php @@ -16,7 +16,7 @@ namespace Symfony\Component\Form; * * @author Bernhard Schussek */ -interface FieldGroupInterface extends FieldInterface, \ArrayAccess, \Traversable, \Countable +interface FormInterface extends FieldInterface, \ArrayAccess, \Traversable, \Countable { /** * Returns whether this field group is virtual @@ -27,7 +27,7 @@ interface FieldGroupInterface extends FieldInterface, \ArrayAccess, \Traversable * Example: * * - * $group = new FieldGroup('address'); + * $group = new Form('address'); * $group->add(new TextField('street')); * $group->add(new TextField('postal_code')); * $form->add($group); diff --git a/src/Symfony/Component/Form/HybridField.php b/src/Symfony/Component/Form/HybridField.php index 97caa4c2e7..b16db73d65 100644 --- a/src/Symfony/Component/Form/HybridField.php +++ b/src/Symfony/Component/Form/HybridField.php @@ -24,7 +24,7 @@ use Symfony\Component\Form\Exception\FormException; * * @author Bernhard Schussek */ -class HybridField extends FieldGroup +class HybridField extends Form { const FIELD = 0; const GROUP = 1; diff --git a/src/Symfony/Component/Form/RecursiveFieldIterator.php b/src/Symfony/Component/Form/RecursiveFieldIterator.php index 59589ca4ae..f0801476d7 100644 --- a/src/Symfony/Component/Form/RecursiveFieldIterator.php +++ b/src/Symfony/Component/Form/RecursiveFieldIterator.php @@ -21,7 +21,7 @@ namespace Symfony\Component\Form; */ class RecursiveFieldIterator extends \IteratorIterator implements \RecursiveIterator { - public function __construct(FieldGroupInterface $group) + public function __construct(FormInterface $group) { parent::__construct($group); } @@ -33,7 +33,7 @@ class RecursiveFieldIterator extends \IteratorIterator implements \RecursiveIter public function hasChildren() { - return $this->current() instanceof FieldGroupInterface + return $this->current() instanceof FormInterface && $this->current()->isVirtual(); } } diff --git a/src/Symfony/Component/Form/RepeatedField.php b/src/Symfony/Component/Form/RepeatedField.php index 7e3fdb657e..fc843bb89a 100644 --- a/src/Symfony/Component/Form/RepeatedField.php +++ b/src/Symfony/Component/Form/RepeatedField.php @@ -21,7 +21,7 @@ namespace Symfony\Component\Form; * * @author Bernhard Schussek */ -class RepeatedField extends FieldGroup +class RepeatedField extends Form { /** * The prototype for the inner fields diff --git a/src/Symfony/Component/Form/TimeField.php b/src/Symfony/Component/Form/TimeField.php index 3fe9c5d03b..3f221db2b4 100644 --- a/src/Symfony/Component/Form/TimeField.php +++ b/src/Symfony/Component/Form/TimeField.php @@ -37,7 +37,7 @@ use Symfony\Component\Form\ValueTransformer\ValueTransformerChain; * * data_timezone: The timezone of the data. Default: UTC. * * user_timezone: The timezone of the user entering a new value. Default: UTC. */ -class TimeField extends FieldGroup +class TimeField extends Form { const INPUT = 'input'; const CHOICE = 'choice'; diff --git a/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php b/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php index 444c0a792a..9f39b91629 100644 --- a/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php +++ b/tests/Symfony/Tests/Component/Form/CollectionFieldTest.php @@ -14,7 +14,7 @@ namespace Symfony\Tests\Component\Form; require_once __DIR__ . '/Fixtures/TestField.php'; use Symfony\Component\Form\CollectionField; -use Symfony\Component\Form\FieldGroup; +use Symfony\Component\Form\Form; use Symfony\Tests\Component\Form\Fixtures\TestField; class CollectionFieldTest extends \PHPUnit_Framework_TestCase diff --git a/tests/Symfony/Tests/Component/Form/FieldGroupTest.php b/tests/Symfony/Tests/Component/Form/FieldGroupTest.php deleted file mode 100644 index a479217e7f..0000000000 --- a/tests/Symfony/Tests/Component/Form/FieldGroupTest.php +++ /dev/null @@ -1,836 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Tests\Component\Form; - -require_once __DIR__ . '/Fixtures/Author.php'; -require_once __DIR__ . '/Fixtures/TestField.php'; -require_once __DIR__ . '/Fixtures/TestFieldGroup.php'; - -use Symfony\Component\Form\Field; -use Symfony\Component\Form\FieldError; -use Symfony\Component\Form\FieldInterface; -use Symfony\Component\Form\FieldGroup; -use Symfony\Component\Form\PropertyPath; -use Symfony\Tests\Component\Form\Fixtures\Author; -use Symfony\Tests\Component\Form\Fixtures\TestField; -use Symfony\Tests\Component\Form\Fixtures\TestFieldGroup; - -class FieldGroupTest extends \PHPUnit_Framework_TestCase -{ - public function testSupportsArrayAccess() - { - $group = new TestFieldGroup('author'); - $group->add($this->createMockField('firstName')); - $this->assertEquals($group->get('firstName'), $group['firstName']); - $this->assertTrue(isset($group['firstName'])); - } - - public function testSupportsUnset() - { - $group = new TestFieldGroup('author'); - $group->add($this->createMockField('firstName')); - unset($group['firstName']); - $this->assertFalse(isset($group['firstName'])); - } - - public function testDoesNotSupportAddingFields() - { - $group = new TestFieldGroup('author'); - $this->setExpectedException('LogicException'); - $group[] = $this->createMockField('lastName'); - } - - public function testSupportsCountable() - { - $group = new TestFieldGroup('group'); - $group->add($this->createMockField('firstName')); - $group->add($this->createMockField('lastName')); - $this->assertEquals(2, count($group)); - - $group->add($this->createMockField('australian')); - $this->assertEquals(3, count($group)); - } - - public function testSupportsIterable() - { - $group = new TestFieldGroup('group'); - $group->add($field1 = $this->createMockField('field1')); - $group->add($field2 = $this->createMockField('field2')); - $group->add($field3 = $this->createMockField('field3')); - - $expected = array( - 'field1' => $field1, - 'field2' => $field2, - 'field3' => $field3, - ); - - $this->assertEquals($expected, iterator_to_array($group)); - } - - public function testIsBound() - { - $group = new TestFieldGroup('author'); - $this->assertFalse($group->isBound()); - $group->bind(array('firstName' => 'Bernhard')); - $this->assertTrue($group->isBound()); - } - - public function testValidIfAllFieldsAreValid() - { - $group = new TestFieldGroup('author'); - $group->add($this->createValidMockField('firstName')); - $group->add($this->createValidMockField('lastName')); - - $group->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); - - $this->assertTrue($group->isValid()); - } - - public function testInvalidIfFieldIsInvalid() - { - $group = new TestFieldGroup('author'); - $group->add($this->createInvalidMockField('firstName')); - $group->add($this->createValidMockField('lastName')); - - $group->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); - - $this->assertFalse($group->isValid()); - } - - public function testInvalidIfBoundWithExtraFields() - { - $group = new TestFieldGroup('author'); - $group->add($this->createValidMockField('firstName')); - $group->add($this->createValidMockField('lastName')); - - $group->bind(array('foo' => 'bar', 'firstName' => 'Bernhard', 'lastName' => 'Potencier')); - - $this->assertTrue($group->isBoundWithExtraFields()); - } - - public function testHasNoErrorsIfOnlyFieldHasErrors() - { - $group = new TestFieldGroup('author'); - $group->add($this->createInvalidMockField('firstName')); - - $group->bind(array('firstName' => 'Bernhard')); - - $this->assertFalse($group->hasErrors()); - } - - public function testBindForwardsPreprocessedData() - { - $field = $this->createMockField('firstName'); - - $group = $this->getMock( - 'Symfony\Tests\Component\Form\Fixtures\TestFieldGroup', - array('preprocessData'), // only mock preprocessData() - array('author') - ); - - // The data array is prepared directly after binding - $group->expects($this->once()) - ->method('preprocessData') - ->with($this->equalTo(array('firstName' => 'Bernhard'))) - ->will($this->returnValue(array('firstName' => 'preprocessed[Bernhard]'))); - $group->add($field); - - // The preprocessed data is then forwarded to the fields - $field->expects($this->once()) - ->method('bind') - ->with($this->equalTo('preprocessed[Bernhard]')); - - $group->bind(array('firstName' => 'Bernhard')); - } - - public function testBindForwardsNullIfValueIsMissing() - { - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('bind') - ->with($this->equalTo(null)); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $group->bind(array()); - } - - public function testAddErrorMapsFieldValidationErrorsOntoFields() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('addError') - ->with($this->equalTo($error)); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('fields[firstName].data'); - - $group->addError($error, $path->getIterator(), FieldGroup::FIELD_ERROR); - } - - public function testAddErrorMapsFieldValidationErrorsOntoFieldsWithinNestedFieldGroups() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('addError') - ->with($this->equalTo($error)); - - $group = new TestFieldGroup('author'); - $innerGroup = new TestFieldGroup('names'); - $innerGroup->add($field); - $group->add($innerGroup); - - $path = new PropertyPath('fields[names].fields[firstName].data'); - - $group->addError($error, $path->getIterator(), FieldGroup::FIELD_ERROR); - } - - public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('foo'); - $field->expects($this->never()) - ->method('addError'); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('fields[bar].data'); - - $group->addError($error, $path->getIterator(), FieldGroup::FIELD_ERROR); - - $this->assertEquals(array($error), $group->getErrors()); - } - - public function testAddErrorKeepsFieldValidationErrorsIfFieldIsHidden() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('firstName'); - $field->expects($this->any()) - ->method('isHidden') - ->will($this->returnValue(true)); - $field->expects($this->never()) - ->method('addError'); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('fields[firstName].data'); - - $group->addError($error, $path->getIterator(), FieldGroup::FIELD_ERROR); - - $this->assertEquals(array($error), $group->getErrors()); - } - - public function testAddErrorMapsDataValidationErrorsOntoFields() - { - $error = new FieldError('Message'); - - // path is expected to point at "firstName" - $expectedPath = new PropertyPath('firstName'); - $expectedPathIterator = $expectedPath->getIterator(); - - $field = $this->createMockField('firstName'); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('firstName'))); - $field->expects($this->once()) - ->method('addError') - ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('firstName'); - - $group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR); - } - - public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('foo'); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('foo'))); - $field->expects($this->never()) - ->method('addError'); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('bar'); - - $group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR); - } - - public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden() - { - $error = new FieldError('Message'); - - $field = $this->createMockField('firstName'); - $field->expects($this->any()) - ->method('isHidden') - ->will($this->returnValue(true)); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('firstName'))); - $field->expects($this->never()) - ->method('addError'); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('firstName'); - - $group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR); - } - - public function testAddErrorMapsDataValidationErrorsOntoNestedFields() - { - $error = new FieldError('Message'); - - // path is expected to point at "street" - $expectedPath = new PropertyPath('address.street'); - $expectedPathIterator = $expectedPath->getIterator(); - $expectedPathIterator->next(); - - $field = $this->createMockField('address'); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('address'))); - $field->expects($this->once()) - ->method('addError') - ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $path = new PropertyPath('address.street'); - - $group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR); - } - - public function testAddErrorMapsErrorsOntoFieldsInVirtualGroups() - { - $error = new FieldError('Message'); - - // path is expected to point at "address" - $expectedPath = new PropertyPath('address'); - $expectedPathIterator = $expectedPath->getIterator(); - - $field = $this->createMockField('address'); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('address'))); - $field->expects($this->once()) - ->method('addError') - ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(FieldGroup::DATA_ERROR)); - - $group = new TestFieldGroup('author'); - $nestedGroup = new TestFieldGroup('nested', array('virtual' => true)); - $nestedGroup->add($field); - $group->add($nestedGroup); - - $path = new PropertyPath('address'); - - $group->addError($error, $path->getIterator(), FieldGroup::DATA_ERROR); - } - - public function testAddThrowsExceptionIfAlreadyBound() - { - $group = new TestFieldGroup('author'); - $group->add($this->createMockField('firstName')); - $group->bind(array('firstName' => 'Bernhard')); - - $this->setExpectedException('Symfony\Component\Form\Exception\AlreadyBoundException'); - $group->add($this->createMockField('lastName')); - } - - public function testAddSetsFieldParent() - { - $group = new TestFieldGroup('author'); - - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('setParent'); - // PHPUnit fails to compare infinitely recursive objects - //->with($this->equalTo($group)); - - $group->add($field); - } - - public function testRemoveUnsetsFieldParent() - { - $group = new TestFieldGroup('author'); - - $field = $this->createMockField('firstName'); - $field->expects($this->exactly(2)) - ->method('setParent'); - // PHPUnit fails to compare subsequent method calls with different arguments - - $group->add($field); - $group->remove('firstName'); - } - - public function testAddUpdatesFieldFromTransformedData() - { - $originalAuthor = new Author(); - $transformedAuthor = new Author(); - // the authors should differ to make sure the test works - $transformedAuthor->firstName = 'Foo'; - - $group = new TestFieldGroup('author'); - - $transformer = $this->createMockTransformer(); - $transformer->expects($this->once()) - ->method('transform') - ->with($this->equalTo($originalAuthor)) - ->will($this->returnValue($transformedAuthor)); - - $group->setValueTransformer($transformer); - $group->setData($originalAuthor); - - $field = $this->createMockField('firstName'); - $field->expects($this->any()) - ->method('getPropertyPath') - ->will($this->returnValue(new PropertyPath('firstName'))); - $field->expects($this->once()) - ->method('updateFromProperty') - ->with($this->equalTo($transformedAuthor)); - - $group->add($field); - } - - public function testAddDoesNotUpdateFieldIfTransformedDataIsEmpty() - { - $originalAuthor = new Author(); - - $group = new TestFieldGroup('author'); - - $transformer = $this->createMockTransformer(); - $transformer->expects($this->once()) - ->method('transform') - ->with($this->equalTo($originalAuthor)) - ->will($this->returnValue('')); - - $group->setValueTransformer($transformer); - $group->setData($originalAuthor); - - $field = $this->createMockField('firstName'); - $field->expects($this->never()) - ->method('updateFromProperty'); - - $group->add($field); - } - - /** - * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - */ - public function testAddThrowsExceptionIfNoFieldOrString() - { - $group = new TestFieldGroup('author'); - - $group->add(1234); - } - - /** - * @expectedException Symfony\Component\Form\Exception\FormException - */ - public function testAddThrowsExceptionIfAnonymousField() - { - $group = new TestFieldGroup('author'); - - $field = $this->createMockField(''); - - $group->add($field); - } - - /** - * @expectedException Symfony\Component\Form\Exception\DanglingFieldException - */ - public function testAddThrowsExceptionIfStringButNoRootForm() - { - $group = new TestFieldGroup('author'); - - $group->add('firstName'); - } - - public function testAddThrowsExceptionIfStringButNoFieldFactory() - { - $form = $this->createMockForm(); - $form->expects($this->once()) - ->method('getFieldFactory') - ->will($this->returnValue(null)); - - $group = new TestFieldGroup('author'); - $group->setParent($form); - - $this->setExpectedException('\LogicException'); - - $group->add('firstName'); - } - - public function testAddUsesFieldFromFactoryIfStringIsGiven() - { - $author = new \stdClass(); - $field = $this->createMockField('firstName'); - - $factory = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryInterface'); - $factory->expects($this->once()) - ->method('getInstance') - ->with($this->equalTo($author), $this->equalTo('firstName'), $this->equalTo(array('foo' => 'bar'))) - ->will($this->returnValue($field)); - $form = $this->createMockForm(); - $form->expects($this->once()) - ->method('getFieldFactory') - ->will($this->returnValue($factory)); - - $group = new TestFieldGroup('author'); - $group->setParent($form); - $group->setData($author); - $group->add('firstName', array('foo' => 'bar')); - - $this->assertSame($field, $group['firstName']); - } - - public function testSetDataUpdatesAllFieldsFromTransformedData() - { - $originalAuthor = new Author(); - $transformedAuthor = new Author(); - // the authors should differ to make sure the test works - $transformedAuthor->firstName = 'Foo'; - - $group = new TestFieldGroup('author'); - - $transformer = $this->createMockTransformer(); - $transformer->expects($this->once()) - ->method('transform') - ->with($this->equalTo($originalAuthor)) - ->will($this->returnValue($transformedAuthor)); - - $group->setValueTransformer($transformer); - - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('updateFromProperty') - ->with($this->equalTo($transformedAuthor)); - - $group->add($field); - - $field = $this->createMockField('lastName'); - $field->expects($this->once()) - ->method('updateFromProperty') - ->with($this->equalTo($transformedAuthor)); - - $group->add($field); - - $group->setData($originalAuthor); - } - - /** - * The use case for this test are groups whose fields should be mapped - * directly onto properties of the form's object. - * - * Example: - * - * - * $dateRangeField = new FieldGroup('dateRange'); - * $dateRangeField->add(new DateField('startDate')); - * $dateRangeField->add(new DateField('endDate')); - * $form->add($dateRangeField); - * - * - * If $dateRangeField is not virtual, the property "dateRange" must be - * present on the form's object. In this property, an object or array - * with the properties "startDate" and "endDate" is expected. - * - * If $dateRangeField is virtual though, it's children are mapped directly - * onto the properties "startDate" and "endDate" of the form's object. - */ - public function testSetDataSkipsVirtualFieldGroups() - { - $author = new Author(); - $author->firstName = 'Foo'; - - $group = new TestFieldGroup('author'); - $nestedGroup = new TestFieldGroup('personal_data', array( - 'virtual' => true, - )); - - // both fields are in the nested group but receive the object of the - // top-level group because the nested group is virtual - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('updateFromProperty') - ->with($this->equalTo($author)); - - $nestedGroup->add($field); - - $field = $this->createMockField('lastName'); - $field->expects($this->once()) - ->method('updateFromProperty') - ->with($this->equalTo($author)); - - $nestedGroup->add($field); - - $group->add($nestedGroup); - $group->setData($author); - } - - public function testSetDataThrowsAnExceptionIfArgumentIsNotObjectOrArray() - { - $group = new TestFieldGroup('author'); - - $this->setExpectedException('InvalidArgumentException'); - - $group->setData('foobar'); - } - - public function testBindUpdatesTransformedDataFromAllFields() - { - $originalAuthor = new Author(); - $transformedAuthor = new Author(); - // the authors should differ to make sure the test works - $transformedAuthor->firstName = 'Foo'; - - $group = new TestFieldGroup('author'); - - $transformer = $this->createMockTransformer(); - $transformer->expects($this->exactly(2)) - ->method('transform') - // the method is first called with NULL, then - // with $originalAuthor -> not testable by PHPUnit - // ->with($this->equalTo(null)) - // ->with($this->equalTo($originalAuthor)) - ->will($this->returnValue($transformedAuthor)); - - $group->setValueTransformer($transformer); - $group->setData($originalAuthor); - - $field = $this->createMockField('firstName'); - $field->expects($this->once()) - ->method('updateProperty') - ->with($this->equalTo($transformedAuthor)); - - $group->add($field); - - $field = $this->createMockField('lastName'); - $field->expects($this->once()) - ->method('updateProperty') - ->with($this->equalTo($transformedAuthor)); - - $group->add($field); - - $group->bind(array()); // irrelevant - } - - public function testGetDataReturnsObject() - { - $group = new TestFieldGroup('author'); - $object = new \stdClass(); - $group->setData($object); - $this->assertEquals($object, $group->getData()); - } - - public function testGetDisplayedDataForwardsCall() - { - $field = $this->createValidMockField('firstName'); - $field->expects($this->atLeastOnce()) - ->method('getDisplayedData') - ->will($this->returnValue('Bernhard')); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $this->assertEquals(array('firstName' => 'Bernhard'), $group->getDisplayedData()); - } - - public function testIsMultipartIfAnyFieldIsMultipart() - { - $group = new TestFieldGroup('author'); - $group->add($this->createMultipartMockField('firstName')); - $group->add($this->createNonMultipartMockField('lastName')); - - $this->assertTrue($group->isMultipart()); - } - - public function testIsNotMultipartIfNoFieldIsMultipart() - { - $group = new TestFieldGroup('author'); - $group->add($this->createNonMultipartMockField('firstName')); - $group->add($this->createNonMultipartMockField('lastName')); - - $this->assertFalse($group->isMultipart()); - } - - public function testSupportsClone() - { - $group = new TestFieldGroup('author'); - $group->add($this->createMockField('firstName')); - - $clone = clone $group; - - $this->assertNotSame($clone['firstName'], $group['firstName']); - } - - public function testBindWithoutPriorSetData() - { - return; // TODO - $field = $this->createMockField('firstName'); - $field->expects($this->any()) - ->method('getData') - ->will($this->returnValue('Bernhard')); - - $group = new TestFieldGroup('author'); - $group->add($field); - - $group->bind(array('firstName' => 'Bernhard')); - - $this->assertEquals(array('firstName' => 'Bernhard'), $group->getData()); - } - - public function testGetHiddenFieldsReturnsOnlyHiddenFields() - { - $group = $this->getGroupWithBothVisibleAndHiddenField(); - - $hiddenFields = $group->getHiddenFields(true, false); - - $this->assertSame(array($group['hiddenField']), $hiddenFields); - } - - public function testGetVisibleFieldsReturnsOnlyVisibleFields() - { - $group = $this->getGroupWithBothVisibleAndHiddenField(); - - $visibleFields = $group->getVisibleFields(true, false); - - $this->assertSame(array($group['visibleField']), $visibleFields); - } - - /** - * Create a group containing two fields, "visibleField" and "hiddenField" - * - * @return FieldGroup - */ - protected function getGroupWithBothVisibleAndHiddenField() - { - $group = new TestFieldGroup('testGroup'); - - // add a visible field - $visibleField = $this->createMockField('visibleField'); - $visibleField->expects($this->once()) - ->method('isHidden') - ->will($this->returnValue(false)); - $group->add($visibleField); - - // add a hidden field - $hiddenField = $this->createMockField('hiddenField'); - $hiddenField->expects($this->once()) - ->method('isHidden') - ->will($this->returnValue(true)); - $group->add($hiddenField); - - return $group; - } - - protected function createMockField($key) - { - $field = $this->getMock( - 'Symfony\Component\Form\FieldInterface', - array(), - array(), - '', - false, // don't use constructor - false // don't call parent::__clone - ); - - $field->expects($this->any()) - ->method('getKey') - ->will($this->returnValue($key)); - - return $field; - } - - protected function createMockForm() - { - $form = $this->getMock( - 'Symfony\Component\Form\Form', - array(), - array(), - '', - false, // don't use constructor - false // don't call parent::__clone) - ); - - $form->expects($this->any()) - ->method('getRoot') - ->will($this->returnValue($form)); - - return $form; - } - - protected function createInvalidMockField($key) - { - $field = $this->createMockField($key); - $field->expects($this->any()) - ->method('isValid') - ->will($this->returnValue(false)); - - return $field; - } - - protected function createValidMockField($key) - { - $field = $this->createMockField($key); - $field->expects($this->any()) - ->method('isValid') - ->will($this->returnValue(true)); - - return $field; - } - - protected function createNonMultipartMockField($key) - { - $field = $this->createMockField($key); - $field->expects($this->any()) - ->method('isMultipart') - ->will($this->returnValue(false)); - - return $field; - } - - protected function createMultipartMockField($key) - { - $field = $this->createMockField($key); - $field->expects($this->any()) - ->method('isMultipart') - ->will($this->returnValue(true)); - - return $field; - } - - protected function createMockTransformer() - { - return $this->getMock('Symfony\Component\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false); - } -} diff --git a/tests/Symfony/Tests/Component/Form/FieldTest.php b/tests/Symfony/Tests/Component/Form/FieldTest.php index d7a25f88b2..ab530f3158 100644 --- a/tests/Symfony/Tests/Component/Form/FieldTest.php +++ b/tests/Symfony/Tests/Component/Form/FieldTest.php @@ -526,7 +526,7 @@ class FieldTest extends \PHPUnit_Framework_TestCase protected function createMockGroup() { return $this->getMock( - 'Symfony\Component\Form\FieldGroup', + 'Symfony\Component\Form\Form', array(), array(), '', diff --git a/tests/Symfony/Tests/Component/Form/Fixtures/TestFieldGroup.php b/tests/Symfony/Tests/Component/Form/Fixtures/TestForm.php similarity index 90% rename from tests/Symfony/Tests/Component/Form/Fixtures/TestFieldGroup.php rename to tests/Symfony/Tests/Component/Form/Fixtures/TestForm.php index d670453dac..553f84949e 100644 --- a/tests/Symfony/Tests/Component/Form/Fixtures/TestFieldGroup.php +++ b/tests/Symfony/Tests/Component/Form/Fixtures/TestForm.php @@ -2,10 +2,10 @@ namespace Symfony\Tests\Component\Form\Fixtures; -use Symfony\Component\Form\FieldGroup; +use Symfony\Component\Form\Form; use Symfony\Component\Form\ValueTransformer\ValueTransformerInterface; -class TestFieldGroup extends FieldGroup +class TestForm extends Form { /** * Expose method for testing purposes diff --git a/tests/Symfony/Tests/Component/Form/FormTest.php b/tests/Symfony/Tests/Component/Form/FormTest.php index 9548e95393..2aa9e366ff 100644 --- a/tests/Symfony/Tests/Component/Form/FormTest.php +++ b/tests/Symfony/Tests/Component/Form/FormTest.php @@ -13,12 +13,13 @@ namespace Symfony\Tests\Component\Form; require_once __DIR__ . '/Fixtures/Author.php'; require_once __DIR__ . '/Fixtures/TestField.php'; +require_once __DIR__ . '/Fixtures/TestForm.php'; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormContext; use Symfony\Component\Form\Field; +use Symfony\Component\Form\FieldError; use Symfony\Component\Form\HiddenField; -use Symfony\Component\Form\FieldGroup; use Symfony\Component\Form\PropertyPath; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -26,6 +27,7 @@ use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Tests\Component\Form\Fixtures\Author; use Symfony\Tests\Component\Form\Fixtures\TestField; +use Symfony\Tests\Component\Form\Fixtures\TestForm; class FormTest_PreconfiguredForm extends Form { @@ -71,29 +73,19 @@ class FormTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->validator = $this->createMockValidator(); - $this->form = new Form('author', new Author(), $this->validator); - } - - public function testConstructInitializesObject() - { - $this->assertEquals(new Author(), $this->form->getData()); - } - - public function testSetDataBeforeConfigure() - { - new TestSetDataBeforeConfigureForm($this, 'author', new Author(), $this->validator); + $this->form = new Form('author', array('validator' => $this->validator)); } public function testNoCsrfProtectionByDefault() { - $form = new Form('author', new Author(), $this->validator); + $form = new Form('author'); $this->assertFalse($form->isCsrfProtected()); } public function testCsrfProtectionCanBeEnabled() { - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'csrf_provider' => $this->createMockCsrfProvider(), )); @@ -102,7 +94,7 @@ class FormTest extends \PHPUnit_Framework_TestCase public function testCsrfFieldNameCanBeSet() { - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'csrf_provider' => $this->createMockCsrfProvider(), 'csrf_field_name' => 'foobar', )); @@ -118,7 +110,7 @@ class FormTest extends \PHPUnit_Framework_TestCase ->with($this->equalTo('Symfony\Component\Form\Form')) ->will($this->returnValue('ABCDEF')); - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'csrf_provider' => $provider, )); @@ -145,8 +137,9 @@ class FormTest extends \PHPUnit_Framework_TestCase ->with($this->equalTo('Symfony\Component\Form\Form'), $this->equalTo('ABCDEF')) ->will($this->returnValue(true)); - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'csrf_provider' => $provider, + 'validator' => $this->validator, )); $field = $form->getCsrfFieldName(); @@ -164,8 +157,9 @@ class FormTest extends \PHPUnit_Framework_TestCase ->with($this->equalTo('Symfony\Component\Form\Form'), $this->equalTo('ABCDEF')) ->will($this->returnValue(false)); - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'csrf_provider' => $provider, + 'validator' => $this->validator, )); $field = $form->getCsrfFieldName(); @@ -182,7 +176,7 @@ class FormTest extends \PHPUnit_Framework_TestCase public function testValidationGroupsCanBeSetToString() { - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'validation_groups' => 'group', )); @@ -191,7 +185,7 @@ class FormTest extends \PHPUnit_Framework_TestCase public function testValidationGroupsCanBeSetToArray() { - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'validation_groups' => array('group1', 'group2'), )); @@ -201,8 +195,9 @@ class FormTest extends \PHPUnit_Framework_TestCase public function testBindUsesValidationGroups() { $field = $this->createMockField('firstName'); - $form = new Form('author', new Author(), $this->validator, array( + $form = new Form('author', array( 'validation_groups' => 'group', + 'validator' => $this->validator, )); $form->add($field); @@ -213,16 +208,16 @@ class FormTest extends \PHPUnit_Framework_TestCase $form->bind(array()); // irrelevant } - public function testBindThrowsExceptionIfNoValidatorIsSet() - { - $field = $this->createMockField('firstName'); - $form = new Form('author', new Author()); - $form->add($field); - - $this->setExpectedException('Symfony\Component\Form\Exception\FormException'); - - $form->bind(array()); // irrelevant - } +// public function testBindThrowsExceptionIfNoValidatorIsSet() +// { +// $field = $this->createMockField('firstName'); +// $form = new Form('author'); +// $form->add($field); +// +// $this->setExpectedException('Symfony\Component\Form\Exception\FormException'); +// +// $form->bind(array()); // irrelevant +// } public function testBindRequest() { @@ -244,9 +239,9 @@ class FormTest extends \PHPUnit_Framework_TestCase $request = new Request(array(), $values, array(), array(), $files); - $form = new Form('author', null, $this->createMockValidator()); + $form = new Form('author', array('validator' => $this->validator)); $form->add(new TestField('name')); - $imageForm = new FieldGroup('image'); + $imageForm = new Form('image'); $imageForm->add(new TestField('file')); $imageForm->add(new TestField('filename')); $form->add($imageForm); @@ -279,9 +274,9 @@ class FormTest extends \PHPUnit_Framework_TestCase ); - $form = new Form('author', null, $this->createMockValidator()); + $form = new Form('author', array('validator' => $this->validator)); $form->add(new TestField('name')); - $imageForm = new FieldGroup('image'); + $imageForm = new Form('image'); $imageForm->add(new TestField('file')); $imageForm->add(new TestField('filename')); $form->add($imageForm); @@ -295,55 +290,752 @@ class FormTest extends \PHPUnit_Framework_TestCase $this->assertEquals($file, $form['image']['file']->getData()); } - public function testUpdateFromPropertyIsIgnoredIfFormHasObject() + public function testUpdateFromPropertyIsIgnoredIfPropertyPathIsNull() { $author = new Author(); $author->child = new Author(); $standaloneChild = new Author(); - $form = new Form('child', $standaloneChild); + $form = new Form('child'); + $form->setData($standaloneChild); + $form->setPropertyPath(null); $form->updateFromProperty($author); // should not be $author->child!! $this->assertSame($standaloneChild, $form->getData()); } - public function testUpdateFromPropertyIsNotIgnoredIfFormHasNoObject() - { - $author = new Author(); - $author->child = new Author(); - - $form = new Form('child'); - $form->updateFromProperty($author); - - // should not be $author->child!! - $this->assertSame($author->child, $form->getData()); - } - - public function testUpdatePropertyIsIgnoredIfFormHasObject() + public function testUpdatePropertyIsIgnoredIfPropertyPathIsNull() { $author = new Author(); $author->child = $child = new Author(); $standaloneChild = new Author(); - $form = new Form('child', $standaloneChild); + $form = new Form('child'); + $form->setData($standaloneChild); + $form->setPropertyPath(null); $form->updateProperty($author); // $author->child was not modified $this->assertSame($child, $author->child); } - public function testUpdatePropertyIsNotIgnoredIfFormHasNoObject() + public function testSupportsArrayAccess() + { + $form = new Form('author'); + $form->add($this->createMockField('firstName')); + $this->assertEquals($form->get('firstName'), $form['firstName']); + $this->assertTrue(isset($form['firstName'])); + } + + public function testSupportsUnset() + { + $form = new Form('author'); + $form->add($this->createMockField('firstName')); + unset($form['firstName']); + $this->assertFalse(isset($form['firstName'])); + } + + public function testDoesNotSupportAddingFields() + { + $form = new Form('author'); + $this->setExpectedException('LogicException'); + $form[] = $this->createMockField('lastName'); + } + + public function testSupportsCountable() + { + $form = new Form('group'); + $form->add($this->createMockField('firstName')); + $form->add($this->createMockField('lastName')); + $this->assertEquals(2, count($form)); + + $form->add($this->createMockField('australian')); + $this->assertEquals(3, count($form)); + } + + public function testSupportsIterable() + { + $form = new Form('group'); + $form->add($field1 = $this->createMockField('field1')); + $form->add($field2 = $this->createMockField('field2')); + $form->add($field3 = $this->createMockField('field3')); + + $expected = array( + 'field1' => $field1, + 'field2' => $field2, + 'field3' => $field3, + ); + + $this->assertEquals($expected, iterator_to_array($form)); + } + + public function testIsBound() + { + $form = new Form('author', array('validator' => $this->validator)); + $this->assertFalse($form->isBound()); + $form->bind(array('firstName' => 'Bernhard')); + $this->assertTrue($form->isBound()); + } + + public function testValidIfAllFieldsAreValid() + { + $form = new Form('author', array('validator' => $this->validator)); + $form->add($this->createValidMockField('firstName')); + $form->add($this->createValidMockField('lastName')); + + $form->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertTrue($form->isValid()); + } + + public function testInvalidIfFieldIsInvalid() + { + $form = new Form('author', array('validator' => $this->validator)); + $form->add($this->createInvalidMockField('firstName')); + $form->add($this->createValidMockField('lastName')); + + $form->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertFalse($form->isValid()); + } + + public function testInvalidIfBoundWithExtraFields() + { + $form = new Form('author', array('validator' => $this->validator)); + $form->add($this->createValidMockField('firstName')); + $form->add($this->createValidMockField('lastName')); + + $form->bind(array('foo' => 'bar', 'firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertTrue($form->isBoundWithExtraFields()); + } + + public function testHasNoErrorsIfOnlyFieldHasErrors() + { + $form = new Form('author', array('validator' => $this->validator)); + $form->add($this->createInvalidMockField('firstName')); + + $form->bind(array('firstName' => 'Bernhard')); + + $this->assertFalse($form->hasErrors()); + } + + public function testBindForwardsPreprocessedData() + { + $field = $this->createMockField('firstName'); + + $form = $this->getMock( + 'Symfony\Component\Form\Form', + array('preprocessData'), // only mock preprocessData() + array('author', array('validator' => $this->validator)) + ); + + // The data array is prepared directly after binding + $form->expects($this->once()) + ->method('preprocessData') + ->with($this->equalTo(array('firstName' => 'Bernhard'))) + ->will($this->returnValue(array('firstName' => 'preprocessed[Bernhard]'))); + $form->add($field); + + // The preprocessed data is then forwarded to the fields + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo('preprocessed[Bernhard]')); + + $form->bind(array('firstName' => 'Bernhard')); + } + + public function testBindForwardsNullIfValueIsMissing() + { + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo(null)); + + $form = new Form('author', array('validator' => $this->validator)); + $form->add($field); + + $form->bind(array()); + } + + public function testAddErrorMapsFieldValidationErrorsOntoFields() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo($error)); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('fields[firstName].data'); + + $form->addError($error, $path->getIterator(), Form::FIELD_ERROR); + } + + public function testAddErrorMapsFieldValidationErrorsOntoFieldsWithinNestedForms() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo($error)); + + $form = new Form('author'); + $innerGroup = new Form('names'); + $innerGroup->add($field); + $form->add($innerGroup); + + $path = new PropertyPath('fields[names].fields[firstName].data'); + + $form->addError($error, $path->getIterator(), Form::FIELD_ERROR); + } + + public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('foo'); + $field->expects($this->never()) + ->method('addError'); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('fields[bar].data'); + + $form->addError($error, $path->getIterator(), Form::FIELD_ERROR); + + $this->assertEquals(array($error), $form->getErrors()); + } + + public function testAddErrorKeepsFieldValidationErrorsIfFieldIsHidden() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('isHidden') + ->will($this->returnValue(true)); + $field->expects($this->never()) + ->method('addError'); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('fields[firstName].data'); + + $form->addError($error, $path->getIterator(), Form::FIELD_ERROR); + + $this->assertEquals(array($error), $form->getErrors()); + } + + public function testAddErrorMapsDataValidationErrorsOntoFields() + { + $error = new FieldError('Message'); + + // path is expected to point at "firstName" + $expectedPath = new PropertyPath('firstName'); + $expectedPathIterator = $expectedPath->getIterator(); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(Form::DATA_ERROR)); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('firstName'); + + $form->addError($error, $path->getIterator(), Form::DATA_ERROR); + } + + public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('foo'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('foo'))); + $field->expects($this->never()) + ->method('addError'); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('bar'); + + $form->addError($error, $path->getIterator(), Form::DATA_ERROR); + } + + public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden() + { + $error = new FieldError('Message'); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('isHidden') + ->will($this->returnValue(true)); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->never()) + ->method('addError'); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('firstName'); + + $form->addError($error, $path->getIterator(), Form::DATA_ERROR); + } + + public function testAddErrorMapsDataValidationErrorsOntoNestedFields() + { + $error = new FieldError('Message'); + + // path is expected to point at "street" + $expectedPath = new PropertyPath('address.street'); + $expectedPathIterator = $expectedPath->getIterator(); + $expectedPathIterator->next(); + + $field = $this->createMockField('address'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('address'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(Form::DATA_ERROR)); + + $form = new Form('author'); + $form->add($field); + + $path = new PropertyPath('address.street'); + + $form->addError($error, $path->getIterator(), Form::DATA_ERROR); + } + + public function testAddErrorMapsErrorsOntoFieldsInVirtualGroups() + { + $error = new FieldError('Message'); + + // path is expected to point at "address" + $expectedPath = new PropertyPath('address'); + $expectedPathIterator = $expectedPath->getIterator(); + + $field = $this->createMockField('address'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('address'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo($error), $this->equalTo($expectedPathIterator), $this->equalTo(Form::DATA_ERROR)); + + $form = new Form('author'); + $nestedForm = new Form('nested', array('virtual' => true)); + $nestedForm->add($field); + $form->add($nestedForm); + + $path = new PropertyPath('address'); + + $form->addError($error, $path->getIterator(), Form::DATA_ERROR); + } + + public function testAddThrowsExceptionIfAlreadyBound() + { + $form = new Form('author', array('validator' => $this->validator)); + $form->add($this->createMockField('firstName')); + $form->bind(array('firstName' => 'Bernhard')); + + $this->setExpectedException('Symfony\Component\Form\Exception\AlreadyBoundException'); + $form->add($this->createMockField('lastName')); + } + + public function testAddSetsFieldParent() + { + $form = new Form('author'); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('setParent'); + // PHPUnit fails to compare infinitely recursive objects + //->with($this->equalTo($form)); + + $form->add($field); + } + + public function testRemoveUnsetsFieldParent() + { + $form = new Form('author'); + + $field = $this->createMockField('firstName'); + $field->expects($this->exactly(2)) + ->method('setParent'); + // PHPUnit fails to compare subsequent method calls with different arguments + + $form->add($field); + $form->remove('firstName'); + } + + public function testAddUpdatesFieldFromTransformedData() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $form = new TestForm('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $form->setValueTransformer($transformer); + $form->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->once()) + ->method('updateFromProperty') + ->with($this->equalTo($transformedAuthor)); + + $form->add($field); + } + + public function testAddDoesNotUpdateFieldIfTransformedDataIsEmpty() + { + $originalAuthor = new Author(); + + $form = new TestForm('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue('')); + + $form->setValueTransformer($transformer); + $form->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->never()) + ->method('updateFromProperty'); + + $form->add($field); + } + + /** + * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testAddThrowsExceptionIfNoFieldOrString() + { + $form = new Form('author'); + + $form->add(1234); + } + + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testAddThrowsExceptionIfAnonymousField() + { + $form = new Form('author'); + + $field = $this->createMockField(''); + + $form->add($field); + } + + public function testAddThrowsExceptionIfStringButNoFieldFactory() + { + $rootForm = $this->createMockForm(); + $rootForm->expects($this->once()) + ->method('getFieldFactory') + ->will($this->returnValue(null)); + + $form = new Form('author'); + $form->setParent($rootForm); + + $this->setExpectedException('\LogicException'); + + $form->add('firstName'); + } + + public function testAddUsesFieldFromFactoryIfStringIsGiven() + { + $author = new \stdClass(); + $field = $this->createMockField('firstName'); + + $factory = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryInterface'); + $factory->expects($this->once()) + ->method('getInstance') + ->with($this->equalTo($author), $this->equalTo('firstName'), $this->equalTo(array('foo' => 'bar'))) + ->will($this->returnValue($field)); + + $rootForm = $this->createMockForm(); + $rootForm->expects($this->once()) + ->method('getFieldFactory') + ->will($this->returnValue($factory)); + + $form = new Form('author'); + $form->setParent($rootForm); + $form->setData($author); + $form->add('firstName', array('foo' => 'bar')); + + $this->assertSame($field, $form['firstName']); + } + + public function testSetDataUpdatesAllFieldsFromTransformedData() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $form = new TestForm('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $form->setValueTransformer($transformer); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('updateFromProperty') + ->with($this->equalTo($transformedAuthor)); + + $form->add($field); + + $field = $this->createMockField('lastName'); + $field->expects($this->once()) + ->method('updateFromProperty') + ->with($this->equalTo($transformedAuthor)); + + $form->add($field); + + $form->setData($originalAuthor); + } + + /** + * The use case for this test are groups whose fields should be mapped + * directly onto properties of the form's object. + * + * Example: + * + * + * $dateRangeField = new Form('dateRange'); + * $dateRangeField->add(new DateField('startDate')); + * $dateRangeField->add(new DateField('endDate')); + * $form->add($dateRangeField); + * + * + * If $dateRangeField is not virtual, the property "dateRange" must be + * present on the form's object. In this property, an object or array + * with the properties "startDate" and "endDate" is expected. + * + * If $dateRangeField is virtual though, it's children are mapped directly + * onto the properties "startDate" and "endDate" of the form's object. + */ + public function testSetDataSkipsVirtualForms() { $author = new Author(); - $child = new Author(); + $author->firstName = 'Foo'; - $form = new Form('child'); - $form->setData($child); - $form->updateProperty($author); + $form = new Form('author'); + $nestedForm = new Form('personal_data', array( + 'virtual' => true, + )); - // $author->child was set - $this->assertSame($child, $author->child); + // both fields are in the nested group but receive the object of the + // top-level group because the nested group is virtual + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('updateFromProperty') + ->with($this->equalTo($author)); + + $nestedForm->add($field); + + $field = $this->createMockField('lastName'); + $field->expects($this->once()) + ->method('updateFromProperty') + ->with($this->equalTo($author)); + + $nestedForm->add($field); + + $form->add($nestedForm); + $form->setData($author); + } + + public function testSetDataThrowsAnExceptionIfArgumentIsNotObjectOrArray() + { + $form = new Form('author'); + + $this->setExpectedException('InvalidArgumentException'); + + $form->setData('foobar'); + } + + public function testBindUpdatesTransformedDataFromAllFields() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $form = new TestForm('author', array('validator' => $this->validator)); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->exactly(2)) + ->method('transform') + // the method is first called with NULL, then + // with $originalAuthor -> not testable by PHPUnit + // ->with($this->equalTo(null)) + // ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $form->setValueTransformer($transformer); + $form->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('updateProperty') + ->with($this->equalTo($transformedAuthor)); + + $form->add($field); + + $field = $this->createMockField('lastName'); + $field->expects($this->once()) + ->method('updateProperty') + ->with($this->equalTo($transformedAuthor)); + + $form->add($field); + + $form->bind(array()); // irrelevant + } + + public function testGetDataReturnsObject() + { + $form = new Form('author'); + $object = new \stdClass(); + $form->setData($object); + $this->assertEquals($object, $form->getData()); + } + + public function testGetDisplayedDataForwardsCall() + { + $field = $this->createValidMockField('firstName'); + $field->expects($this->atLeastOnce()) + ->method('getDisplayedData') + ->will($this->returnValue('Bernhard')); + + $form = new Form('author'); + $form->add($field); + + $this->assertEquals(array('firstName' => 'Bernhard'), $form->getDisplayedData()); + } + + public function testIsMultipartIfAnyFieldIsMultipart() + { + $form = new Form('author'); + $form->add($this->createMultipartMockField('firstName')); + $form->add($this->createNonMultipartMockField('lastName')); + + $this->assertTrue($form->isMultipart()); + } + + public function testIsNotMultipartIfNoFieldIsMultipart() + { + $form = new Form('author'); + $form->add($this->createNonMultipartMockField('firstName')); + $form->add($this->createNonMultipartMockField('lastName')); + + $this->assertFalse($form->isMultipart()); + } + + public function testSupportsClone() + { + $form = new Form('author'); + $form->add($this->createMockField('firstName')); + + $clone = clone $form; + + $this->assertNotSame($clone['firstName'], $form['firstName']); + } + + public function testBindWithoutPriorSetData() + { + return; // TODO + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getData') + ->will($this->returnValue('Bernhard')); + + $form = new Form('author'); + $form->add($field); + + $form->bind(array('firstName' => 'Bernhard')); + + $this->assertEquals(array('firstName' => 'Bernhard'), $form->getData()); + } + + public function testGetHiddenFieldsReturnsOnlyHiddenFields() + { + $form = $this->getGroupWithBothVisibleAndHiddenField(); + + $hiddenFields = $form->getHiddenFields(true, false); + + $this->assertSame(array($form['hiddenField']), $hiddenFields); + } + + public function testGetVisibleFieldsReturnsOnlyVisibleFields() + { + $form = $this->getGroupWithBothVisibleAndHiddenField(); + + $visibleFields = $form->getVisibleFields(true, false); + + $this->assertSame(array($form['visibleField']), $visibleFields); + } + + /** + * Create a group containing two fields, "visibleField" and "hiddenField" + * + * @return Form + */ + protected function getGroupWithBothVisibleAndHiddenField() + { + $form = new Form('testGroup'); + + // add a visible field + $visibleField = $this->createMockField('visibleField'); + $visibleField->expects($this->once()) + ->method('isHidden') + ->will($this->returnValue(false)); + $form->add($visibleField); + + // add a hidden field + $hiddenField = $this->createMockField('hiddenField'); + $hiddenField->expects($this->once()) + ->method('isHidden') + ->will($this->returnValue(true)); + $form->add($hiddenField); + + return $form; } protected function createMockField($key) @@ -364,20 +1056,50 @@ class FormTest extends \PHPUnit_Framework_TestCase return $field; } - protected function createMockFieldGroup($key) + protected function createMockForm() { - $field = $this->getMock( - 'Symfony\Component\Form\FieldGroup', + $form = $this->getMock( + 'Symfony\Component\Form\Form', array(), array(), '', false, // don't use constructor - false // don't call parent::__clone + false // don't call parent::__clone) ); + $form->expects($this->any()) + ->method('getRoot') + ->will($this->returnValue($form)); + + return $form; + } + + protected function createInvalidMockField($key) + { + $field = $this->createMockField($key); $field->expects($this->any()) - ->method('getKey') - ->will($this->returnValue($key)); + ->method('isValid') + ->will($this->returnValue(false)); + + return $field; + } + + protected function createValidMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isValid') + ->will($this->returnValue(true)); + + return $field; + } + + protected function createNonMultipartMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isMultipart') + ->will($this->returnValue(false)); return $field; } @@ -392,6 +1114,11 @@ class FormTest extends \PHPUnit_Framework_TestCase return $field; } + protected function createMockTransformer() + { + return $this->getMock('Symfony\Component\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false); + } + protected function createMockValidator() { return $this->getMock('Symfony\Component\Validator\ValidatorInterface');