MagicPropertiesTrait   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 199
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 46
eloc 61
c 5
b 0
f 0
dl 0
loc 199
rs 8.72

12 Methods

Rating   Name   Duplication   Size   Complexity  
A canSetProperty() 0 5 4
A __set() 0 20 5
A canGetProperty() 0 6 5
A setAttribute() 0 6 2
A setAttributeInternal() 0 9 4
A __get() 0 25 6
A __isset() 0 6 2
A populateAttribute() 0 6 3
A hasProperty() 0 7 6
A hasAttribute() 0 3 2
A getAttributesInternal() 0 3 1
A __unset() 0 14 6

How to fix   Complexity   

Complex Class

Complex classes like MagicPropertiesTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MagicPropertiesTrait, and based on these observations, apply Extract Interface, too.

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_merge;
18
use function get_object_vars;
19
use function in_array;
20
use function method_exists;
21
use function property_exists;
22
use function ucfirst;
23
24
/**
25
 * Trait to define magic methods to access values of an ActiveRecord instance.
26
 *
27
 * @method array getOldAttributes()
28
 * @see AbstractActiveRecord::getOldAttributes()
29
 *
30
 * @method mixed getOldAttribute(string $name)
31
 * @see AbstractActiveRecord::getOldAttribute()
32
 *
33
 * @method array getRelatedRecords()
34
 * @see AbstractActiveRecord::getRelatedRecords()
35
 *
36
 * @method bool hasDependentRelations(string $attribute)
37
 * @see AbstractActiveRecord::hasDependentRelations()
38
 *
39
 * @method bool isRelationPopulated(string $name)
40
 * @see ActiveRecordInterface::isRelationPopulated()
41
 *
42
 * @method void resetDependentRelations(string $attribute)
43
 * @see AbstractActiveRecord::resetDependentRelations()
44
 *
45
 * @method void resetRelation(string $name)
46
 * @see ActiveRecordInterface::resetRelation()
47
 *
48
 * @method ActiveRecordInterface|array|null retrieveRelation(string $name)
49
 * @see AbstractActiveRecord::retrieveRelation()
50
 */
51
trait MagicPropertiesTrait
52
{
53
    /** @psalm-var array<string, mixed> $attributes */
54
    private array $attributes = [];
55
56
    /**
57
     * PHP getter magic method.
58
     *
59
     * This method is overridden so that attributes and related objects can be accessed like properties.
60
     *
61
     * @param string $name property name.
62
     *
63
     * @throws InvalidArgumentException|InvalidCallException|InvalidConfigException|ReflectionException|Throwable
64
     * @throws UnknownPropertyException
65
     *
66
     * @throws Exception
67
     * @return mixed property value.
68
     *
69
     * {@see getAttribute()}
70
     */
71
    public function __get(string $name)
72
    {
73
        if ($this->hasAttribute($name)) {
74
            return $this->getAttribute($name);
0 ignored issues
show
Bug introduced by
The method getAttribute() does not exist on Yiisoft\ActiveRecord\Trait\MagicPropertiesTrait. Did you maybe mean getAttributesInternal()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

74
            return $this->/** @scrutinizer ignore-call */ getAttribute($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
75
        }
76
77
        if ($this->isRelationPopulated($name)) {
78
            return $this->getRelatedRecords()[$name];
79
        }
80
81
        if (method_exists($this, $getter = 'get' . ucfirst($name))) {
82
            /** read getter, e.g. getName() */
83
            return $this->$getter();
84
        }
85
86
        if (method_exists($this, 'get' . ucfirst($name) . 'Query')) {
87
            /** read relation query getter, e.g. getUserQuery() */
88
            return $this->retrieveRelation($name);
89
        }
90
91
        if (method_exists($this, 'set' . ucfirst($name))) {
92
            throw new InvalidCallException('Getting write-only property: ' . static::class . '::' . $name);
93
        }
94
95
        throw new UnknownPropertyException('Getting unknown property: ' . static::class . '::' . $name);
96
    }
97
98
    /**
99
     * Checks if a property value is null.
100
     *
101
     * This method overrides the parent implementation by checking if the named attribute is `null` or not.
102
     *
103
     * @param string $name the property name or the event name.
104
     *
105
     * @return bool whether the property value is null.
106
     */
107
    public function __isset(string $name): bool
108
    {
109
        try {
110
            return $this->__get($name) !== null;
111
        } 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...
112
            return false;
113
        }
114
    }
115
116
    /**
117
     * Sets a component property to be null.
118
     *
119
     * This method overrides the parent implementation by clearing the specified attribute value.
120
     *
121
     * @param string $name the property name or the event name.
122
     */
123
    public function __unset(string $name): void
124
    {
125
        if ($this->hasAttribute($name)) {
126
            unset($this->attributes[$name]);
127
128
            if ($name !== 'attributes' && isset(get_object_vars($this)[$name])) {
129
                $this->$name = null;
130
            }
131
132
            if ($this->hasDependentRelations($name)) {
133
                $this->resetDependentRelations($name);
134
            }
135
        } elseif ($this->isRelationPopulated($name)) {
136
            $this->resetRelation($name);
137
        }
138
    }
139
140
    /**
141
     * PHP setter magic method.
142
     *
143
     * This method is overridden so that AR attributes can be accessed like properties.
144
     *
145
     * @param string $name property name.
146
     *
147
     * @throws InvalidCallException|UnknownPropertyException
148
     */
149
    public function __set(string $name, mixed $value): void
150
    {
151
        if ($this->hasAttribute($name)) {
152
            $this->setAttributeInternal($name, $value);
153
            return;
154
        }
155
156
        if (method_exists($this, $setter = 'set' . ucfirst($name))) {
157
            $this->$setter($value);
158
            return;
159
        }
160
161
        if (
162
            method_exists($this, 'get' . ucfirst($name))
163
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
164
        ) {
165
            throw new InvalidCallException('Setting read-only property: ' . static::class . '::' . $name);
166
        }
167
168
        throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name);
169
    }
170
171
    public function hasAttribute(string $name): bool
172
    {
173
        return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true);
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

173
        return isset($this->attributes[$name]) || in_array($name, $this->/** @scrutinizer ignore-call */ attributes(), true);
Loading history...
174
    }
175
176
    public function setAttribute(string $name, mixed $value): void
177
    {
178
        if ($this->hasAttribute($name)) {
179
            $this->setAttributeInternal($name, $value);
180
        } else {
181
            throw new InvalidArgumentException(static::class . ' has no attribute named "' . $name . '".');
182
        }
183
    }
184
185
    /**
186
     * Returns a value indicating whether a property is defined for this component.
187
     *
188
     * A property is defined if:
189
     *
190
     * - the class has a getter or setter method associated with the specified name (in this case, property name is
191
     *   case-insensitive).
192
     * - the class has a member variable with the specified name (when `$checkVars` is true).
193
     *
194
     * @param string $name the property name.
195
     * @param bool $checkVars whether to treat member variables as properties.
196
     *
197
     * @return bool whether the property is defined.
198
     *
199
     * {@see canGetProperty()}
200
     * {@see canSetProperty()}
201
     */
202
    public function hasProperty(string $name, bool $checkVars = true): bool
203
    {
204
        return method_exists($this, 'get' . ucfirst($name))
205
            || method_exists($this, 'set' . ucfirst($name))
206
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
207
            || ($checkVars && property_exists($this, $name))
208
            || $this->hasAttribute($name);
209
    }
210
211
    public function canGetProperty(string $name, bool $checkVars = true): bool
212
    {
213
        return method_exists($this, 'get' . ucfirst($name))
214
            || method_exists($this, 'get' . ucfirst($name) . 'Query')
215
            || ($checkVars && property_exists($this, $name))
216
            || $this->hasAttribute($name);
217
    }
218
219
    public function canSetProperty(string $name, bool $checkVars = true): bool
220
    {
221
        return method_exists($this, 'set' . ucfirst($name))
222
            || ($checkVars && property_exists($this, $name))
223
            || $this->hasAttribute($name);
224
    }
225
226
    /** @psalm-return array<string, mixed> */
227
    protected function getAttributesInternal(): array
228
    {
229
        return array_merge($this->attributes, parent::getAttributesInternal());
230
    }
231
232
    protected function populateAttribute(string $name, mixed $value): void
233
    {
234
        if ($name !== 'attributes' && property_exists($this, $name)) {
235
            $this->$name = $value;
236
        } else {
237
            $this->attributes[$name] = $value;
238
        }
239
    }
240
241
    private function setAttributeInternal(string $name, mixed $value): void
242
    {
243
        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...
244
            && ($value === null || $this->getAttribute($name) !== $value)
245
        ) {
246
            $this->resetDependentRelations($name);
247
        }
248
249
        $this->populateAttribute($name, $value);
250
    }
251
}
252