PageRenderTime 42ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/cakephp/cakephp/src/ORM/Association/SelectableAssociationTrait.php

https://gitlab.com/alexandresgv/siteentec
PHP | 341 lines | 186 code | 33 blank | 122 comment | 20 complexity | 847bec70c3473dcd9a2ad77ede5a09c5 MD5 | raw file
  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\Association;
  16. use Cake\Database\Expression\IdentifierExpression;
  17. use Cake\Database\Expression\TupleComparison;
  18. use InvalidArgumentException;
  19. /**
  20. * Represents a type of association that that can be fetched using another query
  21. */
  22. trait SelectableAssociationTrait
  23. {
  24. /**
  25. * Returns true if the eager loading process will require a set of the owning table's
  26. * binding keys in order to use them as a filter in the finder query.
  27. *
  28. * @param array $options The options containing the strategy to be used.
  29. * @return bool true if a list of keys will be required
  30. */
  31. public function requiresKeys(array $options = [])
  32. {
  33. $strategy = isset($options['strategy']) ? $options['strategy'] : $this->strategy();
  34. return $strategy === $this::STRATEGY_SELECT;
  35. }
  36. /**
  37. * {@inheritDoc}
  38. */
  39. public function eagerLoader(array $options)
  40. {
  41. $options += $this->_defaultOptions();
  42. $fetchQuery = $this->_buildQuery($options);
  43. $resultMap = $this->_buildResultMap($fetchQuery, $options);
  44. return $this->_resultInjector($fetchQuery, $resultMap, $options);
  45. }
  46. /**
  47. * Returns the default options to use for the eagerLoader
  48. *
  49. * @return array
  50. */
  51. protected function _defaultOptions()
  52. {
  53. return [
  54. 'foreignKey' => $this->foreignKey(),
  55. 'conditions' => [],
  56. 'strategy' => $this->strategy(),
  57. 'nestKey' => $this->_name
  58. ];
  59. }
  60. /**
  61. * Auxiliary function to construct a new Query object to return all the records
  62. * in the target table that are associated to those specified in $options from
  63. * the source table
  64. *
  65. * @param array $options options accepted by eagerLoader()
  66. * @return \Cake\ORM\Query
  67. * @throws \InvalidArgumentException When a key is required for associations but not selected.
  68. */
  69. protected function _buildQuery($options)
  70. {
  71. $target = $this->target();
  72. $alias = $target->alias();
  73. $key = $this->_linkField($options);
  74. $filter = $options['keys'];
  75. $useSubquery = $options['strategy'] === $this::STRATEGY_SUBQUERY;
  76. $finder = isset($options['finder']) ? $options['finder'] : $this->finder();
  77. list($finder, $opts) = $this->_extractFinder($finder);
  78. $fetchQuery = $this
  79. ->find($finder, $opts)
  80. ->where($options['conditions'])
  81. ->eagerLoaded(true)
  82. ->hydrate($options['query']->hydrate());
  83. if ($useSubquery) {
  84. $filter = $this->_buildSubquery($options['query']);
  85. $fetchQuery = $this->_addFilteringJoin($fetchQuery, $key, $filter);
  86. } else {
  87. $fetchQuery = $this->_addFilteringCondition($fetchQuery, $key, $filter);
  88. }
  89. if (!empty($options['fields'])) {
  90. $fields = $fetchQuery->aliasFields($options['fields'], $alias);
  91. if (!in_array($key, $fields)) {
  92. throw new InvalidArgumentException(
  93. sprintf('You are required to select the "%s" field', $key)
  94. );
  95. }
  96. $fetchQuery->select($fields);
  97. }
  98. if (!empty($options['sort'])) {
  99. $fetchQuery->order($options['sort']);
  100. }
  101. if (!empty($options['contain'])) {
  102. $fetchQuery->contain($options['contain']);
  103. }
  104. if (!empty($options['queryBuilder'])) {
  105. $fetchQuery = $options['queryBuilder']($fetchQuery);
  106. }
  107. return $fetchQuery;
  108. }
  109. /**
  110. * Appends any conditions required to load the relevant set of records in the
  111. * target table query given a filter key and some filtering values when the
  112. * filtering needs to be done using a subquery.
  113. *
  114. * @param \Cake\ORM\Query $query Target table's query
  115. * @param string $key the fields that should be used for filtering
  116. * @param \Cake\ORM\Query $subquery The Subquery to use for filtering
  117. * @return \Cake\ORM\Query
  118. */
  119. public function _addFilteringJoin($query, $key, $subquery)
  120. {
  121. $filter = [];
  122. $aliasedTable = $this->source()->alias();
  123. foreach ($subquery->clause('select') as $aliasedField => $field) {
  124. if (is_int($aliasedField)) {
  125. $filter[] = new IdentifierExpression($field);
  126. } else {
  127. $filter[$aliasedField] = $field;
  128. }
  129. }
  130. $subquery->select($filter, true);
  131. if (is_array($key)) {
  132. $conditions = $this->_createTupleCondition($query, $key, $filter, '=');
  133. } else {
  134. $filter = current($filter);
  135. }
  136. $conditions = isset($conditions) ? $conditions : $query->newExpr([$key => $filter]);
  137. return $query->innerJoin(
  138. [$aliasedTable => $subquery],
  139. $conditions
  140. );
  141. }
  142. /**
  143. * Appends any conditions required to load the relevant set of records in the
  144. * target table query given a filter key and some filtering values.
  145. *
  146. * @param \Cake\ORM\Query $query Target table's query
  147. * @param string|array $key the fields that should be used for filtering
  148. * @param mixed $filter the value that should be used to match for $key
  149. * @return \Cake\ORM\Query
  150. */
  151. protected function _addFilteringCondition($query, $key, $filter)
  152. {
  153. if (is_array($key)) {
  154. $conditions = $this->_createTupleCondition($query, $key, $filter, 'IN');
  155. }
  156. $conditions = isset($conditions) ? $conditions : [$key . ' IN' => $filter];
  157. return $query->andWhere($conditions);
  158. }
  159. /**
  160. * Returns a TupleComparison object that can be used for matching all the fields
  161. * from $keys with the tuple values in $filter using the provided operator.
  162. *
  163. * @param \Cake\ORM\Query $query Target table's query
  164. * @param array $keys the fields that should be used for filtering
  165. * @param mixed $filter the value that should be used to match for $key
  166. * @param string $operator The operator for comparing the tuples
  167. * @return \Cake\Database\Expression\TupleComparison
  168. */
  169. protected function _createTupleCondition($query, $keys, $filter, $operator)
  170. {
  171. $types = [];
  172. $defaults = $query->defaultTypes();
  173. foreach ($keys as $k) {
  174. if (isset($defaults[$k])) {
  175. $types[] = $defaults[$k];
  176. }
  177. }
  178. return new TupleComparison($keys, $filter, $types, $operator);
  179. }
  180. /**
  181. * Generates a string used as a table field that contains the values upon
  182. * which the filter should be applied
  183. *
  184. * @param array $options The options for getting the link field.
  185. * @return string|array
  186. */
  187. protected abstract function _linkField($options);
  188. /**
  189. * Builds a query to be used as a condition for filtering records in the
  190. * target table, it is constructed by cloning the original query that was used
  191. * to load records in the source table.
  192. *
  193. * @param \Cake\ORM\Query $query the original query used to load source records
  194. * @return \Cake\ORM\Query
  195. */
  196. protected function _buildSubquery($query)
  197. {
  198. $filterQuery = clone $query;
  199. $filterQuery->autoFields(false);
  200. $filterQuery->mapReduce(null, null, true);
  201. $filterQuery->formatResults(null, true);
  202. $filterQuery->contain([], true);
  203. if (!$filterQuery->clause('limit')) {
  204. $filterQuery->limit(null);
  205. $filterQuery->order([], true);
  206. $filterQuery->offset(null);
  207. }
  208. $fields = $this->_subqueryFields($query);
  209. $filterQuery->select($fields['select'], true)->group($fields['group']);
  210. return $filterQuery;
  211. }
  212. /**
  213. * Calculate the fields that need to participate in a subquery.
  214. *
  215. * Normally this includes the binding key columns. If there is a an ORDER BY,
  216. * those columns are also included as the fields may be calculated or constant values,
  217. * that need to be present to ensure the correct association data is loaded.
  218. *
  219. * @param \Cake\ORM\Query $query The query to get fields from.
  220. * @return array The list of fields for the subquery.
  221. */
  222. protected function _subqueryFields($query)
  223. {
  224. $keys = (array)$this->bindingKey();
  225. if ($this->type() === $this::MANY_TO_ONE) {
  226. $keys = (array)$this->foreignKey();
  227. }
  228. $fields = $query->aliasFields($keys, $this->source()->alias());
  229. $group = $fields = array_values($fields);
  230. $order = $query->clause('order');
  231. if ($order) {
  232. $columns = $query->clause('select');
  233. $order->iterateParts(function ($direction, $field) use (&$fields, $columns) {
  234. if (isset($columns[$field])) {
  235. $fields[$field] = $columns[$field];
  236. }
  237. });
  238. }
  239. return ['select' => $fields, 'group' => $group];
  240. }
  241. /**
  242. * Builds an array containing the results from fetchQuery indexed by
  243. * the foreignKey value corresponding to this association.
  244. *
  245. * @param \Cake\ORM\Query $fetchQuery The query to get results from
  246. * @param array $options The options passed to the eager loader
  247. * @return array
  248. */
  249. protected abstract function _buildResultMap($fetchQuery, $options);
  250. /**
  251. * Returns a callable to be used for each row in a query result set
  252. * for injecting the eager loaded rows
  253. *
  254. * @param \Cake\ORM\Query $fetchQuery the Query used to fetch results
  255. * @param array $resultMap an array with the foreignKey as keys and
  256. * the corresponding target table results as value.
  257. * @param array $options The options passed to the eagerLoader method
  258. * @return \Closure
  259. */
  260. protected function _resultInjector($fetchQuery, $resultMap, $options)
  261. {
  262. $source = $this->source();
  263. $sAlias = $source->alias();
  264. $keys = $this->type() === $this::MANY_TO_ONE ?
  265. $this->foreignKey() :
  266. $this->bindingKey();
  267. $sourceKeys = [];
  268. foreach ((array)$keys as $key) {
  269. $f = $fetchQuery->aliasField($key, $sAlias);
  270. $sourceKeys[] = key($f);
  271. }
  272. $nestKey = $options['nestKey'];
  273. if (count($sourceKeys) > 1) {
  274. return $this->_multiKeysInjector($resultMap, $sourceKeys, $nestKey);
  275. }
  276. $sourceKey = $sourceKeys[0];
  277. return function ($row) use ($resultMap, $sourceKey, $nestKey) {
  278. if (isset($row[$sourceKey], $resultMap[$row[$sourceKey]])) {
  279. $row[$nestKey] = $resultMap[$row[$sourceKey]];
  280. }
  281. return $row;
  282. };
  283. }
  284. /**
  285. * Returns a callable to be used for each row in a query result set
  286. * for injecting the eager loaded rows when the matching needs to
  287. * be done with multiple foreign keys
  288. *
  289. * @param array $resultMap A keyed arrays containing the target table
  290. * @param array $sourceKeys An array with aliased keys to match
  291. * @param string $nestKey The key under which results should be nested
  292. * @return \Closure
  293. */
  294. protected function _multiKeysInjector($resultMap, $sourceKeys, $nestKey)
  295. {
  296. return function ($row) use ($resultMap, $sourceKeys, $nestKey) {
  297. $values = [];
  298. foreach ($sourceKeys as $key) {
  299. $values[] = $row[$key];
  300. }
  301. $key = implode(';', $values);
  302. if (isset($resultMap[$key])) {
  303. $row[$nestKey] = $resultMap[$key];
  304. }
  305. return $row;
  306. };
  307. }
  308. }