Passed
Push — master ( 08b5f5...a963d9 )
by Melech
15:22
created

Model::internalGetNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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