Passed
Push — caching ( 6c9b2f...fd9061 )
by Nate
03:03
created

Excluder::validVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
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 39
    public function __construct(ConstructorConstructor $constructorConstructor)
78
    {
79 39
        $this->constructorConstructor = $constructorConstructor;
80 39
    }
81
82
    /**
83
     * Set the version to test against
84
     *
85
     * @param string $version
86
     * @return Excluder
87
     */
88 15
    public function setVersion(?string $version): Excluder
89
    {
90 15
        $this->version = $version;
91
92 15
        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 5
    public function setExcludedModifiers(int $modifiers): Excluder
102
    {
103 5
        $this->excludedModifiers = $modifiers;
104
105 5
        return $this;
106
    }
107
108
    /**
109
     * Require the [@see Expose] annotation to serialize properties
110
     *
111
     * @param bool $requireExpose
112
     * @return Excluder
113
     */
114 8
    public function setRequireExpose(bool $requireExpose): Excluder
115
    {
116 8
        $this->requireExpose = $requireExpose;
117
118 8
        return $this;
119
    }
120
121
    /**
122
     * Add an exclusion strategy that is cacheable
123
     *
124
     * @param ExclusionStrategy $strategy
125
     */
126 1
    public function addExclusionStrategy(ExclusionStrategy $strategy): void
127
    {
128 1
        $this->exclusionStrategies[] = $strategy;
129 1
    }
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 13
    public function excludeClassSerialize(DefaultClassMetadata $classMetadata): bool
138
    {
139 13
        if ($this->skipClassSerializeByStrategy($classMetadata, null, true)) {
140
            return true;
141
        }
142
143 13
        foreach ($this->exclusionStrategies as $strategy) {
144 1
            if ($strategy->skipSerializingClass($classMetadata)) {
145 1
                return true;
146
            }
147
        }
148
149 12
        return $this->excludeClass($classMetadata, true);
150
    }
151
152
    /**
153
     * Skip serializing class by ExclusionStrategy annotation
154
     *
155
     * @param DefaultClassMetadata $class
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 13
    public function skipClassSerializeByStrategy(DefaultClassMetadata $class, $object = null, bool $cache = false): bool
161
    {
162 13
        foreach ($this->createStrategies($class->name, $class->annotations) as $strategy) {
163
            if (($cache && $strategy->cacheResult()) && $strategy->skipSerializingClass($class, $object)) {
164
                return true;
165
            }
166
        }
167
168 13
        return false;
169
    }
170
171
    /**
172
     * Compile time exclusion checking of classes to determine if we should exclude during deserialization
173
     *
174
     * @param DefaultClassMetadata $class
175
     * @return bool
176
     */
177 13
    public function excludeClassDeserialize(DefaultClassMetadata $class): bool
178
    {
179 13
        if ($this->skipClassDeserializeByStrategy($class, null, null, true)) {
180
            return true;
181
        }
182
183 13
        foreach ($this->exclusionStrategies as $strategy) {
184 1
            if ($strategy->skipDeserializingClass($class)) {
185 1
                return true;
186
            }
187
        }
188
189 12
        return $this->excludeClass($class, false);
190
    }
191
192
    /**
193
     * Skip deserializing class by ExclusionStrategy annotation
194
     *
195
     * @param DefaultClassMetadata $class
196
     * @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...
197
     * @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...
198
     * @param bool $cache
199
     * @return bool
200
     */
201 13
    public function skipClassDeserializeByStrategy(DefaultClassMetadata $class, $object = null, $payload = null, bool $cache = false): bool
202
    {
203 13
        foreach ($this->createStrategies($class->name, $class->annotations) as $strategy) {
204
            if (($cache && $strategy->cacheResult()) && $strategy->skipDeserializingClass($class, $payload, $object)) {
205
                return true;
206
            }
207
        }
208
209 13
        return false;
210
    }
211
212
    /**
213
     * Returns true if runtime strategies exist
214
     *
215
     * @param DefaultClassMetadata $class
216
     * @return bool
217
     */
218 3
    public function hasRuntimeClassStrategies(DefaultClassMetadata $class): bool
219
    {
220 3
        foreach ($this->createStrategies($class->name, $class->annotations) as $strategy) {
221
            if ($strategy->cacheResult() === false) {
222
                return true;
223
            }
224
        }
225
226 3
        return false;
227
    }
228
229
    /**
230
     * Compile time exclusion checking of properties
231
     *
232
     * @param Property $property
233
     * @return bool
234
     */
235 24
    public function excludePropertySerialize(Property $property): bool
236
    {
237
        // exclude the property if the property modifiers are found in the excluded modifiers
238 24
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
239 3
            return true;
240
        }
241
242 21
        if ($this->skipPropertySerializeByStrategy($property, null, true)) {
243
            return true;
244
        }
245
246 21
        foreach ($this->exclusionStrategies as $strategy) {
247 1
            if ($strategy->skipSerializingProperty($property)) {
248 1
                return true;
249
            }
250
        }
251
252 20
        return $this->excludeProperty($property, true);
253
    }
254
255
    /**
256
     * Skip serializing property by ExclusionStrategy annotation
257
     *
258
     * @param Property $property
259
     * @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...
260
     * @param bool $cache
261
     * @return bool
262
     */
263 21
    public function skipPropertySerializeByStrategy(Property $property, $object = null, bool $cache = false): bool
264
    {
265 21
        foreach ($this->createStrategies($property->realName, $property->annotations) as $strategy) {
266
            if (($cache && $strategy->cacheResult()) && $strategy->skipSerializingProperty($property, $object)) {
267
                return true;
268
            }
269
        }
270
271 21
        return false;
272
    }
273
274
    /**
275
     * Returns true if runtime strategies exist
276
     *
277
     * @param Property $property
278
     * @return bool
279
     */
280 2
    public function hasRuntimePropertyStrategies(Property $property): bool
281
    {
282 2
        foreach ($this->createStrategies($property->realName, $property->annotations) as $strategy) {
283
            if ($strategy->cacheResult() === false) {
284
                return true;
285
            }
286
        }
287
288 2
        return false;
289
    }
290
291
    /**
292
     * Compile time exclusion checking of properties
293
     *
294
     * @param Property $property
295
     * @return bool
296
     */
297 24
    public function excludePropertyDeserialize(Property $property): bool
298
    {
299
        // exclude the property if the property modifiers are found in the excluded modifiers
300 24
        if (0 !== ($this->excludedModifiers & $property->modifiers)) {
301 3
            return true;
302
        }
303
304 21
        if ($this->skipPropertyDeserializeByStrategy($property, null, null, true)) {
305
            return true;
306
        }
307
308 21
        foreach ($this->exclusionStrategies as $strategy) {
309 1
            if ($strategy->skipDeserializingProperty($property)) {
310 1
                return true;
311
            }
312
        }
313
314 20
        return $this->excludeProperty($property, false);
315
    }
316
317
    /**
318
     * Skip deserializing property by ExclusionStrategy annotation
319
     *
320
     * @param Property $property
321
     * @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...
322
     * @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...
323
     * @param bool $cache
324
     * @return bool
325
     */
326 21
    public function skipPropertyDeserializeByStrategy(Property $property, $object = null, $payload = null, bool $cache = false): bool
327
    {
328 21
        foreach ($this->createStrategies($property->realName, $property->annotations) as $strategy) {
329
            if (($cache === $strategy->cacheResult()) && $strategy->skipDeserializingProperty($property, $object, $payload)) {
330
                return true;
331
            }
332
        }
333
334 21
        return false;
335
    }
336
337
        /**
338
     * Check if we should exclude an entire class. Returns true if the class has an [@Exclude] annotation
339
     * unless one of the properties has an [@Expose] annotation
340
     *
341
     * @param DefaultClassMetadata $classMetadata
342
     * @param bool $serialize
343
     * @return bool
344
     */
345 12
    private function excludeClass(DefaultClassMetadata $classMetadata, bool $serialize): bool
346
    {
347 12
        $annotations = $classMetadata->annotations;
348
349
        // exclude if version doesn't match
350 12
        if (!$this->validVersion($annotations)) {
351 3
            return true;
352
        }
353
354
        // exclude if requireExpose is set and class doesn't have an expose annotation
355 9
        $expose = $annotations->get(Expose::class);
356 9
        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

356
        if ($this->requireExpose && ($expose !== null && !$expose->/** @scrutinizer ignore-call */ shouldExpose($serialize))) {
Loading history...
357 1
            return true;
358
        }
359
360
        // don't exclude if exclude annotation doesn't exist or only exists for the other direction
361 9
        $exclude = $annotations->get(Exclude::class);
362 9
        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

362
        if ($exclude === null || !$exclude->/** @scrutinizer ignore-call */ shouldExclude($serialize)) {
Loading history...
363 8
            return false;
364
        }
365
366
        // don't exclude if the annotation exists, but a property is exposed
367 2
        foreach ($classMetadata->properties->toArray() as $property) {
368 1
            $expose = $property->annotations->get(Expose::class);
369 1
            if ($expose !== null && $expose->shouldExpose($serialize)) {
370 1
                return false;
371
            }
372
        }
373
374
        // exclude if an annotation is set and no properties are exposed
375 1
        return true;
376
    }
377
378
    /**
379
     * Checks various annotations to see if the property should be excluded
380
     *
381
     * - [@see Since] / [@see Until]
382
     * - [@see Exclude]
383
     * - [@see Expose] (if requireExpose is set)
384
     *
385
     * @param Property $property
386
     * @param bool $serialize
387
     * @return bool
388
     *
389
     */
390 20
    private function excludeProperty(Property $property, bool $serialize): bool
391
    {
392 20
        $annotations = $property->getAnnotations();
393 20
        if (!$this->validVersion($annotations)) {
394 3
            return true;
395
        }
396
397
        // exclude from annotation
398 17
        $exclude = $annotations->get(Exclude::class);
399 17
        if (null !== $exclude && $exclude->shouldExclude($serialize)) {
400 3
            return true;
401
        }
402
403 16
        $classExclude = $property->classMetadata->annotations->get(Exclude::class);
404
405
        // if we need an expose annotation
406 16
        if ($this->requireExpose || ($classExclude !== null && $classExclude->shouldExclude($serialize))) {
407 6
            $expose = $annotations->get(Expose::class);
408 6
            if (null === $expose || !$expose->shouldExpose($serialize)) {
409 4
                return true;
410
            }
411
        }
412
413 14
        return false;
414
    }
415
416
    /**
417
     * Returns true if the set version is valid for [@see Since] and [@see Until] annotations
418
     *
419
     * @param AnnotationCollection $annotations
420
     * @return bool
421
     */
422 30
    private function validVersion(AnnotationCollection $annotations): bool
423
    {
424 30
        return !$this->shouldSkipSince($annotations) && !$this->shouldSkipUntil($annotations);
425
    }
426
427
    /**
428
     * Returns true if we should skip based on the [@see Since] annotation
429
     *
430
     * @param AnnotationCollection $annotations
431
     * @return bool
432
     */
433 30
    private function shouldSkipSince(AnnotationCollection $annotations): bool
434
    {
435 30
        $sinceAnnotation = $annotations->get(Since::class);
436
437
        return
438 30
            null !== $sinceAnnotation
439 30
            && null !== $this->version
440 30
            && version_compare($this->version, $sinceAnnotation->getValue(), '<');
441
    }
442
443
    /**
444
     * Returns true if we should skip based on the [@see Until] annotation
445
     *
446
     * @param AnnotationCollection $annotations
447
     * @return bool
448
     */
449 28
    private function shouldSkipUntil(AnnotationCollection $annotations): bool
450
    {
451 28
        $untilAnnotation = $annotations->get(Until::class);
452
453
        return
454 28
            null !== $untilAnnotation
455 28
            && null !== $this->version
456 28
            && version_compare($this->version, $untilAnnotation->getValue(), '>=');
457
    }
458
459
    /**
460
     * @param string $key
461
     * @param AnnotationCollection $annotationCollection
462
     * @return ExclusionStrategy[]
463
     */
464 31
    private function createStrategies(string $key, AnnotationCollection $annotationCollection): array
465
    {
466 31
        if (!isset($this->runtimeExclusions[$key])) {
467
            $this->runtimeExclusions[$key] = array_map(function (ExclusionStrategyAnnotation $exclusion) {
468
                return $this->constructorConstructor->get(TypeToken::create($exclusion->getValue()))->construct();
469 31
            }, $annotationCollection->getAll(ExclusionStrategyAnnotation::class) ?? []);
470
        }
471
472 31
        return $this->runtimeExclusions[$key];
473
    }
474
}
475