vendor/symfony/form/Form.php line 765

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 \IteratorAggregate, FormInterface, ClearableErrorsInterface
  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($event, FormEvents::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($event, FormEvents::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($submittedData, bool $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->getOption('multiple', false)) {
  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($event, FormEvents::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($event, FormEvents::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($event, FormEvents::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) && 0 === \count($this->modelData)) ||
  633. // traversables that are not countable
  634. ($this->modelData instanceof \Traversable && 0 === 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 0 === \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 = false, bool $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 (0 === \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($child, ?string $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(), $child, null, $options);
  738. } else {
  739. $child = $this->config->getFormFactory()->createNamed($child, $type, null, $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 (0 !== $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. }