Form.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  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\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\FileBag;
  13. use Symfony\Component\Form\Event\DataEvent;
  14. use Symfony\Component\Form\Event\FilterDataEvent;
  15. use Symfony\Component\Form\Exception\FormException;
  16. use Symfony\Component\Form\Exception\MissingOptionsException;
  17. use Symfony\Component\Form\Exception\AlreadyBoundException;
  18. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  19. use Symfony\Component\Form\Exception\DanglingFieldException;
  20. use Symfony\Component\Form\Exception\FieldDefinitionException;
  21. use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
  22. use Symfony\Component\Form\DataTransformer\DataTransformerInterface;
  23. use Symfony\Component\Form\DataTransformer\TransformationFailedException;
  24. use Symfony\Component\Form\DataMapper\DataMapperInterface;
  25. use Symfony\Component\Form\Validator\FormValidatorInterface;
  26. use Symfony\Component\Form\TemplateContext;
  27. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  28. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  29. /**
  30. * Form represents a form.
  31. *
  32. * A form is composed of a validator schema and a widget form schema.
  33. *
  34. * Form also takes care of CSRF protection by default.
  35. *
  36. * A CSRF secret can be any random string. If set to false, it disables the
  37. * CSRF protection, and if set to null, it forces the form to use the global
  38. * CSRF secret. If the global CSRF secret is also null, then a random one
  39. * is generated on the fly.
  40. *
  41. * To implement your own form fields, you need to have a thorough understanding
  42. * of the data flow within a form field. A form field stores its data in three
  43. * different representations:
  44. *
  45. * (1) the format required by the form's object
  46. * (2) a normalized format for internal processing
  47. * (3) the format used for display
  48. *
  49. * A date field, for example, may store a date as "Y-m-d" string (1) in the
  50. * object. To facilitate processing in the field, this value is normalized
  51. * to a DateTime object (2). In the HTML representation of your form, a
  52. * localized string (3) is presented to and modified by the user.
  53. *
  54. * In most cases, format (1) and format (2) will be the same. For example,
  55. * a checkbox field uses a Boolean value both for internal processing as for
  56. * storage in the object. In these cases you simply need to set a value
  57. * transformer to convert between formats (2) and (3). You can do this by
  58. * calling appendClientTransformer() in the configure() method.
  59. *
  60. * In some cases though it makes sense to make format (1) configurable. To
  61. * demonstrate this, let's extend our above date field to store the value
  62. * either as "Y-m-d" string or as timestamp. Internally we still want to
  63. * use a DateTime object for processing. To convert the data from string/integer
  64. * to DateTime you can set a normalization transformer by calling
  65. * appendNormTransformer() in configure(). The normalized data is then
  66. * converted to the displayed data as described before.
  67. *
  68. * @author Fabien Potencier <fabien@symfony.com>
  69. * @author Bernhard Schussek <bernhard.schussek@symfony.com>
  70. */
  71. class Form implements \IteratorAggregate, FormInterface
  72. {
  73. /**
  74. * The name of this form
  75. * @var string
  76. */
  77. private $name;
  78. /**
  79. * The parent fo this form
  80. * @var FormInterface
  81. */
  82. private $parent;
  83. /**
  84. * The children of this form
  85. * @var array
  86. */
  87. private $children = array();
  88. /**
  89. * The mapper for mapping data to children and back
  90. * @var DataMapper\DataMapperInterface
  91. */
  92. private $dataMapper;
  93. /**
  94. * The errors of this form
  95. * @var array
  96. */
  97. private $errors = array();
  98. /**
  99. * Whether added errors should bubble up to the parent
  100. * @var Boolean
  101. */
  102. private $errorBubbling;
  103. /**
  104. * Whether this form is bound
  105. * @var Boolean
  106. */
  107. private $bound = false;
  108. /**
  109. * Whether this form may not be empty
  110. * @var Boolean
  111. */
  112. private $required;
  113. /**
  114. * The form data in application format
  115. * @var mixed
  116. */
  117. private $data;
  118. /**
  119. * The form data in normalized format
  120. * @var mixed
  121. */
  122. private $normData;
  123. /**
  124. * The form data in client format
  125. * @var mixed
  126. */
  127. private $clientData;
  128. /**
  129. * Data used for the client data when no value is bound
  130. * @var mixed
  131. */
  132. private $emptyData = '';
  133. /**
  134. * The names of bound values that don't belong to any children
  135. * @var array
  136. */
  137. private $extraData = array();
  138. /**
  139. * The transformer for transforming from application to normalized format
  140. * and back
  141. * @var DataTransformer\DataTransformerInterface
  142. */
  143. private $normTransformers;
  144. /**
  145. * The transformer for transforming from normalized to client format and
  146. * back
  147. * @var DataTransformer\DataTransformerInterface
  148. */
  149. private $clientTransformers;
  150. /**
  151. * Whether the data in application, normalized and client format is
  152. * synchronized. Data may not be synchronized if transformation errors
  153. * occur.
  154. * @var Boolean
  155. */
  156. private $synchronized = true;
  157. /**
  158. * The validators attached to this form
  159. * @var array
  160. */
  161. private $validators;
  162. /**
  163. * Whether this form may only be read, but not bound
  164. * @var Boolean
  165. */
  166. private $readOnly = false;
  167. /**
  168. * The dispatcher for distributing events of this form
  169. * @var Symfony\Component\EventDispatcher\EventDispatcherInterface
  170. */
  171. private $dispatcher;
  172. /**
  173. * Key-value store for arbitrary attributes attached to this form
  174. * @var array
  175. */
  176. private $attributes;
  177. /**
  178. * The FormTypeInterface instances used to create this form
  179. * @var array
  180. */
  181. private $types;
  182. public function __construct($name, EventDispatcherInterface $dispatcher,
  183. array $types = array(), array $clientTransformers = array(),
  184. array $normTransformers = array(),
  185. DataMapperInterface $dataMapper = null, array $validators = array(),
  186. $required = false, $readOnly = false, $errorBubbling = false,
  187. $emptyData = null, array $attributes = array())
  188. {
  189. foreach ($clientTransformers as $transformer) {
  190. if (!$transformer instanceof DataTransformerInterface) {
  191. throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformer\DataTransformerInterface');
  192. }
  193. }
  194. foreach ($normTransformers as $transformer) {
  195. if (!$transformer instanceof DataTransformerInterface) {
  196. throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformer\DataTransformerInterface');
  197. }
  198. }
  199. foreach ($validators as $validator) {
  200. if (!$validator instanceof FormValidatorInterface) {
  201. throw new UnexpectedTypeException($validator, 'Symfony\Component\Form\Validator\FormValidatorInterface');
  202. }
  203. }
  204. $this->name = (string)$name;
  205. $this->types = $types;
  206. $this->dispatcher = $dispatcher;
  207. $this->clientTransformers = $clientTransformers;
  208. $this->normTransformers = $normTransformers;
  209. $this->validators = $validators;
  210. $this->dataMapper = $dataMapper;
  211. $this->required = $required;
  212. $this->readOnly = $readOnly;
  213. $this->attributes = $attributes;
  214. $this->errorBubbling = $errorBubbling;
  215. $this->emptyData = $emptyData;
  216. $this->setData(null);
  217. }
  218. public function __clone()
  219. {
  220. foreach ($this->children as $key => $child) {
  221. $this->children[$key] = clone $child;
  222. }
  223. }
  224. /**
  225. * {@inheritDoc}
  226. */
  227. public function getName()
  228. {
  229. return $this->name;
  230. }
  231. public function getTypes()
  232. {
  233. return $this->types;
  234. }
  235. /**
  236. * {@inheritDoc}
  237. */
  238. public function isRequired()
  239. {
  240. if (null === $this->parent || $this->parent->isRequired()) {
  241. return $this->required;
  242. }
  243. return false;
  244. }
  245. /**
  246. * {@inheritDoc}
  247. */
  248. public function isReadOnly()
  249. {
  250. if (null === $this->parent || !$this->parent->isReadOnly()) {
  251. return $this->readOnly;
  252. }
  253. return true;
  254. }
  255. /**
  256. * {@inheritDoc}
  257. */
  258. public function setParent(FormInterface $parent = null)
  259. {
  260. $this->parent = $parent;
  261. return $this;
  262. }
  263. /**
  264. * Returns the parent field.
  265. *
  266. * @return FormInterface The parent field
  267. */
  268. public function getParent()
  269. {
  270. return $this->parent;
  271. }
  272. /**
  273. * Returns whether the field has a parent.
  274. *
  275. * @return Boolean
  276. */
  277. public function hasParent()
  278. {
  279. return null !== $this->parent;
  280. }
  281. /**
  282. * Returns the root of the form tree
  283. *
  284. * @return FormInterface The root of the tree
  285. */
  286. public function getRoot()
  287. {
  288. return $this->parent ? $this->parent->getRoot() : $this;
  289. }
  290. /**
  291. * Returns whether the field is the root of the form tree
  292. *
  293. * @return Boolean
  294. */
  295. public function isRoot()
  296. {
  297. return !$this->hasParent();
  298. }
  299. public function hasAttribute($name)
  300. {
  301. return isset($this->attributes[$name]);
  302. }
  303. public function getAttribute($name)
  304. {
  305. return $this->attributes[$name];
  306. }
  307. /**
  308. * Updates the field with default data
  309. *
  310. * @see FormInterface
  311. */
  312. public function setData($appData)
  313. {
  314. $event = new DataEvent($this, $appData);
  315. $this->dispatcher->dispatch(Events::preSetData, $event);
  316. // Hook to change content of the data
  317. $event = new FilterDataEvent($this, $appData);
  318. $this->dispatcher->dispatch(Events::filterSetData, $event);
  319. $appData = $event->getData();
  320. // Treat data as strings unless a value transformer exists
  321. if (!$this->clientTransformers && !$this->normTransformers && is_scalar($appData)) {
  322. $appData = (string)$appData;
  323. }
  324. // Synchronize representations - must not change the content!
  325. $normData = $this->appToNorm($appData);
  326. $clientData = $this->normToClient($normData);
  327. $this->data = $appData;
  328. $this->normData = $normData;
  329. $this->clientData = $clientData;
  330. $this->synchronized = true;
  331. if ($this->dataMapper) {
  332. // Update child forms from the data
  333. $this->dataMapper->mapDataToForms($clientData, $this->children);
  334. }
  335. $event = new DataEvent($this, $appData);
  336. $this->dispatcher->dispatch(Events::postSetData, $event);
  337. return $this;
  338. }
  339. /**
  340. * Binds POST data to the field, transforms and validates it.
  341. *
  342. * @param string|array $data The POST data
  343. */
  344. public function bind($clientData)
  345. {
  346. if ($this->readOnly) {
  347. return;
  348. }
  349. if (is_scalar($clientData) || null === $clientData) {
  350. $clientData = (string)$clientData;
  351. }
  352. // Initialize errors in the very beginning so that we don't lose any
  353. // errors added during listeners
  354. $this->errors = array();
  355. $event = new DataEvent($this, $clientData);
  356. $this->dispatcher->dispatch(Events::preBind, $event);
  357. $appData = null;
  358. $normData = null;
  359. $extraData = array();
  360. $synchronized = false;
  361. // Hook to change content of the data bound by the browser
  362. $event = new FilterDataEvent($this, $clientData);
  363. $this->dispatcher->dispatch(Events::filterBoundClientData, $event);
  364. $clientData = $event->getData();
  365. if (count($this->children) > 0) {
  366. if (null === $clientData || '' === $clientData) {
  367. $clientData = array();
  368. }
  369. if (!is_array($clientData)) {
  370. throw new UnexpectedTypeException($clientData, 'array');
  371. }
  372. foreach ($this->children as $name => $child) {
  373. if (!isset($clientData[$name])) {
  374. $clientData[$name] = null;
  375. }
  376. }
  377. foreach ($clientData as $name => $value) {
  378. if ($this->has($name)) {
  379. $this->children[$name]->bind($value);
  380. } else {
  381. $extraData[$name] = $value;
  382. }
  383. }
  384. // If we have a data mapper, use old client data and merge
  385. // data from the children into it later
  386. if ($this->dataMapper) {
  387. $clientData = $this->getClientData();
  388. }
  389. }
  390. if (null === $clientData || '' === $clientData) {
  391. $clientData = $this->emptyData;
  392. if ($clientData instanceof \Closure) {
  393. $clientData = $clientData->__invoke($this);
  394. }
  395. }
  396. // Merge form data from children into existing client data
  397. if (count($this->children) > 0 && $this->dataMapper) {
  398. $this->dataMapper->mapFormsToData($this->children, $clientData);
  399. }
  400. try {
  401. // Normalize data to unified representation
  402. $normData = $this->clientToNorm($clientData);
  403. $synchronized = true;
  404. } catch (TransformationFailedException $e) {
  405. }
  406. if ($synchronized) {
  407. // Hook to change content of the data in the normalized
  408. // representation
  409. $event = new FilterDataEvent($this, $normData);
  410. $this->dispatcher->dispatch(Events::filterBoundNormData, $event);
  411. $normData = $event->getData();
  412. // Synchronize representations - must not change the content!
  413. $appData = $this->normToApp($normData);
  414. $clientData = $this->normToClient($normData);
  415. }
  416. $this->bound = true;
  417. $this->data = $appData;
  418. $this->normData = $normData;
  419. $this->clientData = $clientData;
  420. $this->extraData = $extraData;
  421. $this->synchronized = $synchronized;
  422. $event = new DataEvent($this, $clientData);
  423. $this->dispatcher->dispatch(Events::postBind, $event);
  424. foreach ($this->validators as $validator) {
  425. $validator->validate($this);
  426. }
  427. }
  428. /**
  429. * Binds a request to the form
  430. *
  431. * If the request was a POST request, the data is bound to the form,
  432. * transformed and written into the form data (an object or an array).
  433. * You can set the form data by passing it in the second parameter
  434. * of this method or by passing it in the "data" option of the form's
  435. * constructor.
  436. *
  437. * @param Request $request The request to bind to the form
  438. * @param array|object $data The data from which to read default values
  439. * and where to write bound values
  440. */
  441. public function bindRequest(Request $request)
  442. {
  443. // Store the bound data in case of a post request
  444. switch ($request->getMethod()) {
  445. case 'POST':
  446. case 'PUT':
  447. $data = array_replace_recursive(
  448. $request->request->get($this->getName(), array()),
  449. $request->files->get($this->getName(), array())
  450. );
  451. break;
  452. case 'GET':
  453. $data = $request->query->get($this->getName(), array());
  454. break;
  455. default:
  456. throw new FormException(sprintf('The request method "%s" is not supported', $request->getMethod()));
  457. }
  458. $this->bind($data);
  459. }
  460. /**
  461. * Returns the data in the format needed for the underlying object.
  462. *
  463. * @return mixed
  464. */
  465. public function getData()
  466. {
  467. return $this->data;
  468. }
  469. /**
  470. * Returns the normalized data of the field.
  471. *
  472. * @return mixed When the field is not bound, the default data is returned.
  473. * When the field is bound, the normalized bound data is
  474. * returned if the field is valid, null otherwise.
  475. */
  476. public function getNormData()
  477. {
  478. return $this->normData;
  479. }
  480. /**
  481. * Returns the data transformed by the value transformer
  482. *
  483. * @return string
  484. */
  485. public function getClientData()
  486. {
  487. return $this->clientData;
  488. }
  489. public function getExtraData()
  490. {
  491. return $this->extraData;
  492. }
  493. /**
  494. * Adds an error to the field.
  495. *
  496. * @see FormInterface
  497. */
  498. public function addError(FormError $error)
  499. {
  500. if ($this->parent && $this->errorBubbling) {
  501. $this->parent->addError($error);
  502. } else {
  503. $this->errors[] = $error;
  504. }
  505. }
  506. /**
  507. * Returns whether errors bubble up to the parent
  508. *
  509. * @return Boolean
  510. */
  511. public function getErrorBubbling()
  512. {
  513. return $this->errorBubbling;
  514. }
  515. /**
  516. * Returns whether the field is bound.
  517. *
  518. * @return Boolean true if the form is bound to input values, false otherwise
  519. */
  520. public function isBound()
  521. {
  522. return $this->bound;
  523. }
  524. /**
  525. * Returns whether the data in the different formats is synchronized
  526. *
  527. * @return Boolean
  528. */
  529. public function isSynchronized()
  530. {
  531. return $this->synchronized;
  532. }
  533. /**
  534. * {@inheritDoc}
  535. */
  536. public function isEmpty()
  537. {
  538. foreach ($this->children as $child) {
  539. if (!$child->isEmpty()) {
  540. return false;
  541. }
  542. }
  543. return array() === $this->data || null === $this->data || '' === $this->data;
  544. }
  545. /**
  546. * Returns whether the field is valid.
  547. *
  548. * @return Boolean
  549. */
  550. public function isValid()
  551. {
  552. if (!$this->isBound() || $this->hasErrors()) {
  553. return false;
  554. }
  555. foreach ($this->children as $child) {
  556. if (!$child->isValid()) {
  557. return false;
  558. }
  559. }
  560. return true;
  561. }
  562. /**
  563. * Returns whether or not there are errors.
  564. *
  565. * @return Boolean true if form is bound and not valid
  566. */
  567. public function hasErrors()
  568. {
  569. // Don't call isValid() here, as its semantics are slightly different
  570. // Field groups are not valid if their children are invalid, but
  571. // hasErrors() returns only true if a field/field group itself has
  572. // errors
  573. return count($this->errors) > 0;
  574. }
  575. /**
  576. * Returns all errors
  577. *
  578. * @return array An array of FormError instances that occurred during binding
  579. */
  580. public function getErrors()
  581. {
  582. return $this->errors;
  583. }
  584. /**
  585. * Returns the DataTransformer.
  586. *
  587. * @return array
  588. */
  589. public function getNormTransformers()
  590. {
  591. return $this->normTransformers;
  592. }
  593. /**
  594. * Returns the DataTransformer.
  595. *
  596. * @return array
  597. */
  598. public function getClientTransformers()
  599. {
  600. return $this->clientTransformers;
  601. }
  602. /**
  603. * Returns all children in this group
  604. *
  605. * @return array
  606. */
  607. public function getChildren()
  608. {
  609. return $this->children;
  610. }
  611. public function hasChildren()
  612. {
  613. return count($this->children) > 0;
  614. }
  615. public function add(FormInterface $child)
  616. {
  617. $this->children[$child->getName()] = $child;
  618. $child->setParent($this);
  619. if ($this->dataMapper) {
  620. $this->dataMapper->mapDataToForm($this->getClientData(), $child);
  621. }
  622. }
  623. public function remove($name)
  624. {
  625. if (isset($this->children[$name])) {
  626. $this->children[$name]->setParent(null);
  627. unset($this->children[$name]);
  628. }
  629. }
  630. /**
  631. * Returns whether a child with the given name exists.
  632. *
  633. * @param string $name
  634. * @return Boolean
  635. */
  636. public function has($name)
  637. {
  638. return isset($this->children[$name]);
  639. }
  640. /**
  641. * Returns the child with the given name.
  642. *
  643. * @param string $name
  644. * @return FormInterface
  645. */
  646. public function get($name)
  647. {
  648. if (isset($this->children[$name])) {
  649. return $this->children[$name];
  650. }
  651. throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $name));
  652. }
  653. /**
  654. * Returns true if the child exists (implements the \ArrayAccess interface).
  655. *
  656. * @param string $name The name of the child
  657. *
  658. * @return Boolean true if the widget exists, false otherwise
  659. */
  660. public function offsetExists($name)
  661. {
  662. return $this->has($name);
  663. }
  664. /**
  665. * Returns the form child associated with the name (implements the \ArrayAccess interface).
  666. *
  667. * @param string $name The offset of the value to get
  668. *
  669. * @return FormInterface A form instance
  670. */
  671. public function offsetGet($name)
  672. {
  673. return $this->get($name);
  674. }
  675. /**
  676. * Adds a child to the form (implements the \ArrayAccess interface).
  677. *
  678. * @param string $name Ignored. The name of the child is used.
  679. * @param FormInterface $child The child to be added
  680. */
  681. public function offsetSet($name, $child)
  682. {
  683. $this->add($child);
  684. }
  685. /**
  686. * Removes the child with the given name from the form (implements the \ArrayAccess interface).
  687. *
  688. * @param string $name The name of the child to be removed
  689. */
  690. public function offsetUnset($name)
  691. {
  692. $this->remove($name);
  693. }
  694. /**
  695. * Returns the iterator for this group.
  696. *
  697. * @return \ArrayIterator
  698. */
  699. public function getIterator()
  700. {
  701. return new \ArrayIterator($this->children);
  702. }
  703. /**
  704. * Returns the number of form children (implements the \Countable interface).
  705. *
  706. * @return integer The number of embedded form children
  707. */
  708. public function count()
  709. {
  710. return count($this->children);
  711. }
  712. /**
  713. * Normalizes the value if a normalization transformer is set
  714. *
  715. * @param mixed $value The value to transform
  716. * @return string
  717. */
  718. private function appToNorm($value)
  719. {
  720. foreach ($this->normTransformers as $transformer) {
  721. $value = $transformer->transform($value);
  722. }
  723. return $value;
  724. }
  725. public function getContext()
  726. {
  727. return TemplateContext::create($this);
  728. }
  729. /**
  730. * Reverse transforms a value if a normalization transformer is set.
  731. *
  732. * @param string $value The value to reverse transform
  733. * @return mixed
  734. */
  735. private function normToApp($value)
  736. {
  737. for ($i = count($this->normTransformers) - 1; $i >= 0; --$i) {
  738. $value = $this->normTransformers[$i]->reverseTransform($value);
  739. }
  740. return $value;
  741. }
  742. /**
  743. * Transforms the value if a value transformer is set.
  744. *
  745. * @param mixed $value The value to transform
  746. * @return string
  747. */
  748. private function normToClient($value)
  749. {
  750. if (!$this->clientTransformers) {
  751. // Scalar values should always be converted to strings to
  752. // facilitate differentiation between empty ("") and zero (0).
  753. return null === $value || is_scalar($value) ? (string)$value : $value;
  754. }
  755. foreach ($this->clientTransformers as $transformer) {
  756. $value = $transformer->transform($value);
  757. }
  758. return $value;
  759. }
  760. /**
  761. * Reverse transforms a value if a value transformer is set.
  762. *
  763. * @param string $value The value to reverse transform
  764. * @return mixed
  765. */
  766. private function clientToNorm($value)
  767. {
  768. if (!$this->clientTransformers) {
  769. return '' === $value ? null : $value;
  770. }
  771. for ($i = count($this->clientTransformers) - 1; $i >= 0; --$i) {
  772. $value = $this->clientTransformers[$i]->reverseTransform($value);
  773. }
  774. return $value;
  775. }
  776. }