Passed
Pull Request — master (#9)
by Vincent
03:39
created

NormalizationContext   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 87
dl 0
loc 327
ccs 79
cts 79
cp 1
rs 9.36
c 1
b 0
f 0
wmc 38

15 Methods

Rating   Name   Duplication   Size   Complexity  
A includeProperties() 0 3 1
A __construct() 0 5 1
B prepareOptions() 0 26 9
A removeDefaultValue() 0 3 1
A groups() 0 3 1
A skipProperty() 0 17 5
A excludeProperties() 0 3 1
A assertNoCircularReference() 0 20 3
A skipPropertyValue() 0 12 3
A version() 0 3 1
A includeMetaType() 0 3 1
B shouldNormalizeProperty() 0 17 7
A releaseReference() 0 10 2
A shouldAddNull() 0 3 1
A throwsOnAccessorError() 0 3 1
1
<?php
2
3
namespace Bdf\Serializer\Context;
4
5
use Bdf\Serializer\Exception\CircularReferenceException;
6
use Bdf\Serializer\Metadata\PropertyMetadata;
7
use Bdf\Serializer\Normalizer\NormalizerInterface;
8
9
/**
10
 * NormalizationContext
11
 *
12
 * context used by normalization
13
 */
14
class NormalizationContext extends Context
15
{
16
    //List of denormalization options
17
    const EXCLUDES = 'exclude';
18
    const INCLUDES = 'include';
19
    const GROUPS = 'groups';
20
    const NULL = 'null';
21
    const META_TYPE = 'include_type';
22
    const VERSION = 'version';
23
    const DATETIME_FORMAT = 'dateFormat';
24
    const TIMEZONE = 'dateTimezone';
25
    const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
26
    const REMOVE_DEFAULT_VALUE = 'remove_default_value';
27
    const THROWS_ON_ACCESSOR_ERROR = 'throws_on_accessor_error';
28
29
    /**
30
     * The default options of this context
31
     *
32
     * @var array
33
     */
34
    protected $defaultOptions = [
35
        /**
36
         * Properties to exclude from normalization
37
         *
38
         * @var array
39
         */
40
        self::EXCLUDES => null,
41
        /**
42
         * Properties to include from normalization
43
         *
44
         * @var array
45
         */
46
        self::INCLUDES => null,
47
        /**
48
         * Groups of properties to include
49
         *
50
         * @var array
51
         */
52
        self::GROUPS => null,
53
        /**
54
         * Null value will be added if true
55
         *
56
         * @var boolean
57
         */
58
        self::NULL => false,
59
        /**
60
         * Include the metadata '@type' in the payload
61
         *
62
         * @var boolean
63
         */
64
        self::META_TYPE => false,
65
        /**
66
         * The version of the object.
67
         *
68
         * @var string|null
69
         */
70
        self::VERSION => null,
71
        /**
72
         * The circular reference limit
73
         *
74
         * @var integer
75
         */
76
        self::CIRCULAR_REFERENCE_LIMIT => 1,
77
        /**
78
         * Default value will be removed if true
79
         *
80
         * @var boolean
81
         */
82
        self::REMOVE_DEFAULT_VALUE => false,
83
        /**
84
         * Throws exception if accessor has error
85
         *
86
         * @var boolean
87
         */
88
        self::THROWS_ON_ACCESSOR_ERROR => false,
89
    ];
90
91
    /**
92
     * The object reference for circular reference
93
     *
94
     * @var array
95
     */
96
    private $objectReferences = [];
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 158
    public function __construct(NormalizerInterface $normalizer, array $options = [])
102
    {
103 158
        parent::__construct($normalizer, $this->defaultOptions);
104
105 158
        $this->prepareOptions($options);
106 158
    }
107
108
    /**
109
     * Get the context groups
110
     *
111
     * @return null|array
112
     */
113 120
    public function groups(): ?array
114
    {
115 120
        return $this->options[self::GROUPS];
116
    }
117
118
    /**
119
     * Get the context exclude properties
120
     *
121
     * @return null|array
122
     */
123 110
    public function excludeProperties(): ?array
124
    {
125 110
        return $this->options[self::EXCLUDES];
126
    }
127
128
    /**
129
     * Get the context include properties
130
     *
131
     * @return null|array
132
     */
133 106
    public function includeProperties(): ?array
134
    {
135 106
        return $this->options[self::INCLUDES];
136
    }
137
138
    /**
139
     * Should the self::NULL value be included
140
     *
141
     * @return boolean
142
     */
143 16
    public function shouldAddNull(): bool
144
    {
145 16
        return $this->options[self::NULL];
146
    }
147
148
    /**
149
     * Should the default value of a property be removed
150
     *
151
     * @return boolean
152
     */
153 84
    public function removeDefaultValue(): bool
154
    {
155 84
        return $this->options[self::REMOVE_DEFAULT_VALUE];
156
    }
157
158
    /**
159
     * Serialize to the version
160
     *
161
     * @return string
162
     */
163 114
    public function version(): ?string
164
    {
165 114
        return $this->options[self::VERSION];
166
    }
167
168
    /**
169
     * Should add metadata of type into the serialization
170
     *
171
     * @return bool
172
     */
173 96
    public function includeMetaType(): bool
174
    {
175 96
        return $this->options[self::META_TYPE];
176
    }
177
178
    /**
179
     * Should add metadata of type into the serialization
180
     *
181
     * @return bool
182
     */
183 6
    public function throwsOnAccessorError(): bool
184
    {
185 6
        return $this->options[self::THROWS_ON_ACCESSOR_ERROR];
186
    }
187
188
    /**
189
     * Skip the property by its value
190
     *
191
     * @param PropertyMetadata $property
192
     * @param mixed $value
193
     *
194
     * @return boolean   Returns true if skipped
195
     */
196 86
    public function skipPropertyValue(PropertyMetadata $property, $value): bool
197
    {
198 86
        if ($value === null) {
199 8
            return !$this->shouldAddNull();
200
        }
201
202
        // This does not remove the 'null' default value.
203 84
        if ($this->removeDefaultValue()) {
204 4
            return $property->defaultValue === $value;
205
        }
206
207 84
        return false;
208
    }
209
210
    /**
211
     * Should the property be skipped
212
     *
213
     * @param PropertyMetadata $property
214
     *
215
     * @return boolean   Returns true if skipped
216
     *
217
     * @todo Add custom filters
218
     */
219 112
    public function skipProperty(PropertyMetadata $property): bool
220
    {
221 112
        $groups = $this->groups();
222
223
        // A group has been set and the property is not in that group: we skip the property
224 112
        if ($groups !== null && !$property->hasGroups($groups)) {
225 6
            return true;
226
        }
227
228 110
        $version = $this->version();
229
230
        // A version has been set and the property does not match this version
231 110
        if ($version !== null && !$property->matchVersion($version)) {
232 8
            return true;
233
        }
234
235 106
        return !$this->shouldNormalizeProperty($property->class, $property->name);
236
    }
237
238
    /**
239
     * @param string $class
240
     * @param string $property
241
     *
242
     * @return bool
243
     */
244 106
    public function shouldNormalizeProperty(string $class, string $property): bool
245
    {
246 106
        $path = "${class}::${property}";
247
248 106
        $excludes = $this->excludeProperties();
249
250 106
        if ($excludes !== null && (isset($excludes[$property]) || isset($excludes[$path]))) {
251 8
            return false;
252
        }
253
254 102
        $includes = $this->includeProperties();
255
256 102
        if ($includes !== null && !isset($includes[$property]) && !isset($includes[$path])) {
257 6
            return false;
258
        }
259
260 100
        return true;
261
    }
262
263
    /**
264
     * Detects if the configured circular reference limit is reached.
265
     *
266
     * @param object $object
267
     *
268
     * @return string The object hash
269
     *
270
     * @throws CircularReferenceException If circular reference found
271
     */
272 100
    public function assertNoCircularReference($object): string
273
    {
274 100
        $objectHash = spl_object_hash($object);
275
276 100
        if (!isset($this->objectReferences[$objectHash])) {
277 100
            $this->objectReferences[$objectHash] = 1;
278
279 100
            return $objectHash;
280
        }
281
282 10
        if ($this->objectReferences[$objectHash] < $this->options[self::CIRCULAR_REFERENCE_LIMIT]) {
283 6
            $this->objectReferences[$objectHash]++;
284
285 6
            return $objectHash;
286
        }
287
288 6
        unset($this->objectReferences[$objectHash]);
289
290 6
        throw new CircularReferenceException(
291 6
            'A circular reference has been detected when serializing the object of class "'.get_class($object).'" (configured limit: '.$this->options[self::CIRCULAR_REFERENCE_LIMIT].')'
292
        );
293
    }
294
295
    /**
296
     * Release the object
297
     *
298
     * @param string $objectHash
299
     */
300 88
    public function releaseReference(string $objectHash): void
301
    {
302
        // Release the memory if depth is equal to 1
303 88
        if ($this->objectReferences[$objectHash] === 1) {
304 86
            unset($this->objectReferences[$objectHash]);
305
306 86
            return;
307
        }
308
309 2
        $this->objectReferences[$objectHash]--;
310 2
    }
311
312
    /**
313
     * {@inheritdoc}
314
     */
315 158
    protected function prepareOptions(array $options): void
316
    {
317 158
        foreach ($options as $name => $value) {
318
            switch ($name) {
319 90
                case 'group':
320 86
                case self::GROUPS:
321 14
                    $this->options[self::GROUPS] = (array)$value;
322 14
                    break;
323
324 82
                case 'serializeNull':
325 76
                case 'serialize_null':
326 76
                case self::NULL:
327 22
                    $this->options[self::NULL] = (bool)$value;
328 22
                    break;
329
330 70
                case self::EXCLUDES:
331 12
                    $this->options[self::EXCLUDES] = array_flip((array)$value);
332 12
                    break;
333
334 60
                case self::INCLUDES:
335 12
                    $this->options[self::INCLUDES] = array_flip((array)$value);
336 12
                    break;
337
338
                default:
339 50
                    $this->options[$name] = $value;
340 50
                    break;
341
            }
342
        }
343
    }
344
}