Model::__set()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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