Passed
Pull Request — master (#83)
by Wilmer
24:07 queued 09:08
created

BaseActiveRecordTrait::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use Yiisoft\Db\Connection\Connection;
8
use Yiisoft\Db\Connection\ConnectionPool;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\InvalidCallException;
11
use Yiisoft\Db\Exception\UnknownMethodException;
12
use Yiisoft\Db\Exception\UnknownPropertyException;
13
14
trait BaseActiveRecordTrait
15
{
16
    private static ?string $connectionId = null;
17
18
    /**
19
     * PHP getter magic method.
20
     *
21
     * This method is overridden so that attributes and related objects can be accessed like properties.
22
     *
23
     * @param string $name property name
24
     *
25
     * @throws InvalidCallException
26 153
     * @throws UnknownPropertyException
27
     *
28 153
     * @return mixed property value
29 141
     *
30
     * {@see getAttribute()}
31
     */
32 99
    public function __get(string $name)
33 18
    {
34
        if (isset($this->attributes[$name]) || \array_key_exists($name, $this->attributes)) {
35
            return $this->attributes[$name];
36 90
        }
37 57
38
        if ($this->hasAttribute($name)) {
0 ignored issues
show
Bug introduced by
It seems like hasAttribute() 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

38
        if ($this->/** @scrutinizer ignore-call */ hasAttribute($name)) {
Loading history...
39
            return null;
40 51
        }
41
42 45
        if (isset($this->related[$name]) || \array_key_exists($name, $this->related)) {
43 45
            return $this->related[$name];
44 45
        }
45
46
        $value = $this->checkRelation($name);
47 3
48
        if ($value instanceof ActiveQueryInterface) {
49
            $this->setRelationDependencies($name, $value);
0 ignored issues
show
Bug introduced by
It seems like setRelationDependencies() 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

49
            $this->/** @scrutinizer ignore-call */ 
50
                   setRelationDependencies($name, $value);
Loading history...
50 51
            return $this->related[$name] = $value->findFor($name, $this);
0 ignored issues
show
Bug introduced by
$this of type Yiisoft\ActiveRecord\BaseActiveRecordTrait is incompatible with the type Yiisoft\ActiveRecord\ActiveRecordInterface expected by parameter $model of Yiisoft\ActiveRecord\Act...eryInterface::findFor(). ( Ignorable by Annotation )

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

50
            return $this->related[$name] = $value->findFor($name, /** @scrutinizer ignore-type */ $this);
Loading history...
Bug Best Practice introduced by
The property related does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
51
        }
52 51
53
        return $value;
54 51
    }
55
56 51
    public function checkRelation(string $name)
57
    {
58
        $getter = 'get' . $name;
59
60
        if (\method_exists($this, $getter)) {
61
            // read property, e.g. getName()
62
            return $this->$getter();
63
        }
64
65
        if (\method_exists($this, 'set' . $name)) {
66
            throw new InvalidCallException('Getting write-only property: ' . \get_class($this) . '::' . $name);
67
        }
68
69
        throw new UnknownPropertyException('Getting unknown property: ' . \get_class($this) . '::' . $name);
70
    }
71
72
73
    /**
74
     * Returns the relation object with the specified name.
75
     *
76
     * A relation is defined by a getter method which returns an {@see ActiveQueryInterface} object.
77
     *
78
     * It can be declared in either the Active Record class itself or one of its behaviors.
79
     *
80
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method
81
     * (case-sensitive).
82
     * @param bool $throwException whether to throw exception if the relation does not exist.
83
     *
84 87
     * @throws InvalidArgumentException if the named relation does not exist.
85
     * @throws \ReflectionException
86 87
     *
87
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist and
88
     * `$throwException` is `false`, `null` will be returned.
89
     */
90 87
    public function getRelation(string $name, bool $throwException = true)
91
    {
92
        $getter = 'get' . $name;
93
94
        try {
95
            // the relation could be defined in a behavior
96
            $relation = $this->$getter();
97
        } catch (UnknownMethodException $e) {
98
            if ($throwException) {
99
                throw new InvalidArgumentException(
100
                    \get_class($this) . ' has no relation named "' . $name . '".',
101
                    0,
0 ignored issues
show
Bug introduced by
0 of type integer is incompatible with the type array|null expected by parameter $errorInfo of Yiisoft\Db\Exception\Inv...xception::__construct(). ( Ignorable by Annotation )

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

101
                    /** @scrutinizer ignore-type */ 0,
Loading history...
102
                    $e
0 ignored issues
show
Bug introduced by
$e of type Yiisoft\Db\Exception\UnknownMethodException is incompatible with the type integer expected by parameter $code of Yiisoft\Db\Exception\Inv...xception::__construct(). ( Ignorable by Annotation )

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

102
                    /** @scrutinizer ignore-type */ $e
Loading history...
103 87
                );
104
            }
105
106
            return null;
107
        }
108
109
        if (!$relation instanceof ActiveQueryInterface) {
110
            if ($throwException) {
111 87
                throw new InvalidArgumentException(\get_class($this) . ' has no relation named "' . $name . '".');
112
            }
113 87
114 87
            return null;
115
        }
116 87
117
        if (\method_exists($this, $getter)) {
118
            /* relation name is case sensitive, trying to validate it when the relation is defined within this class */
119
            $method = new \ReflectionMethod($this, $getter);
120
            $realName = \lcfirst(\substr($method->getName(), 3));
121
122
            if ($realName !== $name) {
123
                if ($throwException) {
124
                    throw new InvalidArgumentException(
125
                        'Relation names are case sensitive. ' . \get_class($this)
126
                        . " has a relation named \"$realName\" instead of \"$name\"."
127
                    );
128 87
                }
129
130
                return null;
131
            }
132
        }
133
134
        return $relation;
135
    }
136
137
    /**
138
     * Checks if a property value is null.
139
     *
140 36
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
141
     *
142
     * @param string $name the property name or the event name
143 36
     *
144 6
     * @return bool whether the property value is null
145 6
     */
146
    public function __isset(string $name): bool
147
    {
148
        try {
149
            return $this->__get($name) !== null;
150
        } catch (\Throwable $t) {
151
            return false;
152
        } catch (\Exception $e) {
153
            return false;
154
        }
155
    }
156
157
    /**
158
     * Sets a component property to be null.
159
     *
160
     * This method overrides the parent implementation by clearing the specified attribute value.
161
     *
162
     * @param string $name the property name or the event name.
163 6
     *
164
     * @throws InvalidArgumentException
165 6
     * @throws \ReflectionException
166 6
     */
167 6
    public function __unset($name): void
168 6
    {
169
        if ($this->hasAttribute($name)) {
170
            unset($this->attributes[$name]);
171
            if (!empty($this->relationsDependencies[$name])) {
0 ignored issues
show
Bug Best Practice introduced by
The property relationsDependencies does not exist on Yiisoft\ActiveRecord\BaseActiveRecordTrait. Since you implemented __get, consider adding a @property annotation.
Loading history...
172
                $this->resetDependentRelations($name);
0 ignored issues
show
Bug introduced by
It seems like resetDependentRelations() 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

172
                $this->/** @scrutinizer ignore-call */ 
173
                       resetDependentRelations($name);
Loading history...
173
            }
174
        } elseif (\array_key_exists($name, $this->related)) {
175 6
            unset($this->related[$name]);
176
        } elseif ($this->getRelation($name, false) === null) {
177
            parent::__unset($name);
178
        }
179
    }
180
181
    /**
182
     * PHP setter magic method.
183
     *
184
     * This method is overridden so that AR attributes can be accessed like properties.
185
     *
186
     * @param string $name property name
187 57
     * @param mixed $value property value
188
     *
189 57
     * @throws InvalidCallException
190
     */
191 57
    public function __set($name, $value): void
192 57
    {
193
        if ($this->hasAttribute($name)) {
194 12
            if (
195
                !empty($this->relationsDependencies[$name])
0 ignored issues
show
Bug Best Practice introduced by
The property relationsDependencies does not exist on Yiisoft\ActiveRecord\BaseActiveRecordTrait. Since you implemented __get, consider adding a @property annotation.
Loading history...
196 57
                && (!\array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
197
            ) {
198 57
                $this->resetDependentRelations($name);
199
            }
200
            $this->attributes[$name] = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property attributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
201
        }
202
203
        if (method_exists($this, 'get' . $name)) {
204
            throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
205
        }
206
    }
207 3
208
    /**
209 3
     * Returns an iterator for traversing the attributes in the ActiveRecord.
210
     *
211 3
     * This method is required by the interface {@see \IteratorAggregate}.
212
     *
213
     * @return \ArrayIterator an iterator for traversing the items in the list.
214
     */
215
    public function getIterator(): \ArrayIterator
216
    {
217
        $attributes = $this->getAttributes();
0 ignored issues
show
Bug introduced by
It seems like getAttributes() 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

217
        /** @scrutinizer ignore-call */ 
218
        $attributes = $this->getAttributes();
Loading history...
218
219
        return new \ArrayIterator($attributes);
220
    }
221
222
    /**
223
     * Returns whether there is an element at the specified offset.
224
     *
225 27
     * This method is required by the SPL interface {@see \ArrayAccess}.
226
     *
227 27
     * It is implicitly called when you use something like `isset($model[$offset])`.
228
     *
229
     * @param mixed $offset the offset to check on.
230
     *
231
     * @return bool whether or not an offset exists.
232
     */
233
    public function offsetExists($offset): bool
234
    {
235
        return isset($this->$offset);
236
    }
237
238
    /**
239
     * Returns the element at the specified offset.
240
     *
241 99
     * This method is required by the SPL interface {@see \ArrayAccess}.
242
     *
243 99
     * It is implicitly called when you use something like `$value = $model[$offset];`.
244
     *
245
     * @param mixed $offset the offset to retrieve element.
246
     *
247
     * @return mixed the element at the offset, null if no element is found at the offset
248
     */
249
    public function offsetGet($offset)
250
    {
251
        return $this->$offset;
252
    }
253
254
    /**
255
     * Sets the element at the specified offset.
256
     *
257
     * This method is required by the SPL interface {@see \ArrayAccess}.
258 3
     *
259
     * It is implicitly called when you use something like `$model[$offset] = $item;`.
260 3
     *
261 3
     * @param mixed $offset the offset to set element
262
     * @param mixed $item the element value
263
     */
264
    public function offsetSet($offset, $item): void
265
    {
266
        $this->$offset = $item;
267
    }
268
269
    /**
270
     * Sets the element value at the specified offset to null.
271
     *
272
     * This method is required by the SPL interface {@see \ArrayAccess}.
273
     *
274
     * It is implicitly called when you use something like `unset($model[$offset])`.
275
     *
276
     * @param mixed $offset the offset to unset element
277
     */
278
    public function offsetUnset($offset): void
279
    {
280
        if (\property_exists($this, $offset)) {
281
            $this->$offset = null;
282
        } else {
283
            unset($this->$offset);
284
        }
285
    }
286
287
    /**
288
     * Returns a value indicating whether a property is defined for this component.
289
     *
290
     * A property is defined if:
291
     *
292
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
293
     *   case-insensitive).
294
     * - the class has a member variable with the specified name (when `$checkVars` is true).
295
     * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
296
     *
297
     * @param string $name the property name
298
     * @param bool $checkVars whether to treat member variables as properties
299
     * @param bool $checkBehaviors whether to treat behaviors' properties as properties of this component
300
     *
301
     * @return bool whether the property is defined
302
     *
303
     * {@see canGetProperty()}
304
     * {@see canSetProperty()}
305
     */
306
    public function hasProperty($name, $checkVars = true, $checkBehaviors = true): bool
307
    {
308
        return $this->canGetProperty($name, $checkVars, $checkBehaviors)
0 ignored issues
show
Unused Code introduced by
The call to Yiisoft\ActiveRecord\Bas...Trait::canGetProperty() has too many arguments starting with $checkBehaviors. ( Ignorable by Annotation )

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

308
        return $this->/** @scrutinizer ignore-call */ canGetProperty($name, $checkVars, $checkBehaviors)

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
309
            || $this->canSetProperty($name, false, $checkBehaviors);
0 ignored issues
show
Unused Code introduced by
The call to Yiisoft\ActiveRecord\Bas...Trait::canSetProperty() has too many arguments starting with $checkBehaviors. ( Ignorable by Annotation )

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

309
            || $this->/** @scrutinizer ignore-call */ canSetProperty($name, false, $checkBehaviors);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
310
    }
311
312
313
    public static function getConnection(): Connection
314
    {
315
        return ConnectionPool::getConnectionPool(self::$connectionId);
0 ignored issues
show
Bug introduced by
It seems like self::connectionId can also be of type null; however, parameter $key of Yiisoft\Db\Connection\Co...ol::getConnectionPool() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

315
        return ConnectionPool::getConnectionPool(/** @scrutinizer ignore-type */ self::$connectionId);
Loading history...
316
    }
317
318
    /**
319
     * @param string|null $value index value list connections in ConnectionPool.
320
     */
321
    public static function connectionId(string $value): void
322
    {
323
        self::$connectionId = $value;
324
    }
325
326
    public function canGetProperty(string $name, bool $checkVars = true): bool
327
    {
328
        if (\method_exists($this, 'get' . $name) || ($checkVars && \property_exists($this, $name))) {
329
            return true;
330
        }
331
332
        try {
333
            return $this->hasAttribute($name);
334
        } catch (\Exception $e) {
335
            /* `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
336
            return false;
337
        }
338
    }
339
340
    public function canSetProperty(string $name, bool $checkVars = true): bool
341
    {
342
        if (\method_exists($this, 'set' . $name) || ($checkVars && \property_exists($this, $name))) {
343
            return true;
344
        }
345
346
        try {
347
            return $this->hasAttribute($name);
348
        } catch (\Exception $e) {
349
            /* `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
350
            return false;
351
        }
352
    }
353
}
354