Passed
Pull Request — master (#316)
by Sergei
21:20 queued 18:29
created

BaseActiveRecordTrait::__unset()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

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

48
        if ($this->/** @scrutinizer ignore-call */ hasAttribute($name)) {
Loading history...
49 368
            return null;
50
        }
51
52 251
        if (isset($this->related[$name]) || array_key_exists($name, $this->related)) {
53 39
            return $this->related[$name];
54
        }
55
56 241
        /** @var mixed $value */
57 152
        $value = $this->checkRelation($name);
58
59
        if ($value instanceof ActiveQuery) {
60 147
            $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

60
            $this->/** @scrutinizer ignore-call */ 
61
                   setRelationDependencies($name, $value);
Loading history...
61
            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\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

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

173
                $this->/** @scrutinizer ignore-call */ 
174
                       resetDependentRelations($name);
Loading history...
174 14
            }
175 14
        } elseif (array_key_exists($name, $this->related)) {
176
            unset($this->related[$name]);
177 10
        }
178 10
    }
179
180 24
    /**
181
     * PHP setter magic method.
182
     *
183
     * This method is overridden so that AR attributes can be accessed like properties.
184
     *
185
     * @param string $name property name.
186
     *
187
     * @throws InvalidCallException
188
     */
189
    public function __set(string $name, mixed $value): void
190
    {
191
        if ($this->hasAttribute($name)) {
192 186
            if (
193
                !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...
194 186
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
195
            ) {
196 186
                $this->resetDependentRelations($name);
197 186
            }
198
            $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...
199 24
            return;
200
        }
201 186
202
        if (method_exists($this, 'get' . ucfirst($name))) {
203
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
204 186
        }
205 5
206
        throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name);
207 186
    }
208
209
    /**
210
     * Returns a value indicating whether a property is defined for this component.
211
     *
212
     * A property is defined if:
213
     *
214
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
215
     *   case-insensitive).
216 5
     * - the class has a member variable with the specified name (when `$checkVars` is true).
217
     * - an attached behavior has a property of the given name (when `$checkBehaviors` is true).
218 5
     *
219
     * @param string $name the property name.
220 5
     * @param bool $checkVars whether to treat member variables as properties.
221
     *
222
     * @return bool whether the property is defined.
223
     *
224
     * {@see canGetProperty()}
225
     * {@see canSetProperty()}
226
     */
227
    public function hasProperty(string $name, bool $checkVars = true): bool
228
    {
229
        return $this->canGetProperty($name, $checkVars)
230
            || $this->canSetProperty($name, false);
231
    }
232
233
    public function canGetProperty(string $name, bool $checkVars = true): bool
234 64
    {
235
        if (method_exists($this, 'get' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
236 64
            return true;
237
        }
238
239
        try {
240
            return $this->hasAttribute($name);
241
        } catch (Exception) {
242
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
243
            return false;
244
        }
245
    }
246
247
    public function canSetProperty(string $name, bool $checkVars = true): bool
248
    {
249
        if (method_exists($this, 'set' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
250 246
            return true;
251
        }
252 246
253
        try {
254
            return $this->hasAttribute($name);
255
        } catch (Exception) {
256
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
257
            return false;
258
        }
259
    }
260
}
261