Completed
Push — caching ( 6c9b2f )
by Nate
03:39
created

Excluder::hasClassDeserializationStrategies()   A

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 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\Gson\Internal;
10
11
use ReflectionProperty;
12
use Tebru\AnnotationReader\AnnotationCollection;
13
use Tebru\Gson\Annotation\Exclude;
14
use Tebru\Gson\Annotation\ExclusionStrategy as ExclusionStrategyAnnotation;
15
use Tebru\Gson\Annotation\Expose;
16
use Tebru\Gson\Annotation\Since;
17
use Tebru\Gson\Annotation\Until;
18
use Tebru\Gson\Exclusion\ExclusionStrategy;
19
use Tebru\Gson\Internal\Data\Property;
20
use Tebru\PhpType\TypeToken;
21
22
/**
23
 * Class Excluder
24
 *
25
 * @author Nate Brunette <[email protected]>
26
 */
27
final class Excluder
28
{
29
    /**
30
     * @var ConstructorConstructor
31
     */
32
    private $constructorConstructor;
33
34
    /**
35
     * Version, if set, will be used with [@see Since] and [@see Until] annotations
36
     *
37
     * @var string
38
     */
39
    private $version;
40
41
    /**
42
     * Which modifiers are excluded
43
     *
44
     * By default only static properties are excluded
45
     *
46
     * @var int
47
     */
48
    private $excludedModifiers = ReflectionProperty::IS_STATIC;
49
50
    /**
51
     * If this is true, properties will need to explicitly have an [@see Expose] annotation
52
     * to be serialized or deserialized
53
     *
54
     * @var bool
55
     */
56
    private $requireExpose = false;
57
58
    /**
59
     * Exclusion strategies that can be cached
60
     *
61
     * @var ExclusionStrategy[]
62
     */
63
    private $exclusionStrategies = [];
64
65
    /**
66
     * A cache of the created strategies from annotations
67
     *
68
     * @var ExclusionStrategy[][]
69
     */
70
    private $runtimeExclusions = [];
71
72
    /**
73
     * Constructor
74
     *
75
     * @param ConstructorConstructor $constructorConstructor
76
     */
77
    public function __construct(ConstructorConstructor $constructorConstructor)
78
    {
79
        $this->constructorConstructor = $constructorConstructor;
80
    }
81
82
    /**
83
     * Set the version to test against
84
     *
85
     * @param string $version
86
     * @return Excluder
87
     */
88
    public function setVersion(?string $version): Excluder
89
    {
90
        $this->version = $version;
91
92
        return $this;
93
    }
94
95
    /**
96
     * Set an integer representing the property modifiers that should be excluded
97
     *
98
     * @param int $modifiers
99
     * @return Excluder
100
     */
101
    public function setExcludedModifiers(int $modifiers): Excluder
102
    {
103
        $this->excludedModifiers = $modifiers;
104
105
        return $this;
106
    }
107
108
    /**
109
     * Require the [@see Expose] annotation to serialize properties
110
     *
111
     * @param bool $requireExpose
112
     * @return Excluder
113
     */
114
    public function setRequireExpose(bool $requireExpose): Excluder
115
    {
116
        $this->requireExpose = $requireExpose;
117
118
        return $this;
119
    }
120
121
    /**
122
     * Add an exclusion strategy that is cacheable
123
     *
124
     * @param ExclusionStrategy $strategy
125
     */
126
    public function addExclusionStrategy(ExclusionStrategy $strategy): void
127
    {
128
        $this->exclusionStrategies[] = $strategy;
129
    }
130
131
    /**
132
     * Compile time exclusion checking of classes to determine if we should exclude during serialization
133
     *
134
     * @param DefaultClassMetadata $classMetadata
135
     * @return bool
136
     */
137
    public function excludeClassSerialize(DefaultClassMetadata $classMetadata): bool
138
    {
139
        if ($this->skipClassSerializeByStrategy($classMetadata, null, true)) {
140
            return true;
141
        }
142
143
        foreach ($this->exclusionStrategies as $strategy) {
144
            if ($strategy->skipSerializingClass($classMetadata)) {
145
                return true;
146
            }
147
        }
148
149
        return $this->excludeClass($classMetadata, true);
150
    }
151
152
    /**
153
     * Skip serializing class by ExclusionStrategy annotation
154
     *
155
     * @param DefaultClassMetadata $classMetadata
156
     * @param null $object
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $object is correct as it would always require null to be passed?
Loading history...
157
     * @param bool $cache
158
     * @return bool
159
     */
160
    public function skipClassSerializeByStrategy(DefaultClassMetadata $classMetadata, $object = null, bool $cache = false): bool
161
    {
162
        /** @var ExclusionStrategyAnnotation[] $exclusions */
163
        $exclusions = $classMetadata->annotations->getAll(ExclusionStrategyAnnotation::class);
164
        foreach ($this->createStrategies($classMetadata->name, $exclusions) as $strategy) {
165
            if (($cache && $strategy->cacheResult()) && $strategy->skipSerializingClass($classMetadata, $object)) {
166
                return true;
167
            }
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * Compile time exclusion checking of classes to determine if we should exclude during deserialization
175
     *
176
     * @param DefaultClassMetadata $class
177
     * @return bool
178
     */
179
    public function excludeClassDeserialize(DefaultClassMetadata $class): bool
180
    {
181
        if ($this->skipClassDeserializeByStrategy($class, null, null, true)) {
182
            return true;
183
        }
184
185
        foreach ($this->exclusionStrategies as $strategy) {
186
            if ($strategy->skipDeserializingClass($class)) {
187
                return true;
188
            }
189
        }
190
191
        return $this->excludeClass($class, false);
192
    }
193
194
    /**
195
     * Skip deserializing class by ExclusionStrategy annotation
196
     *
197
     * @param DefaultClassMetadata $class
198
     * @param null $object
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $object is correct as it would always require null to be passed?
Loading history...
199
     * @param null $payload
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $payload is correct as it would always require null to be passed?
Loading history...
200
     * @param bool $cache
201
     * @return bool
202
     */
203
    public function skipClassDeserializeByStrategy(DefaultClassMetadata $class, $object = null, $payload = null, bool $cache = false): bool
204
    {
205
        /** @var ExclusionStrategyAnnotation[] $exclusions */
206
        $exclusions = $class->annotations->getAll(ExclusionStrategyAnnotation::class);
207
        foreach ($this->createStrategies($class->name, $exclusions) as $strategy) {
208
            if (($cache && $strategy->cacheResult()) && $strategy->skipDeserializingClass($class, $payload, $object)) {
209
                return true;
210
            }
211
        }
212
213
        return false;
214
    }
215
216
    /**
217
     * Compile time exclusion checking of properties
218
     *
219
     * @param Property $property
220
     * @return bool
221
     */
222
    public function excludePropertySerialize(Property $property): bool
223
    {
224
        // exclude the property if the property modifiers are found in the excluded modifiers
225
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
226
            return true;
227
        }
228
229
        if ($this->skipPropertySerializeByStrategy($property, null, true)) {
230
            return true;
231
        }
232
233
        foreach ($this->exclusionStrategies as $strategy) {
234
            if ($strategy->skipSerializingProperty($property)) {
235
                return true;
236
            }
237
        }
238
239
        return $this->excludeProperty($property, true);
240
    }
241
242
    /**
243
     * Skip serializing property by ExclusionStrategy annotation
244
     *
245
     * @param Property $property
246
     * @param null $object
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $object is correct as it would always require null to be passed?
Loading history...
247
     * @param bool $cache
248
     * @return bool
249
     */
250
    public function skipPropertySerializeByStrategy(Property $property, $object = null, bool $cache = false): bool
251
    {
252
        /** @var ExclusionStrategyAnnotation[] $exclusions */
253
        $exclusions = $property->annotations->getAll(ExclusionStrategyAnnotation::class);
254
        foreach ($this->createStrategies($property->realName, $exclusions) as $strategy) {
255
            if (($cache && $strategy->cacheResult()) && $strategy->skipSerializingProperty($property, $object)) {
256
                return true;
257
            }
258
        }
259
260
        return false;
261
    }
262
263
    /**
264
     * Compile time exclusion checking of properties
265
     *
266
     * @param Property $property
267
     * @return bool
268
     */
269
    public function excludePropertyDeserialize(Property $property): bool
270
    {
271
        // exclude the property if the property modifiers are found in the excluded modifiers
272
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
273
            return true;
274
        }
275
276
        if ($this->skipPropertyDeserializeByStrategy($property, null, null, true)) {
277
            return true;
278
        }
279
280
        foreach ($this->exclusionStrategies as $strategy) {
281
            if ($strategy->skipDeserializingProperty($property)) {
282
                return true;
283
            }
284
        }
285
286
        return $this->excludeProperty($property, false);
287
    }
288
289
    /**
290
     * Skip deserializing property by ExclusionStrategy annotation
291
     *
292
     * @param Property $property
293
     * @param null $object
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $object is correct as it would always require null to be passed?
Loading history...
294
     * @param bool $cache
295
     * @return bool
296
     */
297
    public function skipPropertyDeserializeByStrategy(Property $property, $object = null, $payload = null, bool $cache = false): bool
298
    {
299
        /** @var ExclusionStrategyAnnotation[] $exclusions */
300
        $exclusions = $property->annotations->getAll(ExclusionStrategyAnnotation::class);
301
        foreach ($this->createStrategies($property->realName, $exclusions) as $strategy) {
302
            if (($cache && $strategy->cacheResult()) && $strategy->skipDeserializingProperty($property, $object, $payload)) {
303
                return true;
304
            }
305
        }
306
307
        return false;
308
    }
309
310
        /**
311
     * Check if we should exclude an entire class. Returns true if the class has an [@Exclude] annotation
312
     * unless one of the properties has an [@Expose] annotation
313
     *
314
     * @param DefaultClassMetadata $classMetadata
315
     * @param bool $serialize
316
     * @return bool
317
     */
318
    private function excludeClass(DefaultClassMetadata $classMetadata, bool $serialize): bool
319
    {
320
        $annotations = $classMetadata->annotations;
321
322
        // exclude if version doesn't match
323
        if (!$this->validVersion($annotations)) {
324
            return true;
325
        }
326
327
        // exclude if requireExpose is set and class doesn't have an expose annotation
328
        $expose = $annotations->get(Expose::class);
329
        if ($this->requireExpose && ($expose !== null && !$expose->shouldExpose($serialize))) {
0 ignored issues
show
Bug introduced by
The method shouldExpose() does not exist on Tebru\AnnotationReader\AbstractAnnotation. It seems like you code against a sub-type of Tebru\AnnotationReader\AbstractAnnotation such as Tebru\Gson\Annotation\Expose. ( Ignorable by Annotation )

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

329
        if ($this->requireExpose && ($expose !== null && !$expose->/** @scrutinizer ignore-call */ shouldExpose($serialize))) {
Loading history...
330
            return true;
331
        }
332
333
        // don't exclude if exclude annotation doesn't exist or only exists for the other direction
334
        $exclude = $annotations->get(Exclude::class);
335
        if ($exclude === null || !$exclude->shouldExclude($serialize)) {
0 ignored issues
show
Bug introduced by
The method shouldExclude() does not exist on Tebru\AnnotationReader\AbstractAnnotation. It seems like you code against a sub-type of Tebru\AnnotationReader\AbstractAnnotation such as Tebru\Gson\Annotation\Exclude. ( Ignorable by Annotation )

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

335
        if ($exclude === null || !$exclude->/** @scrutinizer ignore-call */ shouldExclude($serialize)) {
Loading history...
336
            return false;
337
        }
338
339
        // don't exclude if the annotation exists, but a property is exposed
340
        foreach ($classMetadata->properties->toArray() as $property) {
341
            $expose = $property->annotations->get(Expose::class);
342
            if ($expose !== null && $expose->shouldExpose($serialize)) {
343
                return false;
344
            }
345
        }
346
347
        // exclude if an annotation is set and no properties are exposed
348
        return true;
349
    }
350
351
    /**
352
     * Checks various annotations to see if the property should be excluded
353
     *
354
     * - [@see Since] / [@see Until]
355
     * - [@see Exclude]
356
     * - [@see Expose] (if requireExpose is set)
357
     *
358
     * @param Property $property
359
     * @param bool $serialize
360
     * @return bool
361
     *
362
     */
363
    private function excludeProperty(Property $property, bool $serialize): bool
364
    {
365
        $annotations = $property->getAnnotations();
366
        if (!$this->validVersion($annotations)) {
367
            return true;
368
        }
369
370
        // exclude from annotation
371
        $exclude = $annotations->get(Exclude::class);
372
        if (null !== $exclude && $exclude->shouldExclude($serialize)) {
373
            return true;
374
        }
375
376
        $classExclude = $property->classMetadata->annotations->get(Exclude::class);
377
378
        // if we need an expose annotation
379
        if ($this->requireExpose || ($classExclude !== null && $classExclude->shouldExclude($serialize))) {
380
            $expose = $annotations->get(Expose::class);
381
            if (null === $expose || !$expose->shouldExpose($serialize)) {
382
                return true;
383
            }
384
        }
385
386
        return false;
387
    }
388
389
    /**
390
     * Returns true if the set version is valid for [@see Since] and [@see Until] annotations
391
     *
392
     * @param AnnotationCollection $annotations
393
     * @return bool
394
     */
395
    private function validVersion(AnnotationCollection $annotations): bool
396
    {
397
        return !$this->shouldSkipSince($annotations) && !$this->shouldSkipUntil($annotations);
398
    }
399
400
    /**
401
     * Returns true if we should skip based on the [@see Since] annotation
402
     *
403
     * @param AnnotationCollection $annotations
404
     * @return bool
405
     */
406
    private function shouldSkipSince(AnnotationCollection $annotations): bool
407
    {
408
        $sinceAnnotation = $annotations->get(Since::class);
409
410
        return
411
            null !== $sinceAnnotation
412
            && null !== $this->version
413
            && version_compare($this->version, $sinceAnnotation->getValue(), '<');
414
    }
415
416
    /**
417
     * Returns true if we should skip based on the [@see Until] annotation
418
     *
419
     * @param AnnotationCollection $annotations
420
     * @return bool
421
     */
422
    private function shouldSkipUntil(AnnotationCollection $annotations): bool
423
    {
424
        $untilAnnotation = $annotations->get(Until::class);
425
426
        return
427
            null !== $untilAnnotation
428
            && null !== $this->version
429
            && version_compare($this->version, $untilAnnotation->getValue(), '>=');
430
    }
431
432
    /**
433
     * @param string $key
434
     * @param ExclusionStrategyAnnotation[] $exclusions
435
     * @return ExclusionStrategy[]
436
     */
437
    private function createStrategies(string $key, array $exclusions): array
438
    {
439
        if (!isset($this->runtimeExclusions[$key])) {
440
            $this->runtimeExclusions[$key] = array_map(function (ExclusionStrategyAnnotation $exclusion) {
441
                return $this->constructorConstructor->get(TypeToken::create($exclusion->getValue()))->construct();
442
            }, $exclusions);
443
        }
444
445
        return $this->runtimeExclusions[$key];
446
    }
447
}
448