ActiveQueryTrait::with()   B
last analyzed

Complexity

Conditions 7
Paths 6

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 11
nc 6
nop 1
dl 0
loc 21
ccs 10
cts 10
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ReflectionException;
8
use Throwable;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\NotSupportedException;
12
use Yiisoft\Definitions\Exception\InvalidConfigException;
13
14
use function is_array;
15
use function is_int;
16
use function reset;
17
use function strpos;
18
use function substr;
19
20
trait ActiveQueryTrait
21
{
22
    private bool|null $asArray = null;
23
24
    /**
25
     * Sets the {@see asArray} property.
26
     *
27
     * @param bool $value whether to return the query results in terms of arrays instead of Active Records.
28
     *
29
     * @return static the query object itself.
30
     */
31
    public function asArray(bool|null $value = true): static
32 189
    {
33
        $this->asArray = $value;
34 189
        return $this;
35
    }
36 189
37
    /**
38
     * Specifies the relations with which this query should be performed.
39
     *
40
     * The parameters to this method can be either one or multiple strings, or a single array of relation names and the
41
     * optional callbacks to customize the relations.
42
     *
43
     * A relation name can refer to a relation defined in {@see modelClass} or a sub-relation that stands for a relation
44
     * of a related record.
45
     *
46
     * For example, `orders.address` means the `address` relation defined in the model class corresponding to the
47
     * `orders` relation.
48
     *
49
     * The following are some usage examples:
50
     *
51
     * ```php
52
     * // Create active query
53
     * CustomerQuery = new ActiveQuery(Customer::class, $db);
54
     * // find customers together with their orders and country
55
     * CustomerQuery->with('orders', 'country')->all();
56
     * // find customers together with their orders and the orders' shipping address
57
     * CustomerQuery->with('orders.address')->all();
58
     * // find customers together with their country and orders of status 1
59
     * CustomerQuery->with([
60
     *     'orders' => function (ActiveQuery $query) {
61
     *         $query->andWhere('status = 1');
62
     *     },
63
     *     'country',
64
     * ])->all();
65
     * ```
66
     *
67
     * You can call `with()` multiple times. Each call will add relations to the existing ones.
68
     *
69
     * For example, the following two statements are equivalent:
70
     *
71
     * ```php
72
     * CustomerQuery->with('orders', 'country')->all();
73
     * CustomerQuery->with('orders')->with('country')->all();
74
     * ```
75
     *
76
     * @param array|string ...$with a list of relation names or relation definitions.
77
     *
78
     * @return static the query object itself.
79
     */
80
    public function with(array|string ...$with): static
81 164
    {
82
        if (isset($with[0]) && is_array($with[0])) {
83 164
            /** the parameter is given as an array */
84
            $with = $with[0];
85 88
        }
86
87
        if (empty($this->with)) {
88 164
            $this->with = $with;
0 ignored issues
show
Bug Best Practice introduced by
The property with does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
89 164
        } elseif (!empty($with)) {
90 68
            foreach ($with as $name => $value) {
91 68
                if (is_int($name)) {
92 68
                    /** repeating relation is fine as normalizeRelations() handle it well */
93
                    $this->with[] = $value;
94 36
                } else {
95
                    $this->with[$name] = $value;
96 32
                }
97
            }
98
        }
99
100
        return $this;
101 164
    }
102
103
    /**
104
     * Converts found rows into model instances.
105
     *
106
     * @throws InvalidConfigException
107
     */
108
    protected function createModels(array $rows): array|null
109
    {
110
        if ($this->asArray) {
111 440
            return $rows;
112
        }
113 440
114 66
        $arClassInstance = [];
115
116
        foreach ($rows as $row) {
117 425
            $arClass = $this->getARInstance();
0 ignored issues
show
Bug introduced by
It seems like getARInstance() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
            /** @scrutinizer ignore-call */ 
118
            $arClass = $this->getARInstance();
Loading history...
118
119 425
            if (method_exists($arClass, 'instantiate')) {
120 425
                $arClass = $arClass->instantiate($row);
121
            }
122 425
123 4
            $arClass->populateRecord($row);
124
125
            $arClassInstance[] = $arClass;
126 425
        }
127
128 425
        return $arClassInstance;
129
    }
130
131 425
    /**
132
     * Finds records corresponding to one or multiple relations and populates them into the primary models.
133
     *
134
     * @param array $with a list of relations that this query should be performed with. Please refer to {@see with()}
135
     * for details about specifying this parameter.
136
     * @param ActiveRecord[]|array $models the primary models (can be either AR instances or arrays)
137
     *
138
     * @throws Exception
139
     * @throws InvalidArgumentException
140
     * @throws NotSupportedException
141
     * @throws ReflectionException
142
     * @throws Throwable
143
     */
144 139
    public function findWith(array $with, array &$models): void
145
    {
146 139
        $primaryModel = reset($models);
147
148 139
        if (!$primaryModel instanceof ActiveRecordInterface) {
149 9
            $primaryModel = $this->getARInstance();
150
        }
151
152 139
        $relations = $this->normalizeRelations($primaryModel, $with);
153
154 139
        foreach ($relations as $name => $relation) {
155 139
            if ($relation->asArray === null) {
156
                /** inherit asArray from primary query */
157 139
                $relation->asArray($this->asArray);
158
            }
159
160 139
            $relation->populateRelation($name, $models);
161
        }
162 139
    }
163
164
    private function normalizeRelations(ActiveRecordInterface $model, array $with): array
165
    {
166
        $relations = [];
167
168
        foreach ($with as $name => $callback) {
169
            if (is_int($name)) {
170 139
                $name = $callback;
171
                $callback = null;
172 139
            }
173
174 139
            if (($pos = strpos($name, '.')) !== false) {
175 139
                /** with sub-relations */
176 135
                $childName = substr($name, $pos + 1);
177 135
                $name = substr($name, 0, $pos);
178
            } else {
179
                $childName = null;
180 139
            }
181
182 21
            if (!isset($relations[$name])) {
183 21
                /** @var ActiveQuery $relation */
184
                $relation = $model->relationQuery($name);
185 139
                $relation->primaryModel = null;
0 ignored issues
show
Bug introduced by
The property primaryModel is declared private in Yiisoft\ActiveRecord\ActiveQuery and cannot be accessed from this context.
Loading history...
186
                $relations[$name] = $relation;
187
            } else {
188 139
                $relation = $relations[$name];
189 139
            }
190 139
191 139
            if (isset($childName)) {
192
                $relation->with[$childName] = $callback;
193 64
            } elseif ($callback !== null) {
194
                $callback($relation);
195
            }
196 139
        }
197 21
198 139
        return $relations;
199 52
    }
200
201
    public function isAsArray(): bool|null
202
    {
203 139
        return $this->asArray;
204
    }
205
206 37
    public function getWith(): array
207
    {
208 37
        return $this->with;
209
    }
210
}
211