Passed
Push — master ( 49b932...79af1c )
by y
02:30
created

Serializer::dehydrate()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
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 18
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
     * Properties that are foreign keys to other entities.
40
     *
41
     * `[ property => foreign entity class ]`
42
     *
43
     * @var string[]
44
     */
45
    protected $foreign = [];
46
47
    /**
48
     * The specific classes used to hydrate classed properties, like `DateTime`.
49
     *
50
     * `[ property => class ]`
51
     *
52
     * @var string[]
53
     */
54
    protected $hydrate = [];
55
56
    /**
57
     * Scalar storage types, after any dehydration is done.
58
     *
59
     * `[property => type]`
60
     *
61
     * @var string[]
62
     */
63
    protected $storageTypes = [];
64
65
    protected DateTimeZone $utc;
66
67
    /**
68
     * @param DB $db
69
     * @param string|object $class
70
     */
71
    public function __construct(DB $db, $class)
72
    {
73
        parent::__construct($db, $class);
74
        $this->utc = new DateTimeZone('UTC');
75
        foreach ($this->columns as $col) {
76
            $type = $this->getType($col);
77
            if (is_a($type, EntityInterface::class, true)) {
78
                $this->foreign[$col] = $type;
79
                $this->dehydrate[$type] = 'int';
80
            }
81
            if (isset($this->dehydrate[$type])) {
82
                $this->hydrate[$col] = $type;
83
                $type = $this->dehydrate[$type];
84
            }
85
            $this->storageTypes[$col] = $type;
86
        }
87
        $this->storageTypes['id'] = 'int';
88
    }
89
90
    /**
91
     * Dehydrates a complex property's value for storage in a scalar column.
92
     *
93
     * @see Serializer::hydrate() inverse
94
     *
95
     * @param string $to The storage type.
96
     * @param string $from The strict type from the class definition.
97
     * @param array|object $hydrated
98
     * @return null|scalar
99
     */
100
    protected function dehydrate(string $to, string $from, $hydrated)
101
    {
102
        // we don't need $from here but it's given for posterity
103
        unset($from);
104
105
        // dehydrate entities to their id
106
        if ($hydrated instanceof EntityInterface) {
107
            return $hydrated->getId();
108
        }
109
110
        // dehydrate DateTime
111
        if ($to === 'DateTime') {
112
            assert($hydrated instanceof DateTime or $hydrated instanceof DateTimeImmutable);
113
            return (clone $hydrated)->setTimezone($this->utc)->format(Schema::DATETIME_FORMAT);
114
        }
115
116
        // dehydrate other complex types
117
        return serialize($hydrated);
118
    }
119
120
    /**
121
     * Returns an entity's property values, dehydrating if needed.
122
     *
123
     * @param EntityInterface $entity
124
     * @return array
125
     */
126
    public function export(EntityInterface $entity): array
127
    {
128
        $values = [];
129
        foreach ($this->columns as $col) {
130
            $value = $this->getValue($entity, $col);
131
            if (isset($value, $this->hydrate[$col])) {
132
                $from = $this->hydrate[$col];
133
                $to = $this->dehydrate[$from];
134
                $value = $this->dehydrate($to, $from, $value);
135
            }
136
            $values[$col] = $value;
137
        }
138
        return $values;
139
    }
140
141
    /**
142
     * @return string[]
143
     */
144
    final public function getForeign(): array
145
    {
146
        return $this->foreign;
147
    }
148
149
    /**
150
     * Returns the scalar storage types of all properties.
151
     *
152
     * This doesn't include whether the properties are nullable.
153
     * Use {@link Serializer::isNullable()} for that.
154
     *
155
     * @return string[]
156
     */
157
    final public function getStorageTypes(): array
158
    {
159
        return $this->storageTypes;
160
    }
161
162
    /**
163
     * Hydrates a complex value from scalar storage.
164
     *
165
     * @see Serializer::dehydrate() inverse
166
     *
167
     * @param string $to The strict type from the class definition.
168
     * @param string $from The storage type.
169
     * @param scalar $dehydrated
170
     * @return array|object
171
     */
172
    protected function hydrate(string $to, string $from, $dehydrated)
173
    {
174
        // hydrate entities from their id
175
        if (is_a($to, EntityInterface::class, true)) {
176
            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

176
            return $this->db->getRecord($to)->load(/** @scrutinizer ignore-type */ $dehydrated);
Loading history...
177
        }
178
179
        // hydrate DateTime
180
        if ($from === 'DateTime') {
181
            return new $to($dehydrated, $this->utc);
182
        }
183
184
        // hydrate other complex types
185
        $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

185
        $complex = unserialize(/** @scrutinizer ignore-type */ $dehydrated);
Loading history...
186
        assert(is_array($complex) or is_object($complex));
187
        return $complex;
188
    }
189
190
    /**
191
     * Sets values from storage on an entity, hydrating if needed.
192
     *
193
     * @param EntityInterface $entity
194
     * @param array $values
195
     */
196
    public function import(EntityInterface $entity, array $values): void
197
    {
198
        foreach ($values as $property => $value) {
199
            if (isset($value)) {
200
                if (isset($this->hydrate[$property])) { // complex
201
                    $to = $this->hydrate[$property];
202
                    $from = $this->dehydrate[$to];
203
                    $value = $this->hydrate($to, $from, $value);
204
                } else { // scalar
205
                    // this function doesn't care about the type's letter case.
206
                    settype($value, $this->storageTypes[$property]);
207
                }
208
            }
209
            $this->setValue($entity, $property, $value);
210
        }
211
    }
212
213
}
214