Passed
Push — master ( 97aa2a...b1b817 )
by y
06:13
created

Record::isUnique()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Helix\DB;
4
5
use DateTime;
6
use DateTimeImmutable;
7
use DateTimeZone;
8
use Generator;
9
use Helix\DB;
10
use ReflectionClass;
11
use ReflectionNamedType;
12
use ReflectionProperty;
13
use stdClass;
14
15
/**
16
 * Represents an "active record" table, derived from an annotated class implementing {@link EntityInterface}.
17
 *
18
 * Class Annotations:
19
 *
20
 * - `@record TABLE`
21
 *
22
 * Property Annotations:
23
 *
24
 * - `@col` or `@column`
25
 * - `@unique` or `@unique <SHARED_IDENTIFIER>` for a single or multi-column unique-key.
26
 *  - The shared identifier must be alphabetical, allowing underscores.
27
 *  - The identifier can be arbitrary, but it's necessary in order to associate component properties.
28
 *  - The column/s may be nullable; MySQL and SQLite don't enforce uniqueness for NULL.
29
 * - `@eav <TABLE>`
30
 *
31
 * Property types are preserved.
32
 * Properties which are objects can be dehydrated/rehydrated if they're strictly typed.
33
 * Strict typing is preferred, but annotations and finally default values are used as fallbacks.
34
 *
35
 * > Annotating the types `String` (capital "S") or `STRING` (all caps) results in `TEXT` and `BLOB`
36
 *
37
 * @method static static factory(DB $db, EntityInterface $proto, string $table, array $properties, array $unique, array $eav = [])
38
 *
39
 * @TODO Auto-map singular foreign entity columns.
40
 */
41
class Record extends Table
42
{
43
44
    protected const RX_RECORD = '/\*\h*@record\h+(?<table>\w+)/i';
45
    protected const RX_IS_COLUMN = '/\*\h*@col(umn)?\b/i';
46
    protected const RX_UNIQUE = '/\*\h*@unique(\h+(?<ident>[a-z_]+))?/i';
47
    protected const RX_VAR = '/\*\h*@var\h+(?<type>\S+)/i'; // includes pipes and backslashes
48
    protected const RX_NULL = '/(\bnull\|)|(\|null\b)/i';
49
    protected const RX_EAV = '/\*\h*@eav\h+(?<table>\w+)/i';
50
    protected const RX_EAV_VAR = '/\*\h*@var\h+(?<type>\w+)\[\]/i'; // typed array
51
52
    /**
53
     * Maps complex types to storage types.
54
     *
55
     * @see Schema::T_CONST_NAMES keys
56
     */
57
    protected const DEHYDRATE_AS = [
58
        'array' => 'STRING', // blob. eav is better than this for 1D arrays.
59
        'object' => 'STRING', // blob.
60
        stdClass::class => 'STRING', // blob
61
        DateTime::class => 'DateTime',
62
        DateTimeImmutable::class => 'DateTime',
63
    ];
64
65
    /**
66
     * The number of entities to load EAV entries for at a time,
67
     * during {@link Record::fetchEach()} iteration.
68
     */
69
    protected const EAV_BATCH_LOAD = 256;
70
71
    /**
72
     * Maps annotated/native scalar types to storage types acceptable for `settype()`
73
     *
74
     * @see Schema::T_CONST_NAMES keys
75
     */
76
    protected const SCALARS = [
77
        'bool' => 'bool',
78
        'boolean' => 'bool',    // gettype()
79
        'double' => 'float',    // gettype()
80
        'false' => 'bool',      // @var
81
        'float' => 'float',
82
        'int' => 'int',
83
        'integer' => 'int',     // gettype()
84
        'NULL' => 'string',     // gettype()
85
        'number' => 'string',   // @var
86
        'scalar' => 'string',   // @var
87
        'string' => 'string',
88
        'String' => 'String',   // @var
89
        'STRING' => 'STRING',   // @var
90
        'true' => 'bool',       // @var
91
    ];
92
93
    /**
94
     * `[property => EAV]`
95
     *
96
     * @var EAV[]
97
     */
98
    protected $eav = [];
99
100
    /**
101
     * The specific classes used to hydrate classed properties, like `DateTime`.
102
     *
103
     * `[ property => class ]`
104
     *
105
     * @var string[]
106
     */
107
    protected $hydration = [];
108
109
    /**
110
     * `[ property => is nullable ]`
111
     *
112
     * @var bool[]
113
     */
114
    protected $nullable = [];
115
116
    /**
117
     * `[property => ReflectionProperty]`
118
     *
119
     * > Programmer's Note: Manipulating subclasses through a parent's reflection is allowed.
120
     *
121
     * @var ReflectionProperty[]
122
     */
123
    protected $properties = [];
124
125
    /**
126
     * A boilerplate instance of the class, to clone and populate.
127
     *
128
     * @var EntityInterface
129
     */
130
    protected $proto;
131
132
    /**
133
     * Storage types.
134
     *
135
     * `[property => type]`
136
     *
137
     * @var string[]
138
     */
139
    protected $types = [];
140
141
    /**
142
     * Column groupings for unique constraints.
143
     * - Column-level constraints are enumerated names.
144
     * - Table-level (multi-column) constraints are names grouped under an arbitrary shared identifier.
145
     *
146
     * `[ 'foo', 'my_multi'=>['bar','baz'], ... ]`
147
     *
148
     * @var array
149
     */
150
    protected $unique;
151
152
    /**
153
     * @var DateTimeZone
154
     */
155
    protected DateTimeZone $utc;
156
157
    /**
158
     * Constructs record-access from an annotated class.
159
     *
160
     * If a prototype isn't given for `$class`, this defaults to creating an instance
161
     * via reflection (without invoking the constructor).
162
     *
163
     * @param DB $db
164
     * @param string|EntityInterface $class Class or prototype instance.
165
     * @return Record
166
     */
167
    public static function fromClass(DB $db, $class)
168
    {
169
        $rClass = new ReflectionClass($class);
170
        assert($rClass->implementsInterface(EntityInterface::class));
171
        $properties = [];
172
        $unique = [];
173
        $eav = [];
174
        foreach ($rClass->getProperties() as $rProp) {
175
            $doc = $rProp->getDocComment();
176
            if (preg_match(static::RX_IS_COLUMN, $doc)) {
177
                $properties[] = $rProp->getName();
178
                if (preg_match(static::RX_UNIQUE, $doc, $rx)) {
179
                    if (isset($rx['ident'])) {
180
                        $unique[$rx['ident']][] = $rProp->getName();
181
                    } else {
182
                        $unique[] = $rProp->getName();
183
                    }
184
                }
185
            } elseif (preg_match(static::RX_EAV, $doc, $rx)) {
186
                preg_match(static::RX_EAV_VAR, $doc, $var);
187
                $type = $var['type'] ?? 'string';
188
                $type = static::SCALARS[$type] ?? 'string';
189
                $eav[$rProp->getName()] = EAV::factory($db, $rx['table'], $type);
190
            }
191
        }
192
        preg_match(static::RX_RECORD, $rClass->getDocComment(), $record);
193
        if (!is_object($class)) {
194
            assert($rClass->isInstantiable());
195
            $class = $rClass->newInstanceWithoutConstructor();
196
        }
197
        return static::factory($db, $class, $record['table'], $properties, $unique, $eav);
198
    }
199
200
    /**
201
     * @param DB $db
202
     * @param EntityInterface $proto
203
     * @param string $table
204
     * @param string[] $properties Property names.
205
     * @param string[] $unique Enumerated property names, or groups of property names keyed by a shared identifier.
206
     * @param EAV[] $eav Keyed by property name.
207
     */
208
    public function __construct(
209
        DB $db,
210
        EntityInterface $proto,
211
        string $table,
212
        array $properties,
213
        array $unique = [],
214
        array $eav = []
215
    ) {
216
        parent::__construct($db, $table, $properties);
217
        $this->proto = $proto;
218
        $this->unique = $unique;
219
        $this->utc = new DateTimeZone('UTC');
220
        $rClass = new ReflectionClass($proto);
221
        $defaults = $rClass->getDefaultProperties();
222
        foreach ($properties as $prop) {
223
            $rProp = $rClass->getProperty($prop);
224
            $type = $this->__construct_getType($prop, $rProp)
225
                ?? static::SCALARS[gettype($defaults[$prop])];
226
            $nullable = $this->__construct_isNullable($prop, $rProp)
227
                ?? !isset($defaults[$prop]);
228
            assert(isset($type, $nullable));
229
            $rProp->setAccessible(true);
230
            $this->properties[$prop] = $rProp;
231
            $this->types[$prop] = $type;
232
            $this->nullable[$prop] = $nullable;
233
        }
234
        $this->types['id'] = 'int';
235
        $this->nullable['id'] = false;
236
        $this->eav = $eav;
237
        foreach (array_keys($eav) as $name) {
238
            $rProp = $rClass->getProperty($name);
239
            $rProp->setAccessible(true);
240
            $this->properties[$name] = $rProp;
241
        }
242
    }
243
244
    /**
245
     * Resolves a property's storage type during {@link Record::__construct()}
246
     *
247
     * Returns `null` for unknown. The constructor will fall back to checking the class default.
248
     *
249
     * @param string $prop
250
     * @param ReflectionProperty $rProp
251
     * @return null|string Storage type, or `null` for unknown.
252
     */
253
    protected function __construct_getType(string $prop, ReflectionProperty $rProp): ?string
254
    {
255
        return $this->__construct_getType_fromReflection($prop, $rProp)
256
            ?? $this->__construct_getType_fromVar($prop, $rProp);
257
    }
258
259
    /**
260
     * This also sets {@link Record::$hydration} for complex types.
261
     *
262
     * @param string $prop
263
     * @param ReflectionProperty $rProp
264
     * @return null|string
265
     */
266
    protected function __construct_getType_fromReflection(string $prop, ReflectionProperty $rProp): ?string
267
    {
268
        if ($rType = $rProp->getType() and $rType instanceof ReflectionNamedType) {
269
            $type = $rType->getName();
270
            if (isset(static::SCALARS[$type])) {
271
                return static::SCALARS[$type];
272
            }
273
            assert(isset(static::DEHYDRATE_AS[$type]));
274
            $this->hydration[$prop] = $type;
275
            return static::DEHYDRATE_AS[$type];
276
        }
277
        return null;
278
    }
279
280
    /**
281
     * This also sets {@link Record::$hydration} for complex types ONLY IF `@var` uses a FQN.
282
     *
283
     * @param string $prop
284
     * @param ReflectionProperty $rProp
285
     * @return null|string
286
     */
287
    protected function __construct_getType_fromVar(string $prop, ReflectionProperty $rProp): ?string
288
    {
289
        if (preg_match(static::RX_VAR, $rProp->getDocComment(), $var)) {
290
            $type = preg_replace(static::RX_NULL, '', $var['type']); // remove null
291
            if (isset(static::SCALARS[$type])) {
292
                return static::SCALARS[$type];
293
            }
294
            // it's beyond the scope of this class to parse "use" statements,
295
            // @var <CLASS> must be a FQN in order to work.
296
            $type = ltrim($type, '\\');
297
            if (isset(static::DEHYDRATE_AS[$type])) {
298
                $this->hydration[$prop] = $type;
299
                return static::DEHYDRATE_AS[$type];
300
            }
301
        }
302
        return null;
303
    }
304
305
    /**
306
     * Resolves a property's nullability during {@link Record::__construct()}
307
     *
308
     * Returns `null` for unknown. The constructor will fall back to checking the class default.
309
     *
310
     * @param string $prop
311
     * @param ReflectionProperty $rProp
312
     * @return null|bool
313
     */
314
    protected function __construct_isNullable(string $prop, ReflectionProperty $rProp): ?bool
315
    {
316
        return $this->__construct_isNullable_fromReflection($prop, $rProp)
317
            ?? $this->__construct_isNullable_fromVar($prop, $rProp);
318
    }
319
320
    /**
321
     * @param string $prop
322
     * @param ReflectionProperty $rProp
323
     * @return null|bool
324
     */
325
    protected function __construct_isNullable_fromReflection(string $prop, ReflectionProperty $rProp): ?bool
326
    {
327
        if ($rType = $rProp->getType()) {
328
            return $rType->allowsNull();
329
        }
330
        return null;
331
    }
332
333
    /**
334
     * @param string $prop
335
     * @param ReflectionProperty $rProp
336
     * @return null|bool
337
     */
338
    protected function __construct_isNullable_fromVar(string $prop, ReflectionProperty $rProp): ?bool
339
    {
340
        if (preg_match(static::RX_VAR, $rProp->getDocComment(), $var)) {
341
            preg_replace(static::RX_NULL, '', $var['type'], -1, $nullable);
342
            return (bool)$nullable;
343
        }
344
        return null;
345
    }
346
347
    /**
348
     * Fetches from a statement into clones of the entity prototype.
349
     *
350
     * @param Statement $statement
351
     * @return EntityInterface[] Keyed by ID
352
     */
353
    public function fetchAll(Statement $statement): array
354
    {
355
        return iterator_to_array($this->fetchEach($statement));
356
    }
357
358
    /**
359
     * Fetches in chunks and yields each loaded entity.
360
     * This is preferable over {@link fetchAll()} for iterating large result sets.
361
     *
362
     * @param Statement $statement
363
     * @return Generator|EntityInterface[] Keyed by ID
364
     */
365
    public function fetchEach(Statement $statement)
366
    {
367
        do {
368
            $entities = [];
369
            for ($i = 0; $i < static::EAV_BATCH_LOAD and false !== $row = $statement->fetch(); $i++) {
370
                $clone = clone $this->proto;
371
                $this->setValues($clone, $row);
372
                $entities[$row['id']] = $clone;
373
            }
374
            $this->loadEav($entities);
375
            yield from $entities;
376
        } while (!empty($entities));
377
    }
378
379
    /**
380
     * Similar to {@link loadAll()} except this can additionally search by {@link EAV} values.
381
     *
382
     * @see DB::match()
383
     *
384
     * @param array $match `[property => value]`
385
     * @param array[] $eavMatch `[eav property => attribute => value]`
386
     * @return Select|EntityInterface[]
387
     */
388
    public function findAll(array $match, array $eavMatch = [])
389
    {
390
        $select = $this->loadAll();
391
        foreach ($match as $a => $b) {
392
            $select->where($this->db->match($this[$a] ?? $a, $b));
393
        }
394
        foreach ($eavMatch as $property => $attributes) {
395
            $inner = $this->eav[$property]->findAll($attributes);
396
            $select->join($inner, $inner['entity']->isEqual($this['id']));
1 ignored issue
show
Bug introduced by
The method isEqual() does not exist on null. ( Ignorable by Annotation )

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

396
            $select->join($inner, $inner['entity']->/** @scrutinizer ignore-call */ isEqual($this['id']));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
397
        }
398
        return $select;
399
    }
400
401
    /**
402
     * Returns an instance for the first row matching the criteria.
403
     *
404
     * @param array $match `[property => value]`
405
     * @param array $eavMatch `[eav property => attribute => value]`
406
     * @return null|EntityInterface
407
     */
408
    public function findFirst(array $match, array $eavMatch = [])
409
    {
410
        return $this->findAll($match, $eavMatch)->limit(1)->getFirst();
411
    }
412
413
    /**
414
     * @return string
415
     */
416
    final public function getClass(): string
417
    {
418
        return get_class($this->proto);
419
    }
420
421
    /**
422
     * @return EAV[]
423
     */
424
    public function getEav()
425
    {
426
        return $this->eav;
427
    }
428
429
    /**
430
     * Enumerated property names.
431
     *
432
     * @return string[]
433
     */
434
    final public function getProperties(): array
435
    {
436
        return array_keys($this->properties);
437
    }
438
439
    /**
440
     * @return EntityInterface
441
     */
442
    public function getProto()
443
    {
444
        return $this->proto;
445
    }
446
447
    /**
448
     * Returns a native/annotated property type.
449
     *
450
     * This doesn't include whether the property is nullable. Use {@link Record::isNullable()} for that.
451
     *
452
     * @param string $property
453
     * @return string
454
     */
455
    final public function getType(string $property): string
456
    {
457
        return $this->types[$property];
458
    }
459
460
    /**
461
     * Returns the native/annotated property types.
462
     *
463
     * This doesn't include whether the properties are nullable. Use {@link Record::isNullable()} for that.
464
     *
465
     * @return string[]
466
     */
467
    final public function getTypes(): array
468
    {
469
        return $this->types;
470
    }
471
472
    /**
473
     * @return array
474
     */
475
    final public function getUnique(): array
476
    {
477
        return $this->unique;
478
    }
479
480
    /**
481
     * The shared identifier if a property is part of a multi-column unique-key.
482
     *
483
     * @param string $property
484
     * @return null|string The shared identifier, or nothing.
485
     */
486
    final public function getUniqueGroup(string $property): ?string
487
    {
488
        foreach ($this->unique as $key => $value) {
489
            if (is_string($key) and in_array($property, $value)) {
490
                return $key;
491
            }
492
        }
493
        return null;
494
    }
495
496
    /**
497
     * @param EntityInterface $entity
498
     * @return array
499
     */
500
    protected function getValues(EntityInterface $entity): array
501
    {
502
        $values = [];
503
        foreach (array_keys($this->columns) as $name) {
504
            $value = $this->properties[$name]->getValue($entity);
505
            if (isset($value, $this->hydration[$name])) {
506
                $from = $this->hydration[$name];
507
                $to = static::DEHYDRATE_AS[$from];
508
                $value = $this->getValues_dehydrate($to, $from, $value);
509
            }
510
            $values[$name] = $value;
511
        }
512
        return $values;
513
    }
514
515
    /**
516
     * Dehydrates a complex property's value for storage in a scalar column.
517
     *
518
     * @see Record::setType_hydrate() inverse
519
     *
520
     * @param string $to The storage type.
521
     * @param string $from The strict type from the class definition.
522
     * @param array|object $hydrated
523
     * @return scalar
524
     */
525
    protected function getValues_dehydrate(string $to, string $from, $hydrated)
526
    {
527
        unset($from); // we don't need it here but it's given for posterity
528
        switch ($to) {
529
            case 'DateTime':
530
                assert($hydrated instanceof DateTime or $hydrated instanceof DateTimeImmutable);
531
                return (clone $hydrated)->setTimezone($this->utc)->format(Schema::DATETIME_FORMAT);
532
            default:
533
                return serialize($hydrated);
534
        }
535
    }
536
537
    /**
538
     * @param string $property
539
     * @return bool
540
     */
541
    final public function isNullable(string $property): bool
542
    {
543
        return $this->nullable[$property];
544
    }
545
546
    /**
547
     * Whether a property has a unique-key constraint of its own.
548
     *
549
     * @param string $property
550
     * @return bool
551
     */
552
    final public function isUnique(string $property): bool
553
    {
554
        return in_array($property, $this->unique);
555
    }
556
557
    /**
558
     * Loads all data for a given ID (clones the prototype), or an existing instance.
559
     *
560
     * @param int|EntityInterface $id The given instance may be a subclass of the prototype.
561
     * @return null|EntityInterface
562
     */
563
    public function load($id)
564
    {
565
        $statement = $this->cache(__FUNCTION__, function () {
566
            return $this->select()->where('id = ?')->prepare();
567
        });
568
        if ($id instanceof EntityInterface) {
569
            assert(is_a($id, get_class($this->proto)));
570
            $entity = $id;
571
            $id = $entity->getId();
572
        } else {
573
            $entity = clone $this->proto;
574
        }
575
        $values = $statement([$id])->fetch();
576
        $statement->closeCursor();
577
        if ($values) {
578
            $this->setValues($entity, $values);
579
            $this->loadEav([$id => $entity]);
580
            return $entity;
581
        }
582
        return null;
583
    }
584
585
    /**
586
     * Returns a {@link Select} that fetches instances.
587
     *
588
     * @return Select|EntityInterface[]
589
     */
590
    public function loadAll()
591
    {
592
        return $this->select()->setFetcher(function (Statement $statement) {
593
            yield from $this->fetchEach($statement);
594
        });
595
    }
596
597
    /**
598
     * Loads and sets all EAV properties for an array of entities keyed by ID.
599
     *
600
     * @param EntityInterface[] $entities Keyed by ID
601
     */
602
    protected function loadEav(array $entities): void
603
    {
604
        $ids = array_keys($entities);
605
        foreach ($this->eav as $name => $eav) {
606
            foreach ($eav->loadAll($ids) as $id => $values) {
607
                $this->properties[$name]->setValue($entities[$id], $values);
608
            }
609
        }
610
    }
611
612
    /**
613
     * Upserts record and EAV data.
614
     *
615
     * @param EntityInterface $entity
616
     * @return int ID
617
     */
618
    public function save(EntityInterface $entity): int
619
    {
620
        if (!$entity->getId()) {
621
            $this->saveInsert($entity);
622
        } else {
623
            $this->saveUpdate($entity);
624
        }
625
        $this->saveEav($entity);
626
        return $entity->getId();
627
    }
628
629
    /**
630
     * @param EntityInterface $entity
631
     */
632
    protected function saveEav(EntityInterface $entity): void
633
    {
634
        $id = $entity->getId();
635
        foreach ($this->eav as $name => $eav) {
636
            // may be null to skip
637
            $values = $this->properties[$name]->getValue($entity);
638
            if (isset($values)) {
639
                $eav->save($id, $values);
640
            }
641
        }
642
    }
643
644
    /**
645
     * Inserts a new row and updates the entity's ID.
646
     *
647
     * @param EntityInterface $entity
648
     */
649
    protected function saveInsert(EntityInterface $entity): void
650
    {
651
        $statement = $this->cache(__FUNCTION__, function () {
652
            $slots = $this->db->slots(array_keys($this->columns));
653
            unset($slots['id']);
654
            $columns = implode(',', array_keys($slots));
655
            $slots = implode(',', $slots);
656
            return $this->db->prepare("INSERT INTO {$this} ({$columns}) VALUES ({$slots})");
657
        });
658
        $values = $this->getValues($entity);
659
        unset($values['id']);
660
        $this->properties['id']->setValue($entity, $statement($values)->getId());
661
        $statement->closeCursor();
662
    }
663
664
    /**
665
     * Updates the existing row for the entity.
666
     *
667
     * @param EntityInterface $entity
668
     */
669
    protected function saveUpdate(EntityInterface $entity): void
670
    {
671
        $statement = $this->cache(__FUNCTION__, function () {
672
            $slots = $this->db->slotsEqual(array_keys($this->columns));
673
            unset($slots['id']);
674
            $slots = implode(', ', $slots);
675
            return $this->db->prepare("UPDATE {$this} SET {$slots} WHERE id = :id");
676
        });
677
        $statement->execute($this->getValues($entity));
678
        $statement->closeCursor();
679
    }
680
681
    /**
682
     * @param EntityInterface $proto
683
     * @return $this
684
     */
685
    public function setProto(EntityInterface $proto)
686
    {
687
        $this->proto = $proto;
688
        return $this;
689
    }
690
691
    /**
692
     * Converts a value from storage into the native/annotated type.
693
     *
694
     * @param string $property
695
     * @param mixed $value
696
     * @return mixed
697
     */
698
    protected function setType(string $property, $value)
699
    {
700
        if (isset($value)) {
701
            // complex?
702
            if (isset($this->hydration[$property])) {
703
                $to = $this->hydration[$property];
704
                $from = static::DEHYDRATE_AS[$to];
705
                return $this->setType_hydrate($to, $from, $value);
706
            }
707
            // scalar. this function doesn't care about the type's letter case.
708
            settype($value, $this->types[$property]);
709
        }
710
        return $value;
711
    }
712
713
    /**
714
     * Hydrates a complex value from scalar storage.
715
     *
716
     * @see Record::getValues_dehydrate() inverse
717
     *
718
     * @param string $to The strict type from the class definition.
719
     * @param string $from The storage type.
720
     * @param scalar $dehydrated
721
     * @return array|object
722
     */
723
    protected function setType_hydrate(string $to, string $from, $dehydrated)
724
    {
725
        switch ($from) {
726
            case 'DateTime':
727
                /**
728
                 * $to might be "DateTime", "DateTimeImmutable", or an extension.
729
                 *
730
                 * @see DateTime::createFromFormat()
731
                 */
732
                return call_user_func(
733
                    [$to, 'createFromFormat'],
734
                    'Y-m-d H:i:s',
735
                    $dehydrated,
736
                    $this->utc
737
                );
738
            default:
739
                return unserialize($dehydrated);
0 ignored issues
show
Bug introduced by
It seems like $dehydrated can also be of type boolean; however, parameter $data of unserialize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

739
                return unserialize(/** @scrutinizer ignore-type */ $dehydrated);
Loading history...
740
        }
741
    }
742
743
    /**
744
     * @param EntityInterface $entity
745
     * @param array $values
746
     */
747
    protected function setValues(EntityInterface $entity, array $values): void
748
    {
749
        foreach ($values as $name => $value) {
750
            if (isset($this->properties[$name])) {
751
                $this->properties[$name]->setValue($entity, $this->setType($name, $value));
752
            } else {
753
                // attempt to set unknown fields directly on the instance.
754
                $entity->{$name} = $value;
755
            }
756
        }
757
    }
758
}
759