Passed
Push — master ( 285c17...9cf790 )
by y
02:17 queued 13s
created

Serializer::hydrate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 16
rs 10
1
<?php
2
3
namespace Helix\DB\Record;
4
5
use DateTime;
6
use DateTimeImmutable;
7
use DateTimeZone;
8
use Helix\DB;
9
use Helix\DB\EntityInterface;
10
use Helix\DB\Reflection;
11
use Helix\DB\Schema;
12
use stdClass;
13
14
/**
15
 * Converts an entity's values to/from storage types.
16
 */
17
class Serializer extends Reflection
18
{
19
20
    /**
21
     * Maps complex types to storage types.
22
     *
23
     * Foreign {@link EntityInterface} columns are automatically added as `"int"`
24
     *
25
     * @see Serializer::dehydrate()
26
     * @see Serializer::hydrate()
27
     * @see Schema::T_CONST_NAMES
28
     * @var string[]
29
     */
30
    protected $dehydrate = [
31
        'array' => 'STRING', // blob. eav is better than this for 1D arrays.
32
        'object' => 'STRING', // blob.
33
        stdClass::class => 'STRING', // blob
34
        DateTime::class => 'DateTime',
35
        DateTimeImmutable::class => 'DateTime',
36
    ];
37
38
    /**
39
     * The specific classes used to hydrate classed properties, like `DateTime`.
40
     *
41
     * `[ property => class ]`
42
     *
43
     * @var string[]
44
     */
45
    protected $hydrate = [];
46
47
    /**
48
     * Scalar storage types, after any dehydration is done.
49
     *
50
     * `[property => type]`
51
     *
52
     * @var string[]
53
     */
54
    protected $storageTypes = [];
55
56
    protected DateTimeZone $utc;
57
58
    /**
59
     * @param DB $db
60
     * @param string|object $class
61
     */
62
    public function __construct(DB $db, $class)
63
    {
64
        parent::__construct($db, $class);
65
        $this->utc = new DateTimeZone('UTC');
66
        foreach ($this->getColumns() as $col) {
67
            $type = $this->getType($col);
68
            if (is_a($type, EntityInterface::class, true)) {
69
                $this->dehydrate[$type] = 'int';
70
            }
71
            if (isset($this->dehydrate[$type])) {
72
                $this->hydrate[$col] = $type;
73
                $type = $this->dehydrate[$type];
74
            }
75
            $this->storageTypes[$col] = $type;
76
        }
77
        $this->storageTypes['id'] = 'int';
78
    }
79
80
    /**
81
     * Dehydrates a complex property's value for storage in a scalar column.
82
     *
83
     * @see Serializer::hydrate() inverse
84
     *
85
     * @param string $to The storage type.
86
     * @param string $from The strict type from the class definition.
87
     * @param array|object $hydrated
88
     * @return null|scalar
89
     */
90
    protected function dehydrate(string $to, string $from, $hydrated)
91
    {
92
        // we don't need $from here but it's given for posterity
93
        unset($from);
94
95
        // dehydrate entities to their id
96
        if ($hydrated instanceof EntityInterface) {
97
            return $hydrated->getId();
98
        }
99
100
        // dehydrate DateTime
101
        if ($to === 'DateTime') {
102
            assert($hydrated instanceof DateTime or $hydrated instanceof DateTimeImmutable);
103
            return (clone $hydrated)->setTimezone($this->utc)->format(Schema::DATETIME_FORMAT);
104
        }
105
106
        // dehydrate other complex types
107
        return serialize($hydrated);
108
    }
109
110
    /**
111
     * Returns an entity's property values, dehydrating if needed.
112
     *
113
     * @param EntityInterface $entity
114
     * @return array
115
     */
116
    public function export(EntityInterface $entity): array
117
    {
118
        $values = [];
119
        foreach ($this->columns as $col) {
120
            $value = $this->getValue($entity, $col);
121
            if (isset($value, $this->hydrate[$col])) {
122
                $from = $this->hydrate[$col];
123
                $to = $this->dehydrate[$from];
124
                $value = $this->dehydrate($to, $from, $value);
125
            }
126
            $values[$col] = $value;
127
        }
128
        return $values;
129
    }
130
131
    /**
132
     * Returns the scalar storage types of all properties.
133
     *
134
     * This doesn't include whether the properties are nullable.
135
     * Use {@link Serializer::isNullable()} for that.
136
     *
137
     * @return string[]
138
     */
139
    final public function getStorageTypes(): array
140
    {
141
        return $this->storageTypes;
142
    }
143
144
    /**
145
     * Hydrates a complex value from scalar storage.
146
     *
147
     * @see Serializer::dehydrate() inverse
148
     *
149
     * @param string $to The strict type from the class definition.
150
     * @param string $from The storage type.
151
     * @param scalar $dehydrated
152
     * @return array|object
153
     */
154
    protected function hydrate(string $to, string $from, $dehydrated)
155
    {
156
        // hydrate entities from their id
157
        if (is_a($to, EntityInterface::class, true)) {
158
            return $this->db->getRecord($to)->load($dehydrated);
0 ignored issues
show
Bug introduced by
It seems like $dehydrated can also be of type boolean and double and string; however, parameter $id of Helix\DB\Record::load() does only seem to accept Helix\DB\EntityInterface|integer, 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

158
            return $this->db->getRecord($to)->load(/** @scrutinizer ignore-type */ $dehydrated);
Loading history...
159
        }
160
161
        // hydrate DateTime
162
        if ($from === 'DateTime') {
163
            return new $to($dehydrated, $this->utc);
164
        }
165
166
        // hydrate other complex types
167
        $complex = 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

167
        $complex = unserialize(/** @scrutinizer ignore-type */ $dehydrated);
Loading history...
168
        assert(is_array($complex) or is_object($complex));
169
        return $complex;
170
    }
171
172
    /**
173
     * Sets values from storage on an entity, hydrating if needed.
174
     *
175
     * @param EntityInterface $entity
176
     * @param array $values
177
     */
178
    public function import(EntityInterface $entity, array $values): void
179
    {
180
        foreach ($values as $property => $value) {
181
            if (isset($value)) {
182
                if (isset($this->hydrate[$property])) { // complex
183
                    $to = $this->hydrate[$property];
184
                    $from = $this->dehydrate[$to];
185
                    $value = $this->hydrate($to, $from, $value);
186
                } else { // scalar
187
                    // this function doesn't care about the type's letter case.
188
                    settype($value, $this->storageTypes[$property]);
189
                }
190
            }
191
            $this->setValue($entity, $property, $value);
192
        }
193
    }
194
195
}
196