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

MagicPropertiesTrait::isAttributeChanged()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
rs 10
cc 3
nc 3
nop 2
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])) {
0 ignored issues
show
Bug Best Practice introduced by
The property oldAttributes does not exist on Yiisoft\ActiveRecord\Trait\MagicPropertiesTrait. Since you implemented __get, consider adding a @property annotation.
Loading history...
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
    protected function populateAttribute(string $name, mixed $value): void
197
    {
198
        if (property_exists($this, $name)) {
199
            $this->$name = $value;
200
        } else {
201
            $this->attributes[$name] = $value;
202
        }
203
    }
204
205
    /**
206
     * Returns a value indicating whether a property is defined for this component.
207
     *
208
     * A property is defined if:
209
     *
210
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
211
     *   case-insensitive).
212
     * - the class has a member variable with the specified name (when `$checkVars` is true).
213
     *
214
     * @param string $name the property name.
215
     * @param bool $checkVars whether to treat member variables as properties.
216
     *
217
     * @return bool whether the property is defined.
218
     *
219
     * {@see canGetProperty()}
220
     * {@see canSetProperty()}
221
     */
222
    public function hasProperty(string $name, bool $checkVars = true): bool
223
    {
224
        return $this->canGetProperty($name, $checkVars)
225
            || $this->canSetProperty($name, false);
226
    }
227
228
    public function canGetProperty(string $name, bool $checkVars = true): bool
229
    {
230
        if (method_exists($this, 'get' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
231
            return true;
232
        }
233
234
        try {
235
            return $this->hasAttribute($name);
236
        } catch (Exception) {
237
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
238
            return false;
239
        }
240
    }
241
242
    public function canSetProperty(string $name, bool $checkVars = true): bool
243
    {
244
        if (method_exists($this, 'set' . ucfirst($name)) || ($checkVars && property_exists($this, $name))) {
245
            return true;
246
        }
247
248
        try {
249
            return $this->hasAttribute($name);
250
        } catch (Exception) {
251
            /** `hasAttribute()` may fail on base/abstract classes in case automatic attribute list fetching used */
252
            return false;
253
        }
254
    }
255
}
256