Form.php 37 KB

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