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 Cake\Collection\Collection;
18: use Cake\Core\App;
19: use Cake\Core\ConventionsTrait;
20: use Cake\Database\Expression\IdentifierExpression;
21: use Cake\Datasource\EntityInterface;
22: use Cake\Datasource\QueryInterface;
23: use Cake\Datasource\ResultSetDecorator;
24: use Cake\ORM\Locator\LocatorAwareTrait;
25: use Cake\Utility\Inflector;
26: use InvalidArgumentException;
27: use RuntimeException;
28:
29: /**
30: * An Association is a relationship established between two tables and is used
31: * to configure and customize the way interconnected records are retrieved.
32: *
33: * @mixin \Cake\ORM\Table
34: */
35: abstract class Association
36: {
37: use ConventionsTrait;
38: use LocatorAwareTrait;
39:
40: /**
41: * Strategy name to use joins for fetching associated records
42: *
43: * @var string
44: */
45: const STRATEGY_JOIN = 'join';
46:
47: /**
48: * Strategy name to use a subquery for fetching associated records
49: *
50: * @var string
51: */
52: const STRATEGY_SUBQUERY = 'subquery';
53:
54: /**
55: * Strategy name to use a select for fetching associated records
56: *
57: * @var string
58: */
59: const STRATEGY_SELECT = 'select';
60:
61: /**
62: * Association type for one to one associations.
63: *
64: * @var string
65: */
66: const ONE_TO_ONE = 'oneToOne';
67:
68: /**
69: * Association type for one to many associations.
70: *
71: * @var string
72: */
73: const ONE_TO_MANY = 'oneToMany';
74:
75: /**
76: * Association type for many to many associations.
77: *
78: * @var string
79: */
80: const MANY_TO_MANY = 'manyToMany';
81:
82: /**
83: * Association type for many to one associations.
84: *
85: * @var string
86: */
87: const MANY_TO_ONE = 'manyToOne';
88:
89: /**
90: * Name given to the association, it usually represents the alias
91: * assigned to the target associated table
92: *
93: * @var string
94: */
95: protected $_name;
96:
97: /**
98: * The class name of the target table object
99: *
100: * @var string
101: */
102: protected $_className;
103:
104: /**
105: * The field name in the owning side table that is used to match with the foreignKey
106: *
107: * @var string|string[]
108: */
109: protected $_bindingKey;
110:
111: /**
112: * The name of the field representing the foreign key to the table to load
113: *
114: * @var string|string[]
115: */
116: protected $_foreignKey;
117:
118: /**
119: * A list of conditions to be always included when fetching records from
120: * the target association
121: *
122: * @var array|callable
123: */
124: protected $_conditions = [];
125:
126: /**
127: * Whether the records on the target table are dependent on the source table,
128: * often used to indicate that records should be removed if the owning record in
129: * the source table is deleted.
130: *
131: * @var bool
132: */
133: protected $_dependent = false;
134:
135: /**
136: * Whether or not cascaded deletes should also fire callbacks.
137: *
138: * @var bool
139: */
140: protected $_cascadeCallbacks = false;
141:
142: /**
143: * Source table instance
144: *
145: * @var \Cake\ORM\Table
146: */
147: protected $_sourceTable;
148:
149: /**
150: * Target table instance
151: *
152: * @var \Cake\ORM\Table
153: */
154: protected $_targetTable;
155:
156: /**
157: * The type of join to be used when adding the association to a query
158: *
159: * @var string
160: */
161: protected $_joinType = QueryInterface::JOIN_TYPE_LEFT;
162:
163: /**
164: * The property name that should be filled with data from the target table
165: * in the source table record.
166: *
167: * @var string
168: */
169: protected $_propertyName;
170:
171: /**
172: * The strategy name to be used to fetch associated records. Some association
173: * types might not implement but one strategy to fetch records.
174: *
175: * @var string
176: */
177: protected $_strategy = self::STRATEGY_JOIN;
178:
179: /**
180: * The default finder name to use for fetching rows from the target table
181: * With array value, finder name and default options are allowed.
182: *
183: * @var string|array
184: */
185: protected $_finder = 'all';
186:
187: /**
188: * Valid strategies for this association. Subclasses can narrow this down.
189: *
190: * @var array
191: */
192: protected $_validStrategies = [
193: self::STRATEGY_JOIN,
194: self::STRATEGY_SELECT,
195: self::STRATEGY_SUBQUERY
196: ];
197:
198: /**
199: * Constructor. Subclasses can override _options function to get the original
200: * list of passed options if expecting any other special key
201: *
202: * @param string $alias The name given to the association
203: * @param array $options A list of properties to be set on this object
204: */
205: public function __construct($alias, array $options = [])
206: {
207: $defaults = [
208: 'cascadeCallbacks',
209: 'className',
210: 'conditions',
211: 'dependent',
212: 'finder',
213: 'bindingKey',
214: 'foreignKey',
215: 'joinType',
216: 'tableLocator',
217: 'propertyName',
218: 'sourceTable',
219: 'targetTable'
220: ];
221: foreach ($defaults as $property) {
222: if (isset($options[$property])) {
223: $this->{'_' . $property} = $options[$property];
224: }
225: }
226:
227: if (empty($this->_className) && strpos($alias, '.')) {
228: $this->_className = $alias;
229: }
230:
231: list(, $name) = pluginSplit($alias);
232: $this->_name = $name;
233:
234: $this->_options($options);
235:
236: if (!empty($options['strategy'])) {
237: $this->setStrategy($options['strategy']);
238: }
239: }
240:
241: /**
242: * Sets the name for this association, usually the alias
243: * assigned to the target associated table
244: *
245: * @param string $name Name to be assigned
246: * @return $this
247: */
248: public function setName($name)
249: {
250: if ($this->_targetTable !== null) {
251: $alias = $this->_targetTable->getAlias();
252: if ($alias !== $name) {
253: throw new InvalidArgumentException('Association name does not match target table alias.');
254: }
255: }
256:
257: $this->_name = $name;
258:
259: return $this;
260: }
261:
262: /**
263: * Gets the name for this association, usually the alias
264: * assigned to the target associated table
265: *
266: * @return string
267: */
268: public function getName()
269: {
270: return $this->_name;
271: }
272:
273: /**
274: * Sets the name for this association.
275: *
276: * @deprecated 3.4.0 Use setName()/getName() instead.
277: * @param string|null $name Name to be assigned
278: * @return string
279: */
280: public function name($name = null)
281: {
282: deprecationWarning(
283: get_called_class() . '::name() is deprecated. ' .
284: 'Use setName()/getName() instead.'
285: );
286: if ($name !== null) {
287: $this->setName($name);
288: }
289:
290: return $this->getName();
291: }
292:
293: /**
294: * Sets whether or not cascaded deletes should also fire callbacks.
295: *
296: * @param bool $cascadeCallbacks cascade callbacks switch value
297: * @return $this
298: */
299: public function setCascadeCallbacks($cascadeCallbacks)
300: {
301: $this->_cascadeCallbacks = $cascadeCallbacks;
302:
303: return $this;
304: }
305:
306: /**
307: * Gets whether or not cascaded deletes should also fire callbacks.
308: *
309: * @return bool
310: */
311: public function getCascadeCallbacks()
312: {
313: return $this->_cascadeCallbacks;
314: }
315:
316: /**
317: * Sets whether or not cascaded deletes should also fire callbacks. If no
318: * arguments are passed, the current configured value is returned
319: *
320: * @deprecated 3.4.0 Use setCascadeCallbacks()/getCascadeCallbacks() instead.
321: * @param bool|null $cascadeCallbacks cascade callbacks switch value
322: * @return bool
323: */
324: public function cascadeCallbacks($cascadeCallbacks = null)
325: {
326: deprecationWarning(
327: get_called_class() . '::cascadeCallbacks() is deprecated. ' .
328: 'Use setCascadeCallbacks()/getCascadeCallbacks() instead.'
329: );
330: if ($cascadeCallbacks !== null) {
331: $this->setCascadeCallbacks($cascadeCallbacks);
332: }
333:
334: return $this->getCascadeCallbacks();
335: }
336:
337: /**
338: * Sets the class name of the target table object.
339: *
340: * @param string $className Class name to set.
341: * @return $this
342: * @throws \InvalidArgumentException In case the class name is set after the target table has been
343: * resolved, and it doesn't match the target table's class name.
344: */
345: public function setClassName($className)
346: {
347: if ($this->_targetTable !== null &&
348: get_class($this->_targetTable) !== App::className($className, 'Model/Table', 'Table')
349: ) {
350: throw new InvalidArgumentException(
351: 'The class name doesn\'t match the target table\'s class name.'
352: );
353: }
354:
355: $this->_className = $className;
356:
357: return $this;
358: }
359:
360: /**
361: * Gets the class name of the target table object.
362: *
363: * @return string
364: */
365: public function getClassName()
366: {
367: return $this->_className;
368: }
369:
370: /**
371: * The class name of the target table object
372: *
373: * @deprecated 3.7.0 Use getClassName() instead.
374: * @return string
375: */
376: public function className()
377: {
378: deprecationWarning(
379: get_called_class() . '::className() is deprecated. ' .
380: 'Use getClassName() instead.'
381: );
382:
383: return $this->getClassName();
384: }
385:
386: /**
387: * Sets the table instance for the source side of the association.
388: *
389: * @param \Cake\ORM\Table $table the instance to be assigned as source side
390: * @return $this
391: */
392: public function setSource(Table $table)
393: {
394: $this->_sourceTable = $table;
395:
396: return $this;
397: }
398:
399: /**
400: * Gets the table instance for the source side of the association.
401: *
402: * @return \Cake\ORM\Table
403: */
404: public function getSource()
405: {
406: return $this->_sourceTable;
407: }
408:
409: /**
410: * Sets the table instance for the source side of the association. If no arguments
411: * are passed, the current configured table instance is returned
412: *
413: * @deprecated 3.4.0 Use setSource()/getSource() instead.
414: * @param \Cake\ORM\Table|null $table the instance to be assigned as source side
415: * @return \Cake\ORM\Table
416: */
417: public function source(Table $table = null)
418: {
419: deprecationWarning(
420: get_called_class() . '::source() is deprecated. ' .
421: 'Use setSource()/getSource() instead.'
422: );
423: if ($table === null) {
424: return $this->_sourceTable;
425: }
426:
427: return $this->_sourceTable = $table;
428: }
429:
430: /**
431: * Sets the table instance for the target side of the association.
432: *
433: * @param \Cake\ORM\Table $table the instance to be assigned as target side
434: * @return $this
435: */
436: public function setTarget(Table $table)
437: {
438: $this->_targetTable = $table;
439:
440: return $this;
441: }
442:
443: /**
444: * Gets the table instance for the target side of the association.
445: *
446: * @return \Cake\ORM\Table
447: */
448: public function getTarget()
449: {
450: if (!$this->_targetTable) {
451: if (strpos($this->_className, '.')) {
452: list($plugin) = pluginSplit($this->_className, true);
453: $registryAlias = $plugin . $this->_name;
454: } else {
455: $registryAlias = $this->_name;
456: }
457:
458: $tableLocator = $this->getTableLocator();
459:
460: $config = [];
461: $exists = $tableLocator->exists($registryAlias);
462: if (!$exists) {
463: $config = ['className' => $this->_className];
464: }
465: $this->_targetTable = $tableLocator->get($registryAlias, $config);
466:
467: if ($exists) {
468: $className = $this->_getClassName($registryAlias, ['className' => $this->_className]);
469:
470: if (!$this->_targetTable instanceof $className) {
471: $errorMessage = '%s association "%s" of type "%s" to "%s" doesn\'t match the expected class "%s". ';
472: $errorMessage .= 'You can\'t have an association of the same name with a different target "className" option anywhere in your app.';
473:
474: throw new RuntimeException(sprintf(
475: $errorMessage,
476: $this->_sourceTable ? get_class($this->_sourceTable) : 'null',
477: $this->getName(),
478: $this->type(),
479: $this->_targetTable ? get_class($this->_targetTable) : 'null',
480: $className
481: ));
482: }
483: }
484: }
485:
486: return $this->_targetTable;
487: }
488:
489: /**
490: * Sets the table instance for the target side of the association. If no arguments
491: * are passed, the current configured table instance is returned
492: *
493: * @deprecated 3.4.0 Use setTarget()/getTarget() instead.
494: * @param \Cake\ORM\Table|null $table the instance to be assigned as target side
495: * @return \Cake\ORM\Table
496: */
497: public function target(Table $table = null)
498: {
499: deprecationWarning(
500: get_called_class() . '::target() is deprecated. ' .
501: 'Use setTarget()/getTarget() instead.'
502: );
503: if ($table !== null) {
504: $this->setTarget($table);
505: }
506:
507: return $this->getTarget();
508: }
509:
510: /**
511: * Sets a list of conditions to be always included when fetching records from
512: * the target association.
513: *
514: * @param array|callable $conditions list of conditions to be used
515: * @see \Cake\Database\Query::where() for examples on the format of the array
516: * @return $this
517: */
518: public function setConditions($conditions)
519: {
520: $this->_conditions = $conditions;
521:
522: return $this;
523: }
524:
525: /**
526: * Gets a list of conditions to be always included when fetching records from
527: * the target association.
528: *
529: * @see \Cake\Database\Query::where() for examples on the format of the array
530: * @return array|callable
531: */
532: public function getConditions()
533: {
534: return $this->_conditions;
535: }
536:
537: /**
538: * Sets a list of conditions to be always included when fetching records from
539: * the target association. If no parameters are passed the current list is returned
540: *
541: * @deprecated 3.4.0 Use setConditions()/getConditions() instead.
542: * @param array|null $conditions list of conditions to be used
543: * @see \Cake\Database\Query::where() for examples on the format of the array
544: * @return array|callable
545: */
546: public function conditions($conditions = null)
547: {
548: deprecationWarning(
549: get_called_class() . '::conditions() is deprecated. ' .
550: 'Use setConditions()/getConditions() instead.'
551: );
552: if ($conditions !== null) {
553: $this->setConditions($conditions);
554: }
555:
556: return $this->getConditions();
557: }
558:
559: /**
560: * Sets the name of the field representing the binding field with the target table.
561: * When not manually specified the primary key of the owning side table is used.
562: *
563: * @param string|string[] $key the table field or fields to be used to link both tables together
564: * @return $this
565: */
566: public function setBindingKey($key)
567: {
568: $this->_bindingKey = $key;
569:
570: return $this;
571: }
572:
573: /**
574: * Gets the name of the field representing the binding field with the target table.
575: * When not manually specified the primary key of the owning side table is used.
576: *
577: * @return string|string[]
578: */
579: public function getBindingKey()
580: {
581: if ($this->_bindingKey === null) {
582: $this->_bindingKey = $this->isOwningSide($this->getSource()) ?
583: $this->getSource()->getPrimaryKey() :
584: $this->getTarget()->getPrimaryKey();
585: }
586:
587: return $this->_bindingKey;
588: }
589:
590: /**
591: * Sets the name of the field representing the binding field with the target table.
592: * When not manually specified the primary key of the owning side table is used.
593: *
594: * If no parameters are passed the current field is returned
595: *
596: * @deprecated 3.4.0 Use setBindingKey()/getBindingKey() instead.
597: * @param string|null $key the table field to be used to link both tables together
598: * @return string|array
599: */
600: public function bindingKey($key = null)
601: {
602: deprecationWarning(
603: get_called_class() . '::bindingKey() is deprecated. ' .
604: 'Use setBindingKey()/getBindingKey() instead.'
605: );
606: if ($key !== null) {
607: $this->setBindingKey($key);
608: }
609:
610: return $this->getBindingKey();
611: }
612:
613: /**
614: * Gets the name of the field representing the foreign key to the target table.
615: *
616: * @return string|string[]
617: */
618: public function getForeignKey()
619: {
620: return $this->_foreignKey;
621: }
622:
623: /**
624: * Sets the name of the field representing the foreign key to the target table.
625: *
626: * @param string|string[] $key the key or keys to be used to link both tables together
627: * @return $this
628: */
629: public function setForeignKey($key)
630: {
631: $this->_foreignKey = $key;
632:
633: return $this;
634: }
635:
636: /**
637: * Sets the name of the field representing the foreign key to the target table.
638: * If no parameters are passed the current field is returned
639: *
640: * @deprecated 3.4.0 Use setForeignKey()/getForeignKey() instead.
641: * @param string|null $key the key to be used to link both tables together
642: * @return string|array
643: */
644: public function foreignKey($key = null)
645: {
646: deprecationWarning(
647: get_called_class() . '::foreignKey() is deprecated. ' .
648: 'Use setForeignKey()/getForeignKey() instead.'
649: );
650: if ($key !== null) {
651: $this->setForeignKey($key);
652: }
653:
654: return $this->getForeignKey();
655: }
656:
657: /**
658: * Sets whether the records on the target table are dependent on the source table.
659: *
660: * This is primarily used to indicate that records should be removed if the owning record in
661: * the source table is deleted.
662: *
663: * If no parameters are passed the current setting is returned.
664: *
665: * @param bool $dependent Set the dependent mode. Use null to read the current state.
666: * @return $this
667: */
668: public function setDependent($dependent)
669: {
670: $this->_dependent = $dependent;
671:
672: return $this;
673: }
674:
675: /**
676: * Sets whether the records on the target table are dependent on the source table.
677: *
678: * This is primarily used to indicate that records should be removed if the owning record in
679: * the source table is deleted.
680: *
681: * @return bool
682: */
683: public function getDependent()
684: {
685: return $this->_dependent;
686: }
687:
688: /**
689: * Sets whether the records on the target table are dependent on the source table.
690: *
691: * This is primarily used to indicate that records should be removed if the owning record in
692: * the source table is deleted.
693: *
694: * If no parameters are passed the current setting is returned.
695: *
696: * @deprecated 3.4.0 Use setDependent()/getDependent() instead.
697: * @param bool|null $dependent Set the dependent mode. Use null to read the current state.
698: * @return bool
699: */
700: public function dependent($dependent = null)
701: {
702: deprecationWarning(
703: get_called_class() . '::dependent() is deprecated. ' .
704: 'Use setDependent()/getDependent() instead.'
705: );
706: if ($dependent !== null) {
707: $this->setDependent($dependent);
708: }
709:
710: return $this->getDependent();
711: }
712:
713: /**
714: * Whether this association can be expressed directly in a query join
715: *
716: * @param array $options custom options key that could alter the return value
717: * @return bool
718: */
719: public function canBeJoined(array $options = [])
720: {
721: $strategy = isset($options['strategy']) ? $options['strategy'] : $this->getStrategy();
722:
723: return $strategy == $this::STRATEGY_JOIN;
724: }
725:
726: /**
727: * Sets the type of join to be used when adding the association to a query.
728: *
729: * @param string $type the join type to be used (e.g. INNER)
730: * @return $this
731: */
732: public function setJoinType($type)
733: {
734: $this->_joinType = $type;
735:
736: return $this;
737: }
738:
739: /**
740: * Gets the type of join to be used when adding the association to a query.
741: *
742: * @return string
743: */
744: public function getJoinType()
745: {
746: return $this->_joinType;
747: }
748:
749: /**
750: * Sets the type of join to be used when adding the association to a query.
751: * If no arguments are passed, the currently configured type is returned.
752: *
753: * @deprecated 3.4.0 Use setJoinType()/getJoinType() instead.
754: * @param string|null $type the join type to be used (e.g. INNER)
755: * @return string
756: */
757: public function joinType($type = null)
758: {
759: deprecationWarning(
760: get_called_class() . '::joinType() is deprecated. ' .
761: 'Use setJoinType()/getJoinType() instead.'
762: );
763: if ($type !== null) {
764: $this->setJoinType($type);
765: }
766:
767: return $this->getJoinType();
768: }
769:
770: /**
771: * Sets the property name that should be filled with data from the target table
772: * in the source table record.
773: *
774: * @param string $name The name of the association property. Use null to read the current value.
775: * @return $this
776: */
777: public function setProperty($name)
778: {
779: $this->_propertyName = $name;
780:
781: return $this;
782: }
783:
784: /**
785: * Gets the property name that should be filled with data from the target table
786: * in the source table record.
787: *
788: * @return string
789: */
790: public function getProperty()
791: {
792: if (!$this->_propertyName) {
793: $this->_propertyName = $this->_propertyName();
794: if (in_array($this->_propertyName, $this->_sourceTable->getSchema()->columns())) {
795: $msg = 'Association property name "%s" clashes with field of same name of table "%s".' .
796: ' You should explicitly specify the "propertyName" option.';
797: trigger_error(
798: sprintf($msg, $this->_propertyName, $this->_sourceTable->getTable()),
799: E_USER_WARNING
800: );
801: }
802: }
803:
804: return $this->_propertyName;
805: }
806:
807: /**
808: * Sets the property name that should be filled with data from the target table
809: * in the source table record.
810: * If no arguments are passed, the currently configured type is returned.
811: *
812: * @deprecated 3.4.0 Use setProperty()/getProperty() instead.
813: * @param string|null $name The name of the association property. Use null to read the current value.
814: * @return string
815: */
816: public function property($name = null)
817: {
818: deprecationWarning(
819: get_called_class() . '::property() is deprecated. ' .
820: 'Use setProperty()/getProperty() instead.'
821: );
822: if ($name !== null) {
823: $this->setProperty($name);
824: }
825:
826: return $this->getProperty();
827: }
828:
829: /**
830: * Returns default property name based on association name.
831: *
832: * @return string
833: */
834: protected function _propertyName()
835: {
836: list(, $name) = pluginSplit($this->_name);
837:
838: return Inflector::underscore($name);
839: }
840:
841: /**
842: * Sets the strategy name to be used to fetch associated records. Keep in mind
843: * that some association types might not implement but a default strategy,
844: * rendering any changes to this setting void.
845: *
846: * @param string $name The strategy type. Use null to read the current value.
847: * @return $this
848: * @throws \InvalidArgumentException When an invalid strategy is provided.
849: */
850: public function setStrategy($name)
851: {
852: if (!in_array($name, $this->_validStrategies)) {
853: throw new InvalidArgumentException(
854: sprintf('Invalid strategy "%s" was provided', $name)
855: );
856: }
857: $this->_strategy = $name;
858:
859: return $this;
860: }
861:
862: /**
863: * Gets the strategy name to be used to fetch associated records. Keep in mind
864: * that some association types might not implement but a default strategy,
865: * rendering any changes to this setting void.
866: *
867: * @return string
868: */
869: public function getStrategy()
870: {
871: return $this->_strategy;
872: }
873:
874: /**
875: * Sets the strategy name to be used to fetch associated records. Keep in mind
876: * that some association types might not implement but a default strategy,
877: * rendering any changes to this setting void.
878: * If no arguments are passed, the currently configured strategy is returned.
879: *
880: * @deprecated 3.4.0 Use setStrategy()/getStrategy() instead.
881: * @param string|null $name The strategy type. Use null to read the current value.
882: * @return string
883: * @throws \InvalidArgumentException When an invalid strategy is provided.
884: */
885: public function strategy($name = null)
886: {
887: deprecationWarning(
888: get_called_class() . '::strategy() is deprecated. ' .
889: 'Use setStrategy()/getStrategy() instead.'
890: );
891: if ($name !== null) {
892: $this->setStrategy($name);
893: }
894:
895: return $this->getStrategy();
896: }
897:
898: /**
899: * Gets the default finder to use for fetching rows from the target table.
900: *
901: * @return string|array
902: */
903: public function getFinder()
904: {
905: return $this->_finder;
906: }
907:
908: /**
909: * Sets the default finder to use for fetching rows from the target table.
910: *
911: * @param string|array $finder the finder name to use or array of finder name and option.
912: * @return $this
913: */
914: public function setFinder($finder)
915: {
916: $this->_finder = $finder;
917:
918: return $this;
919: }
920:
921: /**
922: * Sets the default finder to use for fetching rows from the target table.
923: * If no parameters are passed, it will return the currently configured
924: * finder name.
925: *
926: * @deprecated 3.4.0 Use setFinder()/getFinder() instead.
927: * @param string|null $finder the finder name to use
928: * @return string|array
929: */
930: public function finder($finder = null)
931: {
932: deprecationWarning(
933: get_called_class() . '::finder() is deprecated. ' .
934: 'Use setFinder()/getFinder() instead.'
935: );
936: if ($finder !== null) {
937: $this->setFinder($finder);
938: }
939:
940: return $this->getFinder();
941: }
942:
943: /**
944: * Override this function to initialize any concrete association class, it will
945: * get passed the original list of options used in the constructor
946: *
947: * @param array $options List of options used for initialization
948: * @return void
949: */
950: protected function _options(array $options)
951: {
952: }
953:
954: /**
955: * Alters a Query object to include the associated target table data in the final
956: * result
957: *
958: * The options array accept the following keys:
959: *
960: * - includeFields: Whether to include target model fields in the result or not
961: * - foreignKey: The name of the field to use as foreign key, if false none
962: * will be used
963: * - conditions: array with a list of conditions to filter the join with, this
964: * will be merged with any conditions originally configured for this association
965: * - fields: a list of fields in the target table to include in the result
966: * - type: The type of join to be used (e.g. INNER)
967: * the records found on this association
968: * - aliasPath: A dot separated string representing the path of association names
969: * followed from the passed query main table to this association.
970: * - propertyPath: A dot separated string representing the path of association
971: * properties to be followed from the passed query main entity to this
972: * association
973: * - joinType: The SQL join type to use in the query.
974: * - negateMatch: Will append a condition to the passed query for excluding matches.
975: * with this association.
976: *
977: * @param \Cake\ORM\Query $query the query to be altered to include the target table data
978: * @param array $options Any extra options or overrides to be taken in account
979: * @return void
980: * @throws \RuntimeException if the query builder passed does not return a query
981: * object
982: */
983: public function attachTo(Query $query, array $options = [])
984: {
985: $target = $this->getTarget();
986: $joinType = empty($options['joinType']) ? $this->getJoinType() : $options['joinType'];
987: $table = $target->getTable();
988:
989: $options += [
990: 'includeFields' => true,
991: 'foreignKey' => $this->getForeignKey(),
992: 'conditions' => [],
993: 'fields' => [],
994: 'type' => $joinType,
995: 'table' => $table,
996: 'finder' => $this->getFinder()
997: ];
998:
999: if (!empty($options['foreignKey'])) {
1000: $joinCondition = $this->_joinCondition($options);
1001: if ($joinCondition) {
1002: $options['conditions'][] = $joinCondition;
1003: }
1004: }
1005:
1006: list($finder, $opts) = $this->_extractFinder($options['finder']);
1007: $dummy = $this
1008: ->find($finder, $opts)
1009: ->eagerLoaded(true);
1010:
1011: if (!empty($options['queryBuilder'])) {
1012: $dummy = $options['queryBuilder']($dummy);
1013: if (!($dummy instanceof Query)) {
1014: throw new RuntimeException(sprintf(
1015: 'Query builder for association "%s" did not return a query',
1016: $this->getName()
1017: ));
1018: }
1019: }
1020:
1021: $dummy->where($options['conditions']);
1022: $this->_dispatchBeforeFind($dummy);
1023:
1024: $joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1];
1025: $options['conditions'] = $dummy->clause('where');
1026: $query->join([$this->_name => array_intersect_key($options, $joinOptions)]);
1027:
1028: $this->_appendFields($query, $dummy, $options);
1029: $this->_formatAssociationResults($query, $dummy, $options);
1030: $this->_bindNewAssociations($query, $dummy, $options);
1031: $this->_appendNotMatching($query, $options);
1032: }
1033:
1034: /**
1035: * Conditionally adds a condition to the passed Query that will make it find
1036: * records where there is no match with this association.
1037: *
1038: * @param \Cake\Datasource\QueryInterface $query The query to modify
1039: * @param array $options Options array containing the `negateMatch` key.
1040: * @return void
1041: */
1042: protected function _appendNotMatching($query, $options)
1043: {
1044: $target = $this->_targetTable;
1045: if (!empty($options['negateMatch'])) {
1046: $primaryKey = $query->aliasFields((array)$target->getPrimaryKey(), $this->_name);
1047: $query->andWhere(function ($exp) use ($primaryKey) {
1048: array_map([$exp, 'isNull'], $primaryKey);
1049:
1050: return $exp;
1051: });
1052: }
1053: }
1054:
1055: /**
1056: * Correctly nests a result row associated values into the correct array keys inside the
1057: * source results.
1058: *
1059: * @param array $row The row to transform
1060: * @param string $nestKey The array key under which the results for this association
1061: * should be found
1062: * @param bool $joined Whether or not the row is a result of a direct join
1063: * with this association
1064: * @param string|null $targetProperty The property name in the source results where the association
1065: * data shuld be nested in. Will use the default one if not provided.
1066: * @return array
1067: */
1068: public function transformRow($row, $nestKey, $joined, $targetProperty = null)
1069: {
1070: $sourceAlias = $this->getSource()->getAlias();
1071: $nestKey = $nestKey ?: $this->_name;
1072: $targetProperty = $targetProperty ?: $this->getProperty();
1073: if (isset($row[$sourceAlias])) {
1074: $row[$sourceAlias][$targetProperty] = $row[$nestKey];
1075: unset($row[$nestKey]);
1076: }
1077:
1078: return $row;
1079: }
1080:
1081: /**
1082: * Returns a modified row after appending a property for this association
1083: * with the default empty value according to whether the association was
1084: * joined or fetched externally.
1085: *
1086: * @param array $row The row to set a default on.
1087: * @param bool $joined Whether or not the row is a result of a direct join
1088: * with this association
1089: * @return array
1090: */
1091: public function defaultRowValue($row, $joined)
1092: {
1093: $sourceAlias = $this->getSource()->getAlias();
1094: if (isset($row[$sourceAlias])) {
1095: $row[$sourceAlias][$this->getProperty()] = null;
1096: }
1097:
1098: return $row;
1099: }
1100:
1101: /**
1102: * Proxies the finding operation to the target table's find method
1103: * and modifies the query accordingly based of this association
1104: * configuration
1105: *
1106: * @param string|array|null $type the type of query to perform, if an array is passed,
1107: * it will be interpreted as the `$options` parameter
1108: * @param array $options The options to for the find
1109: * @see \Cake\ORM\Table::find()
1110: * @return \Cake\ORM\Query
1111: */
1112: public function find($type = null, array $options = [])
1113: {
1114: $type = $type ?: $this->getFinder();
1115: list($type, $opts) = $this->_extractFinder($type);
1116:
1117: return $this->getTarget()
1118: ->find($type, $options + $opts)
1119: ->where($this->getConditions());
1120: }
1121:
1122: /**
1123: * Proxies the operation to the target table's exists method after
1124: * appending the default conditions for this association
1125: *
1126: * @param array|callable|\Cake\Database\ExpressionInterface $conditions The conditions to use
1127: * for checking if any record matches.
1128: * @see \Cake\ORM\Table::exists()
1129: * @return bool
1130: */
1131: public function exists($conditions)
1132: {
1133: if ($this->_conditions) {
1134: $conditions = $this
1135: ->find('all', ['conditions' => $conditions])
1136: ->clause('where');
1137: }
1138:
1139: return $this->getTarget()->exists($conditions);
1140: }
1141:
1142: /**
1143: * Proxies the update operation to the target table's updateAll method
1144: *
1145: * @param array $fields A hash of field => new value.
1146: * @param mixed $conditions Conditions to be used, accepts anything Query::where()
1147: * can take.
1148: * @see \Cake\ORM\Table::updateAll()
1149: * @return int Count Returns the affected rows.
1150: */
1151: public function updateAll($fields, $conditions)
1152: {
1153: $target = $this->getTarget();
1154: $expression = $target->query()
1155: ->where($this->getConditions())
1156: ->where($conditions)
1157: ->clause('where');
1158:
1159: return $target->updateAll($fields, $expression);
1160: }
1161:
1162: /**
1163: * Proxies the delete operation to the target table's deleteAll method
1164: *
1165: * @param mixed $conditions Conditions to be used, accepts anything Query::where()
1166: * can take.
1167: * @return int Returns the number of affected rows.
1168: * @see \Cake\ORM\Table::deleteAll()
1169: */
1170: public function deleteAll($conditions)
1171: {
1172: $target = $this->getTarget();
1173: $expression = $target->query()
1174: ->where($this->getConditions())
1175: ->where($conditions)
1176: ->clause('where');
1177:
1178: return $target->deleteAll($expression);
1179: }
1180:
1181: /**
1182: * Returns true if the eager loading process will require a set of the owning table's
1183: * binding keys in order to use them as a filter in the finder query.
1184: *
1185: * @param array $options The options containing the strategy to be used.
1186: * @return bool true if a list of keys will be required
1187: */
1188: public function requiresKeys(array $options = [])
1189: {
1190: $strategy = isset($options['strategy']) ? $options['strategy'] : $this->getStrategy();
1191:
1192: return $strategy === static::STRATEGY_SELECT;
1193: }
1194:
1195: /**
1196: * Triggers beforeFind on the target table for the query this association is
1197: * attaching to
1198: *
1199: * @param \Cake\ORM\Query $query the query this association is attaching itself to
1200: * @return void
1201: */
1202: protected function _dispatchBeforeFind($query)
1203: {
1204: $query->triggerBeforeFind();
1205: }
1206:
1207: /**
1208: * Helper function used to conditionally append fields to the select clause of
1209: * a query from the fields found in another query object.
1210: *
1211: * @param \Cake\ORM\Query $query the query that will get the fields appended to
1212: * @param \Cake\ORM\Query $surrogate the query having the fields to be copied from
1213: * @param array $options options passed to the method `attachTo`
1214: * @return void
1215: */
1216: protected function _appendFields($query, $surrogate, $options)
1217: {
1218: if ($query->getEagerLoader()->isAutoFieldsEnabled() === false) {
1219: return;
1220: }
1221:
1222: $fields = $surrogate->clause('select') ?: $options['fields'];
1223: $target = $this->_targetTable;
1224: $autoFields = $surrogate->isAutoFieldsEnabled();
1225:
1226: if (empty($fields) && !$autoFields) {
1227: if ($options['includeFields'] && ($fields === null || $fields !== false)) {
1228: $fields = $target->getSchema()->columns();
1229: }
1230: }
1231:
1232: if ($autoFields === true) {
1233: $fields = array_filter((array)$fields);
1234: $fields = array_merge($fields, $target->getSchema()->columns());
1235: }
1236:
1237: if ($fields) {
1238: $query->select($query->aliasFields($fields, $this->_name));
1239: }
1240: $query->addDefaultTypes($target);
1241: }
1242:
1243: /**
1244: * Adds a formatter function to the passed `$query` if the `$surrogate` query
1245: * declares any other formatter. Since the `$surrogate` query correspond to
1246: * the associated target table, the resulting formatter will be the result of
1247: * applying the surrogate formatters to only the property corresponding to
1248: * such table.
1249: *
1250: * @param \Cake\ORM\Query $query the query that will get the formatter applied to
1251: * @param \Cake\ORM\Query $surrogate the query having formatters for the associated
1252: * target table.
1253: * @param array $options options passed to the method `attachTo`
1254: * @return void
1255: */
1256: protected function _formatAssociationResults($query, $surrogate, $options)
1257: {
1258: $formatters = $surrogate->getResultFormatters();
1259:
1260: if (!$formatters || empty($options['propertyPath'])) {
1261: return;
1262: }
1263:
1264: $property = $options['propertyPath'];
1265: $propertyPath = explode('.', $property);
1266: $query->formatResults(function ($results) use ($formatters, $property, $propertyPath) {
1267: $extracted = [];
1268: foreach ($results as $result) {
1269: foreach ($propertyPath as $propertyPathItem) {
1270: if (!isset($result[$propertyPathItem])) {
1271: $result = null;
1272: break;
1273: }
1274: $result = $result[$propertyPathItem];
1275: }
1276: $extracted[] = $result;
1277: }
1278: $extracted = new Collection($extracted);
1279: foreach ($formatters as $callable) {
1280: $extracted = new ResultSetDecorator($callable($extracted));
1281: }
1282:
1283: /* @var \Cake\Collection\CollectionInterface $results */
1284: return $results->insert($property, $extracted);
1285: }, Query::PREPEND);
1286: }
1287:
1288: /**
1289: * Applies all attachable associations to `$query` out of the containments found
1290: * in the `$surrogate` query.
1291: *
1292: * Copies all contained associations from the `$surrogate` query into the
1293: * passed `$query`. Containments are altered so that they respect the associations
1294: * chain from which they originated.
1295: *
1296: * @param \Cake\ORM\Query $query the query that will get the associations attached to
1297: * @param \Cake\ORM\Query $surrogate the query having the containments to be attached
1298: * @param array $options options passed to the method `attachTo`
1299: * @return void
1300: */
1301: protected function _bindNewAssociations($query, $surrogate, $options)
1302: {
1303: $loader = $surrogate->getEagerLoader();
1304: $contain = $loader->getContain();
1305: $matching = $loader->getMatching();
1306:
1307: if (!$contain && !$matching) {
1308: return;
1309: }
1310:
1311: $newContain = [];
1312: foreach ($contain as $alias => $value) {
1313: $newContain[$options['aliasPath'] . '.' . $alias] = $value;
1314: }
1315:
1316: $eagerLoader = $query->getEagerLoader();
1317: if ($newContain) {
1318: $eagerLoader->contain($newContain);
1319: }
1320:
1321: foreach ($matching as $alias => $value) {
1322: $eagerLoader->setMatching(
1323: $options['aliasPath'] . '.' . $alias,
1324: $value['queryBuilder'],
1325: $value
1326: );
1327: }
1328: }
1329:
1330: /**
1331: * Returns a single or multiple conditions to be appended to the generated join
1332: * clause for getting the results on the target table.
1333: *
1334: * @param array $options list of options passed to attachTo method
1335: * @return array
1336: * @throws \RuntimeException if the number of columns in the foreignKey do not
1337: * match the number of columns in the source table primaryKey
1338: */
1339: protected function _joinCondition($options)
1340: {
1341: $conditions = [];
1342: $tAlias = $this->_name;
1343: $sAlias = $this->getSource()->getAlias();
1344: $foreignKey = (array)$options['foreignKey'];
1345: $bindingKey = (array)$this->getBindingKey();
1346:
1347: if (count($foreignKey) !== count($bindingKey)) {
1348: if (empty($bindingKey)) {
1349: $table = $this->getTarget()->getTable();
1350: if ($this->isOwningSide($this->getSource())) {
1351: $table = $this->getSource()->getTable();
1352: }
1353: $msg = 'The "%s" table does not define a primary key, and cannot have join conditions generated.';
1354: throw new RuntimeException(sprintf($msg, $table));
1355: }
1356:
1357: $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
1358: throw new RuntimeException(sprintf(
1359: $msg,
1360: $this->_name,
1361: implode(', ', $foreignKey),
1362: implode(', ', $bindingKey)
1363: ));
1364: }
1365:
1366: foreach ($foreignKey as $k => $f) {
1367: $field = sprintf('%s.%s', $sAlias, $bindingKey[$k]);
1368: $value = new IdentifierExpression(sprintf('%s.%s', $tAlias, $f));
1369: $conditions[$field] = $value;
1370: }
1371:
1372: return $conditions;
1373: }
1374:
1375: /**
1376: * Helper method to infer the requested finder and its options.
1377: *
1378: * Returns the inferred options from the finder $type.
1379: *
1380: * ### Examples:
1381: *
1382: * The following will call the finder 'translations' with the value of the finder as its options:
1383: * $query->contain(['Comments' => ['finder' => ['translations']]]);
1384: * $query->contain(['Comments' => ['finder' => ['translations' => []]]]);
1385: * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]);
1386: *
1387: * @param string|array $finderData The finder name or an array having the name as key
1388: * and options as value.
1389: * @return array
1390: */
1391: protected function _extractFinder($finderData)
1392: {
1393: $finderData = (array)$finderData;
1394:
1395: if (is_numeric(key($finderData))) {
1396: return [current($finderData), []];
1397: }
1398:
1399: return [key($finderData), current($finderData)];
1400: }
1401:
1402: /**
1403: * Gets the table class name.
1404: *
1405: * @param string $alias The alias name you want to get.
1406: * @param array $options Table options array.
1407: * @return string
1408: */
1409: protected function _getClassName($alias, array $options = [])
1410: {
1411: if (empty($options['className'])) {
1412: $options['className'] = Inflector::camelize($alias);
1413: }
1414:
1415: $className = App::className($options['className'], 'Model/Table', 'Table') ?: 'Cake\ORM\Table';
1416:
1417: return ltrim($className, '\\');
1418: }
1419:
1420: /**
1421: * Proxies property retrieval to the target table. This is handy for getting this
1422: * association's associations
1423: *
1424: * @param string $property the property name
1425: * @return \Cake\ORM\Association
1426: * @throws \RuntimeException if no association with such name exists
1427: */
1428: public function __get($property)
1429: {
1430: return $this->getTarget()->{$property};
1431: }
1432:
1433: /**
1434: * Proxies the isset call to the target table. This is handy to check if the
1435: * target table has another association with the passed name
1436: *
1437: * @param string $property the property name
1438: * @return bool true if the property exists
1439: */
1440: public function __isset($property)
1441: {
1442: return isset($this->getTarget()->{$property});
1443: }
1444:
1445: /**
1446: * Proxies method calls to the target table.
1447: *
1448: * @param string $method name of the method to be invoked
1449: * @param array $argument List of arguments passed to the function
1450: * @return mixed
1451: * @throws \BadMethodCallException
1452: */
1453: public function __call($method, $argument)
1454: {
1455: return $this->getTarget()->$method(...$argument);
1456: }
1457:
1458: /**
1459: * Get the relationship type.
1460: *
1461: * @return string Constant of either ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY or MANY_TO_MANY.
1462: */
1463: abstract public function type();
1464:
1465: /**
1466: * Eager loads a list of records in the target table that are related to another
1467: * set of records in the source table. Source records can specified in two ways:
1468: * first one is by passing a Query object setup to find on the source table and
1469: * the other way is by explicitly passing an array of primary key values from
1470: * the source table.
1471: *
1472: * The required way of passing related source records is controlled by "strategy"
1473: * When the subquery strategy is used it will require a query on the source table.
1474: * When using the select strategy, the list of primary keys will be used.
1475: *
1476: * Returns a closure that should be run for each record returned in a specific
1477: * Query. This callable will be responsible for injecting the fields that are
1478: * related to each specific passed row.
1479: *
1480: * Options array accepts the following keys:
1481: *
1482: * - query: Query object setup to find the source table records
1483: * - keys: List of primary key values from the source table
1484: * - foreignKey: The name of the field used to relate both tables
1485: * - conditions: List of conditions to be passed to the query where() method
1486: * - sort: The direction in which the records should be returned
1487: * - fields: List of fields to select from the target table
1488: * - contain: List of related tables to eager load associated to the target table
1489: * - strategy: The name of strategy to use for finding target table records
1490: * - nestKey: The array key under which results will be found when transforming the row
1491: *
1492: * @param array $options The options for eager loading.
1493: * @return \Closure
1494: */
1495: abstract public function eagerLoader(array $options);
1496:
1497: /**
1498: * Handles cascading a delete from an associated model.
1499: *
1500: * Each implementing class should handle the cascaded delete as
1501: * required.
1502: *
1503: * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete.
1504: * @param array $options The options for the original delete.
1505: * @return bool Success
1506: */
1507: abstract public function cascadeDelete(EntityInterface $entity, array $options = []);
1508:
1509: /**
1510: * Returns whether or not the passed table is the owning side for this
1511: * association. This means that rows in the 'target' table would miss important
1512: * or required information if the row in 'source' did not exist.
1513: *
1514: * @param \Cake\ORM\Table $side The potential Table with ownership
1515: * @return bool
1516: */
1517: abstract public function isOwningSide(Table $side);
1518:
1519: /**
1520: * Extract the target's association data our from the passed entity and proxies
1521: * the saving operation to the target table.
1522: *
1523: * @param \Cake\Datasource\EntityInterface $entity the data to be saved
1524: * @param array $options The options for saving associated data.
1525: * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns
1526: * the saved entity
1527: * @see \Cake\ORM\Table::save()
1528: */
1529: abstract public function saveAssociated(EntityInterface $entity, array $options = []);
1530: }
1531: