Form.php 39 KB

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