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