NormalizationContext::excludeProperties()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
    public const EXCLUDES = 'exclude';
18
    public const INCLUDES = 'include';
19
    public const GROUPS = 'groups';
20
    public const NULL = 'null';
21
    public const META_TYPE = 'include_type';
22
    public const VERSION = 'version';
23
    public const DATETIME_FORMAT = 'dateFormat';
24
    public const TIMEZONE = 'dateTimezone';
25
    public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit';
26
    public const REMOVE_DEFAULT_VALUE = 'remove_default_value';
27
    public 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 (for non typed properties only)
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 170
    public function __construct(NormalizerInterface $normalizer, array $options = [])
102
    {
103 170
        parent::__construct($normalizer, $this->defaultOptions);
104
105 170
        $this->prepareOptions($options);
106
    }
107
108
    /**
109
     * Get the context groups
110
     *
111
     * @return null|array
112
     */
113 132
    public function groups(): ?array
114
    {
115 132
        return $this->options[self::GROUPS];
116
    }
117
118
    /**
119
     * Get the context exclude properties
120
     *
121
     * @return null|array
122
     */
123 122
    public function excludeProperties(): ?array
124
    {
125 122
        return $this->options[self::EXCLUDES];
126
    }
127
128
    /**
129
     * Get the context include properties
130
     *
131
     * @return null|array
132
     */
133 118
    public function includeProperties(): ?array
134
    {
135 118
        return $this->options[self::INCLUDES];
136
    }
137
138
    /**
139
     * Should the self::NULL value be included (for non typed properties only)
140
     *
141
     * @return boolean
142
     */
143 20
    public function shouldAddNull(): bool
144
    {
145 20
        return $this->options[self::NULL];
146
    }
147
148
    /**
149
     * Should the default value of a property be removed
150
     *
151
     * @return boolean
152
     */
153 94
    public function removeDefaultValue(): bool
154
    {
155 94
        return $this->options[self::REMOVE_DEFAULT_VALUE];
156
    }
157
158
    /**
159
     * Serialize to the version
160
     *
161
     * @return string
162
     */
163 126
    public function version(): ?string
164
    {
165 126
        return $this->options[self::VERSION];
166
    }
167
168
    /**
169
     * Should add metadata of type into the serialization
170
     *
171
     * @return bool
172
     */
173 108
    public function includeMetaType(): bool
174
    {
175 108
        return $this->options[self::META_TYPE];
176
    }
177
178
    /**
179
     * Should add metadata of type into the serialization
180
     *
181
     * @return bool
182
     */
183 10
    public function throwsOnAccessorError(): bool
184
    {
185 10
        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 98
    public function skipPropertyValue(PropertyMetadata $property, $value): bool
197
    {
198 98
        if ($value === null && false === $property->isPhpTyped) {
199 12
            return !$this->shouldAddNull();
200
        }
201
202
        // This does not remove the 'null' default value for non typed properties.
203 94
        return $this->removeDefaultValue() && $property->isDefaultValue($value);
204
    }
205
206
    /**
207
     * Should the property be skipped
208
     *
209
     * @param PropertyMetadata $property
210
     *
211
     * @return boolean   Returns true if skipped
212
     *
213
     * @todo Add custom filters
214
     */
215 124
    public function skipProperty(PropertyMetadata $property): bool
216
    {
217 124
        $groups = $this->groups();
218
219
        // A group has been set and the property is not in that group: we skip the property
220 124
        if ($groups !== null && !$property->hasGroups($groups)) {
221 6
            return true;
222
        }
223
224 122
        $version = $this->version();
225
226
        // A version has been set and the property does not match this version
227 122
        if ($version !== null && !$property->matchVersion($version)) {
228 10
            return true;
229
        }
230
231 118
        return !$this->shouldNormalizeProperty($property->class, $property->name);
232
    }
233
234
    /**
235
     * @param string $class
236
     * @param string $property
237
     *
238
     * @return bool
239
     */
240 118
    public function shouldNormalizeProperty(string $class, string $property): bool
241
    {
242 118
        $path = "{$class}::{$property}";
243
244 118
        $excludes = $this->excludeProperties();
245
246 118
        if ($excludes !== null && (isset($excludes[$property]) || isset($excludes[$path]))) {
247 8
            return false;
248
        }
249
250 114
        $includes = $this->includeProperties();
251
252 114
        if ($includes !== null && !isset($includes[$property]) && !isset($includes[$path])) {
253 6
            return false;
254
        }
255
256 112
        return true;
257
    }
258
259
    /**
260
     * Detects if the configured circular reference limit is reached.
261
     *
262
     * @param object $object
263
     *
264
     * @return string The object hash
265
     *
266
     * @throws CircularReferenceException If circular reference found
267
     */
268 112
    public function assertNoCircularReference($object): string
269
    {
270 112
        $objectHash = spl_object_hash($object);
271
272 112
        if (!isset($this->objectReferences[$objectHash])) {
273 112
            $this->objectReferences[$objectHash] = 1;
274
275 112
            return $objectHash;
276
        }
277
278 10
        if ($this->objectReferences[$objectHash] < $this->options[self::CIRCULAR_REFERENCE_LIMIT]) {
279 6
            $this->objectReferences[$objectHash]++;
280
281 6
            return $objectHash;
282
        }
283
284 6
        unset($this->objectReferences[$objectHash]);
285
286 6
        throw new CircularReferenceException(
287 6
            'A circular reference has been detected when serializing the object of class "'.get_class($object).'" (configured limit: '.$this->options[self::CIRCULAR_REFERENCE_LIMIT].')'
288 6
        );
289
    }
290
291
    /**
292
     * Release the object
293
     *
294
     * @param string $objectHash
295
     */
296 100
    public function releaseReference(string $objectHash): void
297
    {
298
        // Release the memory if depth is equal to 1
299 100
        if ($this->objectReferences[$objectHash] === 1) {
300 98
            unset($this->objectReferences[$objectHash]);
301
302 98
            return;
303
        }
304
305 2
        $this->objectReferences[$objectHash]--;
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311 170
    protected function prepareOptions(array $options): void
312
    {
313 170
        foreach ($options as $name => $value) {
314
            switch ($name) {
315 98
                case 'group':
316 94
                case self::GROUPS:
317 14
                    $this->options[self::GROUPS] = (array)$value;
318 14
                    break;
319
320 90
                case 'serializeNull':
321 84
                case 'serialize_null':
322 84
                case self::NULL:
323 22
                    $this->options[self::NULL] = (bool)$value;
324 22
                    break;
325
326 78
                case self::EXCLUDES:
327 12
                    $this->options[self::EXCLUDES] = array_flip((array)$value);
328 12
                    break;
329
330 68
                case self::INCLUDES:
331 12
                    $this->options[self::INCLUDES] = array_flip((array)$value);
332 12
                    break;
333
334
                default:
335 58
                    $this->options[$name] = $value;
336 58
                    break;
337
            }
338
        }
339
    }
340
}
341