Model::__isset()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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