Passed
Push — master ( 352e04...c5a508 )
by Sergei
02:48
created

MagicPropertiesTrait::setAttributeInternal()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 4
nc 2
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\AbstractActiveRecord;
10
use Yiisoft\ActiveRecord\ActiveRecordInterface;
11
use Yiisoft\Db\Exception\Exception;
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_diff;
18
use function array_flip;
19
use function array_intersect_key;
20
use function array_key_exists;
21
use function array_merge;
22
use function get_object_vars;
23
use function in_array;
24
use function method_exists;
25
use function property_exists;
26
use function ucfirst;
27
28
/**
29
 * Trait to define magic methods to access values of an ActiveRecord instance.
30
 *
31
 * @method array getOldAttributes()
32
 * @see AbstractActiveRecord::getOldAttributes()
33
 *
34
 * @method mixed getOldAttribute(string $name)
35
 * @see AbstractActiveRecord::getOldAttribute()
36
 *
37
 * @method array getRelatedRecords()
38
 * @see AbstractActiveRecord::getRelatedRecords()
39
 *
40
 * @method bool hasDependentRelations(string $attribute)
41
 * @see AbstractActiveRecord::hasDependentRelations()
42
 *
43
 * @method bool isRelationPopulated(string $name)
44
 * @see ActiveRecordInterface::isRelationPopulated()
45
 *
46
 * @method void resetDependentRelations(string $attribute)
47
 * @see AbstractActiveRecord::resetDependentRelations()
48
 *
49
 * @method void resetRelation(string $name)
50
 * @see ActiveRecordInterface::resetRelation()
51
 *
52
 * @method ActiveRecordInterface|array|null retrieveRelation(string $name)
53
 * @see AbstractActiveRecord::retrieveRelation()
54
 */
55
trait MagicPropertiesTrait
56
{
57
    private array $attributes = [];
58
59
    /**
60
     * PHP getter magic method.
61
     *
62
     * This method is overridden so that attributes and related objects can be accessed like properties.
63
     *
64
     * @param string $name property name.
65
     *
66
     * @throws InvalidArgumentException|InvalidCallException|InvalidConfigException|ReflectionException|Throwable
67
     * @throws UnknownPropertyException
68
     *
69
     * @throws Exception
70
     * @return mixed property value.
71
     *
72
     * {@see getAttribute()}
73
     */
74
    public function __get(string $name)
75
    {
76
        if ($this->hasAttribute($name)) {
77
            return $this->getAttribute($name);
78
        }
79
80
        if ($this->isRelationPopulated($name)) {
81
            return $this->getRelatedRecords()[$name];
82
        }
83
84
        if (method_exists($this, $getter = 'get' . ucfirst($name))) {
85
            /** read getter, e.g. getName() */
86
            return $this->$getter();
87
        }
88
89
        if (method_exists($this, 'get' . ucfirst($name) . 'Query')) {
90
            /** read relation query getter, e.g. getUserQuery() */
91
            return $this->retrieveRelation($name);
92
        }
93
94
        if (method_exists($this, 'set' . ucfirst($name))) {
95
            throw new InvalidCallException('Getting write-only property: ' . static::class . '::' . $name);
96
        }
97
98
        throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::' . $name);
99
    }
100
101
    /**
102
     * Checks if a property value is null.
103
     *
104
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
105
     *
106
     * @param string $name the property name or the event name.
107
     *
108
     * @return bool whether the property value is null.
109
     */
110
    public function __isset(string $name): bool
111
    {
112
        try {
113
            return $this->__get($name) !== null;
114
        } 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...
115
            return false;
116
        }
117
    }
118
119
    /**
120
     * Sets a component property to be null.
121
     *
122
     * This method overrides the parent implementation by clearing the specified attribute value.
123
     *
124
     * @param string $name the property name or the event name.
125
     */
126
    public function __unset(string $name): void
127
    {
128
        if ($this->hasAttribute($name)) {
129
            unset($this->attributes[$name]);
130
131
            if (property_exists($this, $name)) {
132
                $this->$name = null;
133
            }
134
135
            if ($this->hasDependentRelations($name)) {
136
                $this->resetDependentRelations($name);
137
            }
138
        } elseif ($this->isRelationPopulated($name)) {
139
            $this->resetRelation($name);
140
        }
141
    }
142
143
    /**
144
     * PHP setter magic method.
145
     *
146
     * This method is overridden so that AR attributes can be accessed like properties.
147
     *
148
     * @param string $name property name.
149
     *
150
     * @throws InvalidCallException|UnknownPropertyException
151
     */
152
    public function __set(string $name, mixed $value): void
153
    {
154
        if ($this->hasAttribute($name)) {
155
            $this->setAttributeInternal($name, $value);
156
            return;
157
        }
158
159
        if (method_exists($this, $setter = 'set' . ucfirst($name))) {
160
            $this->$setter($value);
161
            return;
162
        }
163
164
        if (
165
            method_exists($this, 'get' . ucfirst($name))
166
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
167
        ) {
168
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
169
        }
170
171
        throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name);
172
    }
173
174
    public function getAttribute(string $name): mixed
175
    {
176
        if (property_exists($this, $name)) {
177
            return get_object_vars($this)[$name] ?? null;
178
        }
179
180
        return $this->attributes[$name] ?? null;
181
    }
182
183
    public function getAttributes(array $names = null, array $except = []): array
184
    {
185
        $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

185
        $names ??= $this->/** @scrutinizer ignore-call */ attributes();
Loading history...
186
        $attributes = array_merge($this->attributes, get_object_vars($this));
187
188
        if ($except !== []) {
189
            $names = array_diff($names, $except);
190
        }
191
192
        return array_intersect_key($attributes, array_flip($names));
193
    }
194
195
    public function hasAttribute(string $name): bool
196
    {
197
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
198
    }
199
200
    public function isAttributeChanged(string $name, bool $identical = true): bool
201
    {
202
        $hasOldAttribute = array_key_exists($name, $this->getOldAttributes());
203
204
        if (!$hasOldAttribute) {
205
            return property_exists($this, $name) && array_key_exists($name, get_object_vars($this))
206
                || array_key_exists($name, $this->attributes);
207
        }
208
209
        if (property_exists($this, $name)) {
210
            return !array_key_exists($name, get_object_vars($this))
211
                || $this->getOldAttribute($name) !== $this->$name;
212
        }
213
214
        return !array_key_exists($name, $this->attributes)
215
            || $this->getOldAttribute($name) !== $this->attributes[$name];
216
    }
217
218
    public function setAttribute(string $name, mixed $value): void
219
    {
220
        if ($this->hasAttribute($name)) {
221
            $this->setAttributeInternal($name, $value);
222
        } else {
223
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
224
        }
225
    }
226
227
    /**
228
     * Returns a value indicating whether a property is defined for this component.
229
     *
230
     * A property is defined if:
231
     *
232
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
233
     *   case-insensitive).
234
     * - the class has a member variable with the specified name (when `$checkVars` is true).
235
     *
236
     * @param string $name the property name.
237
     * @param bool $checkVars whether to treat member variables as properties.
238
     *
239
     * @return bool whether the property is defined.
240
     *
241
     * {@see canGetProperty()}
242
     * {@see canSetProperty()}
243
     */
244
    public function hasProperty(string $name, bool $checkVars = true): bool
245
    {
246
        return method_exists($this, 'get' . ucfirst($name))
247
            || method_exists($this, 'set' . ucfirst($name))
248
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
249
            || ($checkVars && property_exists($this, $name))
250
            || $this->hasAttribute($name);
251
    }
252
253
    public function canGetProperty(string $name, bool $checkVars = true): bool
254
    {
255
        return method_exists($this, 'get' . ucfirst($name))
256
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
257
            || ($checkVars && property_exists($this, $name))
258
            || $this->hasAttribute($name);
259
    }
260
261
    public function canSetProperty(string $name, bool $checkVars = true): bool
262
    {
263
        return method_exists($this, 'set' . ucfirst($name))
264
            || ($checkVars && property_exists($this, $name))
265
            || $this->hasAttribute($name);
266
    }
267
268
    protected function populateAttribute(string $name, mixed $value): void
269
    {
270
        if (property_exists($this, $name)) {
271
            $this->$name = $value;
272
        } else {
273
            $this->attributes[$name] = $value;
274
        }
275
    }
276
277
    private function setAttributeInternal(string $name, mixed $value): void
278
    {
279
        if ($this->hasDependentRelations($name)
0 ignored issues
show
Coding Style introduced by
The first expression of a multi-line control structure must be on the line after the opening parenthesis
Loading history...
280
            && ($value === null || $this->getAttribute($name) !== $value)
281
        ) {
282
            $this->resetDependentRelations($name);
283
        }
284
285
        $this->populateAttribute($name, $value);
286
    }
287
}
288