Passed
Push — master ( 94f1d9...2d4769 )
by Pieter
02:23
created

ServiceLibraryFactory::getIdentifierExtractor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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