1: <?php
2: /**
3: *
4: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
5: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
6: *
7: * Licensed under The MIT License
8: * For full copyright and license information, please see the LICENSE.txt
9: * Redistributions of files must retain the above copyright notice.
10: *
11: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
12: * @link https://cakephp.org CakePHP(tm) Project
13: * @since 3.0.0
14: * @license https://opensource.org/licenses/mit-license.php MIT License
15: */
16: namespace Cake\ORM\Association;
17:
18: use Cake\Collection\Collection;
19: use Cake\Database\Expression\FieldInterface;
20: use Cake\Database\Expression\QueryExpression;
21: use Cake\Datasource\EntityInterface;
22: use Cake\Datasource\QueryInterface;
23: use Cake\ORM\Association;
24: use Cake\ORM\Association\DependentDeleteHelper;
25: use Cake\ORM\Association\Loader\SelectLoader;
26: use Cake\ORM\Table;
27: use InvalidArgumentException;
28: use Traversable;
29:
30: /**
31: * Represents an N - 1 relationship where the target side of the relationship
32: * will have one or multiple records per each one in the source side.
33: *
34: * An example of a HasMany association would be Author has many Articles.
35: */
36: class HasMany extends Association
37: {
38: /**
39: * Order in which target records should be returned
40: *
41: * @var mixed
42: */
43: protected $_sort;
44:
45: /**
46: * The type of join to be used when adding the association to a query
47: *
48: * @var string
49: */
50: protected $_joinType = QueryInterface::JOIN_TYPE_INNER;
51:
52: /**
53: * The strategy name to be used to fetch associated records.
54: *
55: * @var string
56: */
57: protected $_strategy = self::STRATEGY_SELECT;
58:
59: /**
60: * Valid strategies for this type of association
61: *
62: * @var array
63: */
64: protected $_validStrategies = [
65: self::STRATEGY_SELECT,
66: self::STRATEGY_SUBQUERY
67: ];
68:
69: /**
70: * Saving strategy that will only append to the links set
71: *
72: * @var string
73: */
74: const SAVE_APPEND = 'append';
75:
76: /**
77: * Saving strategy that will replace the links with the provided set
78: *
79: * @var string
80: */
81: const SAVE_REPLACE = 'replace';
82:
83: /**
84: * Saving strategy to be used by this association
85: *
86: * @var string
87: */
88: protected $_saveStrategy = self::SAVE_APPEND;
89:
90: /**
91: * Returns whether or not the passed table is the owning side for this
92: * association. This means that rows in the 'target' table would miss important
93: * or required information if the row in 'source' did not exist.
94: *
95: * @param \Cake\ORM\Table $side The potential Table with ownership
96: * @return bool
97: */
98: public function isOwningSide(Table $side)
99: {
100: return $side === $this->getSource();
101: }
102:
103: /**
104: * Sets the strategy that should be used for saving.
105: *
106: * @param string $strategy the strategy name to be used
107: * @throws \InvalidArgumentException if an invalid strategy name is passed
108: * @return $this
109: */
110: public function setSaveStrategy($strategy)
111: {
112: if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE], true)) {
113: $msg = sprintf('Invalid save strategy "%s"', $strategy);
114: throw new InvalidArgumentException($msg);
115: }
116:
117: $this->_saveStrategy = $strategy;
118:
119: return $this;
120: }
121:
122: /**
123: * Gets the strategy that should be used for saving.
124: *
125: * @return string the strategy to be used for saving
126: */
127: public function getSaveStrategy()
128: {
129: return $this->_saveStrategy;
130: }
131:
132: /**
133: * Sets the strategy that should be used for saving. If called with no
134: * arguments, it will return the currently configured strategy
135: *
136: * @deprecated 3.4.0 Use setSaveStrategy()/getSaveStrategy() instead.
137: * @param string|null $strategy the strategy name to be used
138: * @throws \InvalidArgumentException if an invalid strategy name is passed
139: * @return string the strategy to be used for saving
140: */
141: public function saveStrategy($strategy = null)
142: {
143: deprecationWarning(
144: 'HasMany::saveStrategy() is deprecated. ' .
145: 'Use setSaveStrategy()/getSaveStrategy() instead.'
146: );
147: if ($strategy !== null) {
148: $this->setSaveStrategy($strategy);
149: }
150:
151: return $this->getSaveStrategy();
152: }
153:
154: /**
155: * Takes an entity from the source table and looks if there is a field
156: * matching the property name for this association. The found entity will be
157: * saved on the target table for this association by passing supplied
158: * `$options`
159: *
160: * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
161: * @param array $options options to be passed to the save method in the target table
162: * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns
163: * the saved entity
164: * @see \Cake\ORM\Table::save()
165: * @throws \InvalidArgumentException when the association data cannot be traversed.
166: */
167: public function saveAssociated(EntityInterface $entity, array $options = [])
168: {
169: $targetEntities = $entity->get($this->getProperty());
170:
171: $isEmpty = in_array($targetEntities, [null, [], '', false], true);
172: if ($isEmpty) {
173: if ($entity->isNew() ||
174: $this->getSaveStrategy() !== self::SAVE_REPLACE
175: ) {
176: return $entity;
177: }
178:
179: $targetEntities = [];
180: }
181:
182: if (!is_array($targetEntities) &&
183: !($targetEntities instanceof Traversable)
184: ) {
185: $name = $this->getProperty();
186: $message = sprintf('Could not save %s, it cannot be traversed', $name);
187: throw new InvalidArgumentException($message);
188: }
189:
190: $foreignKeyReference = array_combine(
191: (array)$this->getForeignKey(),
192: $entity->extract((array)$this->getBindingKey())
193: );
194:
195: $options['_sourceTable'] = $this->getSource();
196:
197: if ($this->_saveStrategy === self::SAVE_REPLACE &&
198: !$this->_unlinkAssociated($foreignKeyReference, $entity, $this->getTarget(), $targetEntities, $options)
199: ) {
200: return false;
201: }
202:
203: if (!$this->_saveTarget($foreignKeyReference, $entity, $targetEntities, $options)) {
204: return false;
205: }
206:
207: return $entity;
208: }
209:
210: /**
211: * Persists each of the entities into the target table and creates links between
212: * the parent entity and each one of the saved target entities.
213: *
214: * @param array $foreignKeyReference The foreign key reference defining the link between the
215: * target entity, and the parent entity.
216: * @param \Cake\Datasource\EntityInterface $parentEntity The source entity containing the target
217: * entities to be saved.
218: * @param array|\Traversable $entities list of entities to persist in target table and to
219: * link to the parent entity
220: * @param array $options list of options accepted by `Table::save()`.
221: * @return bool `true` on success, `false` otherwise.
222: */
223: protected function _saveTarget(array $foreignKeyReference, EntityInterface $parentEntity, $entities, array $options)
224: {
225: $foreignKey = array_keys($foreignKeyReference);
226: $table = $this->getTarget();
227: $original = $entities;
228:
229: foreach ($entities as $k => $entity) {
230: if (!($entity instanceof EntityInterface)) {
231: break;
232: }
233:
234: if (!empty($options['atomic'])) {
235: $entity = clone $entity;
236: }
237:
238: if ($foreignKeyReference !== $entity->extract($foreignKey)) {
239: $entity->set($foreignKeyReference, ['guard' => false]);
240: }
241:
242: if ($table->save($entity, $options)) {
243: $entities[$k] = $entity;
244: continue;
245: }
246:
247: if (!empty($options['atomic'])) {
248: $original[$k]->setErrors($entity->getErrors());
249: $entity->set($this->getProperty(), $original);
250:
251: return false;
252: }
253: }
254:
255: $parentEntity->set($this->getProperty(), $entities);
256:
257: return true;
258: }
259:
260: /**
261: * Associates the source entity to each of the target entities provided.
262: * When using this method, all entities in `$targetEntities` will be appended to
263: * the source entity's property corresponding to this association object.
264: *
265: * This method does not check link uniqueness.
266: * Changes are persisted in the database and also in the source entity.
267: *
268: * ### Example:
269: *
270: * ```
271: * $user = $users->get(1);
272: * $allArticles = $articles->find('all')->toArray();
273: * $users->Articles->link($user, $allArticles);
274: * ```
275: *
276: * `$user->get('articles')` will contain all articles in `$allArticles` after linking
277: *
278: * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
279: * of this association
280: * @param array $targetEntities list of entities belonging to the `target` side
281: * of this association
282: * @param array $options list of options to be passed to the internal `save` call
283: * @return bool true on success, false otherwise
284: */
285: public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
286: {
287: $saveStrategy = $this->getSaveStrategy();
288: $this->setSaveStrategy(self::SAVE_APPEND);
289: $property = $this->getProperty();
290:
291: $currentEntities = array_unique(
292: array_merge(
293: (array)$sourceEntity->get($property),
294: $targetEntities
295: )
296: );
297:
298: $sourceEntity->set($property, $currentEntities);
299:
300: $savedEntity = $this->getConnection()->transactional(function () use ($sourceEntity, $options) {
301: return $this->saveAssociated($sourceEntity, $options);
302: });
303:
304: $ok = ($savedEntity instanceof EntityInterface);
305:
306: $this->setSaveStrategy($saveStrategy);
307:
308: if ($ok) {
309: $sourceEntity->set($property, $savedEntity->get($property));
310: $sourceEntity->setDirty($property, false);
311: }
312:
313: return $ok;
314: }
315:
316: /**
317: * Removes all links between the passed source entity and each of the provided
318: * target entities. This method assumes that all passed objects are already persisted
319: * in the database and that each of them contain a primary key value.
320: *
321: * ### Options
322: *
323: * Additionally to the default options accepted by `Table::delete()`, the following
324: * keys are supported:
325: *
326: * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that
327: * are stored in `$sourceEntity` (default: true)
328: *
329: * By default this method will unset each of the entity objects stored inside the
330: * source entity.
331: *
332: * Changes are persisted in the database and also in the source entity.
333: *
334: * ### Example:
335: *
336: * ```
337: * $user = $users->get(1);
338: * $user->articles = [$article1, $article2, $article3, $article4];
339: * $users->save($user, ['Associated' => ['Articles']]);
340: * $allArticles = [$article1, $article2, $article3];
341: * $users->Articles->unlink($user, $allArticles);
342: * ```
343: *
344: * `$article->get('articles')` will contain only `[$article4]` after deleting in the database
345: *
346: * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
347: * this association
348: * @param array $targetEntities list of entities persisted in the target table for
349: * this association
350: * @param array $options list of options to be passed to the internal `delete` call
351: * @throws \InvalidArgumentException if non persisted entities are passed or if
352: * any of them is lacking a primary key value
353: * @return void
354: */
355: public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = [])
356: {
357: if (is_bool($options)) {
358: $options = [
359: 'cleanProperty' => $options
360: ];
361: } else {
362: $options += ['cleanProperty' => true];
363: }
364: if (count($targetEntities) === 0) {
365: return;
366: }
367:
368: $foreignKey = (array)$this->getForeignKey();
369: $target = $this->getTarget();
370: $targetPrimaryKey = array_merge((array)$target->getPrimaryKey(), $foreignKey);
371: $property = $this->getProperty();
372:
373: $conditions = [
374: 'OR' => (new Collection($targetEntities))
375: ->map(function ($entity) use ($targetPrimaryKey) {
376: /** @var \Cake\Datasource\EntityInterface $entity */
377: return $entity->extract($targetPrimaryKey);
378: })
379: ->toList()
380: ];
381:
382: $this->_unlink($foreignKey, $target, $conditions, $options);
383:
384: $result = $sourceEntity->get($property);
385: if ($options['cleanProperty'] && $result !== null) {
386: $sourceEntity->set(
387: $property,
388: (new Collection($sourceEntity->get($property)))
389: ->reject(
390: function ($assoc) use ($targetEntities) {
391: return in_array($assoc, $targetEntities);
392: }
393: )
394: ->toList()
395: );
396: }
397:
398: $sourceEntity->setDirty($property, false);
399: }
400:
401: /**
402: * Replaces existing association links between the source entity and the target
403: * with the ones passed. This method does a smart cleanup, links that are already
404: * persisted and present in `$targetEntities` will not be deleted, new links will
405: * be created for the passed target entities that are not already in the database
406: * and the rest will be removed.
407: *
408: * For example, if an author has many articles, such as 'article1','article 2' and 'article 3' and you pass
409: * to this method an array containing the entities for articles 'article 1' and 'article 4',
410: * only the link for 'article 1' will be kept in database, the links for 'article 2' and 'article 3' will be
411: * deleted and the link for 'article 4' will be created.
412: *
413: * Existing links are not deleted and created again, they are either left untouched
414: * or updated.
415: *
416: * This method does not check link uniqueness.
417: *
418: * On success, the passed `$sourceEntity` will contain `$targetEntities` as value
419: * in the corresponding property for this association.
420: *
421: * Additional options for new links to be saved can be passed in the third argument,
422: * check `Table::save()` for information on the accepted options.
423: *
424: * ### Example:
425: *
426: * ```
427: * $author->articles = [$article1, $article2, $article3, $article4];
428: * $authors->save($author);
429: * $articles = [$article1, $article3];
430: * $authors->getAssociation('articles')->replace($author, $articles);
431: * ```
432: *
433: * `$author->get('articles')` will contain only `[$article1, $article3]` at the end
434: *
435: * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
436: * this association
437: * @param array $targetEntities list of entities from the target table to be linked
438: * @param array $options list of options to be passed to the internal `save`/`delete` calls
439: * when persisting/updating new links, or deleting existing ones
440: * @throws \InvalidArgumentException if non persisted entities are passed or if
441: * any of them is lacking a primary key value
442: * @return bool success
443: */
444: public function replace(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
445: {
446: $property = $this->getProperty();
447: $sourceEntity->set($property, $targetEntities);
448: $saveStrategy = $this->getSaveStrategy();
449: $this->setSaveStrategy(self::SAVE_REPLACE);
450: $result = $this->saveAssociated($sourceEntity, $options);
451: $ok = ($result instanceof EntityInterface);
452:
453: if ($ok) {
454: $sourceEntity = $result;
455: }
456: $this->setSaveStrategy($saveStrategy);
457:
458: return $ok;
459: }
460:
461: /**
462: * Deletes/sets null the related objects according to the dependency between source and targets and foreign key nullability
463: * Skips deleting records present in $remainingEntities
464: *
465: * @param array $foreignKeyReference The foreign key reference defining the link between the
466: * target entity, and the parent entity.
467: * @param \Cake\Datasource\EntityInterface $entity the entity which should have its associated entities unassigned
468: * @param \Cake\ORM\Table $target The associated table
469: * @param array $remainingEntities Entities that should not be deleted
470: * @param array $options list of options accepted by `Table::delete()`
471: * @return bool success
472: */
473: protected function _unlinkAssociated(array $foreignKeyReference, EntityInterface $entity, Table $target, array $remainingEntities = [], array $options = [])
474: {
475: $primaryKey = (array)$target->getPrimaryKey();
476: $exclusions = new Collection($remainingEntities);
477: $exclusions = $exclusions->map(
478: function ($ent) use ($primaryKey) {
479: /** @var \Cake\Datasource\EntityInterface $ent */
480: return $ent->extract($primaryKey);
481: }
482: )
483: ->filter(
484: function ($v) {
485: return !in_array(null, $v, true);
486: }
487: )
488: ->toList();
489:
490: $conditions = $foreignKeyReference;
491:
492: if (count($exclusions) > 0) {
493: $conditions = [
494: 'NOT' => [
495: 'OR' => $exclusions
496: ],
497: $foreignKeyReference
498: ];
499: }
500:
501: return $this->_unlink(array_keys($foreignKeyReference), $target, $conditions, $options);
502: }
503:
504: /**
505: * Deletes/sets null the related objects matching $conditions.
506: * The action which is taken depends on the dependency between source and targets and also on foreign key nullability
507: *
508: * @param array $foreignKey array of foreign key properties
509: * @param \Cake\ORM\Table $target The associated table
510: * @param array $conditions The conditions that specifies what are the objects to be unlinked
511: * @param array $options list of options accepted by `Table::delete()`
512: * @return bool success
513: */
514: protected function _unlink(array $foreignKey, Table $target, array $conditions = [], array $options = [])
515: {
516: $mustBeDependent = (!$this->_foreignKeyAcceptsNull($target, $foreignKey) || $this->getDependent());
517:
518: if ($mustBeDependent) {
519: if ($this->_cascadeCallbacks) {
520: $conditions = new QueryExpression($conditions);
521: $conditions->traverse(function ($entry) use ($target) {
522: if ($entry instanceof FieldInterface) {
523: $entry->setField($target->aliasField($entry->getField()));
524: }
525: });
526: $query = $this->find('all')->where($conditions);
527: $ok = true;
528: foreach ($query as $assoc) {
529: $ok = $ok && $target->delete($assoc, $options);
530: }
531:
532: return $ok;
533: }
534:
535: $conditions = array_merge($conditions, $this->getConditions());
536: $target->deleteAll($conditions);
537:
538: return true;
539: }
540:
541: $updateFields = array_fill_keys($foreignKey, null);
542: $conditions = array_merge($conditions, $this->getConditions());
543: $target->updateAll($updateFields, $conditions);
544:
545: return true;
546: }
547:
548: /**
549: * Checks the nullable flag of the foreign key
550: *
551: * @param \Cake\ORM\Table $table the table containing the foreign key
552: * @param array $properties the list of fields that compose the foreign key
553: * @return bool
554: */
555: protected function _foreignKeyAcceptsNull(Table $table, array $properties)
556: {
557: return !in_array(
558: false,
559: array_map(
560: function ($prop) use ($table) {
561: return $table->getSchema()->isNullable($prop);
562: },
563: $properties
564: )
565: );
566: }
567:
568: /**
569: * Get the relationship type.
570: *
571: * @return string
572: */
573: public function type()
574: {
575: return self::ONE_TO_MANY;
576: }
577:
578: /**
579: * Whether this association can be expressed directly in a query join
580: *
581: * @param array $options custom options key that could alter the return value
582: * @return bool if the 'matching' key in $option is true then this function
583: * will return true, false otherwise
584: */
585: public function canBeJoined(array $options = [])
586: {
587: return !empty($options['matching']);
588: }
589:
590: /**
591: * Gets the name of the field representing the foreign key to the source table.
592: *
593: * @return string
594: */
595: public function getForeignKey()
596: {
597: if ($this->_foreignKey === null) {
598: $this->_foreignKey = $this->_modelKey($this->getSource()->getTable());
599: }
600:
601: return $this->_foreignKey;
602: }
603:
604: /**
605: * Sets the sort order in which target records should be returned.
606: *
607: * @param mixed $sort A find() compatible order clause
608: * @return $this
609: */
610: public function setSort($sort)
611: {
612: $this->_sort = $sort;
613:
614: return $this;
615: }
616:
617: /**
618: * Gets the sort order in which target records should be returned.
619: *
620: * @return mixed
621: */
622: public function getSort()
623: {
624: return $this->_sort;
625: }
626:
627: /**
628: * Sets the sort order in which target records should be returned.
629: * If no arguments are passed the currently configured value is returned
630: *
631: * @deprecated 3.4.0 Use setSort()/getSort() instead.
632: * @param mixed $sort A find() compatible order clause
633: * @return mixed
634: */
635: public function sort($sort = null)
636: {
637: deprecationWarning(
638: 'HasMany::sort() is deprecated. ' .
639: 'Use setSort()/getSort() instead.'
640: );
641: if ($sort !== null) {
642: $this->setSort($sort);
643: }
644:
645: return $this->getSort();
646: }
647:
648: /**
649: * {@inheritDoc}
650: */
651: public function defaultRowValue($row, $joined)
652: {
653: $sourceAlias = $this->getSource()->getAlias();
654: if (isset($row[$sourceAlias])) {
655: $row[$sourceAlias][$this->getProperty()] = $joined ? null : [];
656: }
657:
658: return $row;
659: }
660:
661: /**
662: * Parse extra options passed in the constructor.
663: *
664: * @param array $opts original list of options passed in constructor
665: * @return void
666: */
667: protected function _options(array $opts)
668: {
669: if (!empty($opts['saveStrategy'])) {
670: $this->setSaveStrategy($opts['saveStrategy']);
671: }
672: if (isset($opts['sort'])) {
673: $this->setSort($opts['sort']);
674: }
675: }
676:
677: /**
678: * {@inheritDoc}
679: *
680: * @return \Closure
681: */
682: public function eagerLoader(array $options)
683: {
684: $loader = new SelectLoader([
685: 'alias' => $this->getAlias(),
686: 'sourceAlias' => $this->getSource()->getAlias(),
687: 'targetAlias' => $this->getTarget()->getAlias(),
688: 'foreignKey' => $this->getForeignKey(),
689: 'bindingKey' => $this->getBindingKey(),
690: 'strategy' => $this->getStrategy(),
691: 'associationType' => $this->type(),
692: 'sort' => $this->getSort(),
693: 'finder' => [$this, 'find']
694: ]);
695:
696: return $loader->buildEagerLoader($options);
697: }
698:
699: /**
700: * {@inheritDoc}
701: */
702: public function cascadeDelete(EntityInterface $entity, array $options = [])
703: {
704: $helper = new DependentDeleteHelper();
705:
706: return $helper->cascadeDelete($this, $entity, $options);
707: }
708: }
709: