Passed
Pull Request — master (#298)
by
unknown
13:17
created

BaseActiveRecordTrait::__isset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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

176
                $this->/** @scrutinizer ignore-call */ 
177
                       resetDependentRelations($name);
Loading history...
177 10
            }
178 10
        } elseif (array_key_exists($name, $this->related)) {
179
            unset($this->related[$name]);
180 24
        }
181
    }
182
183
    /**
184
     * PHP setter magic method.
185
     *
186
     * This method is overridden so that AR attributes can be accessed like properties.
187
     *
188
     * @param string $name property name.
189
     * @param mixed $value property value.
190
     *
191
     * @throws InvalidCallException
192 186
     */
193
    public function __set(string $name, mixed $value): void
194 186
    {
195
        $setter = 'set' . $name;
196 186
        if (method_exists($this, $setter)) {
197 186
            $this->$setter($value);
198
            return;
199 24
        }
200
201 186
        if ($this->hasAttribute($name)) {
202
            if (
203
                !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...
204 186
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
205 5
            ) {
206
                $this->resetDependentRelations($name);
207 186
            }
208
            $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...
209
            return;
210
        }
211
212
        if (method_exists($this, 'get' . ucfirst($name))) {
213
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
214
        }
215
216 5
        throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name);
217
    }
218 5
219
    /**
220 5
     * Returns an iterator for traversing the attributes in the ActiveRecord.
221
     *
222
     * This method is required by the interface {@see IteratorAggregate}.
223
     *
224
     * @return ArrayIterator an iterator for traversing the items in the list.
225
     */
226
    public function getIterator(): ArrayIterator
227
    {
228
        $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

228
        /** @scrutinizer ignore-call */ 
229
        $attributes = $this->getAttributes();
Loading history...
229
230
        return new ArrayIterator($attributes);
231
    }
232
233
    /**
234 64
     * Returns whether there is an element at the specified offset.
235
     *
236 64
     * This method is required by the SPL interface {@see ArrayAccess}.
237
     *
238
     * It is implicitly called when you use something like `isset($model[$offset])`.
239
     *
240
     * @param mixed $offset the offset to check on.
241
     *
242
     * @return bool whether or not an offset exists.
243
     */
244
    public function offsetExists(mixed $offset): bool
245
    {
246
        return isset($this->$offset);
247
    }
248
249
    public function offsetGet(mixed $offset): mixed
250 246
    {
251
        return $this->$offset;
252 246
    }
253
254
    /**
255
     * Sets the element at the specified offset.
256
     *
257
     * This method is required by the SPL interface {@see ArrayAccess}.
258
     *
259
     * It is implicitly called when you use something like `$model[$offset] = $item;`.
260
     *
261
     * @param mixed $offset the offset to set element.
262
     * @param mixed $value the element value.
263
     */
264
    public function offsetSet(mixed $offset, mixed $value): void
265 4
    {
266
        $this->$offset = $value;
267 4
    }
268 4
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(mixed $offset): void
279 5
    {
280
        if (is_string($offset) && property_exists($this, $offset)) {
281 5
            $this->$offset = null;
282
        } else {
283
            unset($this->$offset);
284 5
        }
285
    }
286 5
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
     *
300
     * @return bool whether the property is defined.
301
     *
302
     * {@see canGetProperty()}
303
     * {@see canSetProperty()}
304
     */
305
    public function hasProperty(string $name, bool $checkVars = true): bool
306 5
    {
307
        return $this->canGetProperty($name, $checkVars)
308 5
            || $this->canSetProperty($name, false);
309 5
    }
310
311
    public function canGetProperty(string $name, bool $checkVars = true): bool
312 5
    {
313
        if (method_exists($this, 'get' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
314 5
            return true;
315 5
        }
316
317
        try {
318
            return $this->hasAttribute($name);
319 5
        } catch (Exception) {
320
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
321
            return false;
322
        }
323
    }
324
325
    public function canSetProperty(string $name, bool $checkVars = true): bool
326 13
    {
327
        if (method_exists($this, 'set' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
328 13
            return true;
329 8
        }
330
331
        try {
332
            return $this->hasAttribute($name);
333 5
        } catch (Exception) {
334
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
335
            return false;
336
        }
337
    }
338
}
339