vendor/symfony/form/Form.php line 472

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