Passed
Pull Request — master (#264)
by Wilmer
02:50
created

BaseActiveRecordTrait::__get()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 13
c 2
b 0
f 0
dl 0
loc 26
rs 9.2222
ccs 13
cts 13
cp 1
cc 6
nc 5
nop 1
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord;
6
7
use ArrayAccess;
8
use ArrayIterator;
9
use Error;
10
use Exception;
11
use IteratorAggregate;
12
use ReflectionException;
13
use ReflectionMethod;
14
use Throwable;
15
use Yiisoft\Db\Exception\InvalidArgumentException;
16
use Yiisoft\Db\Exception\InvalidCallException;
17
use Yiisoft\Db\Exception\InvalidConfigException;
18
use Yiisoft\Db\Exception\UnknownPropertyException;
19
20
use function array_key_exists;
21
use function lcfirst;
22
use function method_exists;
23
use function property_exists;
24
use function substr;
25
use function ucfirst;
26
27
trait BaseActiveRecordTrait
28
{
29
    private static string|null $connectionId = null;
30
31
    /**
32
     * PHP getter magic method.
33
     *
34
     * This method is overridden so that attributes and related objects can be accessed like properties.
35
     *
36
     * @param string $name property name.
37
     *
38
     * @throws InvalidArgumentException|InvalidCallException|InvalidConfigException|ReflectionException|Throwable
39
     * @throws UnknownPropertyException
40
     *
41
     * @return mixed property value.
42
     *
43
     * {@see getAttribute()}
44
     */
45
    public function __get(string $name)
46 406
    {
47
        if (isset($this->attributes[$name]) || array_key_exists($name, $this->attributes)) {
48 406
            return $this->attributes[$name];
49 368
        }
50
51
        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

51
        if ($this->/** @scrutinizer ignore-call */ hasAttribute($name)) {
Loading history...
52 251
            return null;
53 39
        }
54
55
        if ($this->activeRelation->has($name)) {
0 ignored issues
show
Bug Best Practice introduced by
The property activeRelation does not exist on Yiisoft\ActiveRecord\BaseActiveRecordTrait. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

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

55
        if ($this->activeRelation->/** @scrutinizer ignore-call */ has($name)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
56 241
            return $this->activeRelation->get($name);
57 152
        }
58
59
        /** @var mixed $value */
60 147
        $value = $this->checkRelation($name);
61
62 131
        if ($value instanceof ActiveQuery) {
63 101
            $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

63
            $this->/** @scrutinizer ignore-call */ 
64
                   setRelationDependencies($name, $value);
Loading history...
64 101
            $valueForFind = $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\ActiveQuery::findFor(). ( Ignorable by Annotation )

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

64
            $valueForFind = $value->findFor($name, /** @scrutinizer ignore-type */ $this);
Loading history...
65
            $this->activeRelation->set($name, $valueForFind);
0 ignored issues
show
Bug introduced by
Accessing activeRelation on the interface Yiisoft\ActiveRecord\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
66
67 44
            return $valueForFind;
68
        }
69
70 147
        return $value;
71
    }
72 147
73
    public function checkRelation(string $name): mixed
74 147
    {
75
        $getter = 'get' . ucfirst($name);
76 139
77
        if (method_exists($this, $getter)) {
78
            /** read property, e.g. getName() */
79 8
            return $this->$getter();
80 4
        }
81
82
        if (method_exists($this, 'set' . ucfirst($name))) {
83 4
            throw new InvalidCallException('Getting write-only property: ' . static::class . '::' . $name);
84
        }
85
86
        throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::' . $name);
87
    }
88
89
    /**
90
     * Returns the relation object with the specified name.
91
     *
92
     * A relation is defined by a getter method which returns an {@see ActiveQueryInterface} object.
93
     *
94
     * It can be declared in either the Active Record class itself or one of its behaviors.
95
     *
96
     * @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method
97
     * (case-sensitive).
98
     * @param bool $throwException whether to throw exception if the relation does not exist.
99
     *
100
     * @throws InvalidArgumentException if the named relation does not exist.
101
     * @throws ReflectionException
102 228
     *
103
     * @return ActiveQueryInterface|null the relational query object. If the relation does not exist and
104 228
     * `$throwException` is `false`, `null` will be returned.
105
     */
106
    public function getRelation(string $name, bool $throwException = true): ActiveQueryInterface|null
107
    {
108 228
        $getter = 'get' . ucfirst($name);
109 4
110 4
        try {
111 4
            /** the relation could be defined in a behavior */
112
            $relation = $this->$getter();
113
        } catch (Error) {
114 4
            if ($throwException) {
115
                throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".');
116
            }
117 224
118 4
            return null;
119 4
        }
120
121
        if (!$relation instanceof ActiveQueryInterface) {
122 4
            if ($throwException) {
123
                throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".');
124
            }
125 220
126
            return null;
127 220
        }
128 220
129
        if (method_exists($this, $getter)) {
130 220
            /** relation name is case sensitive, trying to validate it when the relation is defined within this class */
131 4
            $method = new ReflectionMethod($this, $getter);
132 4
            $realName = lcfirst(substr($method->getName(), 3));
133 4
134 4
            if ($realName !== $name) {
135
                if ($throwException) {
136
                    throw new InvalidArgumentException(
137
                        'Relation names are case sensitive. ' . static::class
138 4
                        . " has a relation named \"$realName\" instead of \"$name\"."
139
                    );
140
                }
141
142 216
                return null;
143
            }
144
        }
145
146
        return $relation;
147
    }
148
149
    /**
150
     * Checks if a property value is null.
151
     *
152
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
153
     *
154 81
     * @param string $name the property name or the event name.
155
     *
156
     * @return bool whether the property value is null.
157 81
     */
158 8
    public function __isset(string $name): bool
159 8
    {
160
        try {
161
            return $this->__get($name) !== null;
162
        } catch (Throwable) {
163
            return false;
164
        }
165
    }
166
167
    /**
168
     * Sets a component property to be null.
169
     *
170 24
     * This method overrides the parent implementation by clearing the specified attribute value.
171
     *
172 24
     * @param string $name the property name or the event name.
173 14
     */
174 14
    public function __unset(string $name): void
175 14
    {
176
        if ($this->hasAttribute($name)) {
177 10
            unset($this->attributes[$name]);
178 10
            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...
179
                $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

179
                $this->/** @scrutinizer ignore-call */ 
180
                       resetDependentRelations($name);
Loading history...
180 24
            }
181
        } elseif ($this->activeRelation->has($name)) {
0 ignored issues
show
Bug Best Practice introduced by
The property activeRelation does not exist on Yiisoft\ActiveRecord\BaseActiveRecordTrait. Since you implemented __get, consider adding a @property annotation.
Loading history...
182
            $this->activeRelation->remove($name);
183
        }
184
    }
185
186
    /**
187
     * PHP setter magic method.
188
     *
189
     * This method is overridden so that AR attributes can be accessed like properties.
190
     *
191
     * @param string $name property name.
192 186
     * @param mixed $value property value.
193
     *
194 186
     * @throws InvalidCallException
195
     */
196 186
    public function __set(string $name, mixed $value): void
197 186
    {
198
        if ($this->hasAttribute($name)) {
199 24
            if (
200
                !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...
201 186
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
202
            ) {
203
                $this->resetDependentRelations($name);
204 186
            }
205 5
            $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...
206
        }
207 186
208
        if (method_exists($this, 'get' . ucfirst($name))) {
209
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
210
        }
211
    }
212
213
    /**
214
     * Returns an iterator for traversing the attributes in the ActiveRecord.
215
     *
216 5
     * This method is required by the interface {@see IteratorAggregate}.
217
     *
218 5
     * @return ArrayIterator an iterator for traversing the items in the list.
219
     */
220 5
    public function getIterator(): ArrayIterator
221
    {
222
        $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

222
        /** @scrutinizer ignore-call */ 
223
        $attributes = $this->getAttributes();
Loading history...
223
224
        return new ArrayIterator($attributes);
225
    }
226
227
    /**
228
     * Returns whether there is an element at the specified offset.
229
     *
230
     * This method is required by the SPL interface {@see ArrayAccess}.
231
     *
232
     * It is implicitly called when you use something like `isset($model[$offset])`.
233
     *
234 64
     * @param mixed $offset the offset to check on.
235
     *
236 64
     * @return bool whether or not an offset exists.
237
     */
238
    public function offsetExists(mixed $offset): bool
239
    {
240
        return isset($this->$offset);
241
    }
242
243
    public function offsetGet(mixed $offset): mixed
244
    {
245
        return $this->$offset;
246
    }
247
248
    /**
249
     * Sets the element at the specified offset.
250 246
     *
251
     * This method is required by the SPL interface {@see ArrayAccess}.
252 246
     *
253
     * It is implicitly called when you use something like `$model[$offset] = $item;`.
254
     *
255
     * @param mixed $offset the offset to set element.
256
     * @param mixed $item the element value.
257
     */
258
    public function offsetSet(mixed $offset, mixed $item): void
259
    {
260
        $this->$offset = $item;
261
    }
262
263
    /**
264
     * Sets the element value at the specified offset to null.
265 4
     *
266
     * This method is required by the SPL interface {@see ArrayAccess}.
267 4
     *
268 4
     * It is implicitly called when you use something like `unset($model[$offset])`.
269
     *
270
     * @param mixed $offset the offset to unset element
271
     */
272
    public function offsetUnset(mixed $offset): void
273
    {
274
        if (is_string($offset) && property_exists($this, $offset)) {
275
            $this->$offset = null;
276
        } else {
277
            unset($this->$offset);
278
        }
279 5
    }
280
281 5
    /**
282
     * Returns a value indicating whether a property is defined for this component.
283
     *
284 5
     * A property is defined if:
285
     *
286 5
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
287
     *   case-insensitive).
288
     * - the class has a member variable with the specified name (when `$checkVars` is true).
289
     * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
290
     *
291
     * @param string $name the property name.
292
     * @param bool $checkVars whether to treat member variables as properties.
293
     *
294
     * @return bool whether the property is defined.
295
     *
296
     * {@see canGetProperty()}
297
     * {@see canSetProperty()}
298
     */
299
    public function hasProperty(string $name, bool $checkVars = true): bool
300
    {
301
        return $this->canGetProperty($name, $checkVars)
302
            || $this->canSetProperty($name, false);
303
    }
304
305
    public function canGetProperty(string $name, bool $checkVars = true): bool
306 5
    {
307
        if (method_exists($this, 'get' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
308 5
            return true;
309 5
        }
310
311
        try {
312 5
            return $this->hasAttribute($name);
313
        } catch (Exception) {
314 5
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
315 5
            return false;
316
        }
317
    }
318
319 5
    public function canSetProperty(string $name, bool $checkVars = true): bool
320
    {
321
        if (method_exists($this, 'set' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
322
            return true;
323
        }
324
325
        try {
326 13
            return $this->hasAttribute($name);
327
        } catch (Exception) {
328 13
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
329 8
            return false;
330
        }
331
    }
332
}
333