vendor/symfony/form/Form.php line 886

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