Field.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. <?php
  2. namespace Symfony\Component\Form;
  3. /*
  4. * This file is part of the Symfony framework.
  5. *
  6. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. use Symfony\Component\Form\ValueTransformer\ValueTransformerInterface;
  12. use Symfony\Component\Form\ValueTransformer\TransformationFailedException;
  13. /**
  14. * Base class for form fields
  15. *
  16. * To implement your own form fields, you need to have a thorough understanding
  17. * of the data flow within a form field. A form field stores its data in three
  18. * different representations:
  19. *
  20. * (1) the format required by the form's object
  21. * (2) a normalized format for internal processing
  22. * (3) the format used for display
  23. *
  24. * A date field, for example, may store a date as "Y-m-d" string (1) in the
  25. * object. To facilitate processing in the field, this value is normalized
  26. * to a DateTime object (2). In the HTML representation of your form, a
  27. * localized string (3) is presented to and modified by the user.
  28. *
  29. * In most cases, format (1) and format (2) will be the same. For example,
  30. * a checkbox field uses a boolean value both for internal processing as for
  31. * storage in the object. In these cases you simply need to set a value
  32. * transformer to convert between formats (2) and (3). You can do this by
  33. * calling setValueTransformer() in the configure() method.
  34. *
  35. * In some cases though it makes sense to make format (1) configurable. To
  36. * demonstrate this, let's extend our above date field to store the value
  37. * either as "Y-m-d" string or as timestamp. Internally we still want to
  38. * use a DateTime object for processing. To convert the data from string/integer
  39. * to DateTime you can set a normalization transformer by calling
  40. * setNormalizationTransformer() in configure(). The normalized data is then
  41. * converted to the displayed data as described before.
  42. *
  43. * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
  44. */
  45. abstract class Field extends Configurable implements FieldInterface
  46. {
  47. protected $taintedData = null;
  48. protected $locale = null;
  49. private $errors = array();
  50. private $key = '';
  51. private $parent = null;
  52. private $bound = false;
  53. private $required = null;
  54. private $data = null;
  55. private $normalizedData = null;
  56. private $transformedData = null;
  57. private $normalizationTransformer = null;
  58. private $valueTransformer = null;
  59. private $propertyPath = null;
  60. public function __construct($key, array $options = array())
  61. {
  62. $this->addOption('trim', true);
  63. $this->addOption('required', true);
  64. $this->addOption('disabled', false);
  65. $this->addOption('property_path', (string)$key);
  66. $this->addOption('value_transformer');
  67. $this->addOption('normalization_transformer');
  68. $this->key = (string)$key;
  69. if ($this->locale === null) {
  70. $this->locale = class_exists('\Locale', false) ? \Locale::getDefault() : 'en';
  71. }
  72. parent::__construct($options);
  73. if ($this->getOption('value_transformer')) {
  74. $this->setValueTransformer($this->getOption('value_transformer'));
  75. }
  76. if ($this->getOption('normalization_transformer')) {
  77. $this->setNormalizationTransformer($this->getOption('normalization_transformer'));
  78. }
  79. $this->normalizedData = $this->normalize($this->data);
  80. $this->transformedData = $this->transform($this->normalizedData);
  81. $this->required = $this->getOption('required');
  82. $this->setPropertyPath($this->getOption('property_path'));
  83. }
  84. /**
  85. * Clones this field.
  86. */
  87. public function __clone()
  88. {
  89. // TODO
  90. }
  91. /**
  92. * Returns the data of the field as it is displayed to the user.
  93. *
  94. * @return string|array When the field is not bound, the transformed
  95. * default data is returned. When the field is bound,
  96. * the bound data is returned.
  97. */
  98. public function getDisplayedData()
  99. {
  100. return $this->getTransformedData();
  101. }
  102. /**
  103. * Returns the data transformed by the value transformer
  104. *
  105. * @return string
  106. */
  107. protected function getTransformedData()
  108. {
  109. return $this->transformedData;
  110. }
  111. /**
  112. * {@inheritDoc}
  113. */
  114. public function setPropertyPath($propertyPath)
  115. {
  116. $this->propertyPath = $propertyPath === null || $propertyPath === '' ? null : new PropertyPath($propertyPath);
  117. }
  118. /**
  119. * {@inheritDoc}
  120. */
  121. public function getPropertyPath()
  122. {
  123. return $this->propertyPath;
  124. }
  125. /**
  126. * {@inheritDoc}
  127. */
  128. public function setKey($key)
  129. {
  130. $this->key = (string)$key;
  131. }
  132. /**
  133. * {@inheritDoc}
  134. */
  135. public function getKey()
  136. {
  137. return $this->key;
  138. }
  139. /**
  140. * {@inheritDoc}
  141. */
  142. public function getName()
  143. {
  144. return null === $this->parent ? $this->key : $this->parent->getName().'['.$this->key.']';
  145. }
  146. /**
  147. * {@inheritDoc}
  148. */
  149. public function getId()
  150. {
  151. return null === $this->parent ? $this->key : $this->parent->getId().'_'.$this->key;
  152. }
  153. /**
  154. * {@inheritDoc}
  155. */
  156. public function setRequired($required)
  157. {
  158. $this->required = $required;
  159. }
  160. /**
  161. * {@inheritDoc}
  162. */
  163. public function isRequired()
  164. {
  165. if (null === $this->parent || $this->parent->isRequired()) {
  166. return $this->required;
  167. }
  168. return false;
  169. }
  170. /**
  171. * {@inheritDoc}
  172. */
  173. public function isDisabled()
  174. {
  175. if (null === $this->parent || !$this->parent->isDisabled()) {
  176. return $this->getOption('disabled');
  177. }
  178. return true;
  179. }
  180. /**
  181. * {@inheritDoc}
  182. */
  183. public function isMultipart()
  184. {
  185. return false;
  186. }
  187. /**
  188. * Returns true if the widget is hidden.
  189. *
  190. * @return Boolean true if the widget is hidden, false otherwise
  191. */
  192. public function isHidden()
  193. {
  194. return false;
  195. }
  196. /**
  197. * {@inheritDoc}
  198. */
  199. public function setParent(FieldInterface $parent = null)
  200. {
  201. $this->parent = $parent;
  202. }
  203. /**
  204. * Returns the parent field.
  205. *
  206. * @return FieldInterface The parent field
  207. */
  208. public function getParent()
  209. {
  210. return $this->parent;
  211. }
  212. /**
  213. * Updates the field with default data
  214. *
  215. * @see FieldInterface
  216. */
  217. public function setData($data)
  218. {
  219. $this->data = $data;
  220. $this->normalizedData = $this->normalize($data);
  221. $this->transformedData = $this->transform($this->normalizedData);
  222. }
  223. /**
  224. * Binds POST data to the field, transforms and validates it.
  225. *
  226. * @param string|array $taintedData The POST data
  227. * @return boolean Whether the form is valid
  228. * @throws AlreadyBoundException when the field is already bound
  229. */
  230. public function bind($taintedData)
  231. {
  232. $this->transformedData = (is_array($taintedData) || is_object($taintedData)) ? $taintedData : (string)$taintedData;
  233. $this->bound = true;
  234. $this->errors = array();
  235. if (is_string($this->transformedData) && $this->getOption('trim')) {
  236. $this->transformedData = trim($this->transformedData);
  237. }
  238. try {
  239. $this->normalizedData = $this->processData($this->reverseTransform($this->transformedData));
  240. $this->data = $this->denormalize($this->normalizedData);
  241. $this->transformedData = $this->transform($this->normalizedData);
  242. } catch (TransformationFailedException $e) {
  243. // TODO better text
  244. // TESTME
  245. $this->addError('invalid (localized)');
  246. }
  247. }
  248. /**
  249. * Processes the bound reverse-transformed data.
  250. *
  251. * This method can be overridden if you want to modify the data entered
  252. * by the user. Note that the data is already in reverse transformed format.
  253. *
  254. * This method will not be called if reverse transformation fails.
  255. *
  256. * @param mixed $data
  257. * @return mixed
  258. */
  259. protected function processData($data)
  260. {
  261. return $data;
  262. }
  263. /**
  264. * Returns the normalized data of the field.
  265. *
  266. * @return mixed When the field is not bound, the default data is returned.
  267. * When the field is bound, the normalized bound data is
  268. * returned if the field is valid, null otherwise.
  269. */
  270. public function getData()
  271. {
  272. return $this->data;
  273. }
  274. protected function getNormalizedData()
  275. {
  276. return $this->normalizedData;
  277. }
  278. /**
  279. * Adds an error to the field.
  280. *
  281. * @see FieldInterface
  282. */
  283. public function addError($messageTemplate, array $messageParameters = array(), PropertyPathIterator $pathIterator = null, $type = null)
  284. {
  285. $this->errors[] = array($messageTemplate, $messageParameters);
  286. }
  287. /**
  288. * Returns whether the field is bound.
  289. *
  290. * @return boolean true if the form is bound to input values, false otherwise
  291. */
  292. public function isBound()
  293. {
  294. return $this->bound;
  295. }
  296. /**
  297. * Returns whether the field is valid.
  298. *
  299. * @return boolean
  300. */
  301. public function isValid()
  302. {
  303. return $this->isBound() ? count($this->errors)==0 : false; // TESTME
  304. }
  305. /**
  306. * Returns weather there are errors.
  307. *
  308. * @return boolean true if form is bound and not valid
  309. */
  310. public function hasErrors()
  311. {
  312. return $this->isBound() && !$this->isValid();
  313. }
  314. /**
  315. * Returns all errors
  316. *
  317. * @return array An array of errors that occured during binding
  318. */
  319. public function getErrors()
  320. {
  321. return $this->errors;
  322. }
  323. /**
  324. * Sets the locale of this field.
  325. *
  326. * @see Localizable
  327. */
  328. public function setLocale($locale)
  329. {
  330. $this->locale = $locale;
  331. if ($this->valueTransformer !== null && $this->valueTransformer instanceof Localizable) {
  332. $this->valueTransformer->setLocale($locale);
  333. }
  334. }
  335. /**
  336. * Injects the locale into the given object, if set.
  337. *
  338. * The locale is injected only if the object implements Localizable.
  339. *
  340. * @param object $object
  341. */
  342. protected function injectLocale($object)
  343. {
  344. if ($object instanceof Localizable) {
  345. $object->setLocale($this->locale);
  346. }
  347. }
  348. /**
  349. * Sets the ValueTransformer.
  350. *
  351. * @param ValueTransformerInterface $valueTransformer
  352. */
  353. protected function setNormalizationTransformer(ValueTransformerInterface $normalizationTransformer)
  354. {
  355. $this->injectLocale($normalizationTransformer);
  356. $this->normalizationTransformer = $normalizationTransformer;
  357. }
  358. /**
  359. * Returns the ValueTransformer.
  360. *
  361. * @return ValueTransformerInterface
  362. */
  363. protected function getNormalizationTransformer()
  364. {
  365. return $this->normalizationTransformer;
  366. }
  367. /**
  368. * Sets the ValueTransformer.
  369. *
  370. * @param ValueTransformerInterface $valueTransformer
  371. */
  372. protected function setValueTransformer(ValueTransformerInterface $valueTransformer)
  373. {
  374. $this->injectLocale($valueTransformer);
  375. $this->valueTransformer = $valueTransformer;
  376. }
  377. /**
  378. * Returns the ValueTransformer.
  379. *
  380. * @return ValueTransformerInterface
  381. */
  382. protected function getValueTransformer()
  383. {
  384. return $this->valueTransformer;
  385. }
  386. /**
  387. * Normalizes the value if a normalization transformer is set
  388. *
  389. * @param mixed $value The value to transform
  390. * @return string
  391. */
  392. protected function normalize($value)
  393. {
  394. if (null === $this->normalizationTransformer) {
  395. return $value;
  396. }
  397. return $this->normalizationTransformer->transform($value);
  398. }
  399. /**
  400. * Reverse transforms a value if a normalization transformer is set.
  401. *
  402. * @param string $value The value to reverse transform
  403. * @return mixed
  404. */
  405. protected function denormalize($value)
  406. {
  407. if (null === $this->normalizationTransformer) {
  408. return $value;
  409. }
  410. return $this->normalizationTransformer->reverseTransform($value, $this->data);
  411. }
  412. /**
  413. * Transforms the value if a value transformer is set.
  414. *
  415. * @param mixed $value The value to transform
  416. * @return string
  417. */
  418. protected function transform($value)
  419. {
  420. if (null === $this->valueTransformer) {
  421. return $value === null ? '' : $value;
  422. }
  423. return $this->valueTransformer->transform($value);
  424. }
  425. /**
  426. * Reverse transforms a value if a value transformer is set.
  427. *
  428. * @param string $value The value to reverse transform
  429. * @return mixed
  430. */
  431. protected function reverseTransform($value)
  432. {
  433. if (null === $this->valueTransformer) {
  434. return $value === '' ? null : $value;
  435. }
  436. return $this->valueTransformer->reverseTransform($value, $this->data);
  437. }
  438. /**
  439. * {@inheritDoc}
  440. */
  441. public function updateFromObject(&$objectOrArray)
  442. {
  443. // TODO throw exception if not object or array
  444. if ($this->propertyPath !== null) {
  445. $this->setData($this->propertyPath->getValue($objectOrArray));
  446. } else {
  447. // pass object through if the property path is empty
  448. $this->setData($objectOrArray);
  449. }
  450. }
  451. /**
  452. * {@inheritDoc}
  453. */
  454. public function updateObject(&$objectOrArray)
  455. {
  456. // TODO throw exception if not object or array
  457. if ($this->propertyPath !== null) {
  458. $this->propertyPath->setValue($objectOrArray, $this->getData());
  459. }
  460. }
  461. }