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