Association.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\ORM;
  16. use Cake\Core\ConventionsTrait;
  17. use Cake\Database\Expression\IdentifierExpression;
  18. use Cake\Datasource\EntityInterface;
  19. use Cake\Datasource\ResultSetDecorator;
  20. use Cake\ORM\Query;
  21. use Cake\ORM\Table;
  22. use Cake\ORM\TableRegistry;
  23. use Cake\Utility\Inflector;
  24. use InvalidArgumentException;
  25. use RuntimeException;
  26. /**
  27. * An Association is a relationship established between two tables and is used
  28. * to configure and customize the way interconnected records are retrieved.
  29. *
  30. */
  31. abstract class Association
  32. {
  33. use ConventionsTrait;
  34. /**
  35. * Strategy name to use joins for fetching associated records
  36. *
  37. * @var string
  38. */
  39. const STRATEGY_JOIN = 'join';
  40. /**
  41. * Strategy name to use a subquery for fetching associated records
  42. *
  43. * @var string
  44. */
  45. const STRATEGY_SUBQUERY = 'subquery';
  46. /**
  47. * Strategy name to use a select for fetching associated records
  48. *
  49. * @var string
  50. */
  51. const STRATEGY_SELECT = 'select';
  52. /**
  53. * Association type for one to one associations.
  54. *
  55. * @var string
  56. */
  57. const ONE_TO_ONE = 'oneToOne';
  58. /**
  59. * Association type for one to many associations.
  60. *
  61. * @var string
  62. */
  63. const ONE_TO_MANY = 'oneToMany';
  64. /**
  65. * Association type for many to many associations.
  66. *
  67. * @var string
  68. */
  69. const MANY_TO_MANY = 'manyToMany';
  70. /**
  71. * Association type for many to one associations.
  72. *
  73. * @var string
  74. */
  75. const MANY_TO_ONE = 'manyToOne';
  76. /**
  77. * Name given to the association, it usually represents the alias
  78. * assigned to the target associated table
  79. *
  80. * @var string
  81. */
  82. protected $_name;
  83. /**
  84. * The class name of the target table object
  85. *
  86. * @var string
  87. */
  88. protected $_className;
  89. /**
  90. * The field name in the owning side table that is used to match with the foreignKey
  91. *
  92. * @var string|array
  93. */
  94. protected $_bindingKey;
  95. /**
  96. * The name of the field representing the foreign key to the table to load
  97. *
  98. * @var string|array
  99. */
  100. protected $_foreignKey;
  101. /**
  102. * A list of conditions to be always included when fetching records from
  103. * the target association
  104. *
  105. * @var array
  106. */
  107. protected $_conditions = [];
  108. /**
  109. * Whether the records on the target table are dependent on the source table,
  110. * often used to indicate that records should be removed if the owning record in
  111. * the source table is deleted.
  112. *
  113. * @var bool
  114. */
  115. protected $_dependent = false;
  116. /**
  117. * Whether or not cascaded deletes should also fire callbacks.
  118. *
  119. * @var string
  120. */
  121. protected $_cascadeCallbacks = false;
  122. /**
  123. * Source table instance
  124. *
  125. * @var \Cake\ORM\Table
  126. */
  127. protected $_sourceTable;
  128. /**
  129. * Target table instance
  130. *
  131. * @var \Cake\ORM\Table
  132. */
  133. protected $_targetTable;
  134. /**
  135. * The type of join to be used when adding the association to a query
  136. *
  137. * @var string
  138. */
  139. protected $_joinType = 'LEFT';
  140. /**
  141. * The property name that should be filled with data from the target table
  142. * in the source table record.
  143. *
  144. * @var string
  145. */
  146. protected $_propertyName;
  147. /**
  148. * The strategy name to be used to fetch associated records. Some association
  149. * types might not implement but one strategy to fetch records.
  150. *
  151. * @var string
  152. */
  153. protected $_strategy = self::STRATEGY_JOIN;
  154. /**
  155. * The default finder name to use for fetching rows from the target table
  156. *
  157. * @var string
  158. */
  159. protected $_finder = 'all';
  160. /**
  161. * Valid strategies for this association. Subclasses can narrow this down.
  162. *
  163. * @var array
  164. */
  165. protected $_validStrategies = [self::STRATEGY_JOIN, self::STRATEGY_SELECT, self::STRATEGY_SUBQUERY];
  166. /**
  167. * Constructor. Subclasses can override _options function to get the original
  168. * list of passed options if expecting any other special key
  169. *
  170. * @param string $alias The name given to the association
  171. * @param array $options A list of properties to be set on this object
  172. */
  173. public function __construct($alias, array $options = [])
  174. {
  175. $defaults = [
  176. 'cascadeCallbacks',
  177. 'className',
  178. 'conditions',
  179. 'dependent',
  180. 'finder',
  181. 'bindingKey',
  182. 'foreignKey',
  183. 'joinType',
  184. 'propertyName',
  185. 'sourceTable',
  186. 'targetTable'
  187. ];
  188. foreach ($defaults as $property) {
  189. if (isset($options[$property])) {
  190. $this->{'_' . $property} = $options[$property];
  191. }
  192. }
  193. if (empty($this->_className) && strpos($alias, '.')) {
  194. $this->_className = $alias;
  195. }
  196. list(, $name) = pluginSplit($alias);
  197. $this->_name = $name;
  198. $this->_options($options);
  199. if (!empty($options['strategy'])) {
  200. $this->strategy($options['strategy']);
  201. }
  202. }
  203. /**
  204. * Sets the name for this association. If no argument is passed then the current
  205. * configured name will be returned
  206. *
  207. * @param string|null $name Name to be assigned
  208. * @return string
  209. */
  210. public function name($name = null)
  211. {
  212. if ($name !== null) {
  213. $this->_name = $name;
  214. }
  215. return $this->_name;
  216. }
  217. /**
  218. * Sets whether or not cascaded deletes should also fire callbacks. If no
  219. * arguments are passed, the current configured value is returned
  220. *
  221. * @param bool|null $cascadeCallbacks cascade callbacks switch value
  222. * @return bool
  223. */
  224. public function cascadeCallbacks($cascadeCallbacks = null)
  225. {
  226. if ($cascadeCallbacks !== null) {
  227. $this->_cascadeCallbacks = $cascadeCallbacks;
  228. }
  229. return $this->_cascadeCallbacks;
  230. }
  231. /**
  232. * Sets the table instance for the source side of the association. If no arguments
  233. * are passed, the current configured table instance is returned
  234. *
  235. * @param \Cake\ORM\Table|null $table the instance to be assigned as source side
  236. * @return \Cake\ORM\Table
  237. */
  238. public function source(Table $table = null)
  239. {
  240. if ($table === null) {
  241. return $this->_sourceTable;
  242. }
  243. return $this->_sourceTable = $table;
  244. }
  245. /**
  246. * Sets the table instance for the target side of the association. If no arguments
  247. * are passed, the current configured table instance is returned
  248. *
  249. * @param \Cake\ORM\Table|null $table the instance to be assigned as target side
  250. * @return \Cake\ORM\Table
  251. */
  252. public function target(Table $table = null)
  253. {
  254. if ($table === null && $this->_targetTable) {
  255. return $this->_targetTable;
  256. }
  257. if ($table !== null) {
  258. return $this->_targetTable = $table;
  259. }
  260. if (strpos($this->_className, '.')) {
  261. list($plugin) = pluginSplit($this->_className, true);
  262. $registryAlias = $plugin . $this->_name;
  263. } else {
  264. $registryAlias = $this->_name;
  265. }
  266. $config = [];
  267. if (!TableRegistry::exists($registryAlias)) {
  268. $config = ['className' => $this->_className];
  269. }
  270. $this->_targetTable = TableRegistry::get($registryAlias, $config);
  271. return $this->_targetTable;
  272. }
  273. /**
  274. * Sets a list of conditions to be always included when fetching records from
  275. * the target association. If no parameters are passed the current list is returned
  276. *
  277. * @param array|null $conditions list of conditions to be used
  278. * @see \Cake\Database\Query::where() for examples on the format of the array
  279. * @return array
  280. */
  281. public function conditions($conditions = null)
  282. {
  283. if ($conditions !== null) {
  284. $this->_conditions = $conditions;
  285. }
  286. return $this->_conditions;
  287. }
  288. /**
  289. * Sets the name of the field representing the binding field with the target table.
  290. * When not manually specified the primary key of the owning side table is used.
  291. *
  292. * If no parameters are passed the current field is returned
  293. *
  294. * @param string|null $key the table field to be used to link both tables together
  295. * @return string|array
  296. */
  297. public function bindingKey($key = null)
  298. {
  299. if ($key !== null) {
  300. $this->_bindingKey = $key;
  301. }
  302. if ($this->_bindingKey === null) {
  303. $this->_bindingKey = $this->isOwningSide($this->source()) ?
  304. $this->source()->primaryKey() :
  305. $this->target()->primaryKey();
  306. }
  307. return $this->_bindingKey;
  308. }
  309. /**
  310. * Sets the name of the field representing the foreign key to the target table.
  311. * If no parameters are passed the current field is returned
  312. *
  313. * @param string|null $key the key to be used to link both tables together
  314. * @return string|array
  315. */
  316. public function foreignKey($key = null)
  317. {
  318. if ($key !== null) {
  319. $this->_foreignKey = $key;
  320. }
  321. return $this->_foreignKey;
  322. }
  323. /**
  324. * Sets whether the records on the target table are dependent on the source table.
  325. *
  326. * This is primarily used to indicate that records should be removed if the owning record in
  327. * the source table is deleted.
  328. *
  329. * If no parameters are passed the current setting is returned.
  330. *
  331. * @param bool|null $dependent Set the dependent mode. Use null to read the current state.
  332. * @return bool
  333. */
  334. public function dependent($dependent = null)
  335. {
  336. if ($dependent !== null) {
  337. $this->_dependent = $dependent;
  338. }
  339. return $this->_dependent;
  340. }
  341. /**
  342. * Whether this association can be expressed directly in a query join
  343. *
  344. * @param array $options custom options key that could alter the return value
  345. * @return bool
  346. */
  347. public function canBeJoined(array $options = [])
  348. {
  349. $strategy = isset($options['strategy']) ? $options['strategy'] : $this->strategy();
  350. return $strategy == $this::STRATEGY_JOIN;
  351. }
  352. /**
  353. * Sets the type of join to be used when adding the association to a query.
  354. * If no arguments are passed, the currently configured type is returned.
  355. *
  356. * @param string $type the join type to be used (e.g. INNER)
  357. * @return string
  358. */
  359. public function joinType($type = null)
  360. {
  361. if ($type === null) {
  362. return $this->_joinType;
  363. }
  364. return $this->_joinType = $type;
  365. }
  366. /**
  367. * Sets the property name that should be filled with data from the target table
  368. * in the source table record.
  369. * If no arguments are passed, the currently configured type is returned.
  370. *
  371. * @param string|null $name The name of the association property. Use null to read the current value.
  372. * @return string
  373. */
  374. public function property($name = null)
  375. {
  376. if ($name !== null) {
  377. $this->_propertyName = $name;
  378. }
  379. if ($name === null && !$this->_propertyName) {
  380. list(, $name) = pluginSplit($this->_name);
  381. $this->_propertyName = Inflector::underscore($name);
  382. }
  383. return $this->_propertyName;
  384. }
  385. /**
  386. * Sets the strategy name to be used to fetch associated records. Keep in mind
  387. * that some association types might not implement but a default strategy,
  388. * rendering any changes to this setting void.
  389. * If no arguments are passed, the currently configured strategy is returned.
  390. *
  391. * @param string|null $name The strategy type. Use null to read the current value.
  392. * @return string
  393. * @throws \InvalidArgumentException When an invalid strategy is provided.
  394. */
  395. public function strategy($name = null)
  396. {
  397. if ($name !== null) {
  398. if (!in_array($name, $this->_validStrategies)) {
  399. throw new InvalidArgumentException(
  400. sprintf('Invalid strategy "%s" was provided', $name)
  401. );
  402. }
  403. $this->_strategy = $name;
  404. }
  405. return $this->_strategy;
  406. }
  407. /**
  408. * Sets the default finder to use for fetching rows from the target table.
  409. * If no parameters are passed, it will return the currently configured
  410. * finder name.
  411. *
  412. * @param string|null $finder the finder name to use
  413. * @return string
  414. */
  415. public function finder($finder = null)
  416. {
  417. if ($finder !== null) {
  418. $this->_finder = $finder;
  419. }
  420. return $this->_finder;
  421. }
  422. /**
  423. * Override this function to initialize any concrete association class, it will
  424. * get passed the original list of options used in the constructor
  425. *
  426. * @param array $options List of options used for initialization
  427. * @return void
  428. */
  429. protected function _options(array $options)
  430. {
  431. }
  432. /**
  433. * Alters a Query object to include the associated target table data in the final
  434. * result
  435. *
  436. * The options array accept the following keys:
  437. *
  438. * - includeFields: Whether to include target model fields in the result or not
  439. * - foreignKey: The name of the field to use as foreign key, if false none
  440. * will be used
  441. * - conditions: array with a list of conditions to filter the join with, this
  442. * will be merged with any conditions originally configured for this association
  443. * - fields: a list of fields in the target table to include in the result
  444. * - type: The type of join to be used (e.g. INNER)
  445. * - matching: Indicates whether the query records should be filtered based on
  446. * the records found on this association. This will force a 'INNER JOIN'
  447. * - aliasPath: A dot separated string representing the path of association names
  448. * followed from the passed query main table to this association.
  449. * - propertyPath: A dot separated string representing the path of association
  450. * properties to be followed from the passed query main entity to this
  451. * association
  452. * - joinType: The SQL join type to use in the query.
  453. *
  454. * @param Query $query the query to be altered to include the target table data
  455. * @param array $options Any extra options or overrides to be taken in account
  456. * @return void
  457. * @throws \RuntimeException if the query builder passed does not return a query
  458. * object
  459. */
  460. public function attachTo(Query $query, array $options = [])
  461. {
  462. $target = $this->target();
  463. $joinType = empty($options['joinType']) ? $this->joinType() : $options['joinType'];
  464. $options += [
  465. 'includeFields' => true,
  466. 'foreignKey' => $this->foreignKey(),
  467. 'conditions' => [],
  468. 'fields' => [],
  469. 'type' => empty($options['matching']) ? $joinType : 'INNER',
  470. 'table' => $target->table(),
  471. 'finder' => $this->finder()
  472. ];
  473. if (!empty($options['foreignKey'])) {
  474. $joinCondition = $this->_joinCondition($options);
  475. if ($joinCondition) {
  476. $options['conditions'][] = $joinCondition;
  477. }
  478. }
  479. list($finder, $opts) = $this->_extractFinder($options['finder']);
  480. $dummy = $this
  481. ->find($finder, $opts)
  482. ->eagerLoaded(true);
  483. if (!empty($options['queryBuilder'])) {
  484. $dummy = $options['queryBuilder']($dummy);
  485. if (!($dummy instanceof Query)) {
  486. throw new RuntimeException(sprintf(
  487. 'Query builder for association "%s" did not return a query',
  488. $this->name()
  489. ));
  490. }
  491. }
  492. $dummy->where($options['conditions']);
  493. $this->_dispatchBeforeFind($dummy);
  494. $joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1];
  495. $options['conditions'] = $dummy->clause('where');
  496. $query->join([$target->alias() => array_intersect_key($options, $joinOptions)]);
  497. $this->_appendFields($query, $dummy, $options);
  498. $this->_formatAssociationResults($query, $dummy, $options);
  499. $this->_bindNewAssociations($query, $dummy, $options);
  500. }
  501. /**
  502. * Correctly nests a result row associated values into the correct array keys inside the
  503. * source results.
  504. *
  505. * @param array $row The row to transform
  506. * @param string $nestKey The array key under which the results for this association
  507. * should be found
  508. * @param bool $joined Whether or not the row is a result of a direct join
  509. * with this association
  510. * @return array
  511. */
  512. public function transformRow($row, $nestKey, $joined)
  513. {
  514. $sourceAlias = $this->source()->alias();
  515. $nestKey = $nestKey ?: $this->_name;
  516. if (isset($row[$sourceAlias])) {
  517. $row[$sourceAlias][$this->property()] = $row[$nestKey];
  518. unset($row[$nestKey]);
  519. }
  520. return $row;
  521. }
  522. /**
  523. * Returns a modified row after appending a property for this association
  524. * with the default empty value according to whether the association was
  525. * joined or fetched externally.
  526. *
  527. * @param array $row The row to set a default on.
  528. * @param bool $joined Whether or not the row is a result of a direct join
  529. * with this association
  530. * @return array
  531. */
  532. public function defaultRowValue($row, $joined)
  533. {
  534. $sourceAlias = $this->source()->alias();
  535. if (isset($row[$sourceAlias])) {
  536. $row[$sourceAlias][$this->property()] = null;
  537. }
  538. return $row;
  539. }
  540. /**
  541. * Proxies the finding operation to the target table's find method
  542. * and modifies the query accordingly based of this association
  543. * configuration
  544. *
  545. * @param string|array $type the type of query to perform, if an array is passed,
  546. * it will be interpreted as the `$options` parameter
  547. * @param array $options The options to for the find
  548. * @see \Cake\ORM\Table::find()
  549. * @return \Cake\ORM\Query
  550. */
  551. public function find($type = null, array $options = [])
  552. {
  553. $type = $type ?: $this->finder();
  554. list($type, $opts) = $this->_extractFinder($type, $options);
  555. return $this->target()
  556. ->find($type, $options + $opts)
  557. ->where($this->conditions());
  558. }
  559. /**
  560. * Proxies the update operation to the target table's updateAll method
  561. *
  562. * @param array $fields A hash of field => new value.
  563. * @param mixed $conditions Conditions to be used, accepts anything Query::where()
  564. * can take.
  565. * @see \Cake\ORM\Table::updateAll()
  566. * @return bool Success Returns true if one or more rows are affected.
  567. */
  568. public function updateAll($fields, $conditions)
  569. {
  570. $target = $this->target();
  571. $expression = $target->query()
  572. ->where($this->conditions())
  573. ->where($conditions)
  574. ->clause('where');
  575. return $target->updateAll($fields, $expression);
  576. }
  577. /**
  578. * Proxies the delete operation to the target table's deleteAll method
  579. *
  580. * @param mixed $conditions Conditions to be used, accepts anything Query::where()
  581. * can take.
  582. * @return bool Success Returns true if one or more rows are affected.
  583. * @see \Cake\ORM\Table::deleteAll()
  584. */
  585. public function deleteAll($conditions)
  586. {
  587. $target = $this->target();
  588. $expression = $target->query()
  589. ->where($this->conditions())
  590. ->where($conditions)
  591. ->clause('where');
  592. return $target->deleteAll($expression);
  593. }
  594. /**
  595. * Triggers beforeFind on the target table for the query this association is
  596. * attaching to
  597. *
  598. * @param \Cake\ORM\Query $query the query this association is attaching itself to
  599. * @return void
  600. */
  601. protected function _dispatchBeforeFind($query)
  602. {
  603. $query->triggerBeforeFind();
  604. }
  605. /**
  606. * Helper function used to conditionally append fields to the select clause of
  607. * a query from the fields found in another query object.
  608. *
  609. * @param \Cake\ORM\Query $query the query that will get the fields appended to
  610. * @param \Cake\ORM\Query $surrogate the query having the fields to be copied from
  611. * @param array $options options passed to the method `attachTo`
  612. * @return void
  613. */
  614. protected function _appendFields($query, $surrogate, $options)
  615. {
  616. $fields = $surrogate->clause('select') ?: $options['fields'];
  617. $target = $this->_targetTable;
  618. $autoFields = $surrogate->autoFields();
  619. if ($query->eagerLoader()->autoFields() === false) {
  620. return;
  621. }
  622. if (empty($fields) && !$autoFields) {
  623. if ($options['includeFields'] && ($fields === null || $fields !== false)) {
  624. $fields = $target->schema()->columns();
  625. }
  626. }
  627. if ($autoFields === true) {
  628. $fields = array_merge((array)$fields, $target->schema()->columns());
  629. }
  630. if (!empty($fields)) {
  631. $query->select($query->aliasFields($fields, $target->alias()));
  632. }
  633. }
  634. /**
  635. * Adds a formatter function to the passed `$query` if the `$surrogate` query
  636. * declares any other formatter. Since the `$surrogate` query correspond to
  637. * the associated target table, the resulting formatter will be the result of
  638. * applying the surrogate formatters to only the property corresponding to
  639. * such table.
  640. *
  641. * @param \Cake\ORM\Query $query the query that will get the formatter applied to
  642. * @param \Cake\ORM\Query $surrogate the query having formatters for the associated
  643. * target table.
  644. * @param array $options options passed to the method `attachTo`
  645. * @return void
  646. */
  647. protected function _formatAssociationResults($query, $surrogate, $options)
  648. {
  649. $formatters = $surrogate->formatResults();
  650. if (!$formatters || empty($options['propertyPath'])) {
  651. return;
  652. }
  653. $property = $options['propertyPath'];
  654. $query->formatResults(function ($results) use ($formatters, $property) {
  655. $extracted = $results->extract($property)->compile();
  656. foreach ($formatters as $callable) {
  657. $extracted = new ResultSetDecorator($callable($extracted));
  658. }
  659. return $results->insert($property, $extracted);
  660. }, Query::PREPEND);
  661. }
  662. /**
  663. * Applies all attachable associations to `$query` out of the containments found
  664. * in the `$surrogate` query.
  665. *
  666. * Copies all contained associations from the `$surrogate` query into the
  667. * passed `$query`. Containments are altered so that they respect the associations
  668. * chain from which they originated.
  669. *
  670. * @param \Cake\ORM\Query $query the query that will get the associations attached to
  671. * @param \Cake\ORM\Query $surrogate the query having the containments to be attached
  672. * @param array $options options passed to the method `attachTo`
  673. * @return void
  674. */
  675. protected function _bindNewAssociations($query, $surrogate, $options)
  676. {
  677. $loader = $surrogate->eagerLoader();
  678. $contain = $loader->contain();
  679. $matching = $loader->matching();
  680. $target = $this->_targetTable;
  681. if (!$contain && !$matching) {
  682. return;
  683. }
  684. $loader->attachAssociations($query, $target, $options['includeFields']);
  685. $newContain = [];
  686. foreach ($contain as $alias => $value) {
  687. $newContain[$options['aliasPath'] . '.' . $alias] = $value;
  688. }
  689. $query->contain($newContain);
  690. foreach ($matching as $alias => $value) {
  691. $query->matching($options['aliasPath'] . '.' . $alias, $value['queryBuilder']);
  692. }
  693. }
  694. /**
  695. * Returns a single or multiple conditions to be appended to the generated join
  696. * clause for getting the results on the target table.
  697. *
  698. * @param array $options list of options passed to attachTo method
  699. * @return array
  700. * @throws \RuntimeException if the number of columns in the foreignKey do not
  701. * match the number of columns in the source table primaryKey
  702. */
  703. protected function _joinCondition($options)
  704. {
  705. $conditions = [];
  706. $tAlias = $this->target()->alias();
  707. $sAlias = $this->source()->alias();
  708. $foreignKey = (array)$options['foreignKey'];
  709. $bindingKey = (array)$this->bindingKey();
  710. if (count($foreignKey) !== count($bindingKey)) {
  711. $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
  712. throw new RuntimeException(sprintf(
  713. $msg,
  714. $this->_name,
  715. implode(', ', $foreignKey),
  716. implode(', ', $bindingKey)
  717. ));
  718. }
  719. foreach ($foreignKey as $k => $f) {
  720. $field = sprintf('%s.%s', $sAlias, $bindingKey[$k]);
  721. $value = new IdentifierExpression(sprintf('%s.%s', $tAlias, $f));
  722. $conditions[$field] = $value;
  723. }
  724. return $conditions;
  725. }
  726. /**
  727. * Helper method to infer the requested finder and its options.
  728. *
  729. * Returns the inferred options from the finder $type.
  730. *
  731. * ### Examples:
  732. *
  733. * The following will call the finder 'translations' with the value of the finder as its options:
  734. * $query->contain(['Comments' => ['finder' => ['translations']]]);
  735. * $query->contain(['Comments' => ['finder' => ['translations' => []]]]);
  736. * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]);
  737. *
  738. * @param string|array $finderData The finder name or an array having the name as key
  739. * and options as value.
  740. * @return array
  741. */
  742. protected function _extractFinder($finderData)
  743. {
  744. $finderData = (array)$finderData;
  745. if (is_numeric(key($finderData))) {
  746. return [current($finderData), []];
  747. }
  748. return [key($finderData), current($finderData)];
  749. }
  750. /**
  751. * Proxies property retrieval to the target table. This is handy for getting this
  752. * association's associations
  753. *
  754. * @param string $property the property name
  755. * @return \Cake\ORM\Association
  756. * @throws \RuntimeException if no association with such name exists
  757. */
  758. public function __get($property)
  759. {
  760. return $this->target()->{$property};
  761. }
  762. /**
  763. * Proxies the isset call to the target table. This is handy to check if the
  764. * target table has another association with the passed name
  765. *
  766. * @param string $property the property name
  767. * @return bool true if the property exists
  768. */
  769. public function __isset($property)
  770. {
  771. return isset($this->target()->{$property});
  772. }
  773. /**
  774. * Proxies method calls to the target table.
  775. *
  776. * @param string $method name of the method to be invoked
  777. * @param array $argument List of arguments passed to the function
  778. * @return mixed
  779. * @throws \BadMethodCallException
  780. */
  781. public function __call($method, $argument)
  782. {
  783. return call_user_func_array([$this->target(), $method], $argument);
  784. }
  785. /**
  786. * Get the relationship type.
  787. *
  788. * @return string Constant of either ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY or MANY_TO_MANY.
  789. */
  790. abstract public function type();
  791. /**
  792. * Eager loads a list of records in the target table that are related to another
  793. * set of records in the source table. Source records can specified in two ways:
  794. * first one is by passing a Query object setup to find on the source table and
  795. * the other way is by explicitly passing an array of primary key values from
  796. * the source table.
  797. *
  798. * The required way of passing related source records is controlled by "strategy"
  799. * When the subquery strategy is used it will require a query on the source table.
  800. * When using the select strategy, the list of primary keys will be used.
  801. *
  802. * Returns a closure that should be run for each record returned in a specific
  803. * Query. This callable will be responsible for injecting the fields that are
  804. * related to each specific passed row.
  805. *
  806. * Options array accepts the following keys:
  807. *
  808. * - query: Query object setup to find the source table records
  809. * - keys: List of primary key values from the source table
  810. * - foreignKey: The name of the field used to relate both tables
  811. * - conditions: List of conditions to be passed to the query where() method
  812. * - sort: The direction in which the records should be returned
  813. * - fields: List of fields to select from the target table
  814. * - contain: List of related tables to eager load associated to the target table
  815. * - strategy: The name of strategy to use for finding target table records
  816. * - nestKey: The array key under which results will be found when transforming the row
  817. *
  818. * @param array $options The options for eager loading.
  819. * @return \Closure
  820. */
  821. abstract public function eagerLoader(array $options);
  822. /**
  823. * Handles cascading a delete from an associated model.
  824. *
  825. * Each implementing class should handle the cascaded delete as
  826. * required.
  827. *
  828. * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete.
  829. * @param array $options The options for the original delete.
  830. * @return bool Success
  831. */
  832. abstract public function cascadeDelete(EntityInterface $entity, array $options = []);
  833. /**
  834. * Returns whether or not the passed table is the owning side for this
  835. * association. This means that rows in the 'target' table would miss important
  836. * or required information if the row in 'source' did not exist.
  837. *
  838. * @param \Cake\ORM\Table $side The potential Table with ownership
  839. * @return bool
  840. */
  841. abstract public function isOwningSide(Table $side);
  842. /**
  843. * Extract the target's association data our from the passed entity and proxies
  844. * the saving operation to the target table.
  845. *
  846. * @param \Cake\Datasource\EntityInterface $entity the data to be saved
  847. * @param array|\ArrayObject $options The options for saving associated data.
  848. * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns
  849. * the saved entity
  850. * @see Table::save()
  851. */
  852. abstract public function saveAssociated(EntityInterface $entity, array $options = []);
  853. }