4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Form
;
14 use Symfony\Component\Form\Exception\RuntimeException
;
15 use Symfony\Component\Form\Exception\UnexpectedTypeException
;
16 use Symfony\Component\Form\Exception\AlreadySubmittedException
;
17 use Symfony\Component\Form\Exception\TransformationFailedException
;
18 use Symfony\Component\Form\Exception\LogicException
;
19 use Symfony\Component\Form\Exception\OutOfBoundsException
;
20 use Symfony\Component\Form\Util\FormUtil
;
21 use Symfony\Component\Form\Util\InheritDataAwareIterator
;
22 use Symfony\Component\PropertyAccess\PropertyPath
;
25 * Form represents a form.
27 * To implement your own form fields, you need to have a thorough understanding
28 * of the data flow within a form. A form stores its data in three different
31 * (1) the "model" format required by the form's object
32 * (2) the "normalized" format for internal processing
33 * (3) the "view" format used for display
35 * A date field, for example, may store a date as "Y-m-d" string (1) in the
36 * object. To facilitate processing in the field, this value is normalized
37 * to a DateTime object (2). In the HTML representation of your form, a
38 * localized string (3) is presented to and modified by the user.
40 * In most cases, format (1) and format (2) will be the same. For example,
41 * a checkbox field uses a Boolean value for both internal processing and
42 * storage in the object. In these cases you simply need to set a value
43 * transformer to convert between formats (2) and (3). You can do this by
44 * calling addViewTransformer().
46 * In some cases though it makes sense to make format (1) configurable. To
47 * demonstrate this, let's extend our above date field to store the value
48 * either as "Y-m-d" string or as timestamp. Internally we still want to
49 * use a DateTime object for processing. To convert the data from string/integer
50 * to DateTime you can set a normalization transformer by calling
51 * addNormTransformer(). The normalized data is then converted to the displayed
52 * data as described before.
54 * The conversions (1) -> (2) -> (3) use the transform methods of the transformers.
55 * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers.
57 * @author Fabien Potencier <fabien@symfony.com>
58 * @author Bernhard Schussek <bschussek@gmail.com>
60 class Form
implements \IteratorAggregate
, FormInterface
63 * The form's configuration
64 * @var FormConfigInterface
69 * The parent of this form
75 * The children of this form
76 * @var FormInterface[] An array of FormInterface instances
78 private $children = array();
81 * The errors of this form
82 * @var FormError[] An array of FormError instances
84 private $errors = array();
87 * Whether this form was submitted
90 private $submitted = false;
93 * The form data in model format
99 * The form data in normalized format
105 * The form data in view format
111 * The submitted values that don't belong to any children
114 private $extraData = array();
117 * Whether the data in model, normalized and view format is
118 * synchronized. Data may not be synchronized if transformation errors
122 private $synchronized = true;
125 * Whether the form's data has been initialized.
127 * When the data is initialized with its default value, that default value
128 * is passed through the transformer chain in order to synchronize the
129 * model, normalized and view format for the first time. This is done
130 * lazily in order to save performance when {@link setData()} is called
131 * manually, making the initialization with the configured default value
136 private $defaultDataSet = false;
139 * Whether setData() is currently being called.
142 private $lockSetData = false;
145 * Creates a new form based on the given configuration.
147 * @param FormConfigInterface $config The form configuration.
149 * @throws LogicException if a data mapper is not provided for a compound form
151 public function __construct(FormConfigInterface
$config)
153 // Compound forms always need a data mapper, otherwise calls to
154 // `setData` and `add` will not lead to the correct population of
156 if ($config->getCompound() && !$config->getDataMapper()) {
157 throw new LogicException('Compound forms need a data mapper');
160 // If the form inherits the data from its parent, it is not necessary
161 // to call setData() with the default data.
162 if ($config->getInheritData()) {
163 $this->defaultDataSet
= true;
166 $this->config
= $config;
169 public function __clone()
171 foreach ($this->children
as $key => $child) {
172 $this->children
[$key] = clone $child;
179 public function getConfig()
181 return $this->config
;
187 public function getName()
189 return $this->config
->getName();
195 public function getPropertyPath()
197 if (null !== $this->config
->getPropertyPath()) {
198 return $this->config
->getPropertyPath();
201 if (null === $this->getName() || '' === $this->getName()) {
205 $parent = $this->parent
;
207 while ($parent && $parent->getConfig()->getInheritData()) {
208 $parent = $parent->getParent();
211 if ($parent && null === $parent->getConfig()->getDataClass()) {
212 return new PropertyPath('['.$this->getName().']');
215 return new PropertyPath($this->getName());
221 public function isRequired()
223 if (null === $this->parent
|| $this->parent
->isRequired()) {
224 return $this->config
->getRequired();
233 public function isDisabled()
235 if (null === $this->parent
|| !$this->parent
->isDisabled()) {
236 return $this->config
->getDisabled();
245 public function setParent(FormInterface
$parent = null)
247 if ($this->submitted
) {
248 throw new AlreadySubmittedException('You cannot set the parent of a submitted form');
251 if (null !== $parent && '' === $this->config
->getName()) {
252 throw new LogicException('A form with an empty name cannot have a parent form.');
255 $this->parent
= $parent;
263 public function getParent()
265 return $this->parent
;
271 public function getRoot()
273 return $this->parent
? $this->parent
->getRoot() : $this;
279 public function isRoot()
281 return null === $this->parent
;
287 public function setData($modelData)
289 // If the form is submitted while disabled, it is set to submitted, but the data is not
290 // changed. In such cases (i.e. when the form is not initialized yet) don't
291 // abort this method.
292 if ($this->submitted
&& $this->defaultDataSet
) {
293 throw new AlreadySubmittedException('You cannot change the data of a submitted form.');
296 // If the form inherits its parent's data, disallow data setting to
297 // prevent merge conflicts
298 if ($this->config
->getInheritData()) {
299 throw new RuntimeException('You cannot change the data of a form inheriting its parent data.');
302 // Don't allow modifications of the configured data if the data is locked
303 if ($this->config
->getDataLocked() && $modelData !== $this->config
->getData()) {
307 if (is_object($modelData) && !$this->config
->getByReference()) {
308 $modelData = clone $modelData;
311 if ($this->lockSetData
) {
312 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.');
315 $this->lockSetData
= true;
316 $dispatcher = $this->config
->getEventDispatcher();
318 // Hook to change content of the data
319 if ($dispatcher->hasListeners(FormEvents
::PRE_SET_DATA
)) {
320 $event = new FormEvent($this, $modelData);
321 $dispatcher->dispatch(FormEvents
::PRE_SET_DATA
, $event);
322 $modelData = $event->getData();
325 // Treat data as strings unless a value transformer exists
326 if (!$this->config
->getViewTransformers() && !$this->config
->getModelTransformers() && is_scalar($modelData)) {
327 $modelData = (string) $modelData;
330 // Synchronize representations - must not change the content!
331 $normData = $this->modelToNorm($modelData);
332 $viewData = $this->normToView($normData);
334 // Validate if view data matches data class (unless empty)
335 if (!FormUtil
::isEmpty($viewData)) {
336 $dataClass = $this->config
->getDataClass();
338 $actualType = is_object($viewData) ? 'an instance of class '.get_class($viewData) : ' a(n) '.gettype($viewData);
340 if (null === $dataClass && is_object($viewData) && !$viewData instanceof \ArrayAccess
) {
341 $expectedType = 'scalar, array or an instance of \ArrayAccess';
343 throw new LogicException(
344 'The form\'s view data is expected to be of type '.$expectedType.', ' .
345 'but is '.$actualType.'. You ' .
346 'can avoid this error by setting the "data_class" option to ' .
347 '"'.get_class($viewData).'" or by adding a view transformer ' .
348 'that transforms '.$actualType.' to '.$expectedType.'.'
352 if (null !== $dataClass && !$viewData instanceof $dataClass) {
353 throw new LogicException(
354 'The form\'s view data is expected to be an instance of class ' .
355 $dataClass.', but is '. $actualType.'. You can avoid this error ' .
356 'by setting the "data_class" option to null or by adding a view ' .
357 'transformer that transforms '.$actualType.' to an instance of ' .
363 $this->modelData
= $modelData;
364 $this->normData
= $normData;
365 $this->viewData
= $viewData;
366 $this->defaultDataSet
= true;
367 $this->lockSetData
= false;
369 // It is not necessary to invoke this method if the form doesn't have children,
370 // even if the form is compound.
371 if (count($this->children
) > 0) {
372 // Update child forms from the data
373 $childrenIterator = new InheritDataAwareIterator($this->children
);
374 $childrenIterator = new \
RecursiveIteratorIterator($childrenIterator);
375 $this->config
->getDataMapper()->mapDataToForms($viewData, $childrenIterator);
378 if ($dispatcher->hasListeners(FormEvents
::POST_SET_DATA
)) {
379 $event = new FormEvent($this, $modelData);
380 $dispatcher->dispatch(FormEvents
::POST_SET_DATA
, $event);
389 public function getData()
391 if ($this->config
->getInheritData()) {
392 if (!$this->parent
) {
393 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
396 return $this->parent
->getData();
399 if (!$this->defaultDataSet
) {
400 $this->setData($this->config
->getData());
403 return $this->modelData
;
409 public function getNormData()
411 if ($this->config
->getInheritData()) {
412 if (!$this->parent
) {
413 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
416 return $this->parent
->getNormData();
419 if (!$this->defaultDataSet
) {
420 $this->setData($this->config
->getData());
423 return $this->normData
;
429 public function getViewData()
431 if ($this->config
->getInheritData()) {
432 if (!$this->parent
) {
433 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
436 return $this->parent
->getViewData();
439 if (!$this->defaultDataSet
) {
440 $this->setData($this->config
->getData());
443 return $this->viewData
;
449 public function getExtraData()
451 return $this->extraData
;
457 public function initialize()
459 if (null !== $this->parent
) {
460 throw new RuntimeException('Only root forms should be initialized.');
463 // Guarantee that the *_SET_DATA events have been triggered once the
464 // form is initialized. This makes sure that dynamically added or
465 // removed fields are already visible after initialization.
466 if (!$this->defaultDataSet
) {
467 $this->setData($this->config
->getData());
476 public function handleRequest($request = null)
478 $this->config
->getRequestHandler()->handleRequest($this, $request);
486 public function submit($submittedData, $clearMissing = true)
488 if ($this->submitted
) {
489 throw new AlreadySubmittedException('A form can only be submitted once');
492 // Initialize errors in the very beginning so that we don't lose any
493 // errors added during listeners
494 $this->errors
= array();
496 // Obviously, a disabled form should not change its data upon submission.
497 if ($this->isDisabled()) {
498 $this->submitted
= true;
503 // The data must be initialized if it was not initialized yet.
504 // This is necessary to guarantee that the *_SET_DATA listeners
505 // are always invoked before submit() takes place.
506 if (!$this->defaultDataSet
) {
507 $this->setData($this->config
->getData());
510 // Treat false as NULL to support binding false to checkboxes.
511 // Don't convert NULL to a string here in order to determine later
512 // whether an empty value has been submitted or whether no value has
513 // been submitted at all. This is important for processing checkboxes
514 // and radio buttons with empty values.
515 if (false === $submittedData) {
516 $submittedData = null;
517 } elseif (is_scalar($submittedData)) {
518 $submittedData = (string) $submittedData;
521 $dispatcher = $this->config
->getEventDispatcher();
523 // Hook to change content of the data submitted by the browser
524 if ($dispatcher->hasListeners(FormEvents
::PRE_SUBMIT
)) {
525 $event = new FormEvent($this, $submittedData);
526 $dispatcher->dispatch(FormEvents
::PRE_SUBMIT
, $event);
527 $submittedData = $event->getData();
530 // Check whether the form is compound.
531 // This check is preferable over checking the number of children,
532 // since forms without children may also be compound.
533 // (think of empty collection forms)
534 if ($this->config
->getCompound()) {
535 if (!is_array($submittedData)) {
536 $submittedData = array();
539 foreach ($this->children
as $name => $child) {
540 if (isset($submittedData[$name]) || $clearMissing) {
541 $child->submit(isset($submittedData[$name]) ? $submittedData[$name] : null, $clearMissing);
542 unset($submittedData[$name]);
546 $this->extraData
= $submittedData;
549 // Forms that inherit their parents' data also are not processed,
550 // because then it would be too difficult to merge the changes in
551 // the child and the parent form. Instead, the parent form also takes
552 // changes in the grandchildren (i.e. children of the form that inherits
553 // its parent's data) into account.
554 // (see InheritDataAwareIterator below)
555 if ($this->config
->getInheritData()) {
556 $this->submitted
= true;
558 // When POST_SUBMIT is reached, the data is not yet updated, so pass
559 // NULL to prevent hard-to-debug bugs.
560 $dataForPostSubmit = null;
562 // If the form is compound, the default data in view format
563 // is reused. The data of the children is merged into this
564 // default data using the data mapper.
565 // If the form is not compound, the submitted data is also the data in view format.
566 $viewData = $this->config
->getCompound() ? $this->viewData
: $submittedData;
568 if (FormUtil
::isEmpty($viewData)) {
569 $emptyData = $this->config
->getEmptyData();
571 if ($emptyData instanceof \Closure
) {
572 /* @var \Closure $emptyData */
573 $emptyData = $emptyData($this, $viewData);
576 $viewData = $emptyData;
579 // Merge form data from children into existing view data
580 // It is not necessary to invoke this method if the form has no children,
581 // even if it is compound.
582 if (count($this->children
) > 0) {
583 // Use InheritDataAwareIterator to process children of
584 // descendants that inherit this form's data.
585 // These descendants will not be submitted normally (see the check
586 // for $this->config->getInheritData() above)
587 $childrenIterator = new InheritDataAwareIterator($this->children
);
588 $childrenIterator = new \
RecursiveIteratorIterator($childrenIterator);
589 $this->config
->getDataMapper()->mapFormsToData($childrenIterator, $viewData);
596 // Normalize data to unified representation
597 $normData = $this->viewToNorm($viewData);
599 // Hook to change content of the data into the normalized
601 if ($dispatcher->hasListeners(FormEvents
::SUBMIT
)) {
602 $event = new FormEvent($this, $normData);
603 $dispatcher->dispatch(FormEvents
::SUBMIT
, $event);
604 $normData = $event->getData();
607 // Synchronize representations - must not change the content!
608 $modelData = $this->normToModel($normData);
609 $viewData = $this->normToView($normData);
610 } catch (TransformationFailedException
$e) {
611 $this->synchronized
= false;
614 $this->submitted
= true;
615 $this->modelData
= $modelData;
616 $this->normData
= $normData;
617 $this->viewData
= $viewData;
619 $dataForPostSubmit = $viewData;
622 if ($dispatcher->hasListeners(FormEvents
::POST_SUBMIT
)) {
623 $event = new FormEvent($this, $dataForPostSubmit);
624 $dispatcher->dispatch(FormEvents
::POST_SUBMIT
, $event);
631 * Alias of {@link submit()}.
633 * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
634 * {@link submit()} instead.
636 public function bind($submittedData)
638 return $this->submit($submittedData);
644 public function addError(FormError
$error)
646 if ($this->parent
&& $this->config
->getErrorBubbling()) {
647 $this->parent
->addError($error);
649 $this->errors
[] = $error;
658 public function isSubmitted()
660 return $this->submitted
;
664 * Alias of {@link isSubmitted()}.
666 * @deprecated Deprecated since version 2.3, to be removed in 3.0. Use
667 * {@link isSubmitted()} instead.
669 public function isBound()
671 return $this->submitted
;
677 public function isSynchronized()
679 return $this->synchronized
;
685 public function isEmpty()
687 foreach ($this->children
as $child) {
688 if (!$child->isEmpty()) {
693 return FormUtil
::isEmpty($this->modelData
) ||
694 // arrays, countables
695 0 === count($this->modelData
) ||
696 // traversables that are not countable
697 ($this->modelData
instanceof \Traversable
&& 0 === iterator_count($this->modelData
));
703 public function isValid()
705 if (!$this->submitted
) {
709 if (count($this->errors
) > 0) {
713 if (!$this->isDisabled()) {
714 foreach ($this->children
as $child) {
715 if (!$child->isValid()) {
727 public function getErrors()
729 return $this->errors
;
733 * Returns a string representation of all form errors (including children errors).
735 * This method should only be used to help debug a form.
737 * @param integer $level The indentation level (used internally)
739 * @return string A string representation of all errors
741 public function getErrorsAsString($level = 0)
744 foreach ($this->errors
as $error) {
745 $errors .= str_repeat(' ', $level).'ERROR: '.$error->getMessage()."\n";
748 foreach ($this->children
as $key => $child) {
749 $errors .= str_repeat(' ', $level).$key.":\n";
750 if ($err = $child->getErrorsAsString($level +
4)) {
753 $errors .= str_repeat(' ', $level +
4)."No errors\n";
763 public function all()
765 return $this->children
;
771 public function add($child, $type = null, array $options = array())
773 if ($this->submitted
) {
774 throw new AlreadySubmittedException('You cannot add children to a submitted form');
777 if (!$this->config
->getCompound()) {
778 throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
781 // Obtain the view data
784 // If setData() is currently being called, there is no need to call
785 // mapDataToForms() here, as mapDataToForms() is called at the end
786 // of setData() anyway. Not doing this check leads to an endless
787 // recursion when initializing the form lazily and an event listener
788 // (such as ResizeFormListener) adds fields depending on the data:
790 // * setData() is called, the form is not initialized yet
791 // * add() is called by the listener (setData() is not complete, so
792 // the form is still not initialized)
793 // * getViewData() is called
794 // * setData() is called since the form is not initialized yet
795 // * ... endless recursion ...
797 // Also skip data mapping if setData() has not been called yet.
798 // setData() will be called upon form initialization and data mapping
799 // will take place by then.
800 if (!$this->lockSetData
&& $this->defaultDataSet
&& !$this->config
->getInheritData()) {
801 $viewData = $this->getViewData();
804 if (!$child instanceof FormInterface
) {
805 if (!is_string($child) && !is_int($child)) {
806 throw new UnexpectedTypeException($child, 'string, integer or Symfony\Component\Form\FormInterface');
809 if (null !== $type && !is_string($type) && !$type instanceof FormTypeInterface
) {
810 throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface');
813 // Never initialize child forms automatically
814 $options['auto_initialize'] = false;
816 if (null === $type) {
817 $child = $this->config
->getFormFactory()->createForProperty($this->config
->getDataClass(), $child, null, $options);
819 $child = $this->config
->getFormFactory()->createNamed($child, $type, null, $options);
821 } elseif ($child->getConfig()->getAutoInitialize()) {
822 throw new RuntimeException(sprintf(
823 'Automatic initialization is only supported on root forms. You '.
824 'should set the "auto_initialize" option to false on the field "%s".',
829 $this->children
[$child->getName()] = $child;
831 $child->setParent($this);
833 if (!$this->lockSetData
&& $this->defaultDataSet
&& !$this->config
->getInheritData()) {
834 $childrenIterator = new InheritDataAwareIterator(array($child));
835 $childrenIterator = new \
RecursiveIteratorIterator($childrenIterator);
836 $this->config
->getDataMapper()->mapDataToForms($viewData, $childrenIterator);
845 public function remove($name)
847 if ($this->submitted
) {
848 throw new AlreadySubmittedException('You cannot remove children from a submitted form');
851 if (isset($this->children
[$name])) {
852 $this->children
[$name]->setParent(null);
854 unset($this->children
[$name]);
863 public function has($name)
865 return isset($this->children
[$name]);
871 public function get($name)
873 if (isset($this->children
[$name])) {
874 return $this->children
[$name];
877 throw new OutOfBoundsException(sprintf('Child "%s" does not exist.', $name));
881 * Returns whether a child with the given name exists (implements the \ArrayAccess interface).
883 * @param string $name The name of the child
887 public function offsetExists($name)
889 return $this->has($name);
893 * Returns the child with the given name (implements the \ArrayAccess interface).
895 * @param string $name The name of the child
897 * @return FormInterface The child form
899 * @throws \OutOfBoundsException If the named child does not exist.
901 public function offsetGet($name)
903 return $this->get($name);
907 * Adds a child to the form (implements the \ArrayAccess interface).
909 * @param string $name Ignored. The name of the child is used.
910 * @param FormInterface $child The child to be added.
912 * @throws AlreadySubmittedException If the form has already been submitted.
913 * @throws LogicException When trying to add a child to a non-compound form.
917 public function offsetSet($name, $child)
923 * Removes the child with the given name from the form (implements the \ArrayAccess interface).
925 * @param string $name The name of the child to remove
927 * @throws AlreadySubmittedException If the form has already been submitted.
929 public function offsetUnset($name)
931 $this->remove($name);
935 * Returns the iterator for this group.
937 * @return \ArrayIterator
939 public function getIterator()
941 return new \
ArrayIterator($this->children
);
945 * Returns the number of form children (implements the \Countable interface).
947 * @return integer The number of embedded form children
949 public function count()
951 return count($this->children
);
957 public function createView(FormView
$parent = null)
959 if (null === $parent && $this->parent
) {
960 $parent = $this->parent
->createView();
963 return $this->config
->getType()->createView($this, $parent);
967 * Normalizes the value if a normalization transformer is set.
969 * @param mixed $value The value to transform
973 private function modelToNorm($value)
975 foreach ($this->config
->getModelTransformers() as $transformer) {
976 $value = $transformer->transform($value);
983 * Reverse transforms a value if a normalization transformer is set.
985 * @param string $value The value to reverse transform
989 private function normToModel($value)
991 $transformers = $this->config
->getModelTransformers();
993 for ($i = count($transformers) - 1; $i >= 0; --$i) {
994 $value = $transformers[$i]->reverseTransform($value);
1001 * Transforms the value if a value transformer is set.
1003 * @param mixed $value The value to transform
1007 private function normToView($value)
1009 // Scalar values should be converted to strings to
1010 // facilitate differentiation between empty ("") and zero (0).
1011 // Only do this for simple forms, as the resulting value in
1012 // compound forms is passed to the data mapper and thus should
1013 // not be converted to a string before.
1014 if (!$this->config
->getViewTransformers() && !$this->config
->getCompound()) {
1015 return null === $value || is_scalar($value) ? (string) $value : $value;
1018 foreach ($this->config
->getViewTransformers() as $transformer) {
1019 $value = $transformer->transform($value);
1026 * Reverse transforms a value if a value transformer is set.
1028 * @param string $value The value to reverse transform
1032 private function viewToNorm($value)
1034 $transformers = $this->config
->getViewTransformers();
1036 if (!$transformers) {
1037 return '' === $value ? null : $value;
1040 for ($i = count($transformers) - 1; $i >= 0; --$i) {
1041 $value = $transformers[$i]->reverseTransform($value);