Passed
Pull Request — master (#318)
by Sergei
02:56
created

MagicPropertiesTrait::__isset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ActiveRecord\Trait;
6
7
use ReflectionException;
8
use Throwable;
9
use Yiisoft\ActiveRecord\ActiveQueryInterface;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidCallException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\UnknownPropertyException;
15
16
use function array_diff;
17
use function array_flip;
18
use function array_intersect_key;
19
use function array_key_exists;
20
use function array_merge;
21
use function get_object_vars;
22
use function in_array;
23
use function method_exists;
24
use function property_exists;
25
use function ucfirst;
26
27
/**
28
 * Trait to define magic methods to access values of an ActiveRecord instance.
29
 */
30
trait MagicPropertiesTrait
31
{
32
    private array $attributes = [];
33
34
    /**
35
     * PHP getter magic method.
36
     *
37
     * This method is overridden so that attributes and related objects can be accessed like properties.
38
     *
39
     * @param string $name property name.
40
     *
41
     * @throws InvalidArgumentException|InvalidCallException|InvalidConfigException|ReflectionException|Throwable
42
     * @throws UnknownPropertyException
43
     *
44
     * @throws Exception
45
     * @return mixed property value.
46
     *
47
     * {@see getAttribute()}
48
     */
49
    public function __get(string $name)
50
    {
51
        if ($this->hasAttribute($name)) {
52
            return $this->getAttribute($name);
53
        }
54
55
        if ($this->isRelationPopulated($name)) {
0 ignored issues
show
Bug introduced by
It seems like isRelationPopulated() 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

55
        if ($this->/** @scrutinizer ignore-call */ isRelationPopulated($name)) {
Loading history...
56
            return $this->getRelatedRecords()[$name];
0 ignored issues
show
Bug introduced by
It seems like getRelatedRecords() 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

56
            return $this->/** @scrutinizer ignore-call */ getRelatedRecords()[$name];
Loading history...
57
        }
58
59
        if (method_exists($this, $getter = 'get' . ucfirst($name))) {
60
            /** read getter, e.g. getName() */
61
            $value = $this->$getter();
62
63
            if ($value instanceof ActiveQueryInterface) {
64
                return $this->retrieveRelation($name);
0 ignored issues
show
Bug introduced by
It seems like retrieveRelation() 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

64
                return $this->/** @scrutinizer ignore-call */ retrieveRelation($name);
Loading history...
65
            }
66
67
            return $value;
68
        }
69
70
        if (method_exists($this, 'set' . ucfirst($name))) {
71
            throw new InvalidCallException('Getting write-only property: ' . static::class . '::' . $name);
72
        }
73
74
        throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::' . $name);
75
    }
76
77
    /**
78
     * Checks if a property value is null.
79
     *
80
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
81
     *
82
     * @param string $name the property name or the event name.
83
     *
84
     * @return bool whether the property value is null.
85
     */
86
    public function __isset(string $name): bool
87
    {
88
        try {
89
            return $this->__get($name) !== null;
90
        } 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...
91
            return false;
92
        }
93
    }
94
95
    /**
96
     * Sets a component property to be null.
97
     *
98
     * This method overrides the parent implementation by clearing the specified attribute value.
99
     *
100
     * @param string $name the property name or the event name.
101
     */
102
    public function __unset(string $name): void
103
    {
104
        if ($this->hasAttribute($name)) {
105
            unset($this->attributes[$name]);
106
107
            if ($this->hasDependentRelations($name)) {
0 ignored issues
show
Bug introduced by
It seems like hasDependentRelations() 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

107
            if ($this->/** @scrutinizer ignore-call */ hasDependentRelations($name)) {
Loading history...
108
                $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

108
                $this->/** @scrutinizer ignore-call */ 
109
                       resetDependentRelations($name);
Loading history...
109
            }
110
        } elseif ($this->isRelationPopulated($name)) {
111
            $this->resetRelation($name);
0 ignored issues
show
Bug introduced by
It seems like resetRelation() 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

111
            $this->/** @scrutinizer ignore-call */ 
112
                   resetRelation($name);
Loading history...
112
        }
113
    }
114
115
    /**
116
     * PHP setter magic method.
117
     *
118
     * This method is overridden so that AR attributes can be accessed like properties.
119
     *
120
     * @param string $name property name.
121
     *
122
     * @throws InvalidCallException|UnknownPropertyException
123
     */
124
    public function __set(string $name, mixed $value): void
125
    {
126
        if ($this->hasAttribute($name)) {
127
            if (
128
                $this->hasDependentRelations($name)
129
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
130
            ) {
131
                $this->resetDependentRelations($name);
132
            }
133
134
            $this->attributes[$name] = $value;
135
            return;
136
        }
137
138
        if (method_exists($this, $setter = 'set' . ucfirst($name))) {
139
            $this->$setter($value);
140
            return;
141
        }
142
143
        if (method_exists($this, 'get' . ucfirst($name))) {
144
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
145
        }
146
147
        throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name);
148
    }
149
150
    public function getAttribute(string $name): mixed
151
    {
152
        return $this->attributes[$name] ?? null;
153
    }
154
155
    public function getAttributes(array $names = null, array $except = []): array
156
    {
157
        $names ??= $this->attributes();
0 ignored issues
show
Bug introduced by
It seems like attributes() 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

157
        $names ??= $this->/** @scrutinizer ignore-call */ attributes();
Loading history...
158
        $attributes = array_merge($this->attributes, get_object_vars($this));
159
160
        if ($except !== []) {
161
            $names = array_diff($names, $except);
162
        }
163
164
        return array_intersect_key($attributes, array_flip($names));
165
    }
166
167
    public function hasAttribute(string $name): bool
168
    {
169
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
170
    }
171
172
    public function isAttributeChanged(string $name, bool $identical = true): bool
173
    {
174
        if (isset($this->attributes[$name], $this->oldAttributes[$name])) {
175
            return $this->attributes[$name] !== $this->oldAttributes[$name];
176
        }
177
178
        return isset($this->attributes[$name]) || isset($this->oldAttributes[$name]);
179
    }
180
181
    public function setAttribute(string $name, mixed $value): void
182
    {
183
        if ($this->hasAttribute($name)) {
184
            if (
185
                $this->hasDependentRelations($name)
186
                && (!array_key_exists($name, $this->attributes) || $this->attributes[$name] !== $value)
187
            ) {
188
                $this->resetDependentRelations($name);
189
            }
190
            $this->attributes[$name] = $value;
191
        } else {
192
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
193
        }
194
    }
195
196
    /**
197
     * Populates an active record object using a row of data from the database/storage.
198
     *
199
     * This is an internal method meant to be called to create active record objects after fetching data from the
200
     * database. It is mainly used by {@see ActiveQuery} to populate the query results into active records.
201
     *
202
     * @param array|object $row Attribute values (name => value).
203
     */
204
    public function populateRecord(array|object $row): void
205
    {
206
        foreach ($row as $name => $value) {
207
            if (property_exists($this, $name)) {
208
                $this->$name = $value;
209
            } else {
210
                $this->attributes[$name] = $value;
211
            }
212
213
            $this->oldAttributes[$name] = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property oldAttributes does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
214
        }
215
216
        $this->related = [];
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...
217
        $this->relationsDependencies = [];
0 ignored issues
show
Bug Best Practice introduced by
The property relationsDependencies does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
218
    }
219
220
    /**
221
     * Returns a value indicating whether a property is defined for this component.
222
     *
223
     * A property is defined if:
224
     *
225
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
226
     *   case-insensitive).
227
     * - the class has a member variable with the specified name (when `$checkVars` is true).
228
     *
229
     * @param string $name the property name.
230
     * @param bool $checkVars whether to treat member variables as properties.
231
     *
232
     * @return bool whether the property is defined.
233
     *
234
     * {@see canGetProperty()}
235
     * {@see canSetProperty()}
236
     */
237
    public function hasProperty(string $name, bool $checkVars = true): bool
238
    {
239
        return $this->canGetProperty($name, $checkVars)
240
            || $this->canSetProperty($name, false);
241
    }
242
243
    public function canGetProperty(string $name, bool $checkVars = true): bool
244
    {
245
        if (method_exists($this, 'get' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
246
            return true;
247
        }
248
249
        try {
250
            return $this->hasAttribute($name);
251
        } catch (Exception) {
252
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
253
            return false;
254
        }
255
    }
256
257
    public function canSetProperty(string $name, bool $checkVars = true): bool
258
    {
259
        if (method_exists($this, 'set' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
260
            return true;
261
        }
262
263
        try {
264
            return $this->hasAttribute($name);
265
        } catch (Exception) {
266
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
267
            return false;
268
        }
269
    }
270
}
271