FieldGroup.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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\Exception\AlreadyBoundException;
  12. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  13. use Symfony\Component\Form\Iterator\RecursiveFieldsWithPropertyPathIterator;
  14. /**
  15. * FieldGroup represents an array of widgets bind to names and values.
  16. *
  17. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  18. */
  19. class FieldGroup extends Field implements \IteratorAggregate, FieldGroupInterface
  20. {
  21. /**
  22. * Contains all the fields of this group
  23. * @var array
  24. */
  25. protected $fields = array();
  26. /**
  27. * Contains the names of bound values who don't belong to any fields
  28. * @var array
  29. */
  30. protected $extraFields = array();
  31. /**
  32. * Clones this group
  33. */
  34. public function __clone()
  35. {
  36. foreach ($this->fields as $name => $field) {
  37. $field = clone $field;
  38. // this condition is only to "bypass" a PHPUnit bug with mocks
  39. if (null !== $field->getParent()) {
  40. $field->setParent($this);
  41. }
  42. $this->fields[$name] = $field;
  43. }
  44. }
  45. /**
  46. * Adds a new field to this group. A field must have a unique name within
  47. * the group. Otherwise the existing field is overwritten.
  48. *
  49. * If you add a nested group, this group should also be represented in the
  50. * object hierarchy. If you want to add a group that operates on the same
  51. * hierarchy level, use merge().
  52. *
  53. * <code>
  54. * class Entity
  55. * {
  56. * public $location;
  57. * }
  58. *
  59. * class Location
  60. * {
  61. * public $longitude;
  62. * public $latitude;
  63. * }
  64. *
  65. * $entity = new Entity();
  66. * $entity->location = new Location();
  67. *
  68. * $form = new Form('entity', $entity, $validator);
  69. *
  70. * $locationGroup = new FieldGroup('location');
  71. * $locationGroup->add(new TextField('longitude'));
  72. * $locationGroup->add(new TextField('latitude'));
  73. *
  74. * $form->add($locationGroup);
  75. * </code>
  76. *
  77. * @param FieldInterface $field
  78. */
  79. public function add(FieldInterface $field)
  80. {
  81. if ($this->isBound()) {
  82. throw new AlreadyBoundException('You cannot add fields after binding a form');
  83. }
  84. $this->fields[$field->getKey()] = $field;
  85. $field->setParent($this);
  86. $field->setLocale($this->locale);
  87. $data = $this->getTransformedData();
  88. // if the property "data" is NULL, getTransformedData() returns an empty
  89. // string
  90. if (!empty($data)) {
  91. $field->updateFromProperty($data);
  92. }
  93. return $field;
  94. }
  95. /**
  96. * Removes the field with the given key.
  97. *
  98. * @param string $key
  99. */
  100. public function remove($key)
  101. {
  102. $this->fields[$key]->setParent(null);
  103. unset($this->fields[$key]);
  104. }
  105. /**
  106. * Returns whether a field with the given key exists.
  107. *
  108. * @param string $key
  109. * @return boolean
  110. */
  111. public function has($key)
  112. {
  113. return isset($this->fields[$key]);
  114. }
  115. /**
  116. * Returns the field with the given key.
  117. *
  118. * @param string $key
  119. * @return FieldInterface
  120. */
  121. public function get($key)
  122. {
  123. if (isset($this->fields[$key])) {
  124. return $this->fields[$key];
  125. }
  126. throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key));
  127. }
  128. /**
  129. * Returns all fields in this group
  130. *
  131. * @return array
  132. */
  133. public function getFields()
  134. {
  135. return $this->fields;
  136. }
  137. /**
  138. * Returns an array of visible fields from the current schema.
  139. *
  140. * @return array
  141. */
  142. public function getVisibleFields()
  143. {
  144. return $this->getFieldsByVisibility(false, false);
  145. }
  146. /**
  147. * Returns an array of visible fields from the current schema.
  148. *
  149. * This variant of the method will recursively get all the
  150. * fields from the nested forms or field groups
  151. *
  152. * @return array
  153. */
  154. public function getAllVisibleFields()
  155. {
  156. return $this->getFieldsByVisibility(false, true);
  157. }
  158. /**
  159. * Returns an array of hidden fields from the current schema.
  160. *
  161. * @return array
  162. */
  163. public function getHiddenFields()
  164. {
  165. return $this->getFieldsByVisibility(true, false);
  166. }
  167. /**
  168. * Returns an array of hidden fields from the current schema.
  169. *
  170. * This variant of the method will recursively get all the
  171. * fields from the nested forms or field groups
  172. *
  173. * @return array
  174. */
  175. public function getAllHiddenFields()
  176. {
  177. return $this->getFieldsByVisibility(true, true);
  178. }
  179. /**
  180. * Returns a filtered array of fields from the current schema.
  181. *
  182. * @param boolean $hidden Whether to return hidden fields only or visible fields only
  183. * @param boolean $recursive Whether to recur through embedded schemas
  184. *
  185. * @return array
  186. */
  187. protected function getFieldsByVisibility($hidden, $recursive)
  188. {
  189. $fields = array();
  190. $hidden = (bool)$hidden;
  191. foreach ($this->fields as $field) {
  192. if ($field instanceof FieldGroup && $recursive) {
  193. $fields = array_merge($fields, $field->getFieldsByVisibility($hidden, $recursive));
  194. } else if ($hidden === $field->isHidden()) {
  195. $fields[] = $field;
  196. }
  197. }
  198. return $fields;
  199. }
  200. /**
  201. * Initializes the field group with an object to operate on
  202. *
  203. * @see FieldInterface
  204. */
  205. public function setData($data)
  206. {
  207. parent::setData($data);
  208. // get transformed data and pass its values to child fields
  209. $data = $this->getTransformedData();
  210. if (!empty($data) && !is_array($data) && !is_object($data)) {
  211. throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data)));
  212. }
  213. if (!empty($data)) {
  214. $this->updateFromObject($data);
  215. }
  216. }
  217. /**
  218. * Returns the data of the field as it is displayed to the user.
  219. *
  220. * @see FieldInterface
  221. */
  222. public function getDisplayedData()
  223. {
  224. $values = array();
  225. foreach ($this->fields as $key => $field) {
  226. $values[$key] = $field->getDisplayedData();
  227. }
  228. return $values;
  229. }
  230. /**
  231. * Binds POST data to the field, transforms and validates it.
  232. *
  233. * @param string|array $taintedData The POST data
  234. * @return boolean Whether the form is valid
  235. */
  236. public function bind($taintedData)
  237. {
  238. if ($taintedData === null) {
  239. $taintedData = array();
  240. }
  241. if (!is_array($taintedData)) {
  242. throw new UnexpectedTypeException('You must pass an array parameter to the bind() method');
  243. }
  244. foreach ($this->fields as $key => $field) {
  245. if (!isset($taintedData[$key])) {
  246. $taintedData[$key] = null;
  247. }
  248. }
  249. $taintedData = $this->preprocessData($taintedData);
  250. foreach ($taintedData as $key => $value) {
  251. if ($this->has($key)) {
  252. $this->fields[$key]->bind($value);
  253. }
  254. }
  255. $data = $this->getTransformedData();
  256. $this->updateObject($data);
  257. // bind and reverse transform the data
  258. parent::bind($data);
  259. $this->extraFields = array();
  260. foreach ($taintedData as $key => $value) {
  261. if (!$this->has($key)) {
  262. $this->extraFields[] = $key;
  263. }
  264. }
  265. }
  266. /**
  267. * Updates the chield fields from the properties of the given data
  268. *
  269. * This method calls updateFromProperty() on all child fields that have a
  270. * property path set. If a child field has no property path set but
  271. * implements FieldGroupInterface, updateProperty() is called on its
  272. * children instead.
  273. *
  274. * @param array|object $objectOrArray
  275. */
  276. protected function updateFromObject(&$objectOrArray)
  277. {
  278. $iterator = new RecursiveFieldsWithPropertyPathIterator($this);
  279. $iterator = new \RecursiveIteratorIterator($iterator);
  280. foreach ($iterator as $field) {
  281. $field->updateFromProperty($objectOrArray);
  282. }
  283. }
  284. /**
  285. * Updates all properties of the given data from the child fields
  286. *
  287. * This method calls updateProperty() on all child fields that have a property
  288. * path set. If a child field has no property path set but implements
  289. * FieldGroupInterface, updateProperty() is called on its children instead.
  290. *
  291. * @param array|object $objectOrArray
  292. */
  293. protected function updateObject(&$objectOrArray)
  294. {
  295. $iterator = new RecursiveFieldsWithPropertyPathIterator($this);
  296. $iterator = new \RecursiveIteratorIterator($iterator);
  297. foreach ($iterator as $field) {
  298. $field->updateProperty($objectOrArray);
  299. }
  300. }
  301. /**
  302. * Processes the bound data before it is passed to the individual fields
  303. *
  304. * The data is in the user format.
  305. *
  306. * @param array $data
  307. * @return array
  308. */
  309. protected function preprocessData(array $data)
  310. {
  311. return $data;
  312. }
  313. /**
  314. * Returns whether this form was bound with extra fields
  315. *
  316. * @return boolean
  317. */
  318. public function isBoundWithExtraFields()
  319. {
  320. // TODO: integrate the field names in the error message
  321. return count($this->extraFields) > 0;
  322. }
  323. /**
  324. * Returns whether the field is valid.
  325. *
  326. * @return boolean
  327. */
  328. public function isValid()
  329. {
  330. if (!parent::isValid()) {
  331. return false;
  332. }
  333. foreach ($this->fields as $field) {
  334. if (!$field->isValid()) {
  335. return false;
  336. }
  337. }
  338. return true;
  339. }
  340. /**
  341. * {@inheritDoc}
  342. */
  343. public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null)
  344. {
  345. if ($pathIterator !== null) {
  346. if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) {
  347. $pathIterator->next();
  348. if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') {
  349. $pathIterator->next();
  350. }
  351. if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) {
  352. $this->get($pathIterator->current())->addError($error, $pathIterator, $type);
  353. return;
  354. }
  355. } else if ($type === self::DATA_ERROR) {
  356. $iterator = new RecursiveFieldsWithPropertyPathIterator($this);
  357. $iterator = new \RecursiveIteratorIterator($iterator);
  358. foreach ($iterator as $field) {
  359. if (null !== ($fieldPath = $field->getPropertyPath())) {
  360. if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) {
  361. if ($pathIterator->hasNext()) {
  362. $pathIterator->next();
  363. }
  364. $field->addError($error, $pathIterator, $type);
  365. return;
  366. }
  367. }
  368. }
  369. }
  370. }
  371. parent::addError($error);
  372. }
  373. /**
  374. * Returns whether the field requires a multipart form.
  375. *
  376. * @return boolean
  377. */
  378. public function isMultipart()
  379. {
  380. foreach ($this->fields as $field) {
  381. if ($field->isMultipart()) {
  382. return true;
  383. }
  384. }
  385. return false;
  386. }
  387. /**
  388. * Returns true if the bound field exists (implements the \ArrayAccess interface).
  389. *
  390. * @param string $key The key of the bound field
  391. *
  392. * @return Boolean true if the widget exists, false otherwise
  393. */
  394. public function offsetExists($key)
  395. {
  396. return $this->has($key);
  397. }
  398. /**
  399. * Returns the form field associated with the name (implements the \ArrayAccess interface).
  400. *
  401. * @param string $key The offset of the value to get
  402. *
  403. * @return Field A form field instance
  404. */
  405. public function offsetGet($key)
  406. {
  407. return $this->get($key);
  408. }
  409. /**
  410. * Throws an exception saying that values cannot be set (implements the \ArrayAccess interface).
  411. *
  412. * @param string $offset (ignored)
  413. * @param string $value (ignored)
  414. *
  415. * @throws \LogicException
  416. */
  417. public function offsetSet($key, $field)
  418. {
  419. throw new \LogicException('Use the method add() to add fields');
  420. }
  421. /**
  422. * Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface).
  423. *
  424. * @param string $key
  425. *
  426. * @throws \LogicException
  427. */
  428. public function offsetUnset($key)
  429. {
  430. return $this->remove($key);
  431. }
  432. /**
  433. * Returns the iterator for this group.
  434. *
  435. * @return \ArrayIterator
  436. */
  437. public function getIterator()
  438. {
  439. return new \ArrayIterator($this->fields);
  440. }
  441. /**
  442. * Returns the number of form fields (implements the \Countable interface).
  443. *
  444. * @return integer The number of embedded form fields
  445. */
  446. public function count()
  447. {
  448. return count($this->fields);
  449. }
  450. /**
  451. * Sets the locale of this field.
  452. *
  453. * @see Localizable
  454. */
  455. public function setLocale($locale)
  456. {
  457. parent::setLocale($locale);
  458. foreach ($this->fields as $field) {
  459. $field->setLocale($locale);
  460. }
  461. }
  462. }