Issues (21)

src/Service/DataCollector.php (6 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiechu\SymfonyCommonsBundle\Service;
6
7
use Doctrine\Common\Annotations\Reader;
8
use Spiechu\SymfonyCommonsBundle\Annotation\Controller\ControllerAnnotationExtractorTrait;
9
use Spiechu\SymfonyCommonsBundle\Annotation\Controller\ResponseSchemaValidator;
10
use Spiechu\SymfonyCommonsBundle\Event\ApiVersion\ApiVersionSetEvent;
11
use Spiechu\SymfonyCommonsBundle\Event\ApiVersion\Events as ApiVersionEvents;
12
use Spiechu\SymfonyCommonsBundle\Event\ResponseSchemaCheck\CheckResult;
13
use Spiechu\SymfonyCommonsBundle\Event\ResponseSchemaCheck\Events as ResponseSchemaCheckEvents;
14
use Spiechu\SymfonyCommonsBundle\EventListener\GetMethodOverrideListener;
15
use Spiechu\SymfonyCommonsBundle\EventListener\RequestSchemaValidatorListener;
16
use Spiechu\SymfonyCommonsBundle\Service\SchemaValidator\ValidationViolation;
17
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
21
use Symfony\Component\HttpKernel\DataCollector\DataCollector as BaseDataCollector;
22
use Symfony\Component\Routing\Route;
23
use Symfony\Component\Routing\RouterInterface;
24
25
class DataCollector extends BaseDataCollector implements EventSubscriberInterface
26
{
27
    use ControllerAnnotationExtractorTrait;
28
29
    public const COLLECTOR_NAME = 'spiechu_symfony_commons.data_collector';
30
31
    protected const DATA_GLOBAL_RESPONSE_SCHEMAS = 'global_response_schemas';
32
33
    protected const DATA_GLOBAL_NON_EXISTING_SCHEMA_FILES = 'global_non_existing_schema_files';
34
35
    protected const DATA_GET_METHOD_OVERRIDE = 'get_method_override';
36
37
    protected const DATA_KNOWN_ROUTE_RESPONSE_SCHEMAS = 'known_route_response_schemas';
38
39
    protected const DATA_VALIDATION_RESULT = 'validation_result';
40
41
    protected const DATA_API_VERSION_SET = 'api_version_set';
42
43
    /**
44
     * @var RouterInterface
45
     */
46
    protected $router;
47
48
    /**
49
     * @var Reader
50
     */
51
    protected $reader;
52
53
    /**
54
     * @var ControllerResolverInterface
55
     */
56
    protected $controllerResolver;
57
58
    /**
59
     * @var SchemaLocator
60
     */
61
    protected $schemaLocator;
62
63
    /**
64
     * @param RouterInterface             $router
65
     * @param Reader                      $reader
66
     * @param ControllerResolverInterface $controllerResolver
67
     * @param SchemaLocator               $schemaLocator
68
     */
69 22
    public function __construct(
70
        RouterInterface $router,
71
        Reader $reader,
72
        ControllerResolverInterface $controllerResolver,
73
        SchemaLocator $schemaLocator
74
    ) {
75 22
        $this->router = $router;
76 22
        $this->reader = $reader;
77 22
        $this->controllerResolver = $controllerResolver;
78 22
        $this->schemaLocator = $schemaLocator;
79 22
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 20
    public function collect(Request $request, Response $response, \Exception $exception = null): void
85
    {
86 20
        $this->data[static::DATA_KNOWN_ROUTE_RESPONSE_SCHEMAS] = $request->attributes->get(RequestSchemaValidatorListener::ATTRIBUTE_RESPONSE_SCHEMAS);
87 20
        $this->data[static::DATA_GET_METHOD_OVERRIDE] = $request->attributes->get(GetMethodOverrideListener::ATTRIBUTE_REQUEST_GET_METHOD_OVERRIDE);
88
89 20
        $this->extractRoutesData();
90 20
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 20
    public function getName(): string
96
    {
97 20
        return static::COLLECTOR_NAME;
98
    }
99
100
    /**
101
     * Forward compatibility with Symfony 3.4.
102
     */
103 22
    public function reset(): void
104
    {
105 22
        $this->data = [];
106 22
    }
107
108
    /**
109
     * @return array
110
     */
111
    public function getGlobalResponseSchemas(): array
112
    {
113
        return $this->data[static::DATA_GLOBAL_RESPONSE_SCHEMAS];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->data[stati...LOBAL_RESPONSE_SCHEMAS] could return the type Symfony\Component\VarDumper\Cloner\Data|null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 8
    public static function getSubscribedEvents(): array
120
    {
121
        return [
122 8
            ResponseSchemaCheckEvents::CHECK_RESULT => ['onCheckResult', 100],
123 8
            ApiVersionEvents::API_VERSION_SET => ['onApiVersionSet', 100],
124
        ];
125
    }
126
127
    /**
128
     * @param CheckResult $checkResult
129
     */
130 6
    public function onCheckResult(CheckResult $checkResult): void
131
    {
132 6
        $this->data[static::DATA_VALIDATION_RESULT] = $checkResult->getValidationResult();
133 6
    }
134
135
    /**
136
     * @param ApiVersionSetEvent $apiVersionSetEvent
137
     */
138 13
    public function onApiVersionSet(ApiVersionSetEvent $apiVersionSetEvent): void
139
    {
140 13
        $this->data[static::DATA_API_VERSION_SET] = $apiVersionSetEvent->getApiVersion();
141 13
    }
142
143
    /**
144
     * @return array
145
     */
146 12
    public function getKnownRouteResponseSchemas(): array
147
    {
148 12
        return empty($this->data[static::DATA_KNOWN_ROUTE_RESPONSE_SCHEMAS]) ? [] : $this->data[static::DATA_KNOWN_ROUTE_RESPONSE_SCHEMAS];
0 ignored issues
show
Bug Best Practice introduced by
The expression return empty($this->data...ROUTE_RESPONSE_SCHEMAS] could return the type Symfony\Component\VarDumper\Cloner\Data|null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
149
    }
150
151
    /**
152
     * @return int
153
     */
154 12
    public function getKnownRouteResponseSchemaNumber(): int
155
    {
156
        return array_reduce($this->getKnownRouteResponseSchemas(), static function (int $counter, array $formatSchemas) {
157 2
            return $counter + \count($formatSchemas);
158 12
        }, 0);
159
    }
160
161
    /**
162
     * @return int
163
     */
164 10
    public function getAllPotentialErrorsCount(): int
165
    {
166 10
        return \count($this->getValidationErrors()) + $this->getGlobalNonExistingSchemaFiles();
167
    }
168
169
    /**
170
     * @return bool
171
     */
172 13
    public function responseWasChecked(): bool
173
    {
174 13
        return array_key_exists(static::DATA_VALIDATION_RESULT, $this->data);
0 ignored issues
show
It seems like $this->data can also be of type Symfony\Component\VarDumper\Cloner\Data; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        return array_key_exists(static::DATA_VALIDATION_RESULT, /** @scrutinizer ignore-type */ $this->data);
Loading history...
175
    }
176
177
    /**
178
     * @return bool
179
     */
180 10
    public function apiVersionWasSet(): bool
181
    {
182 10
        return array_key_exists(static::DATA_API_VERSION_SET, $this->data);
0 ignored issues
show
It seems like $this->data can also be of type Symfony\Component\VarDumper\Cloner\Data; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
        return array_key_exists(static::DATA_API_VERSION_SET, /** @scrutinizer ignore-type */ $this->data);
Loading history...
183
    }
184
185
    /**
186
     * @return null|string
187
     */
188 10
    public function getApiVersion(): ?string
189
    {
190 10
        return $this->apiVersionWasSet() ? $this->data[static::DATA_API_VERSION_SET] : null;
191
    }
192
193
    /**
194
     * @return ValidationViolation[]
195
     */
196 10
    public function getValidationErrors(): array
197
    {
198 10
        return $this->responseWasChecked() ? $this->data[static::DATA_VALIDATION_RESULT]->getViolations() : [];
0 ignored issues
show
The method getViolations() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

198
        return $this->responseWasChecked() ? $this->data[static::DATA_VALIDATION_RESULT]->/** @scrutinizer ignore-call */ getViolations() : [];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
The method getViolations() does not exist on Symfony\Component\VarDumper\Cloner\Data. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

198
        return $this->responseWasChecked() ? $this->data[static::DATA_VALIDATION_RESULT]->/** @scrutinizer ignore-call */ getViolations() : [];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
    }
200
201
    /**
202
     * @return bool
203
     */
204 1
    public function isGetMethodWasOverridden(): bool
205
    {
206 1
        return !empty($this->data[static::DATA_GET_METHOD_OVERRIDE]);
207
    }
208
209
    /**
210
     * @return null|string
211
     */
212 1
    public function getGetMethodOverriddenTo(): ?string
213
    {
214 1
        return $this->isGetMethodWasOverridden() ? $this->data[static::DATA_GET_METHOD_OVERRIDE] : null;
215
    }
216
217 20
    protected function extractRoutesData(): void
218
    {
219 20
        $this->data[static::DATA_GLOBAL_RESPONSE_SCHEMAS] = [];
220 20
        $this->data[static::DATA_GLOBAL_NON_EXISTING_SCHEMA_FILES] = 0;
221
222
        /** @var Route $route */
223
        /** @var string $controllerDefinition */
224
        /** @var ResponseSchemaValidator $responseSchemaValidator */
225 20
        foreach ($this->getRouteCollectionGenerator() as $name => [$route, $controllerDefinition, $responseSchemaValidator]) {
226 18
            $annotationSchemas = $responseSchemaValidator->getSchemas();
227
228 18
            $this->data[static::DATA_GLOBAL_RESPONSE_SCHEMAS][] = [
229 18
                'path' => $route->getPath(),
230 18
                'name' => $name,
231 18
                'controller' => $controllerDefinition,
232 18
                'response_schemas' => $annotationSchemas,
233
            ];
234
235 18
            $this->determineGlobalNonExistingSchemaFiles($annotationSchemas);
236
        }
237 20
    }
238
239
    /**
240
     * @param array $annotationSchemas
241
     */
242 18
    protected function determineGlobalNonExistingSchemaFiles(array $annotationSchemas)
243
    {
244
        /** @var array $schemas */
245 18
        foreach ($annotationSchemas as $schemas) {
246 18
            foreach ($schemas as $schema) {
247 18
                if (!$this->schemaLocator->schemaFileExists($schema)) {
248 18
                    ++$this->data[static::DATA_GLOBAL_NON_EXISTING_SCHEMA_FILES];
249
                }
250
            }
251
        }
252 18
    }
253
254
    /**
255
     * @throws \Exception
256
     *
257
     * @return \Generator string $name => [Route $route, string $controllerDefinition, ResponseSchemaValidator $methodAnnotation]
258
     */
259 20
    protected function getRouteCollectionGenerator(): \Generator
260
    {
261 20
        foreach ($this->router->getRouteCollection() as $name => $route) {
262 20
            if (empty($controllerDefinition = $route->getDefault('_controller'))) {
263
                continue;
264
            }
265
266 20
            $methodAnnotation = $this->extractControllerResponseValidator($controllerDefinition);
267 20
            if (!$methodAnnotation instanceof ResponseSchemaValidator) {
268 20
                continue;
269
            }
270
271 18
            yield $name => [$route, $controllerDefinition, $methodAnnotation];
272
        }
273 20
    }
274
275
    /**
276
     * @param string $controllerDefinition
277
     *
278
     * @throws \Exception
279
     *
280
     * @return null|ResponseSchemaValidator
281
     */
282 20
    protected function extractControllerResponseValidator(string $controllerDefinition): ?ResponseSchemaValidator
283
    {
284 20
        $resolvedController = $this->controllerResolver->getController(new Request(
285 20
            [],
286 20
            [],
287
            [
288 20
                '_controller' => $controllerDefinition,
289
            ]
290
        ));
291
292 20
        if (!\is_callable($resolvedController)) {
293
            return null;
294
        }
295
296 20
        return $this->getMethodAnnotationFromController(/* @scrutinizer ignore-type */$resolvedController, ResponseSchemaValidator::class);
297
    }
298
299
    /**
300
     * @return int
301
     */
302 10
    protected function getGlobalNonExistingSchemaFiles(): int
303
    {
304 10
        return empty($this->data[static::DATA_GLOBAL_NON_EXISTING_SCHEMA_FILES]) ? 0 : \count($this->data[static::DATA_GLOBAL_NON_EXISTING_SCHEMA_FILES]);
305
    }
306
307
    /**
308
     * {@inheritdoc}
309
     */
310 20
    protected function getAnnotationReader(): Reader
311
    {
312 20
        return $this->reader;
313
    }
314
}
315