Passed
Push — master ( a8401c...a7ca13 )
by Pieter
04:05
created

ServiceLibraryFactory::setOpenApiSpecsHook()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
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
     * @var callable|null
193
     */
194
    private $addSpecsHook;
195
196
    /**
197
     * @param string[]|ApiResourcesInterface $apiResourceClasses
198
     * @param bool $debug
199
     * @param string|null $cacheFolder
200
     */
201
    public function __construct($apiResourceClasses = [ApplicationInfo::class, Status::class], bool $debug = false, ?string $cacheFolder = null)
202
    {
203
        $this->apiResources = $apiResourceClasses instanceof ApiResourcesInterface ? $apiResourceClasses : new ApiResources($apiResourceClasses);
204
        $this->debug = $debug;
205
        $this->cacheFolder = $cacheFolder;
206
    }
207
208
    private function isDebug(): bool
209
    {
210
        return $this->debug;
211
    }
212
213
    private function getCacheFolder(): ?string
214
    {
215
        return $this->cacheFolder;
216
    }
217
218
    /**
219
     * Workaround to run a callable to set some values when the Serializer is being instantiated.
220
     *
221
     * @param callable $callable
222
     * @return ServiceLibraryFactory
223
     */
224
    public function runBeforeInstantiation(callable $callable): self
225
    {
226
        $this->callables[] = $callable;
227
        return $this;
228
    }
229
230
    public function setApiResourceFactory(ApiResourceFactoryInterface $apiResourceFactory): self
231
    {
232
        if ($this->apiResourceFactory) {
233
            throw new RuntimeException('I have already instantiated ApiResourceFactory and can no longer set it!');
234
        }
235
        $this->apiResourceFactory = $apiResourceFactory;
236
        return $this;
237
    }
238
239
    public function setInfo(Info $info): self
240
    {
241
        if ($this->info) {
242
            throw new RuntimeException('I have already instantiated Info and can no longer set it!');
243
        }
244
        $this->info = $info;
245
        return $this;
246
    }
247
248
    public function setSerializer(SerializerInterface $serializer): self
249
    {
250
        if ($this->serializer) {
251
            throw new RuntimeException('I have already instantiated the serializer and can no longer set the serializer!');
252
        }
253
        $this->serializer = $serializer;
254
        return $this;
255
    }
256
257
    public function setSerializerCache(CacheItemPoolInterface $serializerCache): self
258
    {
259
        if ($this->serializerCache) {
260
            throw new RuntimeException('I have already instantiated the serializer cache and can no longer set the serializer cache!');
261
        }
262
        $this->serializerCache = $serializerCache;
263
        return $this;
264
    }
265
266
    public function setClassMetadataFactory(ClassMetadataFactoryInterface $classMetadataFactory): self
267
    {
268
        if ($this->classMetadataFactory) {
269
            throw new RuntimeException('I have already instantiated the class metadata factory and can no longer set it!');
270
        }
271
        $this->classMetadataFactory = $classMetadataFactory;
272
        return $this;
273
    }
274
275
    public function setPropertyConverter(NameConverterInterface $propertyConverter): self
276
    {
277
        if ($this->propertyConverter) {
278
            throw new RuntimeException('I have already instantiated the property converter and can no longer set the property converter!');
279
        }
280
        $this->propertyConverter = $propertyConverter;
281
        return $this;
282
    }
283
284
    public function setAdditionalNormalizers(array $additionalNormalizers): self
285
    {
286
        if (is_array($this->additionalNormalizers)) {
287
            throw new RuntimeException('I have already instantiated additional normalizers and can no longer set it!');
288
        }
289
        $this->additionalNormalizers = $additionalNormalizers;
290
        return $this;
291
    }
292
293
    public function setEncoders(array $encoders): self
294
    {
295
        if (is_array($this->encoders)) {
296
            throw new RuntimeException('I have already instantiated encoders and can no longer set it!');
297
        }
298
        $this->encoders = $encoders;
299
        return $this;
300
    }
301
302
    private function getEncoders(): array
303
    {
304
        if (!is_array($this->encoders)) {
305
            $this->encoders = [
306
                new XmlEncoder([XmlEncoder::ROOT_NODE_NAME => 'item']),
307
                new JsonEncoder(
308
                    new JsonEncode([JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES]),
309
                    new JsonDecode([JsonDecode::ASSOCIATIVE => false])
310
                )
311
            ];
312
        }
313
        return $this->encoders;
314
    }
315
316
    public function getApiResourceFacade(): ApiResourceFacade
317
    {
318
        if (!$this->apiResourceFacade) {
319
            $this->apiResourceFacade = new ApiResourceFacade(
320
                $this->getApiResourceRetriever(),
321
                $this->getApiResourcePersister(),
322
                $this->getClassResourceConverter(),
323
                $this->getSerializer(),
324
                $this->getFormatRetriever()
325
            );
326
        }
327
        return $this->apiResourceFacade;
328
    }
329
330
    private function getAdditionalNormalizers(): array
331
    {
332
        if (!is_array($this->additionalNormalizers)) {
333
            $this->additionalNormalizers = [];
334
        }
335
        return $this->additionalNormalizers;
336
    }
337
338
    public function setContainer(ContainerInterface $container): self
339
    {
340
        if ($this->container || $this->apiResourceFactory) {
341
            throw new RuntimeException('I have already instantiated services and can no longer set the container!');
342
        }
343
        $this->container = $container;
344
        return $this;
345
    }
346
347
    private function getContainer(): ?ContainerInterface
348
    {
349
        return $this->container;
350
    }
351
352
    private function getFormatRetriever(): FormatRetriever
353
    {
354
        if (!$this->formatRetriever) {
355
            $this->formatRetriever = new FormatRetriever();
356
        }
357
        return $this->formatRetriever;
358
    }
359
360
    public function getSerializer(): SerializerInterface
361
    {
362
        if (!$this->serializer) {
363
            foreach ($this->callables as $callable) {
364
                $callable('serializer');
365
            }
366
            $normalizers = $this->getNormalizers();
367
            $encoders = $this->getEncoders();
368
            $this->serializer = new Serializer($normalizers, $encoders);
369
        }
370
        return $this->serializer;
371
    }
372
373
    /**
374
     * @return Reader
375
     */
376
    private function getAnnotationReader(): Reader
377
    {
378
        if (!$this->annotationReader) {
379
            /** @scrutinizer ignore-deprecated */AnnotationRegistry::registerLoader('class_exists');
380
            if (class_exists(PhpFileCache::class) && $this->getCacheFolder()) {
381
                $this->annotationReader = new CachedReader(
382
                    new AnnotationReader(),
383
                    new PhpFileCache($this->getCacheFolder() . DIRECTORY_SEPARATOR . '/doctrine-cache'),
384
                    $this->isDebug()
385
                );
386
            } else {
387
                $this->annotationReader = new AnnotationReader();
388
            }
389
            if ($this->overrideAnnotationConfigs) {
390
                $this->annotationReader = new ExtendReaderWithConfigReader($this->annotationReader, $this->overrideAnnotationConfigs);
391
            }
392
        }
393
        return $this->annotationReader;
394
    }
395
396
    /**
397
     * @param ApiResource[] $config
398
     *
399
     * @return ServiceLibraryFactory
400
     */
401
    public function overrideAnnotationConfig(array $config): self
402
    {
403
        if (isset($this->overrideAnnotationConfigs) || isset($this->annotationReader)) {
404
            throw new RuntimeException('I have already instantiated the reader and can no longer override the annotation config!');
405
        }
406
        $this->overrideAnnotationConfigs = $config;
407
        return $this;
408
    }
409
410
    private function getApiResourceMetadataFactory(): ApiResourceMetadataFactory
411
    {
412
        if (!$this->apiResourceMetadatafactory) {
413
            $this->apiResourceMetadatafactory = new ApiResourceMetadataFactory(
414
                $this->getAnnotationReader(),
415
                $this->getApiResourceFactory()
416
            );
417
        }
418
        return $this->apiResourceMetadatafactory;
419
    }
420
421
    private function getApiResourceFactory(): ApiResourceFactoryInterface
422
    {
423
        if (!$this->apiResourceFactory) {
424
            $this->apiResourceFactory = new ApiResourceFactory(
425
                $this->getContainer()
426
            );
427
        }
428
        return $this->apiResourceFactory;
429
    }
430
431
    private function getApiResourceRetriever(): ApiResourceRetriever
432
    {
433
        if (!$this->apiResourceRetriever) {
434
            $this->apiResourceRetriever = new ApiResourceRetriever(
435
                $this->getApiResourceMetadataFactory()
436
            );
437
        }
438
        return $this->apiResourceRetriever;
439
    }
440
441
    private function getApiResourcePersister(): ApiResourcePersister
442
    {
443
        if (!$this->apiResourcePersister) {
444
            $this->apiResourcePersister = new ApiResourcePersister(
445
                $this->getApiResourceMetadataFactory()
446
            );
447
        }
448
        return $this->apiResourcePersister;
449
    }
450
451
    public function getApiResources(): ApiResourcesInterface
452
    {
453
        return $this->apiResources;
454
    }
455
456
    private function getClassResourceConverter(): ClassResourceConverter
457
    {
458
        if (!$this->classResourceConverter) {
459
            $this->classResourceConverter = new ClassResourceConverter(
460
                $this->getPropertyConverter(),
461
                $this->getApiResources(),
462
                $this->isDebug()
463
            );
464
        }
465
        return $this->classResourceConverter;
466
    }
467
468
    private function getPropertyConverter(): NameConverterInterface
469
    {
470
        $classMetadataFactory = $this->getClassMetadataFactory();
471
        if (!$this->propertyConverter) {
472
            $this->propertyConverter = new MetadataAwareNameConverter(
473
                $classMetadataFactory,
474
                new CamelCaseToSnakeCaseNameConverter()
475
            );
476
        }
477
        return $this->propertyConverter;
478
    }
479
480
    public function getClassMetadataFactory(): ClassMetadataFactoryInterface
481
    {
482
        if (!$this->classMetadataFactory) {
483
            $this->classMetadataFactory = new ClassMetadataFactory(
484
                new LoaderChain([
485
                    new AnnotationLoader($this->getAnnotationReader()),
486
                    new BaseGroupLoader(['read', 'write', 'get', 'post', 'put']),
487
                ])
488
            );
489
        }
490
        return $this->classMetadataFactory;
491
    }
492
493
    public function getSerializerCache(): CacheItemPoolInterface
494
    {
495
        if (!$this->serializerCache) {
496
            $this->serializerCache = new ArrayAdapter(0, true);
497
        }
498
        return $this->serializerCache;
499
    }
500
501
    public function getPropertyAccessor(): PropertyAccessor
502
    {
503
        if (!$this->propertyAccessor) {
504
            $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
505
                ->setCacheItemPool($this->getSerializerCache())
506
                ->getPropertyAccessor();
507
        }
508
        return $this->propertyAccessor;
509
    }
510
511
    public function getPropertyTypeExtractor(): PropertyTypeExtractorInterface
512
    {
513
        if (!$this->propertyTypeExtractor) {
514
            $factory = $this->getClassMetadataFactory();
515
            $reflectionExtractor = new ReflectionExtractor();
516
            $phpDocExtractor = new PhpDocExtractor();
517
518
            $this->propertyTypeExtractor = new PropertyInfoExtractor(
519
                [
520
                    new SerializerExtractor($factory),
521
                    $reflectionExtractor,
522
                ],
523
                [
524
                    $phpDocExtractor,
525
                    $reflectionExtractor,
526
                ],
527
                [
528
                    $phpDocExtractor,
529
                ],
530
                [
531
                    $reflectionExtractor,
532
                ],
533
                [
534
                    $reflectionExtractor,
535
                ]
536
            );
537
        }
538
        return $this->propertyTypeExtractor;
539
    }
540
541
    private function getNormalizers(): array
542
    {
543
        if (!is_array($this->normalizers)) {
544
            $classMetadataFactory = $this->getClassMetadataFactory();
545
            $classDiscriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
546
547
            $this->normalizers = $this->getAdditionalNormalizers();
548
            $this->normalizers[] = new ExceptionNormalizer($this->isDebug());
549
550
            if (class_exists(Carbon::class)) {
551
                $this->normalizers[] = new CarbonNormalizer([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']);
552
            } else {
553
                $this->normalizers[] = new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']);
554
            }
555
            $this->normalizers[] = new ValueObjectNormalizer();
556
            $this->normalizers[] = new UuidNormalizer();
557
558
            $this->normalizers[] = new JsonSerializableNormalizer();
559
            $this->normalizers[] = new ArrayDenormalizer();
560
561
            $objectNormalizer = new ApieObjectNormalizer(
562
                $classMetadataFactory,
563
                $this->getPropertyConverter(),
564
                $this->getPropertyAccessor(),
565
                $this->getPropertyTypeExtractor(),
566
                $classDiscriminator,
567
                null,
568
                []
569
            );
570
            $evilObjectNormalizer = new EvilReflectionPropertyNormalizer(
571
                $classMetadataFactory,
572
                $this->getPropertyConverter(),
573
                $this->getPropertyAccessor(),
574
                $this->getPropertyTypeExtractor(),
575
                $classDiscriminator,
576
                null,
577
                []
578
            );
579
            $this->normalizers[] = new ContextualNormalizer([$evilObjectNormalizer]);
580
            $this->normalizers[] = $objectNormalizer;
581
            ContextualNormalizer::disableDenormalizer(EvilReflectionPropertyNormalizer::class);
582
            ContextualNormalizer::disableNormalizer(EvilReflectionPropertyNormalizer::class);
583
        }
584
        return $this->normalizers;
585
    }
586
587
    private function getInfo(): Info
588
    {
589
        if (!$this->info) {
590
            $this->info = new Info('', '');
591
        }
592
        return $this->info;
593
    }
594
595
    public function getSchemaGenerator(): SchemaGenerator
596
    {
597
        if (!$this->schemaGenerator) {
598
            $this->schemaGenerator = new SchemaGenerator(
599
                $this->getClassMetadataFactory(),
600
                $this->getPropertyTypeExtractor(),
601
                $this->getClassResourceConverter(),
602
                $this->getPropertyConverter()
603
            );
604
        }
605
        return $this->schemaGenerator;
606
    }
607
608
    /**
609
     * Call this method in case you want to add a hook to OpenApiSpecGenerator to add your own custom
610
     * modifications.
611
     *
612
     * @param callable $addSpecsHook
613
     * @return ServiceLibraryFactory
614
     */
615
    public function setOpenApiSpecsHook(callable $addSpecsHook): self
616
    {
617
        if ($this->openApiSpecGenerator) {
618
            throw new RuntimeException('I can only set this callback if the OpenAPI spec generator was not called yet!');
619
        }
620
        $this->addSpecsHook = $addSpecsHook;
621
        return $this;
622
    }
623
624
    public function getOpenApiSpecGenerator(string $baseUrl): OpenApiSpecGenerator
625
    {
626
        if (!$this->openApiSpecGenerator) {
627
            $this->openApiSpecGenerator = new OpenApiSpecGenerator(
628
                $this->getApiResources(),
629
                $this->getClassResourceConverter(),
630
                $this->getInfo(),
631
                $this->getSchemaGenerator(),
632
                $this->getApiResourceMetadataFactory(),
633
                $baseUrl,
634
                $this->addSpecsHook
635
            );
636
        }
637
        return $this->openApiSpecGenerator;
638
    }
639
}
640