vendor/symfony/form/Form.php line 1070

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Form;
  11. use Symfony\Component\Form\Event\PostSetDataEvent;
  12. use Symfony\Component\Form\Event\PostSubmitEvent;
  13. use Symfony\Component\Form\Event\PreSetDataEvent;
  14. use Symfony\Component\Form\Event\PreSubmitEvent;
  15. use Symfony\Component\Form\Event\SubmitEvent;
  16. use Symfony\Component\Form\Exception\AlreadySubmittedException;
  17. use Symfony\Component\Form\Exception\LogicException;
  18. use Symfony\Component\Form\Exception\OutOfBoundsException;
  19. use Symfony\Component\Form\Exception\RuntimeException;
  20. use Symfony\Component\Form\Exception\TransformationFailedException;
  21. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  22. use Symfony\Component\Form\Extension\Core\Type\TextType;
  23. use Symfony\Component\Form\Util\FormUtil;
  24. use Symfony\Component\Form\Util\InheritDataAwareIterator;
  25. use Symfony\Component\Form\Util\OrderedHashMap;
  26. use Symfony\Component\PropertyAccess\PropertyPath;
  27. use Symfony\Component\PropertyAccess\PropertyPathInterface;
  28. /**
  29.  * Form represents a form.
  30.  *
  31.  * To implement your own form fields, you need to have a thorough understanding
  32.  * of the data flow within a form. A form stores its data in three different
  33.  * representations:
  34.  *
  35.  *   (1) the "model" format required by the form's object
  36.  *   (2) the "normalized" format for internal processing
  37.  *   (3) the "view" format used for display simple fields
  38.  *       or map children model data for compound fields
  39.  *
  40.  * A date field, for example, may store a date as "Y-m-d" string (1) in the
  41.  * object. To facilitate processing in the field, this value is normalized
  42.  * to a DateTime object (2). In the HTML representation of your form, a
  43.  * localized string (3) may be presented to and modified by the user, or it could be an array of values
  44.  * to be mapped to choices fields.
  45.  *
  46.  * In most cases, format (1) and format (2) will be the same. For example,
  47.  * a checkbox field uses a Boolean value for both internal processing and
  48.  * storage in the object. In these cases you need to set a view transformer
  49.  * to convert between formats (2) and (3). You can do this by calling
  50.  * addViewTransformer().
  51.  *
  52.  * In some cases though it makes sense to make format (1) configurable. To
  53.  * demonstrate this, let's extend our above date field to store the value
  54.  * either as "Y-m-d" string or as timestamp. Internally we still want to
  55.  * use a DateTime object for processing. To convert the data from string/integer
  56.  * to DateTime you can set a model transformer by calling
  57.  * addModelTransformer(). The normalized data is then converted to the displayed
  58.  * data as described before.
  59.  *
  60.  * The conversions (1) -> (2) -> (3) use the transform methods of the transformers.
  61.  * The conversions (3) -> (2) -> (1) use the reverseTransform methods of the transformers.
  62.  *
  63.  * @author Fabien Potencier <fabien@symfony.com>
  64.  * @author Bernhard Schussek <bschussek@gmail.com>
  65.  *
  66.  * @implements \IteratorAggregate<string, FormInterface>
  67.  */
  68. class Form implements \IteratorAggregateFormInterfaceClearableErrorsInterface
  69. {
  70.     /**
  71.      * @var FormConfigInterface
  72.      */
  73.     private $config;
  74.     /**
  75.      * @var FormInterface|null
  76.      */
  77.     private $parent;
  78.     /**
  79.      * A map of FormInterface instances.
  80.      *
  81.      * @var OrderedHashMap<string, FormInterface>
  82.      */
  83.     private $children;
  84.     /**
  85.      * @var FormError[]
  86.      */
  87.     private $errors = [];
  88.     /**
  89.      * @var bool
  90.      */
  91.     private $submitted false;
  92.     /**
  93.      * The button that was used to submit the form.
  94.      *
  95.      * @var FormInterface|ClickableInterface|null
  96.      */
  97.     private $clickedButton;
  98.     /**
  99.      * @var mixed
  100.      */
  101.     private $modelData;
  102.     /**
  103.      * @var mixed
  104.      */
  105.     private $normData;
  106.     /**
  107.      * @var mixed
  108.      */
  109.     private $viewData;
  110.     /**
  111.      * The submitted values that don't belong to any children.
  112.      *
  113.      * @var array
  114.      */
  115.     private $extraData = [];
  116.     /**
  117.      * The transformation failure generated during submission, if any.
  118.      *
  119.      * @var TransformationFailedException|null
  120.      */
  121.     private $transformationFailure;
  122.     /**
  123.      * Whether the form's data has been initialized.
  124.      *
  125.      * When the data is initialized with its default value, that default value
  126.      * is passed through the transformer chain in order to synchronize the
  127.      * model, normalized and view format for the first time. This is done
  128.      * lazily in order to save performance when {@link setData()} is called
  129.      * manually, making the initialization with the configured default value
  130.      * superfluous.
  131.      *
  132.      * @var bool
  133.      */
  134.     private $defaultDataSet false;
  135.     /**
  136.      * Whether setData() is currently being called.
  137.      *
  138.      * @var bool
  139.      */
  140.     private $lockSetData false;
  141.     /**
  142.      * @var string
  143.      */
  144.     private $name '';
  145.     /**
  146.      * Whether the form inherits its underlying data from its parent.
  147.      *
  148.      * @var bool
  149.      */
  150.     private $inheritData;
  151.     /**
  152.      * @var PropertyPathInterface|null
  153.      */
  154.     private $propertyPath;
  155.     /**
  156.      * @throws LogicException if a data mapper is not provided for a compound form
  157.      */
  158.     public function __construct(FormConfigInterface $config)
  159.     {
  160.         // Compound forms always need a data mapper, otherwise calls to
  161.         // `setData` and `add` will not lead to the correct population of
  162.         // the child forms.
  163.         if ($config->getCompound() && !$config->getDataMapper()) {
  164.             throw new LogicException('Compound forms need a data mapper.');
  165.         }
  166.         // If the form inherits the data from its parent, it is not necessary
  167.         // to call setData() with the default data.
  168.         if ($this->inheritData $config->getInheritData()) {
  169.             $this->defaultDataSet true;
  170.         }
  171.         $this->config $config;
  172.         $this->children = new OrderedHashMap();
  173.         $this->name $config->getName();
  174.     }
  175.     public function __clone()
  176.     {
  177.         $this->children = clone $this->children;
  178.         foreach ($this->children as $key => $child) {
  179.             $this->children[$key] = clone $child;
  180.         }
  181.     }
  182.     /**
  183.      * {@inheritdoc}
  184.      */
  185.     public function getConfig()
  186.     {
  187.         return $this->config;
  188.     }
  189.     /**
  190.      * {@inheritdoc}
  191.      */
  192.     public function getName()
  193.     {
  194.         return $this->name;
  195.     }
  196.     /**
  197.      * {@inheritdoc}
  198.      */
  199.     public function getPropertyPath()
  200.     {
  201.         if ($this->propertyPath || $this->propertyPath $this->config->getPropertyPath()) {
  202.             return $this->propertyPath;
  203.         }
  204.         if ('' === $this->name) {
  205.             return null;
  206.         }
  207.         $parent $this->parent;
  208.         while ($parent && $parent->getConfig()->getInheritData()) {
  209.             $parent $parent->getParent();
  210.         }
  211.         if ($parent && null === $parent->getConfig()->getDataClass()) {
  212.             $this->propertyPath = new PropertyPath('['.$this->name.']');
  213.         } else {
  214.             $this->propertyPath = new PropertyPath($this->name);
  215.         }
  216.         return $this->propertyPath;
  217.     }
  218.     /**
  219.      * {@inheritdoc}
  220.      */
  221.     public function isRequired()
  222.     {
  223.         if (null === $this->parent || $this->parent->isRequired()) {
  224.             return $this->config->getRequired();
  225.         }
  226.         return false;
  227.     }
  228.     /**
  229.      * {@inheritdoc}
  230.      */
  231.     public function isDisabled()
  232.     {
  233.         if (null === $this->parent || !$this->parent->isDisabled()) {
  234.             return $this->config->getDisabled();
  235.         }
  236.         return true;
  237.     }
  238.     /**
  239.      * {@inheritdoc}
  240.      */
  241.     public function setParent(FormInterface $parent null)
  242.     {
  243.         if ($this->submitted) {
  244.             throw new AlreadySubmittedException('You cannot set the parent of a submitted form.');
  245.         }
  246.         if (null !== $parent && '' === $this->name) {
  247.             throw new LogicException('A form with an empty name cannot have a parent form.');
  248.         }
  249.         $this->parent $parent;
  250.         return $this;
  251.     }
  252.     /**
  253.      * {@inheritdoc}
  254.      */
  255.     public function getParent()
  256.     {
  257.         return $this->parent;
  258.     }
  259.     /**
  260.      * {@inheritdoc}
  261.      */
  262.     public function getRoot()
  263.     {
  264.         return $this->parent $this->parent->getRoot() : $this;
  265.     }
  266.     /**
  267.      * {@inheritdoc}
  268.      */
  269.     public function isRoot()
  270.     {
  271.         return null === $this->parent;
  272.     }
  273.     /**
  274.      * {@inheritdoc}
  275.      */
  276.     public function setData($modelData)
  277.     {
  278.         // If the form is submitted while disabled, it is set to submitted, but the data is not
  279.         // changed. In such cases (i.e. when the form is not initialized yet) don't
  280.         // abort this method.
  281.         if ($this->submitted && $this->defaultDataSet) {
  282.             throw new AlreadySubmittedException('You cannot change the data of a submitted form.');
  283.         }
  284.         // If the form inherits its parent's data, disallow data setting to
  285.         // prevent merge conflicts
  286.         if ($this->inheritData) {
  287.             throw new RuntimeException('You cannot change the data of a form inheriting its parent data.');
  288.         }
  289.         // Don't allow modifications of the configured data if the data is locked
  290.         if ($this->config->getDataLocked() && $modelData !== $this->config->getData()) {
  291.             return $this;
  292.         }
  293.         if (\is_object($modelData) && !$this->config->getByReference()) {
  294.             $modelData = clone $modelData;
  295.         }
  296.         if ($this->lockSetData) {
  297.             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.');
  298.         }
  299.         $this->lockSetData true;
  300.         $dispatcher $this->config->getEventDispatcher();
  301.         // Hook to change content of the model data before transformation and mapping children
  302.         if ($dispatcher->hasListeners(FormEvents::PRE_SET_DATA)) {
  303.             $event = new PreSetDataEvent($this$modelData);
  304.             $dispatcher->dispatch($eventFormEvents::PRE_SET_DATA);
  305.             $modelData $event->getData();
  306.         }
  307.         // Treat data as strings unless a transformer exists
  308.         if (is_scalar($modelData) && !$this->config->getViewTransformers() && !$this->config->getModelTransformers()) {
  309.             $modelData = (string) $modelData;
  310.         }
  311.         // Synchronize representations - must not change the content!
  312.         // Transformation exceptions are not caught on initialization
  313.         $normData $this->modelToNorm($modelData);
  314.         $viewData $this->normToView($normData);
  315.         // Validate if view data matches data class (unless empty)
  316.         if (!FormUtil::isEmpty($viewData)) {
  317.             $dataClass $this->config->getDataClass();
  318.             if (null !== $dataClass && !$viewData instanceof $dataClass) {
  319.                 $actualType get_debug_type($viewData);
  320.                 throw new LogicException('The form\'s view data is expected to be a "'.$dataClass.'", but it is a "'.$actualType.'". You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms "'.$actualType.'" to an instance of "'.$dataClass.'".');
  321.             }
  322.         }
  323.         $this->modelData $modelData;
  324.         $this->normData $normData;
  325.         $this->viewData $viewData;
  326.         $this->defaultDataSet true;
  327.         $this->lockSetData false;
  328.         // Compound forms don't need to invoke this method if they don't have children
  329.         if (\count($this->children) > 0) {
  330.             // Update child forms from the data (unless their config data is locked)
  331.             $this->config->getDataMapper()->mapDataToForms($viewData, new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)));
  332.         }
  333.         if ($dispatcher->hasListeners(FormEvents::POST_SET_DATA)) {
  334.             $event = new PostSetDataEvent($this$modelData);
  335.             $dispatcher->dispatch($eventFormEvents::POST_SET_DATA);
  336.         }
  337.         return $this;
  338.     }
  339.     /**
  340.      * {@inheritdoc}
  341.      */
  342.     public function getData()
  343.     {
  344.         if ($this->inheritData) {
  345.             if (!$this->parent) {
  346.                 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
  347.             }
  348.             return $this->parent->getData();
  349.         }
  350.         if (!$this->defaultDataSet) {
  351.             if ($this->lockSetData) {
  352.                 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getData() if the form data has not already been set. You should call getData() on the FormEvent object instead.');
  353.             }
  354.             $this->setData($this->config->getData());
  355.         }
  356.         return $this->modelData;
  357.     }
  358.     /**
  359.      * {@inheritdoc}
  360.      */
  361.     public function getNormData()
  362.     {
  363.         if ($this->inheritData) {
  364.             if (!$this->parent) {
  365.                 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
  366.             }
  367.             return $this->parent->getNormData();
  368.         }
  369.         if (!$this->defaultDataSet) {
  370.             if ($this->lockSetData) {
  371.                 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getNormData() if the form data has not already been set.');
  372.             }
  373.             $this->setData($this->config->getData());
  374.         }
  375.         return $this->normData;
  376.     }
  377.     /**
  378.      * {@inheritdoc}
  379.      */
  380.     public function getViewData()
  381.     {
  382.         if ($this->inheritData) {
  383.             if (!$this->parent) {
  384.                 throw new RuntimeException('The form is configured to inherit its parent\'s data, but does not have a parent.');
  385.             }
  386.             return $this->parent->getViewData();
  387.         }
  388.         if (!$this->defaultDataSet) {
  389.             if ($this->lockSetData) {
  390.                 throw new RuntimeException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call getViewData() if the form data has not already been set.');
  391.             }
  392.             $this->setData($this->config->getData());
  393.         }
  394.         return $this->viewData;
  395.     }
  396.     /**
  397.      * {@inheritdoc}
  398.      */
  399.     public function getExtraData()
  400.     {
  401.         return $this->extraData;
  402.     }
  403.     /**
  404.      * {@inheritdoc}
  405.      */
  406.     public function initialize()
  407.     {
  408.         if (null !== $this->parent) {
  409.             throw new RuntimeException('Only root forms should be initialized.');
  410.         }
  411.         // Guarantee that the *_SET_DATA events have been triggered once the
  412.         // form is initialized. This makes sure that dynamically added or
  413.         // removed fields are already visible after initialization.
  414.         if (!$this->defaultDataSet) {
  415.             $this->setData($this->config->getData());
  416.         }
  417.         return $this;
  418.     }
  419.     /**
  420.      * {@inheritdoc}
  421.      */
  422.     public function handleRequest($request null)
  423.     {
  424.         $this->config->getRequestHandler()->handleRequest($this$request);
  425.         return $this;
  426.     }
  427.     /**
  428.      * {@inheritdoc}
  429.      */
  430.     public function submit($submittedDatabool $clearMissing true)
  431.     {
  432.         if ($this->submitted) {
  433.             throw new AlreadySubmittedException('A form can only be submitted once.');
  434.         }
  435.         // Initialize errors in the very beginning so we're sure
  436.         // they are collectable during submission only
  437.         $this->errors = [];
  438.         // Obviously, a disabled form should not change its data upon submission.
  439.         if ($this->isDisabled()) {
  440.             $this->submitted true;
  441.             return $this;
  442.         }
  443.         // The data must be initialized if it was not initialized yet.
  444.         // This is necessary to guarantee that the *_SET_DATA listeners
  445.         // are always invoked before submit() takes place.
  446.         if (!$this->defaultDataSet) {
  447.             $this->setData($this->config->getData());
  448.         }
  449.         // Treat false as NULL to support binding false to checkboxes.
  450.         // Don't convert NULL to a string here in order to determine later
  451.         // whether an empty value has been submitted or whether no value has
  452.         // been submitted at all. This is important for processing checkboxes
  453.         // and radio buttons with empty values.
  454.         if (false === $submittedData) {
  455.             $submittedData null;
  456.         } elseif (is_scalar($submittedData)) {
  457.             $submittedData = (string) $submittedData;
  458.         } elseif ($this->config->getRequestHandler()->isFileUpload($submittedData)) {
  459.             if (!$this->config->getOption('allow_file_upload')) {
  460.                 $submittedData null;
  461.                 $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, file upload given.');
  462.             }
  463.         } elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->hasOption('multiple')) {
  464.             $submittedData null;
  465.             $this->transformationFailure = new TransformationFailedException('Submitted data was expected to be text or number, array given.');
  466.         }
  467.         $dispatcher $this->config->getEventDispatcher();
  468.         $modelData null;
  469.         $normData null;
  470.         $viewData null;
  471.         try {
  472.             if (null !== $this->transformationFailure) {
  473.                 throw $this->transformationFailure;
  474.             }
  475.             // Hook to change content of the data submitted by the browser
  476.             if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
  477.                 $event = new PreSubmitEvent($this$submittedData);
  478.                 $dispatcher->dispatch($eventFormEvents::PRE_SUBMIT);
  479.                 $submittedData $event->getData();
  480.             }
  481.             // Check whether the form is compound.
  482.             // This check is preferable over checking the number of children,
  483.             // since forms without children may also be compound.
  484.             // (think of empty collection forms)
  485.             if ($this->config->getCompound()) {
  486.                 if (null === $submittedData) {
  487.                     $submittedData = [];
  488.                 }
  489.                 if (!\is_array($submittedData)) {
  490.                     throw new TransformationFailedException('Compound forms expect an array or NULL on submission.');
  491.                 }
  492.                 foreach ($this->children as $name => $child) {
  493.                     $isSubmitted = \array_key_exists($name$submittedData);
  494.                     if ($isSubmitted || $clearMissing) {
  495.                         $child->submit($isSubmitted $submittedData[$name] : null$clearMissing);
  496.                         unset($submittedData[$name]);
  497.                         if (null !== $this->clickedButton) {
  498.                             continue;
  499.                         }
  500.                         if ($child instanceof ClickableInterface && $child->isClicked()) {
  501.                             $this->clickedButton $child;
  502.                             continue;
  503.                         }
  504.                         if (method_exists($child'getClickedButton') && null !== $child->getClickedButton()) {
  505.                             $this->clickedButton $child->getClickedButton();
  506.                         }
  507.                     }
  508.                 }
  509.                 $this->extraData $submittedData;
  510.             }
  511.             // Forms that inherit their parents' data also are not processed,
  512.             // because then it would be too difficult to merge the changes in
  513.             // the child and the parent form. Instead, the parent form also takes
  514.             // changes in the grandchildren (i.e. children of the form that inherits
  515.             // its parent's data) into account.
  516.             // (see InheritDataAwareIterator below)
  517.             if (!$this->inheritData) {
  518.                 // If the form is compound, the view data is merged with the data
  519.                 // of the children using the data mapper.
  520.                 // If the form is not compound, the view data is assigned to the submitted data.
  521.                 $viewData $this->config->getCompound() ? $this->viewData $submittedData;
  522.                 if (FormUtil::isEmpty($viewData)) {
  523.                     $emptyData $this->config->getEmptyData();
  524.                     if ($emptyData instanceof \Closure) {
  525.                         $emptyData $emptyData($this$viewData);
  526.                     }
  527.                     $viewData $emptyData;
  528.                 }
  529.                 // Merge form data from children into existing view data
  530.                 // It is not necessary to invoke this method if the form has no children,
  531.                 // even if it is compound.
  532.                 if (\count($this->children) > 0) {
  533.                     // Use InheritDataAwareIterator to process children of
  534.                     // descendants that inherit this form's data.
  535.                     // These descendants will not be submitted normally (see the check
  536.                     // for $this->config->getInheritData() above)
  537.                     $this->config->getDataMapper()->mapFormsToData(
  538.                         new \RecursiveIteratorIterator(new InheritDataAwareIterator($this->children)),
  539.                         $viewData
  540.                     );
  541.                 }
  542.                 // Normalize data to unified representation
  543.                 $normData $this->viewToNorm($viewData);
  544.                 // Hook to change content of the data in the normalized
  545.                 // representation
  546.                 if ($dispatcher->hasListeners(FormEvents::SUBMIT)) {
  547.                     $event = new SubmitEvent($this$normData);
  548.                     $dispatcher->dispatch($eventFormEvents::SUBMIT);
  549.                     $normData $event->getData();
  550.                 }
  551.                 // Synchronize representations - must not change the content!
  552.                 $modelData $this->normToModel($normData);
  553.                 $viewData $this->normToView($normData);
  554.             }
  555.         } catch (TransformationFailedException $e) {
  556.             $this->transformationFailure $e;
  557.             // If $viewData was not yet set, set it to $submittedData so that
  558.             // the erroneous data is accessible on the form.
  559.             // Forms that inherit data never set any data, because the getters
  560.             // forward to the parent form's getters anyway.
  561.             if (null === $viewData && !$this->inheritData) {
  562.                 $viewData $submittedData;
  563.             }
  564.         }
  565.         $this->submitted true;
  566.         $this->modelData $modelData;
  567.         $this->normData $normData;
  568.         $this->viewData $viewData;
  569.         if ($dispatcher->hasListeners(FormEvents::POST_SUBMIT)) {
  570.             $event = new PostSubmitEvent($this$viewData);
  571.             $dispatcher->dispatch($eventFormEvents::POST_SUBMIT);
  572.         }
  573.         return $this;
  574.     }
  575.     /**
  576.      * {@inheritdoc}
  577.      */
  578.     public function addError(FormError $error)
  579.     {
  580.         if (null === $error->getOrigin()) {
  581.             $error->setOrigin($this);
  582.         }
  583.         if ($this->parent && $this->config->getErrorBubbling()) {
  584.             $this->parent->addError($error);
  585.         } else {
  586.             $this->errors[] = $error;
  587.         }
  588.         return $this;
  589.     }
  590.     /**
  591.      * {@inheritdoc}
  592.      */
  593.     public function isSubmitted()
  594.     {
  595.         return $this->submitted;
  596.     }
  597.     /**
  598.      * {@inheritdoc}
  599.      */
  600.     public function isSynchronized()
  601.     {
  602.         return null === $this->transformationFailure;
  603.     }
  604.     /**
  605.      * {@inheritdoc}
  606.      */
  607.     public function getTransformationFailure()
  608.     {
  609.         return $this->transformationFailure;
  610.     }
  611.     /**
  612.      * {@inheritdoc}
  613.      */
  614.     public function isEmpty()
  615.     {
  616.         foreach ($this->children as $child) {
  617.             if (!$child->isEmpty()) {
  618.                 return false;
  619.             }
  620.         }
  621.         if (!method_exists($this->config'getIsEmptyCallback')) {
  622.             trigger_deprecation('symfony/form''5.1''Not implementing the "%s::getIsEmptyCallback()" method in "%s" is deprecated.'FormConfigInterface::class, \get_class($this->config));
  623.             $isEmptyCallback null;
  624.         } else {
  625.             $isEmptyCallback $this->config->getIsEmptyCallback();
  626.         }
  627.         if (null !== $isEmptyCallback) {
  628.             return $isEmptyCallback($this->modelData);
  629.         }
  630.         return FormUtil::isEmpty($this->modelData) ||
  631.             // arrays, countables
  632.             ((\is_array($this->modelData) || $this->modelData instanceof \Countable) && === \count($this->modelData)) ||
  633.             // traversables that are not countable
  634.             ($this->modelData instanceof \Traversable && === iterator_count($this->modelData));
  635.     }
  636.     /**
  637.      * {@inheritdoc}
  638.      */
  639.     public function isValid()
  640.     {
  641.         if (!$this->submitted) {
  642.             throw new LogicException('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().');
  643.         }
  644.         if ($this->isDisabled()) {
  645.             return true;
  646.         }
  647.         return === \count($this->getErrors(true));
  648.     }
  649.     /**
  650.      * Returns the button that was used to submit the form.
  651.      *
  652.      * @return FormInterface|ClickableInterface|null
  653.      */
  654.     public function getClickedButton()
  655.     {
  656.         if ($this->clickedButton) {
  657.             return $this->clickedButton;
  658.         }
  659.         return $this->parent && method_exists($this->parent'getClickedButton') ? $this->parent->getClickedButton() : null;
  660.     }
  661.     /**
  662.      * {@inheritdoc}
  663.      */
  664.     public function getErrors(bool $deep falsebool $flatten true)
  665.     {
  666.         $errors $this->errors;
  667.         // Copy the errors of nested forms to the $errors array
  668.         if ($deep) {
  669.             foreach ($this as $child) {
  670.                 /** @var FormInterface $child */
  671.                 if ($child->isSubmitted() && $child->isValid()) {
  672.                     continue;
  673.                 }
  674.                 $iterator $child->getErrors(true$flatten);
  675.                 if (=== \count($iterator)) {
  676.                     continue;
  677.                 }
  678.                 if ($flatten) {
  679.                     foreach ($iterator as $error) {
  680.                         $errors[] = $error;
  681.                     }
  682.                 } else {
  683.                     $errors[] = $iterator;
  684.                 }
  685.             }
  686.         }
  687.         return new FormErrorIterator($this$errors);
  688.     }
  689.     /**
  690.      * {@inheritdoc}
  691.      */
  692.     public function clearErrors(bool $deep false): self
  693.     {
  694.         $this->errors = [];
  695.         if ($deep) {
  696.             // Clear errors from children
  697.             foreach ($this as $child) {
  698.                 if ($child instanceof ClearableErrorsInterface) {
  699.                     $child->clearErrors(true);
  700.                 }
  701.             }
  702.         }
  703.         return $this;
  704.     }
  705.     /**
  706.      * {@inheritdoc}
  707.      */
  708.     public function all()
  709.     {
  710.         return iterator_to_array($this->children);
  711.     }
  712.     /**
  713.      * {@inheritdoc}
  714.      */
  715.     public function add($childstring $type null, array $options = [])
  716.     {
  717.         if ($this->submitted) {
  718.             throw new AlreadySubmittedException('You cannot add children to a submitted form.');
  719.         }
  720.         if (!$this->config->getCompound()) {
  721.             throw new LogicException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
  722.         }
  723.         if (!$child instanceof FormInterface) {
  724.             if (!\is_string($child) && !\is_int($child)) {
  725.                 throw new UnexpectedTypeException($child'string or Symfony\Component\Form\FormInterface');
  726.             }
  727.             $child = (string) $child;
  728.             if (null !== $type && !\is_string($type)) {
  729.                 throw new UnexpectedTypeException($type'string or null');
  730.             }
  731.             // Never initialize child forms automatically
  732.             $options['auto_initialize'] = false;
  733.             if (null === $type && null === $this->config->getDataClass()) {
  734.                 $type TextType::class;
  735.             }
  736.             if (null === $type) {
  737.                 $child $this->config->getFormFactory()->createForProperty($this->config->getDataClass(), $childnull$options);
  738.             } else {
  739.                 $child $this->config->getFormFactory()->createNamed($child$typenull$options);
  740.             }
  741.         } elseif ($child->getConfig()->getAutoInitialize()) {
  742.             throw new RuntimeException(sprintf('Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "%s".'$child->getName()));
  743.         }
  744.         $this->children[$child->getName()] = $child;
  745.         $child->setParent($this);
  746.         // If setData() is currently being called, there is no need to call
  747.         // mapDataToForms() here, as mapDataToForms() is called at the end
  748.         // of setData() anyway. Not doing this check leads to an endless
  749.         // recursion when initializing the form lazily and an event listener
  750.         // (such as ResizeFormListener) adds fields depending on the data:
  751.         //
  752.         //  * setData() is called, the form is not initialized yet
  753.         //  * add() is called by the listener (setData() is not complete, so
  754.         //    the form is still not initialized)
  755.         //  * getViewData() is called
  756.         //  * setData() is called since the form is not initialized yet
  757.         //  * ... endless recursion ...
  758.         //
  759.         // Also skip data mapping if setData() has not been called yet.
  760.         // setData() will be called upon form initialization and data mapping
  761.         // will take place by then.
  762.         if (!$this->lockSetData && $this->defaultDataSet && !$this->inheritData) {
  763.             $viewData $this->getViewData();
  764.             $this->config->getDataMapper()->mapDataToForms(
  765.                 $viewData,
  766.                 new \RecursiveIteratorIterator(new InheritDataAwareIterator(new \ArrayIterator([$child->getName() => $child])))
  767.             );
  768.         }
  769.         return $this;
  770.     }
  771.     /**
  772.      * {@inheritdoc}
  773.      */
  774.     public function remove(string $name)
  775.     {
  776.         if ($this->submitted) {
  777.             throw new AlreadySubmittedException('You cannot remove children from a submitted form.');
  778.         }
  779.         if (isset($this->children[$name])) {
  780.             if (!$this->children[$name]->isSubmitted()) {
  781.                 $this->children[$name]->setParent(null);
  782.             }
  783.             unset($this->children[$name]);
  784.         }
  785.         return $this;
  786.     }
  787.     /**
  788.      * {@inheritdoc}
  789.      */
  790.     public function has(string $name)
  791.     {
  792.         return isset($this->children[$name]);
  793.     }
  794.     /**
  795.      * {@inheritdoc}
  796.      */
  797.     public function get(string $name)
  798.     {
  799.         if (isset($this->children[$name])) {
  800.             return $this->children[$name];
  801.         }
  802.         throw new OutOfBoundsException(sprintf('Child "%s" does not exist.'$name));
  803.     }
  804.     /**
  805.      * Returns whether a child with the given name exists (implements the \ArrayAccess interface).
  806.      *
  807.      * @param string $name The name of the child
  808.      *
  809.      * @return bool
  810.      */
  811.     #[\ReturnTypeWillChange]
  812.     public function offsetExists($name)
  813.     {
  814.         return $this->has($name);
  815.     }
  816.     /**
  817.      * Returns the child with the given name (implements the \ArrayAccess interface).
  818.      *
  819.      * @param string $name The name of the child
  820.      *
  821.      * @return FormInterface
  822.      *
  823.      * @throws OutOfBoundsException if the named child does not exist
  824.      */
  825.     #[\ReturnTypeWillChange]
  826.     public function offsetGet($name)
  827.     {
  828.         return $this->get($name);
  829.     }
  830.     /**
  831.      * Adds a child to the form (implements the \ArrayAccess interface).
  832.      *
  833.      * @param string        $name  Ignored. The name of the child is used
  834.      * @param FormInterface $child The child to be added
  835.      *
  836.      * @return void
  837.      *
  838.      * @throws AlreadySubmittedException if the form has already been submitted
  839.      * @throws LogicException            when trying to add a child to a non-compound form
  840.      *
  841.      * @see self::add()
  842.      */
  843.     #[\ReturnTypeWillChange]
  844.     public function offsetSet($name$child)
  845.     {
  846.         $this->add($child);
  847.     }
  848.     /**
  849.      * Removes the child with the given name from the form (implements the \ArrayAccess interface).
  850.      *
  851.      * @param string $name The name of the child to remove
  852.      *
  853.      * @return void
  854.      *
  855.      * @throws AlreadySubmittedException if the form has already been submitted
  856.      */
  857.     #[\ReturnTypeWillChange]
  858.     public function offsetUnset($name)
  859.     {
  860.         $this->remove($name);
  861.     }
  862.     /**
  863.      * Returns the iterator for this group.
  864.      *
  865.      * @return \Traversable<string, FormInterface>
  866.      */
  867.     #[\ReturnTypeWillChange]
  868.     public function getIterator()
  869.     {
  870.         return $this->children;
  871.     }
  872.     /**
  873.      * Returns the number of form children (implements the \Countable interface).
  874.      *
  875.      * @return int
  876.      */
  877.     #[\ReturnTypeWillChange]
  878.     public function count()
  879.     {
  880.         return \count($this->children);
  881.     }
  882.     /**
  883.      * {@inheritdoc}
  884.      */
  885.     public function createView(FormView $parent null)
  886.     {
  887.         if (null === $parent && $this->parent) {
  888.             $parent $this->parent->createView();
  889.         }
  890.         $type $this->config->getType();
  891.         $options $this->config->getOptions();
  892.         // The methods createView(), buildView() and finishView() are called
  893.         // explicitly here in order to be able to override either of them
  894.         // in a custom resolved form type.
  895.         $view $type->createView($this$parent);
  896.         $type->buildView($view$this$options);
  897.         foreach ($this->children as $name => $child) {
  898.             $view->children[$name] = $child->createView($view);
  899.         }
  900.         $this->sort($view->children);
  901.         $type->finishView($view$this$options);
  902.         return $view;
  903.     }
  904.     /**
  905.      * Sorts view fields based on their priority value.
  906.      */
  907.     private function sort(array &$children): void
  908.     {
  909.         $c = [];
  910.         $i 0;
  911.         $needsSorting false;
  912.         foreach ($children as $name => $child) {
  913.             $c[$name] = ['p' => $child->vars['priority'] ?? 0'i' => $i++];
  914.             if (!== $c[$name]['p']) {
  915.                 $needsSorting true;
  916.             }
  917.         }
  918.         if (!$needsSorting) {
  919.             return;
  920.         }
  921.         uksort($children, static function ($a$b) use ($c): int {
  922.             return [$c[$b]['p'], $c[$a]['i']] <=> [$c[$a]['p'], $c[$b]['i']];
  923.         });
  924.     }
  925.     /**
  926.      * Normalizes the underlying data if a model transformer is set.
  927.      *
  928.      * @return mixed
  929.      *
  930.      * @throws TransformationFailedException If the underlying data cannot be transformed to "normalized" format
  931.      */
  932.     private function modelToNorm($value)
  933.     {
  934.         try {
  935.             foreach ($this->config->getModelTransformers() as $transformer) {
  936.                 $value $transformer->transform($value);
  937.             }
  938.         } catch (TransformationFailedException $exception) {
  939.             throw new TransformationFailedException(sprintf('Unable to transform data for property path "%s": '$this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception$exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
  940.         }
  941.         return $value;
  942.     }
  943.     /**
  944.      * Reverse transforms a value if a model transformer is set.
  945.      *
  946.      * @return mixed
  947.      *
  948.      * @throws TransformationFailedException If the value cannot be transformed to "model" format
  949.      */
  950.     private function normToModel($value)
  951.     {
  952.         try {
  953.             $transformers $this->config->getModelTransformers();
  954.             for ($i = \count($transformers) - 1$i >= 0; --$i) {
  955.                 $value $transformers[$i]->reverseTransform($value);
  956.             }
  957.         } catch (TransformationFailedException $exception) {
  958.             throw new TransformationFailedException(sprintf('Unable to reverse value for property path "%s": '$this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception$exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
  959.         }
  960.         return $value;
  961.     }
  962.     /**
  963.      * Transforms the value if a view transformer is set.
  964.      *
  965.      * @return mixed
  966.      *
  967.      * @throws TransformationFailedException If the normalized value cannot be transformed to "view" format
  968.      */
  969.     private function normToView($value)
  970.     {
  971.         // Scalar values should  be converted to strings to
  972.         // facilitate differentiation between empty ("") and zero (0).
  973.         // Only do this for simple forms, as the resulting value in
  974.         // compound forms is passed to the data mapper and thus should
  975.         // not be converted to a string before.
  976.         if (!($transformers $this->config->getViewTransformers()) && !$this->config->getCompound()) {
  977.             return null === $value || is_scalar($value) ? (string) $value $value;
  978.         }
  979.         try {
  980.             foreach ($transformers as $transformer) {
  981.                 $value $transformer->transform($value);
  982.             }
  983.         } catch (TransformationFailedException $exception) {
  984.             throw new TransformationFailedException(sprintf('Unable to transform value for property path "%s": '$this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception$exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
  985.         }
  986.         return $value;
  987.     }
  988.     /**
  989.      * Reverse transforms a value if a view transformer is set.
  990.      *
  991.      * @return mixed
  992.      *
  993.      * @throws TransformationFailedException If the submitted value cannot be transformed to "normalized" format
  994.      */
  995.     private function viewToNorm($value)
  996.     {
  997.         if (!$transformers $this->config->getViewTransformers()) {
  998.             return '' === $value null $value;
  999.         }
  1000.         try {
  1001.             for ($i = \count($transformers) - 1$i >= 0; --$i) {
  1002.                 $value $transformers[$i]->reverseTransform($value);
  1003.             }
  1004.         } catch (TransformationFailedException $exception) {
  1005.             throw new TransformationFailedException(sprintf('Unable to reverse value for property path "%s": '$this->getPropertyPath()).$exception->getMessage(), $exception->getCode(), $exception$exception->getInvalidMessage(), $exception->getInvalidMessageParameters());
  1006.         }
  1007.         return $value;
  1008.     }
  1009. }