Form.php 24 KB

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