Model   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 537
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 50
eloc 115
dl 0
loc 537
rs 8.4
c 0
b 0
f 0

37 Methods

Rating   Name   Duplication   Size   Complexity  
A internalSetIfPropertyExists() 0 5 2
A asChangedArray() 0 4 1
A __get() 0 10 2
A updateProperties() 0 4 1
A offsetSet() 0 5 1
A asFlatValue() 0 4 1
A internalGetNew() 0 3 1
A jsonSerialize() 0 8 1
A fromValue() 0 17 5
A internalGetJsonPropertyValue() 0 3 1
A modify() 0 6 1
A asValue() 0 4 1
A internalGetPropertyTypeMethodName() 0 4 1
A shouldSetOriginalProperties() 0 3 1
A __toString() 0 3 1
A hasProperty() 0 4 1
A offsetUnset() 0 4 1
A internalOnlyProperties() 0 6 1
A internalGetAllProperties() 0 4 1
A internalSetOriginalProperty() 0 4 3
A __clone() 0 3 1
A internalCheckOnlyProperties() 0 7 2
A internalRemoveInternalProperties() 0 3 1
A asArray() 0 11 1
A withProperties() 0 8 1
A internalDoesPropertyTypeMethodExist() 0 4 1
A internalSetProperties() 0 15 2
A __isset() 0 16 3
A __set() 0 14 2
A internalSetPropertyValues() 0 6 1
A internalGetChangedProperties() 0 11 1
A internalSetOriginalPropertiesSetProperty() 0 3 1
A offsetExists() 0 5 1
A asOriginalArray() 0 4 1
A fromArray() 0 8 1
A getOriginalPropertyValue() 0 4 1
A offsetGet() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Model 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 Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Type\Model;
15
16
use Closure;
17
use JsonException;
18
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Valkyrja\Type\BuiltIn\Support\Arr;
20
use Valkyrja\Type\BuiltIn\Support\StrCase;
21
use Valkyrja\Type\Model\Contract\Model as Contract;
22
use Valkyrja\Type\Model\Exception\RuntimeException;
23
24
use function array_filter;
25
use function array_walk;
26
use function in_array;
27
use function is_array;
28
use function is_bool;
29
use function is_string;
30
use function json_encode;
31
use function property_exists;
32
33
use const ARRAY_FILTER_USE_BOTH;
34
use const JSON_THROW_ON_ERROR;
35
36
/**
37
 * Class Model.
38
 *
39
 * @author Melech Mizrachi
40
 *
41
 * @phpstan-consistent-constructor
42
 *  Will be overridden if need be
43
 */
44
abstract class Model implements Contract
45
{
46
    /**
47
     * Cached list of validation logic for models.
48
     *
49
     * @var array<string, string>
50
     */
51
    protected static array $cachedValidations = [];
52
53
    /**
54
     * Cached list of property/method exists validation logic for models.
55
     *
56
     * @var array<string, bool>
57
     */
58
    protected static array $cachedExistsValidations = [];
59
60
    /**
61
     * Whether to set the original properties on creation via static::fromArray().
62
     *
63
     * @var bool
64
     */
65
    protected static bool $shouldSetOriginalProperties = true;
66
67
    /**
68
     * The original properties.
69
     *
70
     * @var array<string, mixed>
71
     */
72
    private array $internalOriginalProperties = [];
73
74
    /**
75
     * Whether the original properties have been set.
76
     *
77
     * @var bool
78
     */
79
    private bool $internalOriginalPropertiesSet = false;
80
81
    /**
82
     * @inheritDoc
83
     *
84
     * @see https://psalm.dev/r/309e3a322e
85
     *
86
     * @param array<string, mixed> $properties The properties
87
     *
88
     * @return static
89
     */
90
    #[Override]
91
    public static function fromArray(array $properties): static
92
    {
93
        $model = static::internalGetNew($properties);
94
95
        $model->internalSetProperties($properties);
96
97
        return $model;
98
    }
99
100
    /**
101
     * @inheritDoc
102
     *
103
     * @throws JsonException
104
     */
105
    #[Override]
106
    public static function fromValue(mixed $value): static
107
    {
108
        if ($value instanceof static) {
109
            return $value;
110
        }
111
112
        if (! is_array($value) && ! is_string($value)) {
113
            $value = json_encode($value, JSON_THROW_ON_ERROR);
114
        }
115
116
        if (is_string($value)) {
117
            $value = Arr::fromString($value);
118
        }
119
120
        /** @var array<string, mixed> $value */
121
        return static::fromArray($value);
122
    }
123
124
    /**
125
     * Get a new static instance.
126
     *
127
     * @param array<string, mixed> $properties The properties
128
     *
129
     * @return static
130
     */
131
    protected static function internalGetNew(array $properties): static
132
    {
133
        return new static();
134
    }
135
136
    /**
137
     * Whether to set the original properties array.
138
     *
139
     * @return bool
140
     */
141
    protected static function shouldSetOriginalProperties(): bool
142
    {
143
        return static::$shouldSetOriginalProperties;
144
    }
145
146
    /**
147
     * @inheritDoc
148
     */
149
    #[Override]
150
    public function __get(string $name)
151
    {
152
        $methodName = $this->internalGetPropertyTypeMethodName($name, 'get');
153
154
        if ($this->internalDoesPropertyTypeMethodExist($methodName)) {
155
            return $this->$methodName();
156
        }
157
158
        return $this->{$name} ?? null;
159
    }
160
161
    /**
162
     * @inheritDoc
163
     */
164
    #[Override]
165
    public function __set(string $name, mixed $value): void
166
    {
167
        $methodName = $this->internalGetPropertyTypeMethodName($name, 'set');
168
169
        $this->internalSetOriginalProperty($name, $value);
170
171
        if ($this->internalDoesPropertyTypeMethodExist($methodName)) {
172
            $this->$methodName($value);
173
174
            return;
175
        }
176
177
        $this->{$name} = $value;
178
    }
179
180
    /**
181
     * @inheritDoc
182
     */
183
    #[Override]
184
    public function __isset(string $name): bool
185
    {
186
        $methodName = $this->internalGetPropertyTypeMethodName($name, 'isset');
187
188
        if ($this->internalDoesPropertyTypeMethodExist($methodName)) {
189
            $isset = $this->$methodName();
190
191
            if (! is_bool($isset)) {
192
                throw new RuntimeException("$methodName must return a boolean");
193
            }
194
195
            return $isset;
196
        }
197
198
        return isset($this->$name);
199
    }
200
201
    /**
202
     * @inheritDoc
203
     */
204
    #[Override]
205
    public function offsetExists($offset): bool
206
    {
207
        /** @var string $offset */
208
        return isset($this->{$offset});
209
    }
210
211
    /**
212
     * @inheritDoc
213
     */
214
    #[Override]
215
    public function offsetGet($offset): mixed
216
    {
217
        /** @var string $offset */
218
        return $this->__get($offset);
219
    }
220
221
    /**
222
     * @inheritDoc
223
     */
224
    #[Override]
225
    public function offsetSet($offset, $value): void
226
    {
227
        /** @var string $offset */
228
        $this->__set($offset, $value);
229
    }
230
231
    /**
232
     * @inheritDoc
233
     */
234
    #[Override]
235
    public function offsetUnset(mixed $offset): void
236
    {
237
        unset($this->{$offset});
238
    }
239
240
    /**
241
     * Determine whether the model has a property.
242
     *
243
     * @param string $property The property
244
     *
245
     * @return bool
246
     */
247
    #[Override]
248
    public function hasProperty(string $property): bool
249
    {
250
        return self::$cachedExistsValidations[static::class . $property] ??= property_exists($this, $property);
251
    }
252
253
    /**
254
     * @inheritDoc
255
     */
256
    #[Override]
257
    public function updateProperties(array $properties): void
258
    {
259
        $this->internalSetProperties($properties);
260
    }
261
262
    /**
263
     * @inheritDoc
264
     */
265
    #[Override]
266
    public function withProperties(array $properties): static
267
    {
268
        $model = clone $this;
269
270
        $model->internalSetProperties($properties);
271
272
        return $model;
273
    }
274
275
    /**
276
     * @inheritDoc
277
     */
278
    #[Override]
279
    public function modify(callable $closure): static
280
    {
281
        $new = clone $this;
282
283
        return $closure($new);
284
    }
285
286
    /**
287
     * @inheritDoc
288
     */
289
    #[Override]
290
    public function asValue(): static
291
    {
292
        return $this;
293
    }
294
295
    /**
296
     * @inheritDoc
297
     *
298
     * @throws JsonException
299
     */
300
    #[Override]
301
    public function asFlatValue(): string
302
    {
303
        return $this->__toString();
304
    }
305
306
    /**
307
     * @inheritDoc
308
     *
309
     * @param string ...$properties [optional] An array of properties to return
310
     *
311
     * @return array<string, mixed>
312
     */
313
    #[Override]
314
    public function asArray(string ...$properties): array
315
    {
316
        // Get the public properties
317
        $allProperties = $this->internalGetAllProperties();
318
319
        $this->internalRemoveInternalProperties($allProperties);
320
321
        $allProperties = $this->internalCheckOnlyProperties($allProperties, $properties);
322
323
        return $this->internalSetPropertyValues($allProperties, [$this, '__get']);
324
    }
325
326
    /**
327
     * @inheritDoc
328
     */
329
    #[Override]
330
    public function asChangedArray(): array
331
    {
332
        return $this->internalGetChangedProperties($this->asArray());
333
    }
334
335
    /**
336
     * @inheritDoc
337
     */
338
    #[Override]
339
    public function getOriginalPropertyValue(string $name): mixed
340
    {
341
        return $this->internalOriginalProperties[$name] ?? null;
342
    }
343
344
    /**
345
     * @inheritDoc
346
     */
347
    #[Override]
348
    public function asOriginalArray(): array
349
    {
350
        return $this->internalOriginalProperties;
351
    }
352
353
    /**
354
     * @inheritDoc
355
     */
356
    #[Override]
357
    public function jsonSerialize(): array
358
    {
359
        $allProperties = $this->internalGetAllProperties();
360
361
        $this->internalRemoveInternalProperties($allProperties);
362
363
        return $this->internalSetPropertyValues($allProperties, [$this, 'internalGetJsonPropertyValue']);
364
    }
365
366
    /**
367
     * @inheritDoc
368
     *
369
     * @throws JsonException
370
     */
371
    public function __toString(): string
372
    {
373
        return Arr::toString($this->jsonSerialize());
374
    }
375
376
    /**
377
     * Clone model.
378
     */
379
    public function __clone()
380
    {
381
        $this->internalSetOriginalPropertiesSetProperty();
382
    }
383
384
    /**
385
     * Set properties from an array of properties.
386
     *
387
     * @param array<string, mixed>               $properties  The properties to set
388
     * @param Closure(string, mixed): mixed|null $modifyValue [optional] The closure to modify the value before setting
389
     *
390
     * @return void
391
     */
392
    protected function internalSetProperties(array $properties, Closure|null $modifyValue = null): void
393
    {
394
        array_walk(
395
            $properties,
396
            function (mixed $value, string $property) use ($modifyValue): void {
397
                $this->internalSetIfPropertyExists(
398
                    $property,
399
                    $modifyValue !== null
400
                        ? $modifyValue($property, $value)
401
                        : $value
402
                );
403
            }
404
        );
405
406
        $this->internalSetOriginalPropertiesSetProperty();
407
    }
408
409
    /**
410
     * Set a property if it exists.
411
     *
412
     * @param string $property The property
413
     * @param mixed  $value    The value
414
     *
415
     * @return void
416
     */
417
    protected function internalSetIfPropertyExists(string $property, mixed $value): void
418
    {
419
        if ($this->hasProperty($property)) {
420
            // Set the property
421
            $this->__set($property, $value);
422
        }
423
    }
424
425
    /**
426
     * Set that original properties have been set.
427
     *
428
     * @return void
429
     */
430
    protected function internalSetOriginalPropertiesSetProperty(): void
431
    {
432
        $this->internalOriginalPropertiesSet = true;
433
    }
434
435
    /**
436
     * Get a property's isset method name.
437
     *
438
     * @param string $property The property
439
     * @param string $type     The type (get|set|isset)
440
     *
441
     * @return string
442
     */
443
    protected function internalGetPropertyTypeMethodName(string $property, string $type): string
444
    {
445
        return self::$cachedValidations[static::class . "$type$property"]
446
            ??= $type . StrCase::toStudlyCase($property);
447
    }
448
449
    /**
450
     * Determine if a property type method exists.
451
     *
452
     * @param string $methodName The method name
453
     *
454
     * @return bool
455
     */
456
    protected function internalDoesPropertyTypeMethodExist(string $methodName): bool
457
    {
458
        return self::$cachedExistsValidations[static::class . "exists$methodName"]
459
            ??= method_exists($this, $methodName);
460
    }
461
462
    /**
463
     * Set an original property.
464
     *
465
     * @param string $name  The property name
466
     * @param mixed  $value The value
467
     *
468
     * @return void
469
     */
470
    protected function internalSetOriginalProperty(string $name, mixed $value): void
471
    {
472
        if (! $this->internalOriginalPropertiesSet && static::shouldSetOriginalProperties()) {
473
            $this->internalOriginalProperties[$name] ??= $value;
474
        }
475
    }
476
477
    /**
478
     * Get all properties.
479
     *
480
     * @return array<string, mixed>
481
     */
482
    protected function internalGetAllProperties(): array
483
    {
484
        /** @var array<string, mixed> */
485
        return get_object_vars($this);
486
    }
487
488
    /**
489
     * Remove internal model properties from an array of properties.
490
     *
491
     * @param array<string, mixed> $properties The properties
492
     *
493
     * @return void
494
     */
495
    protected function internalRemoveInternalProperties(array &$properties): void
496
    {
497
        unset($properties['internalOriginalProperties'], $properties['internalOriginalPropertiesSet']);
498
    }
499
500
    /**
501
     * Check if an array of all properties should be filtered by another list of properties.
502
     *
503
     * @param array<string, mixed> $properties     The properties
504
     * @param string[]             $onlyProperties A list of properties to return
505
     *
506
     * @return array<string, mixed>
507
     */
508
    protected function internalCheckOnlyProperties(array $properties, array $onlyProperties): array
509
    {
510
        if (! empty($onlyProperties)) {
511
            return $this->internalOnlyProperties($properties, $onlyProperties);
512
        }
513
514
        return $properties;
515
    }
516
517
    /**
518
     * Get an array subset of properties to return from a given list out of the returnable properties.
519
     *
520
     * @param array<string, mixed> $allProperties All the properties returnable
521
     * @param string[]             $properties    The properties we wish to return
522
     *
523
     * @return array<string, mixed>
524
     */
525
    protected function internalOnlyProperties(array $allProperties, array $properties): array
526
    {
527
        return array_filter(
528
            $allProperties,
529
            static fn (mixed $value, string $property) => in_array($property, $properties, true),
530
            ARRAY_FILTER_USE_BOTH
531
        );
532
    }
533
534
    /**
535
     * Get the changed properties given an array of properties.
536
     *
537
     * @param array<string, mixed> $properties The properties to check the original properties against
538
     *
539
     * @return array<string, mixed>
540
     */
541
    protected function internalGetChangedProperties(array $properties): array
542
    {
543
        return array_filter(
544
            $properties,
545
            function (mixed $value, string $property) {
546
                /** @var mixed $originalProperty */
547
                $originalProperty = $this->internalOriginalProperties[$property] ?? null;
548
549
                return $originalProperty !== $value;
550
            },
551
            ARRAY_FILTER_USE_BOTH
552
        );
553
    }
554
555
    /**
556
     * Set property values.
557
     *
558
     * @param array<string, mixed> $properties The properties
559
     * @param callable             $callable   The callable
560
     *
561
     * @return array<string, mixed>
562
     */
563
    protected function internalSetPropertyValues(array $properties, callable $callable): array
564
    {
565
        array_walk($properties, static fn (mixed &$value, string $property): mixed => /** @var mixed $value */ $value = $callable($property));
566
567
        /** @var array<string, mixed> $properties */
568
        return $properties;
569
    }
570
571
    /**
572
     * Get a property's value for jsonSerialize.
573
     *
574
     * @param string $property The property
575
     *
576
     * @return mixed
577
     */
578
    protected function internalGetJsonPropertyValue(string $property): mixed
579
    {
580
        return $this->__get($property);
581
    }
582
}
583