PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/ORM/Marshaller.php

https://github.com/binondord/cakephp
PHP | 692 lines | 417 code | 72 blank | 203 comment | 72 complexity | 7f357bedc377be2d9018c14906e52aae MD5 | raw file
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\ORM;
  16. use Cake\Collection\Collection;
  17. use Cake\Database\Expression\TupleComparison;
  18. use Cake\Database\Type;
  19. use Cake\Datasource\EntityInterface;
  20. use Cake\ORM\Association;
  21. use Cake\ORM\AssociationsNormalizerTrait;
  22. use Cake\ORM\Table;
  23. use \RuntimeException;
  24. /**
  25. * Contains logic to convert array data into entities.
  26. *
  27. * Useful when converting request data into entities.
  28. *
  29. * @see \Cake\ORM\Table::newEntity()
  30. * @see \Cake\ORM\Table::newEntities()
  31. * @see \Cake\ORM\Table::patchEntity()
  32. * @see \Cake\ORM\Table::patchEntities()
  33. */
  34. class Marshaller
  35. {
  36. use AssociationsNormalizerTrait;
  37. /**
  38. * The table instance this marshaller is for.
  39. *
  40. * @var \Cake\ORM\Table
  41. */
  42. protected $_table;
  43. /**
  44. * Constructor.
  45. *
  46. * @param \Cake\ORM\Table $table The table this marshaller is for.
  47. */
  48. public function __construct(Table $table)
  49. {
  50. $this->_table = $table;
  51. }
  52. /**
  53. * Build the map of property => association names.
  54. *
  55. * @param array $options List of options containing the 'associated' key.
  56. * @return array
  57. */
  58. protected function _buildPropertyMap($options)
  59. {
  60. if (empty($options['associated'])) {
  61. return [];
  62. }
  63. $include = $options['associated'];
  64. $map = [];
  65. $include = $this->_normalizeAssociations($include);
  66. foreach ($include as $key => $nested) {
  67. if (is_int($key) && is_scalar($nested)) {
  68. $key = $nested;
  69. $nested = [];
  70. }
  71. $assoc = $this->_table->association($key);
  72. if ($assoc) {
  73. $map[$assoc->property()] = ['association' => $assoc] + $nested + ['associated' => []];
  74. }
  75. }
  76. return $map;
  77. }
  78. /**
  79. * Hydrate one entity and its associated data.
  80. *
  81. * ### Options:
  82. *
  83. * * associated: Associations listed here will be marshalled as well.
  84. * * fieldList: A whitelist of fields to be assigned to the entity. If not present,
  85. * the accessible fields list in the entity will be used.
  86. * * accessibleFields: A list of fields to allow or deny in entity accessible fields.
  87. *
  88. * @param array $data The data to hydrate.
  89. * @param array $options List of options
  90. * @return \Cake\ORM\Entity
  91. * @see \Cake\ORM\Table::newEntity()
  92. */
  93. public function one(array $data, array $options = [])
  94. {
  95. list($data, $options) = $this->_prepareDataAndOptions($data, $options);
  96. $propertyMap = $this->_buildPropertyMap($options);
  97. $schema = $this->_table->schema();
  98. $primaryKey = $schema->primaryKey();
  99. $entityClass = $this->_table->entityClass();
  100. $entity = new $entityClass();
  101. $entity->source($this->_table->registryAlias());
  102. if (isset($options['accessibleFields'])) {
  103. foreach ((array)$options['accessibleFields'] as $key => $value) {
  104. $entity->accessible($key, $value);
  105. }
  106. }
  107. $errors = $this->_validate($data, $options, true);
  108. $properties = [];
  109. foreach ($data as $key => $value) {
  110. if (!empty($errors[$key])) {
  111. continue;
  112. }
  113. $columnType = $schema->columnType($key);
  114. if (isset($propertyMap[$key])) {
  115. $assoc = $propertyMap[$key]['association'];
  116. $value = $this->_marshalAssociation($assoc, $value, $propertyMap[$key]);
  117. } elseif ($value === '' && in_array($key, $primaryKey, true)) {
  118. // Skip marshalling '' for pk fields.
  119. continue;
  120. } elseif ($columnType) {
  121. $converter = Type::build($columnType);
  122. $value = $converter->marshal($value);
  123. }
  124. $properties[$key] = $value;
  125. }
  126. if (!isset($options['fieldList'])) {
  127. $entity->set($properties);
  128. $entity->errors($errors);
  129. return $entity;
  130. }
  131. foreach ((array)$options['fieldList'] as $field) {
  132. if (array_key_exists($field, $properties)) {
  133. $entity->set($field, $properties[$field]);
  134. }
  135. }
  136. $entity->errors($errors);
  137. return $entity;
  138. }
  139. /**
  140. * Returns the validation errors for a data set based on the passed options
  141. *
  142. * @param array $data The data to validate.
  143. * @param array $options The options passed to this marshaller.
  144. * @param bool $isNew Whether it is a new entity or one to be updated.
  145. * @return array The list of validation errors.
  146. * @throws \RuntimeException If no validator can be created.
  147. */
  148. protected function _validate($data, $options, $isNew)
  149. {
  150. if (!$options['validate']) {
  151. return [];
  152. }
  153. if ($options['validate'] === true) {
  154. $options['validate'] = $this->_table->validator('default');
  155. }
  156. if (is_string($options['validate'])) {
  157. $options['validate'] = $this->_table->validator($options['validate']);
  158. }
  159. if (!is_object($options['validate'])) {
  160. throw new RuntimeException(
  161. sprintf('validate must be a boolean, a string or an object. Got %s.', gettype($options['validate']))
  162. );
  163. }
  164. return $options['validate']->errors($data, $isNew);
  165. }
  166. /**
  167. * Returns data and options prepared to validate and marshall.
  168. *
  169. * @param array $data The data to prepare.
  170. * @param array $options The options passed to this marshaller.
  171. * @return array An array containing prepared data and options.
  172. */
  173. protected function _prepareDataAndOptions($data, $options)
  174. {
  175. $options += ['validate' => true];
  176. $tableName = $this->_table->alias();
  177. if (isset($data[$tableName])) {
  178. $data = $data[$tableName];
  179. }
  180. $data = new \ArrayObject($data);
  181. $options = new \ArrayObject($options);
  182. $this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options'));
  183. return [(array)$data, (array)$options];
  184. }
  185. /**
  186. * Create a new sub-marshaller and marshal the associated data.
  187. *
  188. * @param \Cake\ORM\Association $assoc The association to marshall
  189. * @param array $value The data to hydrate
  190. * @param array $options List of options.
  191. * @return mixed
  192. */
  193. protected function _marshalAssociation($assoc, $value, $options)
  194. {
  195. if (!is_array($value)) {
  196. return;
  197. }
  198. $targetTable = $assoc->target();
  199. $marshaller = $targetTable->marshaller();
  200. $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
  201. if (in_array($assoc->type(), $types)) {
  202. return $marshaller->one($value, (array)$options);
  203. }
  204. if ($assoc->type() === Association::MANY_TO_MANY) {
  205. return $marshaller->_belongsToMany($assoc, $value, (array)$options);
  206. }
  207. if ($assoc->type() === Association::ONE_TO_MANY && array_key_exists('_ids', $value) && is_array($value['_ids'])) {
  208. return $this->_loadAssociatedByIds($assoc, $value['_ids']);
  209. }
  210. return $marshaller->many($value, (array)$options);
  211. }
  212. /**
  213. * Hydrate many entities and their associated data.
  214. *
  215. * ### Options:
  216. *
  217. * * associated: Associations listed here will be marshalled as well.
  218. * * fieldList: A whitelist of fields to be assigned to the entity. If not present,
  219. * the accessible fields list in the entity will be used.
  220. * * accessibleFields: A list of fields to allow or deny in entity accessible fields.
  221. *
  222. * @param array $data The data to hydrate.
  223. * @param array $options List of options
  224. * @return array An array of hydrated records.
  225. * @see \Cake\ORM\Table::newEntities()
  226. */
  227. public function many(array $data, array $options = [])
  228. {
  229. $output = [];
  230. foreach ($data as $record) {
  231. if (!is_array($record)) {
  232. continue;
  233. }
  234. $output[] = $this->one($record, $options);
  235. }
  236. return $output;
  237. }
  238. /**
  239. * Marshals data for belongsToMany associations.
  240. *
  241. * Builds the related entities and handles the special casing
  242. * for junction table entities.
  243. *
  244. * @param Association $assoc The association to marshal.
  245. * @param array $data The data to convert into entities.
  246. * @param array $options List of options.
  247. * @return array An array of built entities.
  248. */
  249. protected function _belongsToMany(Association $assoc, array $data, $options = [])
  250. {
  251. // Accept _ids = [1, 2]
  252. $associated = isset($options['associated']) ? $options['associated'] : [];
  253. $hasIds = array_key_exists('_ids', $data);
  254. if ($hasIds && is_array($data['_ids'])) {
  255. return $this->_loadAssociatedByIds($assoc, $data['_ids']);
  256. }
  257. if ($hasIds) {
  258. return [];
  259. }
  260. $data = array_values($data);
  261. $target = $assoc->target();
  262. $primaryKey = array_flip($target->schema()->primaryKey());
  263. $records = $conditions = [];
  264. $primaryCount = count($primaryKey);
  265. foreach ($data as $i => $row) {
  266. if (!is_array($row)) {
  267. continue;
  268. }
  269. if (array_intersect_key($primaryKey, $row) === $primaryKey) {
  270. $keys = array_intersect_key($row, $primaryKey);
  271. if (count($keys) === $primaryCount) {
  272. foreach ($keys as $key => $value) {
  273. $conditions[][$target->aliasfield($key)] = $value;
  274. }
  275. }
  276. } else {
  277. $records[$i] = $this->one($row, $options);
  278. }
  279. }
  280. if (!empty($conditions)) {
  281. $query = $target->find();
  282. $query->andWhere(function ($exp) use ($conditions) {
  283. return $exp->or_($conditions);
  284. });
  285. }
  286. if (isset($query)) {
  287. $keyFields = array_keys($primaryKey);
  288. $existing = [];
  289. foreach ($query as $row) {
  290. $k = implode(';', $row->extract($keyFields));
  291. $existing[$k] = $row;
  292. }
  293. foreach ($data as $i => $row) {
  294. $key = [];
  295. foreach ($keyFields as $k) {
  296. if (isset($row[$k])) {
  297. $key[] = $row[$k];
  298. }
  299. }
  300. $key = implode(';', $key);
  301. if (isset($existing[$key])) {
  302. $records[$i] = $existing[$key];
  303. }
  304. }
  305. }
  306. $jointMarshaller = $assoc->junction()->marshaller();
  307. $nested = [];
  308. if (isset($associated['_joinData'])) {
  309. $nested = (array)$associated['_joinData'];
  310. }
  311. foreach ($records as $i => $record) {
  312. if (isset($data[$i]['_joinData'])) {
  313. $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested);
  314. $record->set('_joinData', $joinData);
  315. }
  316. }
  317. return $records;
  318. }
  319. /**
  320. * Loads a list of belongs to many from ids.
  321. *
  322. * @param Association $assoc The association class for the belongsToMany association.
  323. * @param array $ids The list of ids to load.
  324. * @return array An array of entities.
  325. */
  326. protected function _loadAssociatedByIds($assoc, $ids)
  327. {
  328. $target = $assoc->target();
  329. $primaryKey = (array)$target->primaryKey();
  330. $multi = count($primaryKey) > 1;
  331. $primaryKey = array_map(function ($key) use ($target) {
  332. return $target->alias() . '.' . $key;
  333. }, $primaryKey);
  334. if ($multi) {
  335. if (count(current($ids)) !== count($primaryKey)) {
  336. return [];
  337. }
  338. $filter = new TupleComparison($primaryKey, $ids, [], 'IN');
  339. } else {
  340. $filter = [$primaryKey[0] . ' IN' => $ids];
  341. }
  342. return $target->find()->where($filter)->toArray();
  343. }
  344. /**
  345. * Loads a list of belongs to many from ids.
  346. *
  347. * @param Association $assoc The association class for the belongsToMany association.
  348. * @param array $ids The list of ids to load.
  349. * @return array An array of entities.
  350. * @deprecated Use _loadAssociatedByIds()
  351. */
  352. protected function _loadBelongsToMany($assoc, $ids)
  353. {
  354. return $this->_loadAssociatedByIds($assoc, $ids);
  355. }
  356. /**
  357. * Merges `$data` into `$entity` and recursively does the same for each one of
  358. * the association names passed in `$options`. When merging associations, if an
  359. * entity is not present in the parent entity for a given association, a new one
  360. * will be created.
  361. *
  362. * When merging HasMany or BelongsToMany associations, all the entities in the
  363. * `$data` array will appear, those that can be matched by primary key will get
  364. * the data merged, but those that cannot, will be discarded.
  365. *
  366. * ### Options:
  367. *
  368. * * associated: Associations listed here will be marshalled as well.
  369. * * validate: Whether or not to validate data before hydrating the entities. Can
  370. * also be set to a string to use a specific validator. Defaults to true/default.
  371. * * fieldList: A whitelist of fields to be assigned to the entity. If not present
  372. * the accessible fields list in the entity will be used.
  373. * * accessibleFields: A list of fields to allow or deny in entity accessible fields.
  374. *
  375. * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
  376. * data merged in
  377. * @param array $data key value list of fields to be merged into the entity
  378. * @param array $options List of options.
  379. * @return \Cake\Datasource\EntityInterface
  380. */
  381. public function merge(EntityInterface $entity, array $data, array $options = [])
  382. {
  383. list($data, $options) = $this->_prepareDataAndOptions($data, $options);
  384. $propertyMap = $this->_buildPropertyMap($options);
  385. $isNew = $entity->isNew();
  386. $keys = [];
  387. if (!$isNew) {
  388. $keys = $entity->extract((array)$this->_table->primaryKey());
  389. }
  390. if (isset($options['accessibleFields'])) {
  391. foreach ((array)$options['accessibleFields'] as $key => $value) {
  392. $entity->accessible($key, $value);
  393. }
  394. }
  395. $errors = $this->_validate($data + $keys, $options, $isNew);
  396. $schema = $this->_table->schema();
  397. $properties = $marshalledAssocs = [];
  398. foreach ($data as $key => $value) {
  399. if (!empty($errors[$key])) {
  400. continue;
  401. }
  402. $columnType = $schema->columnType($key);
  403. $original = $entity->get($key);
  404. if (isset($propertyMap[$key])) {
  405. $assoc = $propertyMap[$key]['association'];
  406. $value = $this->_mergeAssociation($original, $assoc, $value, $propertyMap[$key]);
  407. $marshalledAssocs[$key] = true;
  408. } elseif ($columnType) {
  409. $converter = Type::build($columnType);
  410. $value = $converter->marshal($value);
  411. $isObject = is_object($value);
  412. if ((!$isObject && $original === $value) ||
  413. ($isObject && $original == $value)
  414. ) {
  415. continue;
  416. }
  417. }
  418. $properties[$key] = $value;
  419. }
  420. if (!isset($options['fieldList'])) {
  421. $entity->set($properties);
  422. $entity->errors($errors);
  423. foreach (array_keys($marshalledAssocs) as $field) {
  424. if ($properties[$field] instanceof EntityInterface) {
  425. $entity->dirty($field, $properties[$field]->dirty());
  426. }
  427. }
  428. return $entity;
  429. }
  430. foreach ((array)$options['fieldList'] as $field) {
  431. if (array_key_exists($field, $properties)) {
  432. $entity->set($field, $properties[$field]);
  433. if ($properties[$field] instanceof EntityInterface && isset($marshalledAssocs[$field])) {
  434. $entity->dirty($field, $properties[$field]->dirty());
  435. }
  436. }
  437. }
  438. $entity->errors($errors);
  439. return $entity;
  440. }
  441. /**
  442. * Merges each of the elements from `$data` into each of the entities in `$entities`
  443. * and recursively does the same for each of the association names passed in
  444. * `$options`. When merging associations, if an entity is not present in the parent
  445. * entity for a given association, a new one will be created.
  446. *
  447. * Records in `$data` are matched against the entities using the primary key
  448. * column. Entries in `$entities` that cannot be matched to any record in
  449. * `$data` will be discarded. Records in `$data` that could not be matched will
  450. * be marshalled as a new entity.
  451. *
  452. * When merging HasMany or BelongsToMany associations, all the entities in the
  453. * `$data` array will appear, those that can be matched by primary key will get
  454. * the data merged, but those that cannot, will be discarded.
  455. *
  456. * ### Options:
  457. *
  458. * - associated: Associations listed here will be marshalled as well.
  459. * - fieldList: A whitelist of fields to be assigned to the entity. If not present,
  460. * the accessible fields list in the entity will be used.
  461. * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
  462. *
  463. * @param array|\Traversable $entities the entities that will get the
  464. * data merged in
  465. * @param array $data list of arrays to be merged into the entities
  466. * @param array $options List of options.
  467. * @return array
  468. */
  469. public function mergeMany($entities, array $data, array $options = [])
  470. {
  471. $primary = (array)$this->_table->primaryKey();
  472. $indexed = (new Collection($data))
  473. ->groupBy(function ($el) use ($primary) {
  474. $keys = [];
  475. foreach ($primary as $key) {
  476. $keys[] = isset($el[$key]) ? $el[$key] : '';
  477. }
  478. return implode(';', $keys);
  479. })
  480. ->map(function ($element, $key) {
  481. return $key === '' ? $element : $element[0];
  482. })
  483. ->toArray();
  484. $new = isset($indexed[null]) ? $indexed[null] : [];
  485. unset($indexed[null]);
  486. $output = [];
  487. foreach ($entities as $entity) {
  488. if (!($entity instanceof EntityInterface)) {
  489. continue;
  490. }
  491. $key = implode(';', $entity->extract($primary));
  492. if ($key === null || !isset($indexed[$key])) {
  493. continue;
  494. }
  495. $output[] = $this->merge($entity, $indexed[$key], $options);
  496. unset($indexed[$key]);
  497. }
  498. $maybeExistentQuery = (new Collection($indexed))
  499. ->map(function ($data, $key) {
  500. return explode(';', $key);
  501. })
  502. ->filter(function ($keys) use ($primary) {
  503. return count(array_filter($keys, 'strlen')) === count($primary);
  504. })
  505. ->reduce(function ($query, $keys) use ($primary) {
  506. return $query->orWhere($query->newExpr()->and_(array_combine($primary, $keys)));
  507. }, $this->_table->find());
  508. if (!empty($indexed) && count($maybeExistentQuery->clause('where'))) {
  509. foreach ($maybeExistentQuery as $entity) {
  510. $key = implode(';', $entity->extract($primary));
  511. if (isset($indexed[$key])) {
  512. $output[] = $this->merge($entity, $indexed[$key], $options);
  513. unset($indexed[$key]);
  514. }
  515. }
  516. }
  517. foreach ((new Collection($indexed))->append($new) as $value) {
  518. if (!is_array($value)) {
  519. continue;
  520. }
  521. $output[] = $this->one($value, $options);
  522. }
  523. return $output;
  524. }
  525. /**
  526. * Creates a new sub-marshaller and merges the associated data.
  527. *
  528. * @param \Cake\Datasource\EntityInterface $original The original entity
  529. * @param \Cake\ORM\Association $assoc The association to merge
  530. * @param array $value The data to hydrate
  531. * @param array $options List of options.
  532. * @return mixed
  533. */
  534. protected function _mergeAssociation($original, $assoc, $value, $options)
  535. {
  536. if (!$original) {
  537. return $this->_marshalAssociation($assoc, $value, $options);
  538. }
  539. $targetTable = $assoc->target();
  540. $marshaller = $targetTable->marshaller();
  541. $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
  542. if (in_array($assoc->type(), $types)) {
  543. return $marshaller->merge($original, $value, (array)$options);
  544. }
  545. if ($assoc->type() === Association::MANY_TO_MANY) {
  546. return $marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options);
  547. }
  548. return $marshaller->mergeMany($original, $value, (array)$options);
  549. }
  550. /**
  551. * Creates a new sub-marshaller and merges the associated data for a BelongstoMany
  552. * association.
  553. *
  554. * @param \Cake\Datasource\EntityInterface $original The original entity
  555. * @param \Cake\ORM\Association $assoc The association to marshall
  556. * @param array $value The data to hydrate
  557. * @param array $options List of options.
  558. * @return array
  559. */
  560. protected function _mergeBelongsToMany($original, $assoc, $value, $options)
  561. {
  562. $hasIds = array_key_exists('_ids', $value);
  563. $associated = isset($options['associated']) ? $options['associated'] : [];
  564. if ($hasIds && is_array($value['_ids'])) {
  565. return $this->_loadAssociatedByIds($assoc, $value['_ids']);
  566. }
  567. if ($hasIds) {
  568. return [];
  569. }
  570. if (!in_array('_joinData', $associated) && !isset($associated['_joinData'])) {
  571. return $this->mergeMany($original, $value, $options);
  572. }
  573. return $this->_mergeJoinData($original, $assoc, $value, $options);
  574. }
  575. /**
  576. * Merge the special _joinData property into the entity set.
  577. *
  578. * @param \Cake\Datasource\EntityInterface $original The original entity
  579. * @param \Cake\ORM\Association $assoc The association to marshall
  580. * @param array $value The data to hydrate
  581. * @param array $options List of options.
  582. * @return array An array of entities
  583. */
  584. protected function _mergeJoinData($original, $assoc, $value, $options)
  585. {
  586. $associated = isset($options['associated']) ? $options['associated'] : [];
  587. $extra = [];
  588. foreach ($original as $entity) {
  589. // Mark joinData as accessible so we can marshal it properly.
  590. $entity->accessible('_joinData', true);
  591. $joinData = $entity->get('_joinData');
  592. if ($joinData && $joinData instanceof EntityInterface) {
  593. $extra[spl_object_hash($entity)] = $joinData;
  594. }
  595. }
  596. $joint = $assoc->junction();
  597. $marshaller = $joint->marshaller();
  598. $nested = [];
  599. if (isset($associated['_joinData'])) {
  600. $nested = (array)$associated['_joinData'];
  601. }
  602. $options['accessibleFields'] = ['_joinData' => true];
  603. $records = $this->mergeMany($original, $value, $options);
  604. foreach ($records as $record) {
  605. $hash = spl_object_hash($record);
  606. $value = $record->get('_joinData');
  607. if (!is_array($value)) {
  608. $record->unsetProperty('_joinData');
  609. continue;
  610. }
  611. if (isset($extra[$hash])) {
  612. $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested));
  613. } else {
  614. $joinData = $marshaller->one($value, $nested);
  615. $record->set('_joinData', $joinData);
  616. }
  617. }
  618. return $records;
  619. }
  620. }