1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\ActiveRecord; |
||
6 | |||
7 | use Closure; |
||
8 | use ReflectionException; |
||
9 | use Throwable; |
||
10 | use Yiisoft\Db\Exception\Exception; |
||
11 | use Yiisoft\Db\Exception\InvalidArgumentException; |
||
12 | use Yiisoft\Db\Exception\InvalidConfigException; |
||
13 | use Yiisoft\Db\Exception\NotSupportedException; |
||
14 | |||
15 | use function array_column; |
||
16 | use function array_combine; |
||
17 | use function array_diff_key; |
||
18 | use function array_fill_keys; |
||
19 | use function array_filter; |
||
20 | use function array_intersect_key; |
||
21 | use function array_keys; |
||
22 | use function array_merge; |
||
23 | use function array_unique; |
||
24 | use function count; |
||
25 | use function is_array; |
||
26 | use function is_object; |
||
27 | use function is_string; |
||
28 | use function key; |
||
29 | use function reset; |
||
30 | use function serialize; |
||
31 | |||
32 | /** |
||
33 | * ActiveRelationTrait implements the common methods and properties for active record relational queries. |
||
34 | */ |
||
35 | trait ActiveRelationTrait |
||
36 | { |
||
37 | private bool $multiple = false; |
||
38 | private ActiveRecordInterface|null $primaryModel = null; |
||
39 | /** @psalm-var string[] */ |
||
40 | private array $link = []; |
||
41 | /** |
||
42 | * @var string|null the name of the relation that is the inverse of this relation. |
||
43 | * |
||
44 | * For example, an order has a customer, which means the inverse of the "customer" relation is the "orders", and the |
||
45 | * inverse of the "orders" relation is the "customer". If this property is set, the primary record(s) will be |
||
46 | * referenced through the specified relation. |
||
47 | * |
||
48 | * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer |
||
49 | * of an order will not trigger new DB query. |
||
50 | * |
||
51 | * This property is only used in relational context. |
||
52 | * |
||
53 | * {@see inverseOf()} |
||
54 | */ |
||
55 | private string|null $inverseOf = null; |
||
56 | private array|ActiveQuery|null $via = null; |
||
57 | private array $viaMap = []; |
||
58 | |||
59 | /** |
||
60 | * Clones internal objects. |
||
61 | */ |
||
62 | public function __clone() |
||
63 | { |
||
64 | /** make a clone of "via" object so that the same query object can be reused multiple times */ |
||
65 | if (is_object($this->via)) { |
||
66 | $this->via = clone $this->via; |
||
67 | } elseif (is_array($this->via)) { |
||
68 | $this->via = [$this->via[0], clone $this->via[1], $this->via[2]]; |
||
69 | } |
||
70 | } |
||
71 | |||
72 | /** |
||
73 | * Specifies the relation associated with the junction table. |
||
74 | * |
||
75 | * Use this method to specify a pivot record/table when declaring a relation in the {@see ActiveRecord} class: |
||
76 | * |
||
77 | * ```php |
||
78 | * class Order extends ActiveRecord |
||
79 | * { |
||
80 | * public function getOrderItems() { |
||
81 | * return $this->hasMany(OrderItem::class, ['order_id' => 'id']); |
||
82 | * } |
||
83 | * |
||
84 | * public function getItems() { |
||
85 | * return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItems'); |
||
86 | * } |
||
87 | * } |
||
88 | * ``` |
||
89 | * |
||
90 | * @param string $relationName the relation name. This refers to a relation declared in {@see primaryModel}. |
||
91 | * @param callable|null $callable a PHP callback for customizing the relation associated with the junction table. |
||
92 | * Its signature should be `function($query)`, where `$query` is the query to be customized. |
||
93 | * |
||
94 | * @return static the relation object itself. |
||
95 | */ |
||
96 | public function via(string $relationName, callable $callable = null): static |
||
97 | { |
||
98 | $relation = $this->primaryModel?->relationQuery($relationName); |
||
99 | $callableUsed = $callable !== null; |
||
100 | $this->via = [$relationName, $relation, $callableUsed]; |
||
101 | |||
102 | 107 | if ($callableUsed) { |
|
103 | $callable($relation); |
||
104 | 107 | } |
|
105 | 107 | ||
106 | 107 | return $this; |
|
107 | } |
||
108 | 107 | ||
109 | 73 | /** |
|
110 | * Sets the name of the relation that is the inverse of this relation. |
||
111 | * |
||
112 | 107 | * For example, a customer has orders, which means the inverse of the "orders" relation is the "customer". |
|
113 | * |
||
114 | * If this property is set, the primary record(s) will be referenced through the specified relation. |
||
115 | * |
||
116 | * For example, `$customer->orders[0]->customer` and `$customer` will be the same object, and accessing the customer |
||
117 | * of an order will not trigger a new DB query. |
||
118 | * |
||
119 | * Use this method when declaring a relation in the {@see ActiveRecord} class, e.g. in Customer model: |
||
120 | * |
||
121 | * ```php |
||
122 | * public function getOrders() |
||
123 | * { |
||
124 | * return $this->hasMany(Order::class, ['customer_id' => 'id'])->inverseOf('customer'); |
||
125 | * } |
||
126 | * ``` |
||
127 | * |
||
128 | * This also may be used for Order model, but with caution: |
||
129 | * |
||
130 | * ```php |
||
131 | * public function getCustomer() |
||
132 | * { |
||
133 | * return $this->hasOne(Customer::class, ['id' => 'customer_id'])->inverseOf('orders'); |
||
134 | * } |
||
135 | * ``` |
||
136 | * |
||
137 | * in this case result will depend on how order(s) was loaded. |
||
138 | * Let's suppose customer has several orders. If only one order was loaded: |
||
139 | * |
||
140 | * ```php |
||
141 | * $orderQuery = new ActiveQuery(Order::class, $db); |
||
142 | * $orders = $orderQuery->where(['id' => 1])->all(); |
||
143 | * $customerOrders = $orders[0]->customer->orders; |
||
144 | * ``` |
||
145 | * |
||
146 | * variable `$customerOrders` will contain only one order. If orders was loaded like this: |
||
147 | * |
||
148 | * ```php |
||
149 | * $orderQuery = new ActiveQuery(Order::class, $db); |
||
150 | * $orders = $orderQuery->with('customer')->where(['customer_id' => 1])->all(); |
||
151 | * $customerOrders = $orders[0]->customer->orders; |
||
152 | * ``` |
||
153 | * |
||
154 | * variable `$customerOrders` will contain all orders of the customer. |
||
155 | * |
||
156 | * @param string $relationName the name of the relation that is the inverse of this relation. |
||
157 | * |
||
158 | * @return static the relation object itself. |
||
159 | */ |
||
160 | public function inverseOf(string $relationName): static |
||
161 | { |
||
162 | $this->inverseOf = $relationName; |
||
163 | |||
164 | return $this; |
||
165 | } |
||
166 | 16 | ||
167 | /** |
||
168 | 16 | * Returns query records depends on {@see $multiple} . |
|
169 | * |
||
170 | 16 | * This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion. |
|
171 | * |
||
172 | * @throws Exception |
||
173 | * @throws InvalidArgumentException |
||
174 | * @throws InvalidConfigException |
||
175 | * @throws ReflectionException |
||
176 | * @throws Throwable if the relation is invalid. |
||
177 | * |
||
178 | * @return ActiveRecordInterface|array|null the related record(s). |
||
179 | */ |
||
180 | public function relatedRecords(): ActiveRecordInterface|array|null |
||
181 | { |
||
182 | return $this->multiple ? $this->all() : $this->onePopulate(); |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | 101 | * If applicable, populate the query's primary model into the related records' inverse relationship. |
|
187 | * |
||
188 | 101 | * @param array $result the array of related records as generated by {@see populate()} |
|
189 | 101 | * |
|
190 | 101 | * @throws \Yiisoft\Definitions\Exception\InvalidConfigException |
|
191 | 101 | */ |
|
192 | private function addInverseRelations(array &$result): void |
||
193 | { |
||
194 | if ($this->inverseOf === null) { |
||
195 | return; |
||
196 | } |
||
197 | |||
198 | $relatedModel = reset($result); |
||
199 | 101 | ||
200 | if ($relatedModel instanceof ActiveRecordInterface) { |
||
201 | $inverseRelation = $relatedModel->relationQuery($this->inverseOf); |
||
202 | $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel; |
||
203 | |||
204 | foreach ($result as $relatedModel) { |
||
205 | $relatedModel->populateRelation($this->inverseOf, $primaryModel); |
||
206 | } |
||
207 | 16 | } else { |
|
208 | $inverseRelation = $this->getARInstance()->relationQuery($this->inverseOf); |
||
209 | 16 | $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel; |
|
210 | |||
211 | foreach ($result as &$relatedModel) { |
||
212 | $relatedModel[$this->inverseOf] = $primaryModel; |
||
213 | 16 | } |
|
214 | 16 | } |
|
215 | 16 | } |
|
216 | 16 | ||
217 | /** |
||
218 | 16 | * Finds the related records and populates them into the primary models. |
|
219 | 16 | * |
|
220 | 16 | * @param string $name the relation name |
|
221 | * @param array $primaryModels primary models |
||
222 | * |
||
223 | 8 | * @throws InvalidArgumentException|InvalidConfigException|NotSupportedException|Throwable if {@see link()} is |
|
224 | 8 | * invalid. |
|
225 | * @throws Exception |
||
226 | * |
||
227 | 8 | * @return array the related models |
|
228 | 8 | */ |
|
229 | public function populateRelation(string $name, array &$primaryModels): array |
||
230 | { |
||
231 | 16 | if ($this->via instanceof self) { |
|
232 | $viaQuery = $this->via; |
||
233 | $viaModels = $viaQuery->findJunctionRows($primaryModels); |
||
234 | $this->filterByModels($viaModels); |
||
235 | } elseif (is_array($this->via)) { |
||
236 | [$viaName, $viaQuery] = $this->via; |
||
237 | |||
238 | if ($viaQuery->asArray === null) { |
||
239 | /** inherit asArray from primary query */ |
||
240 | $viaQuery->asArray($this->asArray); |
||
241 | } |
||
242 | |||
243 | $viaQuery->primaryModel = null; |
||
244 | $viaModels = $viaQuery->populateRelation($viaName, $primaryModels); |
||
245 | 139 | $this->filterByModels($viaModels); |
|
246 | } else { |
||
247 | 139 | $this->filterByModels($primaryModels); |
|
248 | } |
||
249 | |||
250 | if (!$this->multiple && count($primaryModels) === 1) { |
||
251 | 139 | $model = $this->onePopulate(); |
|
252 | $primaryModel = reset($primaryModels); |
||
253 | |||
254 | if ($primaryModel instanceof ActiveRecordInterface) { |
||
255 | $primaryModel->populateRelation($name, $model); |
||
256 | } else { |
||
257 | 12 | $primaryModels[key($primaryModels)][$name] = $model; |
|
258 | 12 | } |
|
259 | 12 | ||
260 | 139 | if ($this->inverseOf !== null) { |
|
261 | $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf); |
||
262 | } |
||
263 | |||
264 | return [$model]; |
||
265 | } |
||
266 | 72 | ||
267 | /** |
||
268 | 72 | * {@see https://github.com/yiisoft/yii2/issues/3197} |
|
269 | * |
||
270 | 72 | * delay indexing related models after buckets are built. |
|
271 | */ |
||
272 | $indexBy = $this->getIndexBy(); |
||
273 | 72 | $this->indexBy(null); |
|
274 | 72 | $models = $this->all(); |
|
275 | 72 | ||
276 | if (isset($viaModels, $viaQuery)) { |
||
277 | 139 | $buckets = $this->buildBuckets($models, $viaModels, $viaQuery); |
|
278 | } else { |
||
279 | $buckets = $this->buildBuckets($models); |
||
280 | 139 | } |
|
281 | 28 | ||
282 | 28 | $this->indexBy($indexBy); |
|
283 | |||
284 | 28 | if ($indexBy !== null && $this->multiple) { |
|
285 | 28 | $buckets = $this->indexBuckets($buckets, $indexBy); |
|
286 | } |
||
287 | 4 | ||
288 | if (isset($viaQuery)) { |
||
289 | $deepViaQuery = $viaQuery; |
||
290 | 28 | ||
291 | 8 | while ($deepViaQuery->via) { |
|
292 | $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via; |
||
293 | } |
||
294 | 28 | ||
295 | $link = $deepViaQuery->link; |
||
296 | } else { |
||
297 | $link = $this->link; |
||
298 | } |
||
299 | |||
300 | foreach ($primaryModels as $i => $primaryModel) { |
||
301 | $keys = null; |
||
302 | 123 | ||
303 | 123 | if ($this->multiple && count($link) === 1) { |
|
304 | 123 | $primaryModelKey = reset($link); |
|
305 | |||
306 | 123 | if ($primaryModel instanceof ActiveRecordInterface) { |
|
307 | 72 | $keys = $primaryModel->getAttribute($primaryModelKey); |
|
308 | } else { |
||
309 | 123 | $keys = $primaryModel[$primaryModelKey] ?? null; |
|
310 | } |
||
311 | } |
||
312 | 123 | ||
313 | if (is_array($keys)) { |
||
314 | 123 | $value = []; |
|
315 | 17 | ||
316 | foreach ($keys as $key) { |
||
317 | $key = (string) $key; |
||
318 | 123 | ||
319 | 123 | if (isset($buckets[$key])) { |
|
320 | 72 | $value[] = $buckets[$key]; |
|
321 | } |
||
322 | 72 | } |
|
323 | 5 | ||
324 | if ($indexBy !== null) { |
||
325 | /** if indexBy is set, array_merge will cause renumbering of numeric array */ |
||
326 | 72 | $value = array_replace(...$value); |
|
327 | } else { |
||
328 | $value = array_merge(...$value); |
||
329 | 123 | } |
|
330 | 123 | } else { |
|
331 | $key = $this->getModelKey($primaryModel, $link); |
||
332 | $value = $buckets[$key] ?? ($this->multiple ? [] : null); |
||
333 | } |
||
334 | |||
335 | if ($primaryModel instanceof ActiveRecordInterface) { |
||
336 | $primaryModel->populateRelation($name, $value); |
||
337 | } else { |
||
338 | $primaryModels[$i][$name] = $value; |
||
339 | } |
||
340 | } |
||
341 | |||
342 | if ($this->inverseOf !== null) { |
||
343 | $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf); |
||
344 | } |
||
345 | |||
346 | 123 | return $models; |
|
347 | 123 | } |
|
348 | |||
349 | /** |
||
350 | 123 | * @throws \Yiisoft\Definitions\Exception\InvalidConfigException |
|
351 | 123 | */ |
|
352 | private function populateInverseRelation( |
||
353 | 9 | array &$primaryModels, |
|
354 | array $models, |
||
355 | string $primaryName, |
||
356 | 123 | string $name |
|
357 | 8 | ): void { |
|
358 | if (empty($models) || empty($primaryModels)) { |
||
359 | return; |
||
360 | 123 | } |
|
361 | |||
362 | $model = reset($models); |
||
363 | 12 | ||
364 | if ($model instanceof ActiveRecordInterface) { |
||
365 | $this->populateInverseRelationToModels($models, $primaryModels, $name); |
||
366 | return; |
||
367 | } |
||
368 | |||
369 | 12 | $primaryModel = reset($primaryModels); |
|
370 | |||
371 | if ($primaryModel instanceof ActiveRecordInterface) { |
||
372 | 12 | if ($this->multiple) { |
|
373 | foreach ($primaryModels as $primaryModel) { |
||
374 | $models = $primaryModel->relation($primaryName); |
||
375 | 12 | if (!empty($models)) { |
|
376 | 12 | $this->populateInverseRelationToModels($models, $primaryModels, $name); |
|
377 | $primaryModel->populateRelation($primaryName, $models); |
||
378 | 4 | } |
|
379 | } |
||
380 | } else { |
||
381 | 12 | foreach ($primaryModels as $primaryModel) { |
|
382 | 8 | $model = $primaryModel->relation($primaryName); |
|
383 | 8 | if (!empty($model)) { |
|
384 | 8 | $models = [$model]; |
|
385 | 8 | $this->populateInverseRelationToModels($models, $primaryModels, $name); |
|
386 | 8 | $primaryModel->populateRelation($primaryName, $models[0]); |
|
387 | } |
||
388 | } |
||
389 | 8 | } |
|
390 | 4 | } else { |
|
391 | if ($this->multiple) { |
||
392 | foreach ($primaryModels as &$primaryModel) { |
||
393 | if (!empty($primaryModel[$primaryName])) { |
||
394 | $this->populateInverseRelationToModels($primaryModel[$primaryName], $primaryModels, $name); |
||
395 | 4 | } |
|
396 | 4 | } |
|
397 | 4 | } else { |
|
398 | foreach ($primaryModels as &$primaryModel) { |
||
399 | if (!empty($primaryModel[$primaryName])) { |
||
400 | $models = [$primaryModel[$primaryName]]; |
||
401 | 8 | $this->populateInverseRelationToModels($models, $primaryModels, $name); |
|
402 | 8 | $primaryModel[$primaryName] = $models[0]; |
|
403 | 8 | } |
|
404 | 8 | } |
|
405 | 8 | } |
|
406 | } |
||
407 | 4 | } |
|
408 | |||
409 | private function populateInverseRelationToModels(array &$models, array $primaryModels, string $name): void |
||
410 | { |
||
411 | $model = reset($models); |
||
412 | $isArray = is_array($model); |
||
413 | |||
414 | /** @var ActiveQuery $relation */ |
||
415 | $relation = $isArray ? $this->getARInstance()->relationQuery($name) : $model->relationQuery($name); |
||
416 | $buckets = $relation->buildBuckets($primaryModels); |
||
417 | $link = $relation->getLink(); |
||
418 | $default = $relation->getMultiple() ? [] : null; |
||
419 | |||
420 | 12 | if ($isArray) { |
|
421 | /** @var array $model */ |
||
422 | 127 | foreach ($models as &$model) { |
|
423 | $key = $this->getModelKey($model, $link); |
||
424 | $model[$name] = $buckets[$key] ?? $default; |
||
425 | } |
||
426 | } else { |
||
427 | /** @var ActiveRecordInterface $model */ |
||
428 | foreach ($models as $model) { |
||
429 | 127 | $key = $this->getModelKey($model, $link); |
|
430 | 72 | $model->populateRelation($name, $buckets[$key] ?? $default); |
|
431 | 72 | } |
|
432 | 72 | } |
|
433 | 72 | } |
|
434 | |||
435 | 72 | private function buildBuckets( |
|
436 | 71 | array $models, |
|
437 | 71 | array $viaModels = null, |
|
438 | 71 | self $viaQuery = null |
|
439 | ): array { |
||
440 | if ($viaModels !== null) { |
||
441 | 72 | $map = []; |
|
442 | $linkValues = $this->link; |
||
443 | 72 | $viaLink = $viaQuery->link ?? []; |
|
444 | 72 | $viaLinkKeys = array_keys($viaLink); |
|
445 | 5 | $viaVia = null; |
|
446 | 5 | ||
447 | foreach ($viaModels as $viaModel) { |
||
448 | 5 | $key1 = $this->getModelKey($viaModel, $viaLinkKeys); |
|
449 | $key2 = $this->getModelKey($viaModel, $linkValues); |
||
450 | $map[$key2][$key1] = true; |
||
451 | } |
||
452 | 127 | ||
453 | 127 | if ($viaQuery !== null) { |
|
454 | $viaQuery->viaMap = $map; |
||
455 | 127 | $viaVia = $viaQuery->getVia(); |
|
456 | 72 | } |
|
457 | 71 | ||
458 | 71 | while ($viaVia) { |
|
459 | 71 | /** |
|
460 | 71 | * @var ActiveQuery $viaViaQuery |
|
461 | * |
||
462 | * @psalm-suppress RedundantCondition |
||
463 | */ |
||
464 | $viaViaQuery = is_array($viaVia) ? $viaVia[1] : $viaVia; |
||
465 | 127 | $map = $this->mapVia($map, $viaViaQuery->viaMap); |
|
466 | 127 | ||
467 | 127 | $viaVia = $viaViaQuery->getVia(); |
|
468 | } |
||
469 | } |
||
470 | |||
471 | 127 | $buckets = []; |
|
472 | 56 | $linkKeys = array_keys($this->link); |
|
473 | 56 | ||
474 | if (isset($map)) { |
||
475 | foreach ($models as $model) { |
||
476 | $key = $this->getModelKey($model, $linkKeys); |
||
477 | 127 | if (isset($map[$key])) { |
|
478 | foreach (array_keys($map[$key]) as $key2) { |
||
479 | /** @psalm-suppress InvalidArrayOffset */ |
||
480 | 5 | $buckets[$key2][] = $model; |
|
481 | } |
||
482 | 5 | } |
|
483 | } |
||
484 | 5 | } else { |
|
485 | 5 | foreach ($models as $model) { |
|
486 | 5 | $key = $this->getModelKey($model, $linkKeys); |
|
487 | $buckets[$key][] = $model; |
||
488 | } |
||
489 | } |
||
490 | 5 | ||
491 | if (!$this->multiple) { |
||
492 | return array_combine( |
||
493 | array_keys($buckets), |
||
494 | array_column($buckets, 0) |
||
495 | ); |
||
496 | } |
||
497 | |||
498 | return $buckets; |
||
499 | } |
||
500 | |||
501 | private function mapVia(array $map, array $viaMap): array |
||
502 | 17 | { |
|
503 | $resultMap = []; |
||
504 | 17 | ||
505 | foreach ($map as $key => $linkKeys) { |
||
506 | 17 | $resultMap[$key] = []; |
|
507 | 17 | foreach (array_keys($linkKeys) as $linkKey) { |
|
508 | 17 | /** @psalm-suppress InvalidArrayOffset */ |
|
509 | 17 | $resultMap[$key] += $viaMap[$linkKey]; |
|
510 | 17 | } |
|
511 | } |
||
512 | |||
513 | return $resultMap; |
||
514 | 17 | } |
|
515 | |||
516 | /** |
||
517 | * Indexes buckets by a column name. |
||
518 | * |
||
519 | * @param Closure|string $indexBy the name of the column by which the query results should be indexed by. This can |
||
520 | * also be a {@see Closure} that returns the index value based on the given models data. |
||
521 | */ |
||
522 | 236 | private function indexBuckets(array $buckets, Closure|string $indexBy): array |
|
523 | { |
||
524 | 236 | foreach ($buckets as &$models) { |
|
525 | 36 | $models = ArArrayHelper::index($models, $indexBy); |
|
526 | 12 | } |
|
527 | |||
528 | 32 | return $buckets; |
|
529 | 32 | } |
|
530 | 32 | ||
531 | /** |
||
532 | 32 | * @param array $attributes the attributes to prefix. |
|
533 | * |
||
534 | * @throws \Yiisoft\Definitions\Exception\InvalidConfigException |
||
535 | */ |
||
536 | 36 | private function prefixKeyColumns(array $attributes): array |
|
537 | 36 | { |
|
538 | 36 | if (!empty($this->join) || !empty($this->joinWith)) { |
|
539 | if (empty($this->from)) { |
||
540 | $alias = $this->getARInstance()->getTableName(); |
||
541 | } else { |
||
542 | foreach ($this->from as $alias => $table) { |
||
543 | 236 | if (!is_string($alias)) { |
|
544 | $alias = $table; |
||
545 | } |
||
546 | 236 | break; |
|
547 | } |
||
548 | 236 | } |
|
549 | |||
550 | 236 | if (isset($alias)) { |
|
551 | foreach ($attributes as $i => $attribute) { |
||
552 | 236 | $attributes[$i] = "$alias.$attribute"; |
|
553 | 236 | } |
|
554 | } |
||
555 | 232 | } |
|
556 | 232 | ||
557 | 232 | return $attributes; |
|
558 | 228 | } |
|
559 | |||
560 | 228 | /** |
|
561 | * @throws \Yiisoft\Definitions\Exception\InvalidConfigException |
||
562 | */ |
||
563 | 228 | protected function filterByModels(array $models): void |
|
564 | { |
||
565 | $attributes = array_keys($this->link); |
||
566 | $attributes = $this->prefixKeyColumns($attributes); |
||
567 | |||
568 | 232 | $model = reset($models); |
|
569 | 232 | $values = []; |
|
570 | |||
571 | if (count($attributes) === 1) { |
||
572 | /** single key */ |
||
573 | $attribute = reset($this->link); |
||
574 | |||
575 | 8 | if ($model instanceof ActiveRecordInterface) { |
|
576 | foreach ($models as $model) { |
||
577 | 8 | $value = $model->getAttribute($attribute); |
|
578 | 8 | ||
579 | if ($value !== null) { |
||
580 | 8 | if (is_array($value)) { |
|
581 | 8 | $values = [...$values, ...$value]; |
|
582 | } else { |
||
583 | $values[] = $value; |
||
584 | 8 | } |
|
585 | } |
||
586 | 8 | } |
|
587 | } else { |
||
588 | foreach ($models as $model) { |
||
589 | if (isset($model[$attribute])) { |
||
590 | $value = $model[$attribute]; |
||
591 | |||
592 | 236 | if (is_array($value)) { |
|
593 | 232 | $values = [...$values, ...$value]; |
|
594 | 232 | } else { |
|
595 | 232 | $values[] = $value; |
|
596 | 232 | } |
|
597 | 228 | } |
|
598 | } |
||
599 | 8 | } |
|
600 | |||
601 | if (!empty($values)) { |
||
602 | $scalarValues = array_filter($values, 'is_scalar'); |
||
603 | 232 | $nonScalarValues = array_diff_key($values, $scalarValues); |
|
604 | 232 | ||
605 | $scalarValues = array_unique($scalarValues); |
||
606 | $values = [...$scalarValues, ...$nonScalarValues]; |
||
607 | 236 | } |
|
608 | 236 | } else { |
|
609 | $nulls = array_fill_keys($this->link, null); |
||
610 | |||
611 | if ($model instanceof ActiveRecordInterface) { |
||
612 | foreach ($models as $model) { |
||
613 | $value = $model->getAttributes($this->link); |
||
614 | |||
615 | if (!empty($value)) { |
||
616 | 127 | $values[] = array_combine($attributes, array_merge($nulls, $value)); |
|
617 | } |
||
618 | 127 | } |
|
619 | } else { |
||
620 | 127 | foreach ($models as $model) { |
|
621 | 127 | $value = array_intersect_key($model, $nulls); |
|
622 | |||
623 | if (!empty($value)) { |
||
624 | 127 | $values[] = array_combine($attributes, array_merge($nulls, $value)); |
|
625 | } |
||
626 | } |
||
627 | } |
||
628 | 127 | } |
|
629 | |||
630 | 127 | if (empty($values)) { |
|
631 | $this->emulateExecution(); |
||
632 | $this->andWhere('1=0'); |
||
633 | return; |
||
634 | } |
||
635 | |||
636 | $this->andWhere(['in', $attributes, $values]); |
||
637 | } |
||
638 | 127 | ||
639 | private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): string |
||
640 | 127 | { |
|
641 | $key = []; |
||
642 | |||
643 | if (is_array($activeRecord)) { |
||
644 | foreach ($attributes as $attribute) { |
||
645 | if (isset($activeRecord[$attribute])) { |
||
646 | $key[] = (string) $activeRecord[$attribute]; |
||
647 | } |
||
648 | 127 | } |
|
649 | } else { |
||
650 | foreach ($attributes as $attribute) { |
||
651 | $value = $activeRecord->getAttribute($attribute); |
||
652 | |||
653 | if ($value !== null) { |
||
654 | $key[] = (string) $value; |
||
655 | } |
||
656 | 28 | } |
|
657 | } |
||
658 | 28 | ||
659 | return match (count($key)) { |
||
660 | 0 => '', |
||
661 | 1 => $key[0], |
||
662 | 28 | default => serialize($key), |
|
663 | }; |
||
664 | } |
||
665 | 28 | ||
666 | /** |
||
667 | 28 | * @param array $primaryModels either array of AR instances or arrays. |
|
668 | * |
||
669 | * @throws Exception |
||
670 | * @throws Throwable |
||
671 | * @throws \Yiisoft\Definitions\Exception\InvalidConfigException |
||
672 | 28 | */ |
|
673 | private function findJunctionRows(array $primaryModels): array |
||
674 | { |
||
675 | if (empty($primaryModels)) { |
||
676 | return []; |
||
677 | } |
||
678 | |||
679 | $this->filterByModels($primaryModels); |
||
680 | |||
681 | /* @var $primaryModel ActiveRecord */ |
||
682 | 39 | $primaryModel = reset($primaryModels); |
|
683 | |||
684 | 39 | if (!$primaryModel instanceof ActiveRecordInterface) { |
|
685 | /** when primaryModels are array of arrays (asArray case) */ |
||
686 | $primaryModel = $this->arClass; |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
687 | } |
||
688 | |||
689 | return $this->asArray()->all(); |
||
690 | } |
||
691 | |||
692 | 63 | public function getMultiple(): bool |
|
693 | { |
||
694 | 63 | return $this->multiple; |
|
695 | } |
||
696 | |||
697 | /** |
||
698 | * @return ActiveRecordInterface|null the primary model of a relational query. |
||
699 | * |
||
700 | * This is used only in lazy loading with dynamic query options. |
||
701 | */ |
||
702 | public function getPrimaryModel(): ActiveRecordInterface|null |
||
703 | { |
||
704 | return $this->primaryModel; |
||
705 | } |
||
706 | 113 | ||
707 | /** |
||
708 | 113 | * @psalm-return string[] |
|
709 | */ |
||
710 | public function getLink(): array |
||
711 | { |
||
712 | return $this->link; |
||
713 | } |
||
714 | |||
715 | public function getVia(): array|ActiveQueryInterface|null |
||
716 | { |
||
717 | return $this->via; |
||
718 | } |
||
719 | 117 | ||
720 | public function multiple(bool $value): self |
||
721 | 117 | { |
|
722 | $this->multiple = $value; |
||
723 | |||
724 | 252 | return $this; |
|
725 | } |
||
726 | 252 | ||
727 | public function primaryModel(ActiveRecordInterface $value): self |
||
728 | 252 | { |
|
729 | $this->primaryModel = $value; |
||
730 | |||
731 | 252 | return $this; |
|
732 | } |
||
733 | 252 | ||
734 | public function link(array $value): self |
||
735 | 252 | { |
|
736 | $this->link = $value; |
||
737 | |||
738 | 252 | return $this; |
|
739 | } |
||
740 | } |
||
741 |