Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.24% covered (warning)
88.24%
60 / 68
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddAcceptLanguageEventSubscriber
88.24% covered (warning)
88.24%
60 / 68
40.00% covered (danger)
40.00%
2 / 5
17.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSubscribedEvents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onOpenApiOperationAdded
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 onOpenApiSchemaGenerated
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
4
 getExamples
66.67% covered (warning)
66.67%
12 / 18
0.00% covered (danger)
0.00%
0 / 1
10.37
1<?php
2namespace Apie\RestApi\EventListeners;
3
4use Apie\Core\Attributes\ExampleValue;
5use Apie\Core\Identifiers\SnakeCaseSlug;
6use Apie\Core\Utils\ConverterUtils;
7use Apie\RestApi\Events\OpenApiOperationAddedEvent;
8use Apie\RestApi\Events\OpenApiSchemaGeneratedEvent;
9use Apie\SchemaGenerator\SchemaGenerator;
10use Apie\TypeConverter\ReflectionTypeFactory;
11use cebe\openapi\spec\Example;
12use cebe\openapi\spec\Reference;
13use Symfony\Component\EventDispatcher\EventSubscriberInterface;
14
15class AddAcceptLanguageEventSubscriber implements EventSubscriberInterface
16{
17    public function __construct(
18        private readonly ?string $languageTypehint,
19        private readonly SchemaGenerator $schemaFactory,
20    ) {
21    }
22    public static function getSubscribedEvents(): array
23    {
24        return [
25            OpenApiOperationAddedEvent::class => 'onOpenApiOperationAdded',
26            OpenApiSchemaGeneratedEvent::class => 'onOpenApiSchemaGenerated',
27        ];
28    }
29
30    public function onOpenApiOperationAdded(OpenApiOperationAddedEvent $event): void
31    {
32        if ($this->languageTypehint === null) {
33            return;
34        }
35        $parameters = $event->operation->parameters ?? [];
36        $ref = new Reference([
37            '$ref' => '#/components/parameters/AcceptLanguage',
38        ]);
39        $parameters[] = $ref;
40        if (!$event->method->hasOptionalOrNoContentBody()) {
41            $ref = new Reference([
42                '$ref' => '#/components/parameters/ContentLanguage',
43            ]);
44            $parameters[] = $ref;
45        }
46        $event->operation->parameters = $parameters;
47    }
48
49    public function onOpenApiSchemaGenerated(OpenApiSchemaGeneratedEvent $event): void
50    {
51        if ($this->languageTypehint === null) {
52            return;
53        }
54        $openApi = $event->openApi;
55
56        $parameters = $openApi->components->parameters ?? [];
57        $examples = $this->getExamples(ReflectionTypeFactory::createReflectionType($this->languageTypehint));
58        $parameter = new \cebe\openapi\spec\Parameter([
59            'name' => 'Accept-Language',
60            'in' => 'header',
61            'required' => false,
62            'description' => 'Preferred language for the response.',
63            'schema' => $this->schemaFactory->createSchema(
64                $this->languageTypehint,
65                display: false
66            ),
67        ]);
68        if ($examples) {
69            $parameter->examples = $examples;
70        }
71        $parameters['AcceptLanguage'] = $parameter;
72        $parameter = new \cebe\openapi\spec\Parameter([
73            'name' => 'Content-Language',
74            'in' => 'header',
75            'required' => false,
76            'description' => 'Language of content body.',
77            'schema' => $this->schemaFactory->createSchema(
78                $this->languageTypehint,
79                display: false
80            ),
81        ]);
82        if ($examples) {
83            $parameter->examples = $examples;
84        }
85        $parameters['ContentLanguage'] = $parameter;
86        $openApi->components->parameters = $parameters;
87    }
88
89    /**
90     * @return array<string, Example>
91     */
92    private function getExamples(\ReflectionType $input): array
93    {
94        $examples = [];
95        if ($input instanceof \ReflectionNamedType) {
96            $class = ConverterUtils::toReflectionClass($input);
97            if ($class === null) {
98                return [];
99            }
100            foreach ($class->getAttributes(ExampleValue::class) as $attribute) {
101                $exampleValue = $attribute->newInstance();
102                $id = SnakeCaseSlug::fromText($exampleValue->name)->toNative();
103                $examples[$id] = new Example([
104                    'summary' => $exampleValue->name,
105                    'value' => $exampleValue->toExample(),
106                ]);
107            }
108            return $examples;
109        }
110        assert($input instanceof \ReflectionIntersectionType || $input instanceof \ReflectionUnionType);
111        foreach ($input->getTypes() as $type) {
112            // only allow interfaces or union types. Merge examples of intersections make no sense otherwise.
113            if ($input instanceof \ReflectionUnionType || ConverterUtils::toReflectionClass($type)?->isInterface()) {
114                $examples = array_merge($examples, $this->getExamples($type));
115            }
116        }
117        return $examples;
118    }
119}