Form.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.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\Validator\ValidatorInterface;
  14. use Symfony\Component\Form\Exception\FormException;
  15. use Symfony\Component\Form\Exception\MissingOptionsException;
  16. use Symfony\Component\Form\Exception\AlreadyBoundException;
  17. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  18. use Symfony\Component\Form\Exception\DanglingFieldException;
  19. use Symfony\Component\Form\Exception\FieldDefinitionException;
  20. use Symfony\Component\Form\CsrfProvider\CsrfProviderInterface;
  21. /**
  22. * Form represents a form.
  23. *
  24. * A form is composed of a validator schema and a widget form schema.
  25. *
  26. * Form also takes care of CSRF protection by default.
  27. *
  28. * A CSRF secret can be any random string. If set to false, it disables the
  29. * CSRF protection, and if set to null, it forces the form to use the global
  30. * CSRF secret. If the global CSRF secret is also null, then a random one
  31. * is generated on the fly.
  32. *
  33. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  34. * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
  35. */
  36. class Form extends Field implements \IteratorAggregate, FormInterface
  37. {
  38. /**
  39. * Contains all the fields of this group
  40. * @var array
  41. */
  42. protected $fields = array();
  43. /**
  44. * Contains the names of submitted values who don't belong to any fields
  45. * @var array
  46. */
  47. protected $extraFields = array();
  48. /**
  49. * Whether a request was bound to the form
  50. * @var Boolean
  51. */
  52. protected $bound = false;
  53. /**
  54. * Stores the class that the data of this form must be instances of
  55. * @var string
  56. */
  57. protected $dataClass;
  58. /**
  59. * The context used when creating the form
  60. * @var FormContext
  61. */
  62. protected $context = null;
  63. /**
  64. * Creates a new form with the options stored in the given context
  65. *
  66. * @param FormContextInterface $context
  67. * @param string $name
  68. * @param array $options
  69. * @return Form
  70. */
  71. public static function create(FormContextInterface $context, $name = null, array $options = array())
  72. {
  73. return new static($name, array_merge($context->getOptions(), $options));
  74. }
  75. /**
  76. * Constructor.
  77. *
  78. * @param string $name
  79. * @param array $options
  80. */
  81. public function __construct($name = null, array $options = array())
  82. {
  83. $this->addOption('data_class');
  84. $this->addOption('csrf_field_name', '_token');
  85. $this->addOption('csrf_provider');
  86. $this->addOption('field_factory');
  87. $this->addOption('validation_groups');
  88. $this->addOption('virtual', false);
  89. $this->addOption('validator');
  90. $this->addOption('context');
  91. if (isset($options['validation_groups'])) {
  92. $options['validation_groups'] = (array)$options['validation_groups'];
  93. }
  94. if (isset($options['data_class'])) {
  95. $this->dataClass = $options['data_class'];
  96. }
  97. parent::__construct($name, $options);
  98. // Enable CSRF protection
  99. if ($this->getOption('csrf_provider')) {
  100. if (!$this->getOption('csrf_provider') instanceof CsrfProviderInterface) {
  101. throw new FormException('The object passed to the "csrf_provider" option must implement CsrfProviderInterface');
  102. }
  103. $fieldName = $this->getOption('csrf_field_name');
  104. $token = $this->getOption('csrf_provider')->generateCsrfToken(get_class($this));
  105. $this->add(new HiddenField($fieldName, array('data' => $token)));
  106. }
  107. }
  108. /**
  109. * Clones this group
  110. */
  111. public function __clone()
  112. {
  113. foreach ($this->fields as $name => $field) {
  114. $field = clone $field;
  115. // this condition is only to "bypass" a PHPUnit bug with mocks
  116. if (null !== $field->getParent()) {
  117. $field->setParent($this);
  118. }
  119. $this->fields[$name] = $field;
  120. }
  121. }
  122. /**
  123. * Adds a new field to this group. A field must have a unique name within
  124. * the group. Otherwise the existing field is overwritten.
  125. *
  126. * If you add a nested group, this group should also be represented in the
  127. * object hierarchy. If you want to add a group that operates on the same
  128. * hierarchy level, use merge().
  129. *
  130. * <code>
  131. * class Entity
  132. * {
  133. * public $location;
  134. * }
  135. *
  136. * class Location
  137. * {
  138. * public $longitude;
  139. * public $latitude;
  140. * }
  141. *
  142. * $entity = new Entity();
  143. * $entity->location = new Location();
  144. *
  145. * $form = new Form('entity', $entity, $validator);
  146. *
  147. * $locationGroup = new Form('location');
  148. * $locationGroup->add(new TextField('longitude'));
  149. * $locationGroup->add(new TextField('latitude'));
  150. *
  151. * $form->add($locationGroup);
  152. * </code>
  153. *
  154. * @param FieldInterface|string $field
  155. * @return FieldInterface
  156. */
  157. public function add($field)
  158. {
  159. if ($this->isBound()) {
  160. throw new AlreadyBoundException('You cannot add fields after binding a form');
  161. }
  162. // if the field is given as string, ask the field factory of the form
  163. // to create a field
  164. if (!$field instanceof FieldInterface) {
  165. if (!is_string($field)) {
  166. throw new UnexpectedTypeException($field, 'FieldInterface or string');
  167. }
  168. $factory = $this->getFieldFactory();
  169. if (!$factory) {
  170. throw new FormException('A field factory must be set to automatically create fields');
  171. }
  172. $class = $this->getDataClass();
  173. if (!$class) {
  174. throw new FormException('The data class must be set to automatically create fields');
  175. }
  176. $options = func_num_args() > 1 ? func_get_arg(1) : array();
  177. $field = $factory->getInstance($class, $field, $options);
  178. }
  179. if ('' === $field->getKey() || null === $field->getKey()) {
  180. throw new FieldDefinitionException('You cannot add anonymous fields');
  181. }
  182. $this->fields[$field->getKey()] = $field;
  183. $field->setParent($this);
  184. $data = $this->getTransformedData();
  185. // if the property "data" is NULL, getTransformedData() returns an empty
  186. // string
  187. if (!empty($data)) {
  188. $field->readProperty($data);
  189. }
  190. return $field;
  191. }
  192. /**
  193. * Removes the field with the given key.
  194. *
  195. * @param string $key
  196. */
  197. public function remove($key)
  198. {
  199. $this->fields[$key]->setParent(null);
  200. unset($this->fields[$key]);
  201. }
  202. /**
  203. * Returns whether a field with the given key exists.
  204. *
  205. * @param string $key
  206. * @return Boolean
  207. */
  208. public function has($key)
  209. {
  210. return isset($this->fields[$key]);
  211. }
  212. /**
  213. * Returns the field with the given key.
  214. *
  215. * @param string $key
  216. * @return FieldInterface
  217. */
  218. public function get($key)
  219. {
  220. if (isset($this->fields[$key])) {
  221. return $this->fields[$key];
  222. }
  223. throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key));
  224. }
  225. /**
  226. * Returns all fields in this group
  227. *
  228. * @return array
  229. */
  230. public function getFields()
  231. {
  232. return $this->fields;
  233. }
  234. /**
  235. * Returns an array of visible fields from the current schema.
  236. *
  237. * @return array
  238. */
  239. public function getVisibleFields()
  240. {
  241. return $this->getFieldsByVisibility(false, false);
  242. }
  243. /**
  244. * Returns an array of visible fields from the current schema.
  245. *
  246. * This variant of the method will recursively get all the
  247. * fields from the nested forms or field groups
  248. *
  249. * @return array
  250. */
  251. public function getAllVisibleFields()
  252. {
  253. return $this->getFieldsByVisibility(false, true);
  254. }
  255. /**
  256. * Returns an array of hidden fields from the current schema.
  257. *
  258. * @return array
  259. */
  260. public function getHiddenFields()
  261. {
  262. return $this->getFieldsByVisibility(true, false);
  263. }
  264. /**
  265. * Returns an array of hidden fields from the current schema.
  266. *
  267. * This variant of the method will recursively get all the
  268. * fields from the nested forms or field groups
  269. *
  270. * @return array
  271. */
  272. public function getAllHiddenFields()
  273. {
  274. return $this->getFieldsByVisibility(true, true);
  275. }
  276. /**
  277. * Returns a filtered array of fields from the current schema.
  278. *
  279. * @param Boolean $hidden Whether to return hidden fields only or visible fields only
  280. * @param Boolean $recursive Whether to recur through embedded schemas
  281. *
  282. * @return array
  283. */
  284. protected function getFieldsByVisibility($hidden, $recursive)
  285. {
  286. $fields = array();
  287. $hidden = (Boolean)$hidden;
  288. foreach ($this->fields as $field) {
  289. if ($field instanceof Form && $recursive) {
  290. $fields = array_merge($fields, $field->getFieldsByVisibility($hidden, $recursive));
  291. } else if ($hidden === $field->isHidden()) {
  292. $fields[] = $field;
  293. }
  294. }
  295. return $fields;
  296. }
  297. /**
  298. * Initializes the field group with an object to operate on
  299. *
  300. * @see FieldInterface
  301. */
  302. public function setData($data)
  303. {
  304. parent::setData($data);
  305. // get transformed data and pass its values to child fields
  306. $data = $this->getTransformedData();
  307. if (!empty($data) && !is_array($data) && !is_object($data)) {
  308. throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data)));
  309. }
  310. if (!empty($data)) {
  311. if ($this->dataClass && !$data instanceof $this->dataClass) {
  312. throw new FormException(sprintf('Form data should be instance of %s', $this->dataClass));
  313. }
  314. $this->readObject($data);
  315. }
  316. }
  317. /**
  318. * Returns the data of the field as it is displayed to the user.
  319. *
  320. * @see FieldInterface
  321. * @return array of field name => value
  322. */
  323. public function getDisplayedData()
  324. {
  325. $values = array();
  326. foreach ($this->fields as $key => $field) {
  327. $values[$key] = $field->getDisplayedData();
  328. }
  329. return $values;
  330. }
  331. /**
  332. * Binds POST data to the field, transforms and validates it.
  333. *
  334. * @param string|array $data The POST data
  335. */
  336. public function submit($data)
  337. {
  338. if (null === $data) {
  339. $data = array();
  340. }
  341. if (!is_array($data)) {
  342. throw new UnexpectedTypeException($data, 'array');
  343. }
  344. // remember for later
  345. $submittedData = $data;
  346. foreach ($this->fields as $key => $field) {
  347. if (!isset($data[$key])) {
  348. $data[$key] = null;
  349. }
  350. }
  351. $data = $this->preprocessData($data);
  352. foreach ($data as $key => $value) {
  353. if ($this->has($key)) {
  354. $this->fields[$key]->submit($value);
  355. }
  356. }
  357. $data = $this->getTransformedData();
  358. $this->writeObject($data);
  359. // set and reverse transform the data
  360. parent::submit($data);
  361. $this->extraFields = array();
  362. foreach ($submittedData as $key => $value) {
  363. if (!$this->has($key)) {
  364. $this->extraFields[] = $key;
  365. }
  366. }
  367. }
  368. /**
  369. * Updates the child fields from the properties of the given data
  370. *
  371. * This method calls readProperty() on all child fields that have a
  372. * property path set. If a child field has no property path set but
  373. * implements FormInterface, writeProperty() is called on its
  374. * children instead.
  375. *
  376. * @param array|object $objectOrArray
  377. */
  378. protected function readObject(&$objectOrArray)
  379. {
  380. $iterator = new RecursiveFieldIterator($this);
  381. $iterator = new \RecursiveIteratorIterator($iterator);
  382. foreach ($iterator as $field) {
  383. $field->readProperty($objectOrArray);
  384. }
  385. }
  386. /**
  387. * Updates all properties of the given data from the child fields
  388. *
  389. * This method calls writeProperty() on all child fields that have a property
  390. * path set. If a child field has no property path set but implements
  391. * FormInterface, writeProperty() is called on its children instead.
  392. *
  393. * @param array|object $objectOrArray
  394. */
  395. protected function writeObject(&$objectOrArray)
  396. {
  397. $iterator = new RecursiveFieldIterator($this);
  398. $iterator = new \RecursiveIteratorIterator($iterator);
  399. foreach ($iterator as $field) {
  400. $field->writeProperty($objectOrArray);
  401. }
  402. }
  403. /**
  404. * Processes the submitted data before it is passed to the individual fields
  405. *
  406. * The data is in the user format.
  407. *
  408. * @param array $data
  409. * @return array
  410. */
  411. protected function preprocessData(array $data)
  412. {
  413. return $data;
  414. }
  415. /**
  416. * @inheritDoc
  417. */
  418. public function isVirtual()
  419. {
  420. return $this->getOption('virtual');
  421. }
  422. /**
  423. * Returns whether this form was submitted with extra fields
  424. *
  425. * @return Boolean
  426. */
  427. public function isSubmittedWithExtraFields()
  428. {
  429. // TODO: integrate the field names in the error message
  430. return count($this->extraFields) > 0;
  431. }
  432. /**
  433. * Returns whether the field is valid.
  434. *
  435. * @return Boolean
  436. */
  437. public function isValid()
  438. {
  439. if (!parent::isValid()) {
  440. return false;
  441. }
  442. foreach ($this->fields as $field) {
  443. if (!$field->isValid()) {
  444. return false;
  445. }
  446. }
  447. return true;
  448. }
  449. /**
  450. * {@inheritDoc}
  451. */
  452. public function addError(FieldError $error, PropertyPathIterator $pathIterator = null, $type = null)
  453. {
  454. if (null !== $pathIterator) {
  455. if ($type === self::FIELD_ERROR && $pathIterator->hasNext()) {
  456. $pathIterator->next();
  457. if ($pathIterator->isProperty() && $pathIterator->current() === 'fields') {
  458. $pathIterator->next();
  459. }
  460. if ($this->has($pathIterator->current()) && !$this->get($pathIterator->current())->isHidden()) {
  461. $this->get($pathIterator->current())->addError($error, $pathIterator, $type);
  462. return;
  463. }
  464. } else if ($type === self::DATA_ERROR) {
  465. $iterator = new RecursiveFieldIterator($this);
  466. $iterator = new \RecursiveIteratorIterator($iterator);
  467. foreach ($iterator as $field) {
  468. if (null !== ($fieldPath = $field->getPropertyPath())) {
  469. if ($fieldPath->getElement(0) === $pathIterator->current() && !$field->isHidden()) {
  470. if ($pathIterator->hasNext()) {
  471. $pathIterator->next();
  472. }
  473. $field->addError($error, $pathIterator, $type);
  474. return;
  475. }
  476. }
  477. }
  478. }
  479. }
  480. parent::addError($error);
  481. }
  482. /**
  483. * Returns whether the field requires a multipart form.
  484. *
  485. * @return Boolean
  486. */
  487. public function isMultipart()
  488. {
  489. foreach ($this->fields as $field) {
  490. if ($field->isMultipart()) {
  491. return true;
  492. }
  493. }
  494. return false;
  495. }
  496. /**
  497. * Returns true if the field exists (implements the \ArrayAccess interface).
  498. *
  499. * @param string $key The key of the field
  500. *
  501. * @return Boolean true if the widget exists, false otherwise
  502. */
  503. public function offsetExists($key)
  504. {
  505. return $this->has($key);
  506. }
  507. /**
  508. * Returns the form field associated with the name (implements the \ArrayAccess interface).
  509. *
  510. * @param string $key The offset of the value to get
  511. *
  512. * @return Field A form field instance
  513. */
  514. public function offsetGet($key)
  515. {
  516. return $this->get($key);
  517. }
  518. /**
  519. * Throws an exception saying that values cannot be set (implements the \ArrayAccess interface).
  520. *
  521. * @param string $offset (ignored)
  522. * @param string $value (ignored)
  523. *
  524. * @throws \LogicException
  525. */
  526. public function offsetSet($key, $field)
  527. {
  528. throw new \LogicException('Use the method add() to add fields');
  529. }
  530. /**
  531. * Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface).
  532. *
  533. * @param string $key
  534. *
  535. * @throws \LogicException
  536. */
  537. public function offsetUnset($key)
  538. {
  539. return $this->remove($key);
  540. }
  541. /**
  542. * Returns the iterator for this group.
  543. *
  544. * @return \ArrayIterator
  545. */
  546. public function getIterator()
  547. {
  548. return new \ArrayIterator($this->fields);
  549. }
  550. /**
  551. * Returns the number of form fields (implements the \Countable interface).
  552. *
  553. * @return integer The number of embedded form fields
  554. */
  555. public function count()
  556. {
  557. return count($this->fields);
  558. }
  559. /**
  560. * Returns a factory for automatically creating fields based on metadata
  561. * available for a form's object
  562. *
  563. * @return FieldFactoryInterface The factory
  564. */
  565. public function getFieldFactory()
  566. {
  567. return $this->getOption('field_factory');
  568. }
  569. /**
  570. * Returns the validator used by the form
  571. *
  572. * @return ValidatorInterface The validator instance
  573. */
  574. public function getValidator()
  575. {
  576. return $this->getOption('validator');
  577. }
  578. /**
  579. * Returns the validation groups validated by the form
  580. *
  581. * @return array A list of validation groups or null
  582. */
  583. public function getValidationGroups()
  584. {
  585. return $this->getOption('validation_groups');
  586. }
  587. /**
  588. * Returns the name used for the CSRF protection field
  589. *
  590. * @return string The field name
  591. */
  592. public function getCsrfFieldName()
  593. {
  594. return $this->getOption('csrf_field_name');
  595. }
  596. /**
  597. * Returns the provider used for generating and validating CSRF tokens
  598. *
  599. * @return CsrfProviderInterface The provider instance
  600. */
  601. public function getCsrfProvider()
  602. {
  603. return $this->getOption('csrf_provider');
  604. }
  605. /**
  606. * Binds a request to the form
  607. *
  608. * If the request was a POST request, the data is submitted to the form,
  609. * transformed and written into the form data (an object or an array).
  610. * You can set the form data by passing it in the second parameter
  611. * of this method or by passing it in the "data" option of the form's
  612. * constructor.
  613. *
  614. * @param Request $request The request to bind to the form
  615. * @param array|object $data The data from which to read default values
  616. * and where to write submitted values
  617. */
  618. public function bind(Request $request, $data = null)
  619. {
  620. $this->bound = true;
  621. // Store object from which to read the default values and where to
  622. // write the submitted values
  623. if (null !== $data) {
  624. $this->setData($data);
  625. }
  626. // Store the submitted data in case of a post request
  627. if ('POST' == $request->getMethod()) {
  628. $values = $request->request->get($this->getName(), array());
  629. $files = $request->files->get($this->getName(), array());
  630. $this->submit(self::deepArrayUnion($values, $files));
  631. $this->validate();
  632. }
  633. }
  634. /**
  635. * @var Boolean whether a request and an object were bound to the form
  636. */
  637. public function isBound()
  638. {
  639. return $this->bound;
  640. }
  641. /**
  642. * Validates the form and its domain object
  643. *
  644. * @throws FormException If the option "validator" was not set
  645. */
  646. public function validate()
  647. {
  648. if (null === $this->getOption('validator')) {
  649. throw new MissingOptionsException('The option "validator" is required for validating', array('validator'));
  650. }
  651. // Validate the submitted data
  652. if ($violations = $this->getOption('validator')->validate($this, $this->getOption('validation_groups'))) {
  653. // TODO: test me
  654. foreach ($violations as $violation) {
  655. $propertyPath = new PropertyPath($violation->getPropertyPath());
  656. $iterator = $propertyPath->getIterator();
  657. if ($iterator->current() == 'data') {
  658. $type = self::DATA_ERROR;
  659. $iterator->next(); // point at the first data element
  660. } else {
  661. $type = self::FIELD_ERROR;
  662. }
  663. $this->addError(new FieldError($violation->getMessageTemplate(), $violation->getMessageParameters()), $iterator, $type);
  664. }
  665. }
  666. }
  667. /**
  668. * @return true if this form is CSRF protected
  669. */
  670. public function isCsrfProtected()
  671. {
  672. return $this->has($this->getOption('csrf_field_name'));
  673. }
  674. /**
  675. * Returns whether the CSRF token is valid
  676. *
  677. * @return Boolean
  678. */
  679. public function isCsrfTokenValid()
  680. {
  681. if (!$this->isCsrfProtected()) {
  682. return true;
  683. } else {
  684. $token = $this->get($this->getOption('csrf_field_name'))->getDisplayedData();
  685. return $this->getOption('csrf_provider')->isCsrfTokenValid(get_class($this), $token);
  686. }
  687. }
  688. /**
  689. * Returns whether the maximum POST size was reached in this request.
  690. *
  691. * @return Boolean
  692. */
  693. public function isPostMaxSizeReached()
  694. {
  695. if ($this->isRoot() && isset($_SERVER['CONTENT_LENGTH'])) {
  696. $length = (int) $_SERVER['CONTENT_LENGTH'];
  697. $max = trim(ini_get('post_max_size'));
  698. switch (strtolower(substr($max, -1))) {
  699. // The 'G' modifier is available since PHP 5.1.0
  700. case 'g':
  701. $max *= 1024;
  702. case 'm':
  703. $max *= 1024;
  704. case 'k':
  705. $max *= 1024;
  706. }
  707. return $length > $max;
  708. } else {
  709. return false;
  710. }
  711. }
  712. /**
  713. * Sets the class that object bound to this form must be instances of
  714. *
  715. * @param string A fully qualified class name
  716. */
  717. protected function setDataClass($class)
  718. {
  719. $this->dataClass = $class;
  720. }
  721. /**
  722. * Returns the class that object must have that are bound to this form
  723. *
  724. * @return string A fully qualified class name
  725. */
  726. public function getDataClass()
  727. {
  728. return $this->dataClass;
  729. }
  730. /**
  731. * Returns the context used when creating this form
  732. *
  733. * @return FormContext The context instance
  734. */
  735. public function getContext()
  736. {
  737. return $this->getOption('context');
  738. }
  739. /**
  740. * Merges two arrays without reindexing numeric keys.
  741. *
  742. * @param array $array1 An array to merge
  743. * @param array $array2 An array to merge
  744. *
  745. * @return array The merged array
  746. */
  747. static protected function deepArrayUnion($array1, $array2)
  748. {
  749. foreach ($array2 as $key => $value) {
  750. if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
  751. $array1[$key] = self::deepArrayUnion($array1[$key], $value);
  752. } else {
  753. $array1[$key] = $value;
  754. }
  755. }
  756. return $array1;
  757. }
  758. }