Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.81% covered (success)
98.81%
83 / 84
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
OpenApiOperationAddedEventSubscriber
98.81% covered (success)
98.81%
83 / 84
66.67% covered (warning)
66.67%
2 / 3
11
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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 onOperationAdded
98.75% covered (success)
98.75%
79 / 80
0.00% covered (danger)
0.00%
0 / 1
9
1<?php
2namespace Apie\RestApi\EventListeners;
3
4use Apie\Common\Actions\GetListAction;
5use Apie\Core\BoundedContext\BoundedContextId;
6use Apie\Core\Context\ApieContext;
7use Apie\Core\ContextConstants;
8use Apie\Core\Datalayers\ApieDatalayer;
9use Apie\Core\Datalayers\ApieDatalayerWithFilters;
10use Apie\Core\Metadata\MetadataFactory;
11use Apie\RestApi\Events\OpenApiOperationAddedEvent;
12use Apie\TypeConverter\ReflectionTypeFactory;
13use cebe\openapi\spec\Parameter;
14use cebe\openapi\spec\Schema;
15use ReflectionClass;
16use Symfony\Component\EventDispatcher\EventSubscriberInterface;
17
18class OpenApiOperationAddedEventSubscriber implements EventSubscriberInterface
19{
20    public function __construct(
21        private readonly ApieDatalayer $apieDatalayer
22    ) {
23    }
24
25    public static function getSubscribedEvents(): array
26    {
27        return [
28            OpenApiOperationAddedEvent::class => 'onOperationAdded'
29        ];
30    }
31
32    public function onOperationAdded(OpenApiOperationAddedEvent $event): void
33    {
34        if ($this->apieDatalayer instanceof ApieDatalayerWithFilters) {
35            $routeAttributes = $event->routeDefinition->getRouteAttributes();
36            $resourceName = $routeAttributes[ContextConstants::RESOURCE_NAME] ?? null;
37            $boundedContextId = $routeAttributes[ContextConstants::BOUNDED_CONTEXT_ID] ?? 'default';
38            if ($resourceName && $event->routeDefinition->getAction() === GetListAction::class) {
39                $refl = new ReflectionClass($resourceName);
40                $fieldMetadata = MetadataFactory::getResultMetadata(
41                    $refl,
42                    new ApieContext()
43                )->getHashmap();
44                $filterColumns = $this->apieDatalayer->getFilterColumns($refl, new BoundedContextId($boundedContextId));
45                // TODO: this assumes that if there are no filters, there is also no global search?
46                if (null === $filterColumns) {
47                    return;
48                }
49                $operation = $event->operation;
50                $parameters = $operation->parameters ?? [];
51                $parameters ??= [];
52                $parameters[] = new Parameter([
53                    'name' => 'items_per_page',
54                    'in' => 'query',
55                    'schema' => new Schema([
56                        'type' => 'int',
57                        'min' => 1
58                    ])
59                ]);
60                $parameters[] = new Parameter([
61                    'name' => 'page',
62                    'in' => 'query',
63                    'schema' => new Schema([
64                        'type' => 'int',
65                        'min' => 0
66                    ])
67                ]);
68                $parameters[] = new Parameter([
69                    'name' => 'search',
70                    'in' => 'query',
71                    'schema' => new Schema([
72                        'type' => 'string',
73                        'minLength' => 1,
74                    ])
75                ]);
76                foreach ($filterColumns as $filterColumn) {
77                    $schema = new Schema([
78                        'type' => 'string',
79                        'minLength' => 1,
80                    ]);
81                    if (isset($fieldMetadata[$filterColumn])) {
82                        $typehint = ReflectionTypeFactory::createReflectionType('string');
83                        $schema = $event->componentsBuilder->getSchemaForType(
84                            $typehint,
85                            false,
86                            true,
87                            $fieldMetadata[$filterColumn]->allowsNull()
88                        );
89                    }
90
91                    $parameters[] = new Parameter([
92                        'name' => 'query[' . $filterColumn . ']',
93                        'in' => 'query',
94                        'schema' => $schema,
95                    ]);
96                }
97                $orderByColumns = $this->apieDatalayer->getOrderByColumns($refl, new BoundedContextId($boundedContextId));
98                if ($orderByColumns?->count()) {
99                    $values = [];
100                    foreach ($orderByColumns as $orderByColumn) {
101                        array_push(
102                            $values,
103                            $orderByColumn,
104                            '+' . $orderByColumn,
105                            '-' . $orderByColumn
106                        );
107                    }
108
109                    $parameters[] = new Parameter([
110                        'name' => 'order_by',
111                        'in' => 'query',
112                        'schema' => new Schema([
113                            'type' => 'array',
114                            'items' => [
115                                'type' => 'string',
116                                'enum' => $values,
117                            ]
118                        ])
119                    ]);
120                }
121                $operation->parameters = $parameters;
122            }
123        }
124    }
125}