Passed
Push — main ( be769d...9c5d5f )
by Diego
03:51
created

ModelTrait::__call()   D

Complexity

Conditions 17
Paths 207

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 29
nc 207
nop 2
dl 0
loc 50
rs 4.2958
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Blackmine\Model;
6
7
use Carbon\Carbon;
8
use Carbon\CarbonImmutable;
9
use Carbon\CarbonInterface;
10
use DateTimeImmutable;
11
use Blackmine\Tool\Inflect;
12
use Doctrine\Common\Collections\ArrayCollection;
13
use Doctrine\Common\Collections\Collection;
14
use ReflectionException;
15
use ReflectionProperty;
16
17
trait ModelTrait
18
{
19
    protected static array $direct_types = [
20
        "int", "string", "float", "array", "bool", "mixed"
21
    ];
22
23
    /**
24
     * @throws ReflectionException
25
     */
26
    public function __call(string $method, array $args): mixed
27
    {
28
        if ($this->isSetter($method)) {
29
            $property = $this->getProperty($method);
30
            if (property_exists($this, $property) && !is_null($args[0])) {
31
                $this->$property = $this->normalizeValue($property, $this->getPropertyType($property), $args[0]);
32
            }
33
        }
34
35
        if ($this->isGetter($method)) {
36
            $property = $this->getProperty($method);
37
            if (property_exists($this, $property)) {
38
                return $this->$property;
39
            }
40
        }
41
42
        if ($this->isIsser($method)) {
43
            $property = $this->getProperty($method);
44
            if (property_exists($this, $property)) {
45
                return $this->$property;
46
            }
47
48
            $property = "is_" . $property;
49
            if (property_exists($this, $property)) {
50
                return $this->$property;
51
            }
52
        }
53
54
        if ($this->isAdder($method)) {
55
            $property = Inflect::pluralize($this->getProperty($method));
56
            if (property_exists($this, $property) && $this->$property instanceof Collection) {
57
                $found = $this->$property->find($args[0]);
58
                if (!$found) {
59
                    $this->$property->add($args[0]);
60
                }
61
            }
62
        }
63
64
        if ($this->isRemover($method)) {
65
            $property = Inflect::pluralize($this->getProperty($method));
66
            if (property_exists($this, $property) && $this->$property instanceof Collection) {
67
                $found = $this->$property->find($args[0]);
68
                if ($found) {
69
                    $key = $this->$property->indexOf($found);
70
                    $this->$property->remove($key);
71
                }
72
            }
73
        }
74
75
        return null;
76
    }
77
78
    protected function isSetter(string $method): bool
79
    {
80
        return str_starts_with($method, "set");
81
    }
82
83
    protected function isGetter(string $method): bool
84
    {
85
        return str_starts_with($method, "get");
86
    }
87
88
    protected function isIsser(string $method): bool
89
    {
90
        return str_starts_with($method, "is");
91
    }
92
93
    protected function isAdder(string $method): bool
94
    {
95
        return str_starts_with($method, "add");
96
    }
97
98
    protected function isRemover(string $method): bool
99
    {
100
        return str_starts_with($method, "remove");
101
    }
102
103
    protected function getSetter(string $property): string
104
    {
105
        return "set" . Inflect::camelize($property);
106
    }
107
108
    protected function getGetter(string $property): string
109
    {
110
        return "get" . Inflect::camelize($property);
111
    }
112
113
    protected function getIsser(string $property): string
114
    {
115
        return "is" . Inflect::camelize($property);
116
    }
117
118
    protected function getAdder(string $property): string
119
    {
120
        return "add" . Inflect::camelize($property);
121
    }
122
123
    protected function getRemover(string $property): string
124
    {
125
        return "remove" . Inflect::camelize($property);
126
    }
127
128
    protected function getProperty(string $method): string
129
    {
130
        $test = preg_match('/[A-Z]/', $method, $matches, PREG_OFFSET_CAPTURE);
131
        if ($test) {
132
            return Inflect::snakeize(substr($method, $matches[0][1]));
133
        }
134
135
        return $method;
136
    }
137
138
    /**
139
     * @throws ReflectionException
140
     */
141
    protected function getPropertyType(string $property): string
142
    {
143
        $p = new ReflectionProperty($this, $property);
144
        return $p->getType()?->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

144
        return $p->getType()?->/** @scrutinizer ignore-call */ getName();
Loading history...
145
    }
146
147
    protected function normalizeValue(string $property, string $type, mixed $raw_value): mixed
148
    {
149
        if ($this->isRelatedModel($type)) {
150
            if ($raw_value instanceof $type) {
151
                return $raw_value;
152
            }
153
154
            $value = new $type();
155
            $value->fromArray($raw_value);
156
157
            return $value;
158
        }
159
160
        if (is_array($raw_value) && $this->isCollection($type)) {
161
            return $this->populateRelation($property, $type, $raw_value);
162
        }
163
164
        if ($raw_value instanceof Collection && $this->isCollection($type)) {
165
            if (get_class($raw_value) !== $type) {
166
                if (method_exists($raw_value, "getElements")) {
167
                    return new $type($raw_value->getElements());
168
                }
169
170
                return new $type($raw_value->toArray());
171
            }
172
173
            return $raw_value;
174
        }
175
176
        if ($type === CarbonImmutable::class) {
177
            if ($raw_value instanceof CarbonImmutable) {
178
                return $raw_value;
179
            }
180
181
            if ($raw_value instanceof DateTimeImmutable) {
182
                return CarbonImmutable::createFromTimestamp($raw_value->getTimestamp());
183
            }
184
185
            $timestamp = strtotime($raw_value);
186
            return Carbon::createFromTimestamp($timestamp)->toImmutable();
187
        }
188
189
        if ($this->isDirectType($type)) {
190
            return $raw_value;
191
        }
192
193
        return null;
194
    }
195
196
    protected function isDirectType(string $type): bool
197
    {
198
        return in_array($type, self::$direct_types, true);
199
    }
200
201
    protected function populateRelation(string $property, string $type, array $raw_value): ?ArrayCollection
202
    {
203
        $repository_class = static::getRepositoryClass();
204
        $model_class = $repository_class::getRelationClassFor($property);
205
206
        if ($model_class) {
207
            $ret = new $type();
208
            foreach ($raw_value as $item) {
209
                if ($item instanceof $model_class) {
210
                    $ret->add($item);
211
                }
212
213
                if (is_array($item)) {
214
                    $model = new $model_class();
215
                    $model->fromArray($item);
216
                    $ret->add($model);
217
                }
218
            }
219
220
            return $ret;
221
        }
222
223
        return new $type($raw_value);
224
    }
225
226
    protected function isRelatedModel(string $type): bool
227
    {
228
        if (class_exists($type)) {
229
            $interfaces = class_implements($type);
230
            return $interfaces && in_array(ModelInterface::class, $interfaces, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression $interfaces of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
231
        }
232
233
        return false;
234
    }
235
236
    protected function isCollection(string $type): bool
237
    {
238
        if (class_exists($type)) {
239
            $interfaces = class_implements($type);
240
            return $interfaces && in_array(Collection::class, $interfaces, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression $interfaces of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
241
        }
242
243
        return false;
244
    }
245
}
246