Passed
Branch feature/configurable-annotatio... (1a9d97)
by Pieter
02:03
created

ServiceLibraryFactory::overrideAnnotationConfig()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
namespace W2w\Lib\Apie;
3
4
use Carbon\Carbon;
5
use Doctrine\Common\Annotations\AnnotationReader;
6
use Doctrine\Common\Annotations\AnnotationRegistry;
7
use Doctrine\Common\Annotations\CachedReader;
8
use Doctrine\Common\Annotations\Reader;
9
use Doctrine\Common\Cache\PhpFileCache;
10
use erasys\OpenApi\Spec\v3\Info;
11
use Psr\Cache\CacheItemPoolInterface;
12
use Psr\Container\ContainerInterface;
13
use RuntimeException;
14
use Symfony\Component\Cache\Adapter\ArrayAdapter;
15
use Symfony\Component\PropertyAccess\PropertyAccess;
16
use Symfony\Component\PropertyAccess\PropertyAccessor;
17
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
18
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
19
use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor;
20
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
21
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
22
use Symfony\Component\Serializer\Encoder\EncoderInterface;
23
use Symfony\Component\Serializer\Encoder\JsonDecode;
24
use Symfony\Component\Serializer\Encoder\JsonEncode;
25
use Symfony\Component\Serializer\Encoder\JsonEncoder;
26
use Symfony\Component\Serializer\Encoder\XmlEncoder;
27
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
28
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
29
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
30
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
31
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
32
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
33
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
34
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
35
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
36
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
37
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
38
use Symfony\Component\Serializer\Serializer;
39
use Symfony\Component\Serializer\SerializerInterface;
40
use W2w\Lib\Apie\ApiResources\ApplicationInfo;
41
use W2w\Lib\Apie\ApiResources\Status;
42
use W2w\Lib\Apie\Encodings\FormatRetriever;
43
use W2w\Lib\Apie\Normalizers\ApieObjectNormalizer;
44
use W2w\Lib\Apie\Normalizers\CarbonNormalizer;
45
use W2w\Lib\Apie\Normalizers\ContextualNormalizer;
46
use W2w\Lib\Apie\Normalizers\EvilReflectionPropertyNormalizer;
47
use W2w\Lib\Apie\Normalizers\ExceptionNormalizer;
48
use W2w\Lib\Apie\Normalizers\UuidNormalizer;
49
use W2w\Lib\Apie\Normalizers\ValueObjectNormalizer;
50
use W2w\Lib\Apie\OpenApiSchema\OpenApiSpecGenerator;
51
use W2w\Lib\Apie\OpenApiSchema\SchemaGenerator;
52
use W2w\Lib\Apie\Resources\ApiResources;
53
use W2w\Lib\Apie\Resources\ApiResourcesInterface;
54
55
/**
56
 * To avoid lots of boilerplate in using the library, this class helps in making sensible defaults.
57
 * @codeCoverageIgnore
58
 */
59
class ServiceLibraryFactory
60
{
61
    /**
62
     * @var boolean
63
     */
64
    private $debug;
65
66
    /**
67
     * @var ContainerInterface
68
     */
69
    private $container;
70
71
    /**
72
     * @var ApiResourceFacade
73
     */
74
    private $apiResourceFacade;
75
76
    /**
77
     * @var ApiResourcesInterface
78
     */
79
    private $apiResources;
80
81
    /**
82
     * @var ApiResourceRetriever
83
     */
84
    private $apiResourceRetriever;
85
86
    /**
87
     * @var ApiResourcePersister
88
     */
89
    private $apiResourcePersister;
90
91
    /**
92
     * @var ApiResourceFactoryInterface
93
     */
94
    private $apiResourceFactory;
95
96
    /**
97
     * @var ApiResourceMetadataFactory
98
     */
99
    private $apiResourceMetadatafactory;
100
101
    /**
102
     * @var Reader
103
     */
104
    private $annotationReader;
105
106
    /**
107
     * @var ClassResourceConverter
108
     */
109
    private $classResourceConverter;
110
111
    /**
112
     * @var FormatRetriever
113
     */
114
    private $formatRetriever;
115
116
    /**
117
     * @var SerializerInterface
118
     */
119
    private $serializer;
120
121
    /**
122
     * @var NameConverterInterface
123
     */
124
    private $propertyConverter;
125
126
    /**
127
     * @var ClassMetadataFactoryInterface
128
     */
129
    private $classMetadataFactory;
130
131
    /**
132
     * @var string|null
133
     */
134
    private $cacheFolder;
135
136
    /**
137
     * @var (NormalizerInterface|DenormalizerInterface)[]|null
138
     */
139
    private $normalizers;
140
141
    /**
142
     * @var (NormalizerInterface|DenormalizerInterface)[]|null
143
     */
144
    private $additionalNormalizers;
145
146
    /**
147
     * @var EncoderInterface[]|null
148
     */
149
    private $encoders;
150
151
    /**
152
     * @var CacheItemPoolInterface
153
     */
154
    private $serializerCache;
155
156
    /**
157
     * @var PropertyAccessor
158
     */
159
    private $propertyAccessor;
160
161
    /**
162
     * @var PropertyTypeExtractorInterface
163
     */
164
    private $propertyTypeExtractor;
165
166
    /**
167
     * @var Info
168
     */
169
    private $info;
170
171
    /**
172
     * @var SchemaGenerator
173
     */
174
    private $schemaGenerator;
175
176
    /**
177
     * @var OpenApiSpecGenerator
178
     */
179
    private $openApiSpecGenerator;
180
181
    /**
182
     * @var callable[]
183
     */
184
    private $callables = [];
185
186
    /**
187
     * @var ApiResource[]|null
188
     */
189
    private $overrideAnnotationConfigs;
190
191
    /**
192
     * @param string[]|ApiResourcesInterface $apiResourceClasses
193
     * @param bool $debug
194
     * @param string|null $cacheFolder
195
     */
196
    public function __construct($apiResourceClasses = [ApplicationInfo::class, Status::class], bool $debug = false, ?string $cacheFolder = null)
197
    {
198
        $this->apiResources = $apiResourceClasses instanceof ApiResourcesInterface ? $apiResourceClasses : new ApiResources($apiResourceClasses);
199
        $this->debug = $debug;
200
        $this->cacheFolder = $cacheFolder;
201
    }
202
203
    private function isDebug(): bool
204
    {
205
        return $this->debug;
206
    }
207
208
    private function getCacheFolder(): ?string
209
    {
210
        return $this->cacheFolder;
211
    }
212
213
    /**
214
     * Workaround to run a callable to set some values when the Serializer is being instantiated.
215
     *
216
     * @param callable $callable
217
     * @return ServiceLibraryFactory
218
     */
219
    public function runBeforeInstantiation(callable $callable): self
220
    {
221
        $this->callables[] = $callable;
222
        return $this;
223
    }
224
225
    public function setApiResourceFactory(ApiResourceFactoryInterface $apiResourceFactory): self
226
    {
227
        if ($this->apiResourceFactory) {
228
            throw new RuntimeException('I have already instantiated ApiResourceFactory and can no longer set it!');
229
        }
230
        $this->apiResourceFactory = $apiResourceFactory;
231
        return $this;
232
    }
233
234
    public function setInfo(Info $info): self
235
    {
236
        if ($this->info) {
237
            throw new RuntimeException('I have already instantiated Info and can no longer set it!');
238
        }
239
        $this->info = $info;
240
        return $this;
241
    }
242
243
    public function setSerializer(SerializerInterface $serializer): self
244
    {
245
        if ($this->serializer) {
246
            throw new RuntimeException('I have already instantiated the serializer and can no longer set the serializer!');
247
        }
248
        $this->serializer = $serializer;
249
        return $this;
250
    }
251
252
    public function setSerializerCache(CacheItemPoolInterface $serializerCache): self
253
    {
254
        if ($this->serializerCache) {
255
            throw new RuntimeException('I have already instantiated the serializer cache and can no longer set the serializer cache!');
256
        }
257
        $this->serializerCache = $serializerCache;
258
        return $this;
259
    }
260
261
    public function setClassMetadataFactory(ClassMetadataFactoryInterface $classMetadataFactory): self
262
    {
263
        if ($this->classMetadataFactory) {
264
            throw new RuntimeException('I have already instantiated the class metadata factory and can no longer set it!');
265
        }
266
        $this->classMetadataFactory = $classMetadataFactory;
267
        return $this;
268
    }
269
270
    public function setPropertyConverter(NameConverterInterface $propertyConverter): self
271
    {
272
        if ($this->propertyConverter) {
273
            throw new RuntimeException('I have already instantiated the property converter and can no longer set the property converter!');
274
        }
275
        $this->propertyConverter = $propertyConverter;
276
        return $this;
277
    }
278
279
    public function setAdditionalNormalizers(array $additionalNormalizers): self
280
    {
281
        if (is_array($this->additionalNormalizers)) {
282
            throw new RuntimeException('I have already instantiated additional normalizers and can no longer set it!');
283
        }
284
        $this->additionalNormalizers = $additionalNormalizers;
285
        return $this;
286
    }
287
288
    public function setEncoders(array $encoders): self
289
    {
290
        if (is_array($this->encoders)) {
291
            throw new RuntimeException('I have already instantiated encoders and can no longer set it!');
292
        }
293
        $this->encoders = $encoders;
294
        return $this;
295
    }
296
297
    private function getEncoders(): array
298
    {
299
        if (!is_array($this->encoders)) {
300
            $this->encoders = [
301
                new XmlEncoder([XmlEncoder::ROOT_NODE_NAME => 'item']),
302
                new JsonEncoder(
303
                    new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES]),
304
                    new JsonDecode([JsonDecode::ASSOCIATIVE => false])
305
                )
306
            ];
307
        }
308
        return $this->encoders;
309
    }
310
311
    public function getApiResourceFacade(): ApiResourceFacade
312
    {
313
        if (!$this->apiResourceFacade) {
314
            $this->apiResourceFacade = new ApiResourceFacade(
315
                $this->getApiResourceRetriever(),
316
                $this->getApiResourcePersister(),
317
                $this->getClassResourceConverter(),
318
                $this->getSerializer(),
319
                $this->getFormatRetriever()
320
            );
321
        }
322
        return $this->apiResourceFacade;
323
    }
324
325
    private function getAdditionalNormalizers(): array
326
    {
327
        if (!is_array($this->additionalNormalizers)) {
328
            $this->additionalNormalizers = [];
329
        }
330
        return $this->additionalNormalizers;
331
    }
332
333
    public function setContainer(ContainerInterface $container): self
334
    {
335
        if ($this->container || $this->apiResourceFactory) {
336
            throw new RuntimeException('I have already instantiated services and can no longer set the container!');
337
        }
338
        $this->container = $container;
339
        return $this;
340
    }
341
342
    private function getContainer(): ?ContainerInterface
343
    {
344
        return $this->container;
345
    }
346
347
    private function getFormatRetriever(): FormatRetriever
348
    {
349
        if (!$this->formatRetriever) {
350
            $this->formatRetriever = new FormatRetriever();
351
        }
352
        return $this->formatRetriever;
353
    }
354
355
    public function getSerializer(): SerializerInterface
356
    {
357
        if (!$this->serializer) {
358
            foreach ($this->callables as $callable) {
359
                $callable('serializer');
360
            }
361
            $normalizers = $this->getNormalizers();
362
            $encoders = $this->getEncoders();
363
            $this->serializer = new Serializer($normalizers, $encoders);
364
        }
365
        return $this->serializer;
366
    }
367
368
    /**
369
     * @return Reader
370
     */
371
    private function getAnnotationReader(): Reader
372
    {
373
        if (!$this->annotationReader) {
374
            /** @scrutinizer ignore-deprecated */AnnotationRegistry::registerLoader('class_exists');
375
            if (class_exists(PhpFileCache::class) && $this->getCacheFolder()) {
376
                $this->annotationReader = new CachedReader(
377
                    new AnnotationReader(),
378
                    new PhpFileCache($this->getCacheFolder() . DIRECTORY_SEPARATOR . '/doctrine-cache'),
379
                    $this->isDebug()
380
                );
381
            } else {
382
                $this->annotationReader = new AnnotationReader();
383
            }
384
            if ($this->overrideAnnotationConfigs) {
385
                $this->annotationReader = new ExtendReaderWithConfigReader($this->annotationReader, $this->overrideAnnotationConfigs);
386
            }
387
        }
388
        return $this->annotationReader;
389
    }
390
391
    /**
392
     * @param ApiResource[] $config
393
     *
394
     * @return ServiceLibraryFactory
395
     */
396
    public function overrideAnnotationConfig(array $config): self
397
    {
398
        if (isset($this->overrideAnnotationConfigs) || isset($this->annotationReader)) {
399
            throw new RuntimeException('I have already instantiated the reader and can no longer override the annotation config!');
400
        }
401
        $this->overrideAnnotationConfigs = $config;
402
        return $this;
403
    }
404
405
    private function getApiResourceMetadataFactory(): ApiResourceMetadataFactory
406
    {
407
        if (!$this->apiResourceMetadatafactory) {
408
            $this->apiResourceMetadatafactory = new ApiResourceMetadataFactory(
409
                $this->getAnnotationReader(),
410
                $this->getApiResourceFactory()
411
            );
412
        }
413
        return $this->apiResourceMetadatafactory;
414
    }
415
416
    private function getApiResourceFactory(): ApiResourceFactoryInterface
417
    {
418
        if (!$this->apiResourceFactory) {
419
            $this->apiResourceFactory = new ApiResourceFactory(
420
                $this->getContainer()
421
            );
422
        }
423
        return $this->apiResourceFactory;
424
    }
425
426
    private function getApiResourceRetriever(): ApiResourceRetriever
427
    {
428
        if (!$this->apiResourceRetriever) {
429
            $this->apiResourceRetriever = new ApiResourceRetriever(
430
                $this->getApiResourceMetadataFactory()
431
            );
432
        }
433
        return $this->apiResourceRetriever;
434
    }
435
436
    private function getApiResourcePersister(): ApiResourcePersister
437
    {
438
        if (!$this->apiResourcePersister) {
439
            $this->apiResourcePersister = new ApiResourcePersister(
440
                $this->getApiResourceMetadataFactory()
441
            );
442
        }
443
        return $this->apiResourcePersister;
444
    }
445
446
    public function getApiResources(): ApiResourcesInterface
447
    {
448
        return $this->apiResources;
449
    }
450
451
    private function getClassResourceConverter(): ClassResourceConverter
452
    {
453
        if (!$this->classResourceConverter) {
454
            $this->classResourceConverter = new ClassResourceConverter(
455
                $this->getPropertyConverter(),
456
                $this->getApiResources(),
457
                $this->isDebug()
458
            );
459
        }
460
        return $this->classResourceConverter;
461
    }
462
463
    private function getPropertyConverter(): NameConverterInterface
464
    {
465
        $classMetadataFactory = $this->getClassMetadataFactory();
466
        if (!$this->propertyConverter) {
467
            $this->propertyConverter = new MetadataAwareNameConverter(
468
                $classMetadataFactory,
469
                new CamelCaseToSnakeCaseNameConverter()
470
            );
471
        }
472
        return $this->propertyConverter;
473
    }
474
475
    private function getClassMetadataFactory(): ClassMetadataFactoryInterface
476
    {
477
        if (!$this->classMetadataFactory) {
478
            $this->classMetadataFactory = new ClassMetadataFactory(
479
                new LoaderChain([
480
                    new AnnotationLoader($this->getAnnotationReader()),
481
                    new BaseGroupLoader(['read', 'write', 'get', 'post', 'put']),
482
                ])
483
            );
484
        }
485
        return $this->classMetadataFactory;
486
    }
487
488
    public function getSerializerCache(): CacheItemPoolInterface
489
    {
490
        if (!$this->serializerCache) {
491
            $this->serializerCache = new ArrayAdapter(0, true);
492
        }
493
        return $this->serializerCache;
494
    }
495
496
    public function getPropertyAccessor(): PropertyAccessor
497
    {
498
        if (!$this->propertyAccessor) {
499
            $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
500
                ->setCacheItemPool($this->getSerializerCache())
501
                ->getPropertyAccessor();
502
        }
503
        return $this->propertyAccessor;
504
    }
505
506
    private function getPropertyTypeExtractor(): PropertyTypeExtractorInterface
507
    {
508
        if (!$this->propertyTypeExtractor) {
509
            $factory = $this->getClassMetadataFactory();
510
            $reflectionExtractor = new ReflectionExtractor();
511
            $phpDocExtractor = new PhpDocExtractor();
512
513
            $this->propertyTypeExtractor = new PropertyInfoExtractor(
514
                [
515
                    new SerializerExtractor($factory),
516
                    $reflectionExtractor,
517
                ],
518
                [
519
                    $phpDocExtractor,
520
                    $reflectionExtractor,
521
                ],
522
                [
523
                    $phpDocExtractor,
524
                ],
525
                [
526
                    $reflectionExtractor,
527
                ],
528
                [
529
                    $reflectionExtractor,
530
                ]
531
            );
532
        }
533
        return $this->propertyTypeExtractor;
534
    }
535
536
    private function getNormalizers(): array
537
    {
538
        if (!is_array($this->normalizers)) {
539
            $classMetadataFactory = $this->getClassMetadataFactory();
540
            $classDiscriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
541
542
            $this->normalizers = $this->getAdditionalNormalizers();
543
            $this->normalizers[] = new ExceptionNormalizer($this->isDebug());
544
545
            if (class_exists(Carbon::class)) {
546
                $this->normalizers[] = new CarbonNormalizer([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']);
547
            } else {
548
                $this->normalizers[] = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']);
549
            }
550
            $this->normalizers[] = new ValueObjectNormalizer();
551
            $this->normalizers[] = new UuidNormalizer();
552
553
            $this->normalizers[] = new JsonSerializableNormalizer();
554
            $this->normalizers[] = new ArrayDenormalizer();
555
556
            $objectNormalizer = new ApieObjectNormalizer(
557
                $classMetadataFactory,
558
                $this->getPropertyConverter(),
559
                $this->getPropertyAccessor(),
560
                $this->getPropertyTypeExtractor(),
561
                $classDiscriminator,
562
                null,
563
                []
564
            );
565
            $evilObjectNormalizer = new EvilReflectionPropertyNormalizer(
566
                $classMetadataFactory,
567
                $this->getPropertyConverter(),
568
                $this->getPropertyAccessor(),
569
                $this->getPropertyTypeExtractor(),
570
                $classDiscriminator,
571
                null,
572
                []
573
            );
574
            $this->normalizers[] = new ContextualNormalizer([$evilObjectNormalizer]);
575
            $this->normalizers[] = $objectNormalizer;
576
            ContextualNormalizer::disableDenormalizer(EvilReflectionPropertyNormalizer::class);
577
            ContextualNormalizer::disableNormalizer(EvilReflectionPropertyNormalizer::class);
578
        }
579
        return $this->normalizers;
580
    }
581
582
    private function getInfo(): Info
583
    {
584
        if (!$this->info) {
585
            $this->info = new Info('', '');
586
        }
587
        return $this->info;
588
    }
589
590
    public function getSchemaGenerator(): SchemaGenerator
591
    {
592
        if (!$this->schemaGenerator) {
593
            $this->schemaGenerator = new SchemaGenerator(
594
                $this->getClassMetadataFactory(),
595
                $this->getPropertyTypeExtractor(),
596
                $this->getClassResourceConverter(),
597
                $this->getPropertyConverter()
598
            );
599
        }
600
        return $this->schemaGenerator;
601
    }
602
603
    public function getOpenApiSpecGenerator(string $baseUrl): OpenApiSpecGenerator
604
    {
605
        if (!$this->openApiSpecGenerator) {
606
            $this->openApiSpecGenerator = new OpenApiSpecGenerator(
607
                $this->getApiResources(),
608
                $this->getClassResourceConverter(),
609
                $this->getInfo(),
610
                $this->getSchemaGenerator(),
611
                $this->getApiResourceMetadataFactory(),
612
                $baseUrl
613
            );
614
        }
615
        return $this->openApiSpecGenerator;
616
    }
617
}
618