ActiveRelationTrait.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\db;
  8. use yii\base\InvalidConfigException;
  9. /**
  10. * ActiveRelationTrait implements the common methods and properties for active record relation classes.
  11. *
  12. * @author Qiang Xue <[email protected]>
  13. * @author Carsten Brandt <[email protected]>
  14. * @since 2.0
  15. */
  16. trait ActiveRelationTrait
  17. {
  18. /**
  19. * @var boolean whether this relation should populate all query results into AR instances.
  20. * If false, only the first row of the results will be retrieved.
  21. */
  22. public $multiple;
  23. /**
  24. * @var ActiveRecord the primary model that this relation is associated with.
  25. * This is used only in lazy loading with dynamic query options.
  26. */
  27. public $primaryModel;
  28. /**
  29. * @var array the columns of the primary and foreign tables that establish the relation.
  30. * The array keys must be columns of the table for this relation, and the array values
  31. * must be the corresponding columns from the primary table.
  32. * Do not prefix or quote the column names as this will be done automatically by Yii.
  33. */
  34. public $link;
  35. /**
  36. * @var array the query associated with the pivot table. Please call [[via()]]
  37. * to set this property instead of directly setting it.
  38. */
  39. public $via;
  40. /**
  41. * Clones internal objects.
  42. */
  43. public function __clone()
  44. {
  45. // make a clone of "via" object so that the same query object can be reused multiple times
  46. if (is_object($this->via)) {
  47. $this->via = clone $this->via;
  48. } elseif (is_array($this->via)) {
  49. $this->via = [$this->via[0], clone $this->via[1]];
  50. }
  51. }
  52. /**
  53. * Specifies the relation associated with the pivot table.
  54. * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]].
  55. * @param callable $callable a PHP callback for customizing the relation associated with the pivot table.
  56. * Its signature should be `function($query)`, where `$query` is the query to be customized.
  57. * @return static the relation object itself.
  58. */
  59. public function via($relationName, $callable = null)
  60. {
  61. $relation = $this->primaryModel->getRelation($relationName);
  62. $this->via = [$relationName, $relation];
  63. if ($callable !== null) {
  64. call_user_func($callable, $relation);
  65. }
  66. return $this;
  67. }
  68. /**
  69. * Finds the related records and populates them into the primary models.
  70. * @param string $name the relation name
  71. * @param array $primaryModels primary models
  72. * @return array the related models
  73. * @throws InvalidConfigException if [[link]] is invalid
  74. */
  75. public function populateRelation($name, &$primaryModels)
  76. {
  77. if (!is_array($this->link)) {
  78. throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.');
  79. }
  80. if ($this->via instanceof self) {
  81. // via pivot table
  82. /** @var ActiveRelationTrait $viaQuery */
  83. $viaQuery = $this->via;
  84. $viaModels = $viaQuery->findPivotRows($primaryModels);
  85. $this->filterByModels($viaModels);
  86. } elseif (is_array($this->via)) {
  87. // via relation
  88. /** @var ActiveRelationTrait $viaQuery */
  89. list($viaName, $viaQuery) = $this->via;
  90. $viaQuery->primaryModel = null;
  91. $viaModels = $viaQuery->populateRelation($viaName, $primaryModels);
  92. $this->filterByModels($viaModels);
  93. } else {
  94. $this->filterByModels($primaryModels);
  95. }
  96. if (count($primaryModels) === 1 && !$this->multiple) {
  97. $model = $this->one();
  98. foreach ($primaryModels as $i => $primaryModel) {
  99. if ($primaryModel instanceof ActiveRecordInterface) {
  100. $primaryModel->populateRelation($name, $model);
  101. } else {
  102. $primaryModels[$i][$name] = $model;
  103. }
  104. }
  105. return [$model];
  106. } else {
  107. $models = $this->all();
  108. if (isset($viaModels, $viaQuery)) {
  109. $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link);
  110. } else {
  111. $buckets = $this->buildBuckets($models, $this->link);
  112. }
  113. $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
  114. foreach ($primaryModels as $i => $primaryModel) {
  115. $key = $this->getModelKey($primaryModel, $link);
  116. $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
  117. if ($primaryModel instanceof ActiveRecordInterface) {
  118. $primaryModel->populateRelation($name, $value);
  119. } else {
  120. $primaryModels[$i][$name] = $value;
  121. }
  122. }
  123. return $models;
  124. }
  125. }
  126. /**
  127. * @param array $models
  128. * @param array $link
  129. * @param array $viaModels
  130. * @param array $viaLink
  131. * @return array
  132. */
  133. private function buildBuckets($models, $link, $viaModels = null, $viaLink = null)
  134. {
  135. if ($viaModels !== null) {
  136. $map = [];
  137. $viaLinkKeys = array_keys($viaLink);
  138. $linkValues = array_values($link);
  139. foreach ($viaModels as $viaModel) {
  140. $key1 = $this->getModelKey($viaModel, $viaLinkKeys);
  141. $key2 = $this->getModelKey($viaModel, $linkValues);
  142. $map[$key2][$key1] = true;
  143. }
  144. }
  145. $buckets = [];
  146. $linkKeys = array_keys($link);
  147. if (isset($map)) {
  148. foreach ($models as $i => $model) {
  149. $key = $this->getModelKey($model, $linkKeys);
  150. if (isset($map[$key])) {
  151. foreach (array_keys($map[$key]) as $key2) {
  152. if ($this->indexBy !== null) {
  153. $buckets[$key2][$i] = $model;
  154. } else {
  155. $buckets[$key2][] = $model;
  156. }
  157. }
  158. }
  159. }
  160. } else {
  161. foreach ($models as $i => $model) {
  162. $key = $this->getModelKey($model, $linkKeys);
  163. if ($this->indexBy !== null) {
  164. $buckets[$key][$i] = $model;
  165. } else {
  166. $buckets[$key][] = $model;
  167. }
  168. }
  169. }
  170. if (!$this->multiple) {
  171. foreach ($buckets as $i => $bucket) {
  172. $buckets[$i] = reset($bucket);
  173. }
  174. }
  175. return $buckets;
  176. }
  177. /**
  178. * @param array $models
  179. */
  180. private function filterByModels($models)
  181. {
  182. $attributes = array_keys($this->link);
  183. $values = [];
  184. if (count($attributes) === 1) {
  185. // single key
  186. $attribute = reset($this->link);
  187. foreach ($models as $model) {
  188. if (($value = $model[$attribute]) !== null) {
  189. $values[] = $value;
  190. }
  191. }
  192. } else {
  193. // composite keys
  194. foreach ($models as $model) {
  195. $v = [];
  196. foreach ($this->link as $attribute => $link) {
  197. $v[$attribute] = $model[$link];
  198. }
  199. $values[] = $v;
  200. }
  201. }
  202. $this->andWhere(['in', $attributes, array_unique($values, SORT_REGULAR)]);
  203. }
  204. /**
  205. * @param ActiveRecord|array $model
  206. * @param array $attributes
  207. * @return string
  208. */
  209. private function getModelKey($model, $attributes)
  210. {
  211. if (count($attributes) > 1) {
  212. $key = [];
  213. foreach ($attributes as $attribute) {
  214. $key[] = $model[$attribute];
  215. }
  216. return serialize($key);
  217. } else {
  218. $attribute = reset($attributes);
  219. $key = $model[$attribute];
  220. return is_scalar($key) ? $key : serialize($key);
  221. }
  222. }
  223. /**
  224. * @param array $primaryModels either array of AR instances or arrays
  225. * @return array
  226. */
  227. private function findPivotRows($primaryModels)
  228. {
  229. if (empty($primaryModels)) {
  230. return [];
  231. }
  232. $this->filterByModels($primaryModels);
  233. /** @var ActiveRecord $primaryModel */
  234. $primaryModel = reset($primaryModels);
  235. if (!$primaryModel instanceof ActiveRecordInterface) {
  236. // when primaryModels are array of arrays (asArray case)
  237. $primaryModel = new $this->modelClass;
  238. }
  239. return $this->asArray()->all($primaryModel->getDb());
  240. }
  241. }