1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\ORM;
16:
17: use ArrayIterator;
18: use Cake\Datasource\EntityInterface;
19: use Cake\ORM\Locator\LocatorAwareTrait;
20: use Cake\ORM\Locator\LocatorInterface;
21: use InvalidArgumentException;
22: use IteratorAggregate;
23:
24: /**
25: * A container/collection for association classes.
26: *
27: * Contains methods for managing associations, and
28: * ordering operations around saving and deleting.
29: */
30: class AssociationCollection implements IteratorAggregate
31: {
32: use AssociationsNormalizerTrait;
33: use LocatorAwareTrait;
34:
35: /**
36: * Stored associations
37: *
38: * @var \Cake\ORM\Association[]
39: */
40: protected $_items = [];
41:
42: /**
43: * Constructor.
44: *
45: * Sets the default table locator for associations.
46: * If no locator is provided, the global one will be used.
47: *
48: * @param \Cake\ORM\Locator\LocatorInterface|null $tableLocator Table locator instance.
49: */
50: public function __construct(LocatorInterface $tableLocator = null)
51: {
52: if ($tableLocator !== null) {
53: $this->_tableLocator = $tableLocator;
54: }
55: }
56:
57: /**
58: * Add an association to the collection
59: *
60: * If the alias added contains a `.` the part preceding the `.` will be dropped.
61: * This makes using plugins simpler as the Plugin.Class syntax is frequently used.
62: *
63: * @param string $alias The association alias
64: * @param \Cake\ORM\Association $association The association to add.
65: * @return \Cake\ORM\Association The association object being added.
66: */
67: public function add($alias, Association $association)
68: {
69: list(, $alias) = pluginSplit($alias);
70:
71: return $this->_items[strtolower($alias)] = $association;
72: }
73:
74: /**
75: * Creates and adds the Association object to this collection.
76: *
77: * @param string $className The name of association class.
78: * @param string $associated The alias for the target table.
79: * @param array $options List of options to configure the association definition.
80: * @return \Cake\ORM\Association
81: * @throws \InvalidArgumentException
82: */
83: public function load($className, $associated, array $options = [])
84: {
85: $options += [
86: 'tableLocator' => $this->getTableLocator()
87: ];
88:
89: $association = new $className($associated, $options);
90: if (!$association instanceof Association) {
91: $message = sprintf('The association must extend `%s` class, `%s` given.', Association::class, get_class($association));
92: throw new InvalidArgumentException($message);
93: }
94:
95: return $this->add($association->getName(), $association);
96: }
97:
98: /**
99: * Fetch an attached association by name.
100: *
101: * @param string $alias The association alias to get.
102: * @return \Cake\ORM\Association|null Either the association or null.
103: */
104: public function get($alias)
105: {
106: $alias = strtolower($alias);
107: if (isset($this->_items[$alias])) {
108: return $this->_items[$alias];
109: }
110:
111: return null;
112: }
113:
114: /**
115: * Fetch an association by property name.
116: *
117: * @param string $prop The property to find an association by.
118: * @return \Cake\ORM\Association|null Either the association or null.
119: */
120: public function getByProperty($prop)
121: {
122: foreach ($this->_items as $assoc) {
123: if ($assoc->getProperty() === $prop) {
124: return $assoc;
125: }
126: }
127:
128: return null;
129: }
130:
131: /**
132: * Check for an attached association by name.
133: *
134: * @param string $alias The association alias to get.
135: * @return bool Whether or not the association exists.
136: */
137: public function has($alias)
138: {
139: return isset($this->_items[strtolower($alias)]);
140: }
141:
142: /**
143: * Get the names of all the associations in the collection.
144: *
145: * @return string[]
146: */
147: public function keys()
148: {
149: return array_keys($this->_items);
150: }
151:
152: /**
153: * Get an array of associations matching a specific type.
154: *
155: * @param string|array $class The type of associations you want.
156: * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne']
157: * @return array An array of Association objects.
158: * @deprecated 3.5.3 Use getByType() instead.
159: */
160: public function type($class)
161: {
162: deprecationWarning(
163: 'AssociationCollection::type() is deprecated. ' .
164: 'Use getByType() instead.'
165: );
166:
167: return $this->getByType($class);
168: }
169:
170: /**
171: * Get an array of associations matching a specific type.
172: *
173: * @param string|string[] $class The type of associations you want.
174: * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne']
175: * @return array An array of Association objects.
176: * @since 3.5.3
177: */
178: public function getByType($class)
179: {
180: $class = array_map('strtolower', (array)$class);
181:
182: $out = array_filter($this->_items, function ($assoc) use ($class) {
183: list(, $name) = namespaceSplit(get_class($assoc));
184:
185: return in_array(strtolower($name), $class, true);
186: });
187:
188: return array_values($out);
189: }
190:
191: /**
192: * Drop/remove an association.
193: *
194: * Once removed the association will not longer be reachable
195: *
196: * @param string $alias The alias name.
197: * @return void
198: */
199: public function remove($alias)
200: {
201: unset($this->_items[strtolower($alias)]);
202: }
203:
204: /**
205: * Remove all registered associations.
206: *
207: * Once removed associations will not longer be reachable
208: *
209: * @return void
210: */
211: public function removeAll()
212: {
213: foreach ($this->_items as $alias => $object) {
214: $this->remove($alias);
215: }
216: }
217:
218: /**
219: * Save all the associations that are parents of the given entity.
220: *
221: * Parent associations include any association where the given table
222: * is the owning side.
223: *
224: * @param \Cake\ORM\Table $table The table entity is for.
225: * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for.
226: * @param array $associations The list of associations to save parents from.
227: * associations not in this list will not be saved.
228: * @param array $options The options for the save operation.
229: * @return bool Success
230: */
231: public function saveParents(Table $table, EntityInterface $entity, $associations, array $options = [])
232: {
233: if (empty($associations)) {
234: return true;
235: }
236:
237: return $this->_saveAssociations($table, $entity, $associations, $options, false);
238: }
239:
240: /**
241: * Save all the associations that are children of the given entity.
242: *
243: * Child associations include any association where the given table
244: * is not the owning side.
245: *
246: * @param \Cake\ORM\Table $table The table entity is for.
247: * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for.
248: * @param array $associations The list of associations to save children from.
249: * associations not in this list will not be saved.
250: * @param array $options The options for the save operation.
251: * @return bool Success
252: */
253: public function saveChildren(Table $table, EntityInterface $entity, array $associations, array $options)
254: {
255: if (empty($associations)) {
256: return true;
257: }
258:
259: return $this->_saveAssociations($table, $entity, $associations, $options, true);
260: }
261:
262: /**
263: * Helper method for saving an association's data.
264: *
265: * @param \Cake\ORM\Table $table The table the save is currently operating on
266: * @param \Cake\Datasource\EntityInterface $entity The entity to save
267: * @param array $associations Array of associations to save.
268: * @param array $options Original options
269: * @param bool $owningSide Compared with association classes'
270: * isOwningSide method.
271: * @return bool Success
272: * @throws \InvalidArgumentException When an unknown alias is used.
273: */
274: protected function _saveAssociations($table, $entity, $associations, $options, $owningSide)
275: {
276: unset($options['associated']);
277: foreach ($associations as $alias => $nested) {
278: if (is_int($alias)) {
279: $alias = $nested;
280: $nested = [];
281: }
282: $relation = $this->get($alias);
283: if (!$relation) {
284: $msg = sprintf(
285: 'Cannot save %s, it is not associated to %s',
286: $alias,
287: $table->getAlias()
288: );
289: throw new InvalidArgumentException($msg);
290: }
291: if ($relation->isOwningSide($table) !== $owningSide) {
292: continue;
293: }
294: if (!$this->_save($relation, $entity, $nested, $options)) {
295: return false;
296: }
297: }
298:
299: return true;
300: }
301:
302: /**
303: * Helper method for saving an association's data.
304: *
305: * @param \Cake\ORM\Association $association The association object to save with.
306: * @param \Cake\Datasource\EntityInterface $entity The entity to save
307: * @param array $nested Options for deeper associations
308: * @param array $options Original options
309: * @return bool Success
310: */
311: protected function _save($association, $entity, $nested, $options)
312: {
313: if (!$entity->isDirty($association->getProperty())) {
314: return true;
315: }
316: if (!empty($nested)) {
317: $options = (array)$nested + $options;
318: }
319:
320: return (bool)$association->saveAssociated($entity, $options);
321: }
322:
323: /**
324: * Cascade a delete across the various associations.
325: * Cascade first across associations for which cascadeCallbacks is true.
326: *
327: * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for.
328: * @param array $options The options used in the delete operation.
329: * @return void
330: */
331: public function cascadeDelete(EntityInterface $entity, array $options)
332: {
333: $noCascade = $this->_getNoCascadeItems($entity, $options);
334: foreach ($noCascade as $assoc) {
335: $assoc->cascadeDelete($entity, $options);
336: }
337: }
338:
339: /**
340: * Returns items that have no cascade callback.
341: *
342: * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for.
343: * @param array $options The options used in the delete operation.
344: * @return \Cake\ORM\Association[]
345: */
346: protected function _getNoCascadeItems($entity, $options)
347: {
348: $noCascade = [];
349: foreach ($this->_items as $assoc) {
350: if (!$assoc->getCascadeCallbacks()) {
351: $noCascade[] = $assoc;
352: continue;
353: }
354: $assoc->cascadeDelete($entity, $options);
355: }
356:
357: return $noCascade;
358: }
359:
360: /**
361: * Returns an associative array of association names out a mixed
362: * array. If true is passed, then it returns all association names
363: * in this collection.
364: *
365: * @param bool|array $keys the list of association names to normalize
366: * @return array
367: */
368: public function normalizeKeys($keys)
369: {
370: if ($keys === true) {
371: $keys = $this->keys();
372: }
373:
374: if (empty($keys)) {
375: return [];
376: }
377:
378: return $this->_normalizeAssociations($keys);
379: }
380:
381: /**
382: * Allow looping through the associations
383: *
384: * @return \ArrayIterator
385: */
386: public function getIterator()
387: {
388: return new ArrayIterator($this->_items);
389: }
390: }
391: