Issues (40)

php-src/DI/RestfulExtension.php (6 issues)

Labels
1
<?php
2
3
namespace kalanis\Restful\DI;
4
5
6
use kalanis\OAuth2\KeyGenerator;
7
use kalanis\Restful\Application;
8
use kalanis\Restful\Converters;
9
use kalanis\Restful\Diagnostics;
10
use kalanis\Restful\Http;
11
use kalanis\Restful\IResource;
12
use kalanis\Restful\Mapping;
13
use kalanis\Restful\ResourceFactory;
14
use kalanis\Restful\Security;
15
use kalanis\Restful\Utils;
16
use kalanis\Restful\Validation;
17
use Nette;
18
use Nette\Bootstrap\Configurator;
19
use Nette\DI\CompilerExtension;
20
use Nette\DI\ContainerBuilder;
21
use Nette\DI\Definitions\ServiceDefinition;
22
use Nette\DI\Definitions\Statement;
23
use Nette\DI\MissingServiceException;
24
use Nette\Utils\Validators;
25
26
27
/**
28
 * RestfulExtension
29
 * @package kalanis\Restful\DI
30
 * @template Conf of array{
31
 *     convention: string|null,
32
 *     timeFormat: string,
33
 *     cacheDir: string,
34
 *     jsonpKey: string,
35
 *     prettyPrint: bool,
36
 *     prettyPrintKey: string,
37
 *     routes: array{
38
 *         generateAtStart: bool,
39
 *         presentersRoot: string,
40
 *         autoGenerated: bool,
41
 *         autoRebuild: bool,
42
 *         module: string,
43
 *         prefix: string,
44
 *         panel: bool
45
 *     },
46
 *     security: array{
47
 *         privateKey: string|null,
48
 *         requestTimeKey?: string|null,
49
 *         requestTimeout?: int|null
50
 *     },
51
 *     resourceRoute?: array{
52
 *         mask?: string,
53
 *         metadata?: string|array{
54
 *             action?: string|string[],
55
 *         },
56
 *         flags?: int,
57
 *     },
58
 *     mappers?: array<string, array{
59
 *         contentType: string,
60
 *         class: string
61
 *     }>
62
 * }
63
 */
64
class RestfulExtension extends CompilerExtension
65
{
66
67
    /** Converter tag name */
68
    public const CONVERTER_TAG = 'restful.converter';
69
70
    /** Snake case convention config name */
71
    public const CONVENTION_SNAKE_CASE = 'snake_case';
72
73
    /** Camel case convention config name */
74
    public const CONVENTION_CAMEL_CASE = 'camelCase';
75
76
    /** Pascal case convention config name */
77
    public const CONVENTION_PASCAL_CASE = 'PascalCase';
78
79
    /**
80
     * Default DI settings
81
     * @var Conf
0 ignored issues
show
The type kalanis\Restful\DI\Conf was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
82
     */
83
    protected array $defaults = [
84
        'convention' => null,
85
        'timeFormat' => 'c',
86
        'cacheDir' => '%tempDir%/cache',
87
        'jsonpKey' => 'jsonp',
88
        'prettyPrint' => true,
89
        'prettyPrintKey' => 'pretty',
90
        'routes' => [
91
            'generateAtStart' => false,
92
            'presentersRoot' => '%appDir%',
93
            'autoGenerated' => true,
94
            'autoRebuild' => true,
95
            'module' => '',
96
            'prefix' => '',
97
            'panel' => true
98
        ],
99
        'resourceRoute' => [
100
            'mask' => '',
101
            'metadata' => [],
102
            'flags' => Application\IResourceRouter::CRUD,
103
        ],
104
        'security' => [
105
            'privateKey' => null,
106
            'requestTimeKey' => 'timestamp',
107
            'requestTimeout' => 300
108
        ]
109
    ];
110
111
    /**
112
     * Register REST API extension
113
     */
114
    public static function install(Configurator $configurator): void
115
    {
116
        $configurator->onCompile[] = function ($configurator, $compiler): void {
117
            $compiler->addExtension('restful', new RestfulExtension);
118
        };
119
    }
120
121
    /**
122
     * Load DI configuration
123
     */
124
    public function loadConfiguration(): void
125
    {
126
        $container = $this->getContainerBuilder();
127
        /** @var Conf $config */
128
        $config = array_merge($this->defaults, (array) $this->getConfig());
129
130
        // Additional module
131
        $this->loadRestful($container, $config);
0 ignored issues
show
$config of type kalanis\Restful\DI\Conf is incompatible with the type array expected by parameter $config of kalanis\Restful\DI\RestfulExtension::loadRestful(). ( Ignorable by Annotation )

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

131
        $this->loadRestful($container, /** @scrutinizer ignore-type */ $config);
Loading history...
132
        $this->loadValidation($container);
133
        $this->loadResourceConverters($container, $config);
0 ignored issues
show
$config of type kalanis\Restful\DI\Conf is incompatible with the type array expected by parameter $config of kalanis\Restful\DI\Restf...oadResourceConverters(). ( Ignorable by Annotation )

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

133
        $this->loadResourceConverters($container, /** @scrutinizer ignore-type */ $config);
Loading history...
134
        $this->loadSecuritySection($container, $config);
0 ignored issues
show
$config of type kalanis\Restful\DI\Conf is incompatible with the type array expected by parameter $config of kalanis\Restful\DI\Restf...::loadSecuritySection(). ( Ignorable by Annotation )

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

134
        $this->loadSecuritySection($container, /** @scrutinizer ignore-type */ $config);
Loading history...
135
        if ($config['routes']['autoGenerated']) {
136
            $this->loadAutoGeneratedRoutes($container, $config);
0 ignored issues
show
$config of type kalanis\Restful\DI\Conf is incompatible with the type array expected by parameter $config of kalanis\Restful\DI\Restf...adAutoGeneratedRoutes(). ( Ignorable by Annotation )

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

136
            $this->loadAutoGeneratedRoutes($container, /** @scrutinizer ignore-type */ $config);
Loading history...
137
        }
138
        if ($config['routes']['panel']) {
139
            $this->loadResourceRoutePanel($container, $config);
0 ignored issues
show
$config of type kalanis\Restful\DI\Conf is incompatible with the type array expected by parameter $config of kalanis\Restful\DI\Restf...oadResourceRoutePanel(). ( Ignorable by Annotation )

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

139
            $this->loadResourceRoutePanel($container, /** @scrutinizer ignore-type */ $config);
Loading history...
140
        }
141
    }
142
143
    /**
144
     * @param ContainerBuilder $container
145
     * @param Conf $config
146
     * @throws Nette\Utils\AssertionException
147
     * @return void
148
     */
149
    private function loadRestful(ContainerBuilder $container, array $config): void
150
    {
151
        Validators::assert($config['prettyPrintKey'], 'string');
152
        $this->startLocalRouter($container, $config);
153
154
        $container->addDefinition($this->prefix('responseFactory'))
155
            ->setType(Application\ResponseFactory::class)
156
            ->addSetup('$service->setJsonp(?)', [$config['jsonpKey']])
157
            ->addSetup('$service->setPrettyPrintKey(?)', [$config['prettyPrintKey']])
158
            ->addSetup('$service->setPrettyPrint(?)', [$config['prettyPrint']]);
159
160
        $container->addDefinition($this->prefix('resourceFactory'))
161
            ->setType(ResourceFactory::class);
162
        $container->addDefinition($this->prefix('resource'))
163
            ->setFactory($this->prefix('@resourceFactory') . '::create');
164
165
        $container->addDefinition($this->prefix('methodOptions'))
166
            ->setType(Application\MethodOptions::class)
167
            ->setArguments([$container->getDefinition('router')])
168
        ;
169
170
        // Mappers
171
        $container->addDefinition($this->prefix('xmlMapper'))
172
            ->setType(Mapping\XmlMapper::class);
173
        $container->addDefinition($this->prefix('jsonMapper'))
174
            ->setType(Mapping\JsonMapper::class);
175
        $container->addDefinition($this->prefix('queryMapper'))
176
            ->setType(Mapping\QueryMapper::class);
177
        $container->addDefinition($this->prefix('dataUrlMapper'))
178
            ->setType(Mapping\DataUrlMapper::class);
179
        $container->addDefinition($this->prefix('nullMapper'))
180
            ->setType(Mapping\NullMapper::class);
181
182
        $container->addDefinition($this->prefix('mapperContext'))
183
            ->setType(Mapping\MapperContext::class)
184
            ->addSetup('$service->addMapper(?, ?)', [IResource::XML, $this->prefix('@xmlMapper')])
185
            ->addSetup('$service->addMapper(?, ?)', [IResource::JSON, $this->prefix('@jsonMapper')])
186
            ->addSetup('$service->addMapper(?, ?)', [IResource::JSONP, $this->prefix('@jsonMapper')])
187
            ->addSetup('$service->addMapper(?, ?)', [IResource::QUERY, $this->prefix('@queryMapper')])
188
            ->addSetup('$service->addMapper(?, ?)', [IResource::DATA_URL, $this->prefix('@dataUrlMapper')])
189
            ->addSetup('$service->addMapper(?, ?)', [IResource::FILE, $this->prefix('@nullMapper')])
190
            ->addSetup('$service->addMapper(?, ?)', [IResource::NULL, $this->prefix('@nullMapper')])
191
        ;
192
193
        if (isset($config['mappers'])) {
194
            foreach ($config['mappers'] as $mapperName => $mapper) {
195
                $container->addDefinition($this->prefix($mapperName))
196
                    ->setType($mapper['class']);
197
198
                $mapperService = $container->getDefinition($this->prefix('mapperContext'));
199
                /** @var ServiceDefinition $mapperService */
200
                $mapperService->addSetup('$service->addMapper(?, ?)', [$mapper['contentType'], $this->prefix('@' . $mapperName)]);
201
            }
202
        }
203
204
        // Input & validation
205
        $container->addDefinition($this->prefix('inputFactory'))
206
            ->setType(Http\InputFactory::class);
207
208
        // Http
209
        $container->addDefinition($this->prefix('httpResponseFactory'))
210
            ->setType(Http\ResponseFactory::class);
211
212
        $container->addDefinition($this->prefix('httpRequestFactory'))
213
            ->setType(Http\ApiRequestFactory::class);
214
215
        $request = $container->getDefinition('httpRequest');
216
        /** @var ServiceDefinition $request */
217
        $request->setFactory($this->prefix('@httpRequestFactory') . '::createHttpRequest');
218
219
        $response = $container->getDefinition('httpResponse');
220
        /** @var ServiceDefinition $response */
221
        $response->setFactory($this->prefix('@httpResponseFactory') . '::createHttpResponse');
222
223
        $container->addDefinition($this->prefix('requestFilter'))
224
            ->setType(Utils\RequestFilter::class)
225
            ->setArguments(['@httpRequest', [$config['jsonpKey'], $config['prettyPrintKey']]]);
226
227
        $container->addDefinition($this->prefix('methodHandler'))
228
            ->setType(Application\Events\MethodHandler::class);
229
230
        $app = $container->getDefinition('application');
231
        /** @var ServiceDefinition $app */
232
        $app->addSetup('$service->onStartup[] = ?', [[$this->prefix('@methodHandler'), 'run']])
233
            ->addSetup('$service->onError[] = ?', [[$this->prefix('@methodHandler'), 'error']]);
234
    }
235
236
    private function loadValidation(ContainerBuilder $container): void
237
    {
238
        $container->addDefinition($this->prefix('validator'))
239
            ->setType(Validation\Validator::class);
240
241
        $container->addDefinition($this->prefix('validationScopeFactory'))
242
            ->setType(Validation\ValidationScopeFactory::class);
243
244
        $container->addDefinition($this->prefix('validationScope'))
245
            ->setType(Validation\ValidationScope::class)
246
            ->setFactory($this->prefix('@validationScopeFactory') . '::create');
247
248
    }
249
250
    /**
251
     * @param ContainerBuilder $container
252
     * @param Conf $config
253
     * @throws Nette\Utils\AssertionException
254
     * @return void
255
     */
256
    private function loadResourceConverters(ContainerBuilder $container, array $config): void
257
    {
258
        Validators::assert($config['timeFormat'], 'string');
259
260
        // Default used converters
261
        $container->addDefinition($this->prefix('objectConverter'))
262
            ->setType(Converters\ObjectConverter::class)
263
            ->addTag(self::CONVERTER_TAG);
264
        $container->addDefinition($this->prefix('dateTimeConverter'))
265
            ->setType(Converters\DateTimeConverter::class)
266
            ->setArguments([$config['timeFormat']])
267
            ->addTag(self::CONVERTER_TAG);
268
269
        // Other available converters
270
        $container->addDefinition($this->prefix('camelCaseConverter'))
271
            ->setType(Converters\CamelCaseConverter::class);
272
        $container->addDefinition($this->prefix('pascalCaseConverter'))
273
            ->setType(Converters\PascalCaseConverter::class);
274
        $container->addDefinition($this->prefix('snakeCaseConverter'))
275
            ->setType(Converters\SnakeCaseConverter::class);
276
277
        // Determine which converter to use if any
278
        if (self::CONVENTION_SNAKE_CASE === $config['convention']) {
279
            $container->getDefinition($this->prefix('snakeCaseConverter'))
280
                ->addTag(self::CONVERTER_TAG);
281
        } elseif (self::CONVENTION_CAMEL_CASE === $config['convention']) {
282
            $container->getDefinition($this->prefix('camelCaseConverter'))
283
                ->addTag(self::CONVERTER_TAG);
284
        } elseif (self::CONVENTION_PASCAL_CASE === $config['convention']) {
285
            $container->getDefinition($this->prefix('pascalCaseConverter'))
286
                ->addTag(self::CONVERTER_TAG);
287
        }
288
289
        // Load converters by tag
290
        $container->addDefinition($this->prefix('resourceConverter'))
291
            ->setType(Converters\ResourceConverter::class);
292
    }
293
294
    /**
295
     * @param ContainerBuilder $container
296
     * @param Conf $config
297
     * @return void
298
     */
299
    private function loadSecuritySection(ContainerBuilder $container, array $config): void
300
    {
301
        $container->addDefinition($this->prefix('security.hashCalculator'))
302
            ->setType(Security\HashCalculator::class)
303
            ->addSetup('$service->setPrivateKey(?)', [$config['security']['privateKey']]);
304
305
        $container->addDefinition($this->prefix('security.hashAuthenticator'))
306
            ->setType(Security\Authentication\HashAuthenticator::class)
307
        ;
308
        $container->addDefinition($this->prefix('security.timeoutAuthenticator'))
309
            ->setType(Security\Authentication\TimeoutAuthenticator::class)
310
            ->setArguments([
311
                $config['security']['requestTimeKey'] ?? 'timestamp',
312
                $config['security']['requestTimeout'] ?? 600
313
            ]);
314
315
        $container->addDefinition($this->prefix('security.nullAuthentication'))
316
            ->setType(Security\Process\NullAuthentication::class);
317
        $container->addDefinition($this->prefix('security.securedAuthentication'))
318
            ->setType(Security\Process\SecuredAuthentication::class);
319
        $container->addDefinition($this->prefix('security.basicAuthentication'))
320
            ->setType(Security\Process\BasicAuthentication::class);
321
322
        $container->addDefinition($this->prefix('security.authentication'))
323
            ->setType(Security\AuthenticationContext::class)
324
            ->addSetup('$service->setAuthProcess(?)', [$this->prefix('@security.nullAuthentication')]);
325
326
        // enable OAuth2 in Restful
327
        if ($this->getByType($container, KeyGenerator::class)) {
328
            $container->addDefinition($this->prefix('security.oauth2Authentication'))
329
                ->setType(Security\Process\OAuth2Authentication::class);
330
        }
331
    }
332
333
    private function getByType(ContainerBuilder $container, string $type): ?ServiceDefinition
334
    {
335
        $definitions = $container->getDefinitions();
336
        foreach ($definitions as $definition) {
337
            if (($definition instanceof ServiceDefinition) && ($definition->class === $type)) {
338
                return $definition;
339
            }
340
        }
341
        return null;
342
    }
343
344
    /**
345
     * @param ContainerBuilder $container
346
     * @param Conf $config
347
     * @return void
348
     */
349
    private function loadAutoGeneratedRoutes(ContainerBuilder $container, array $config): void
350
    {
351
        $container->addDefinition($this->prefix('routeAnnotation'))
352
            ->setType(Application\RouteAnnotation::class);
353
354
        $container->addDefinition($this->prefix('routeListFactory'))
355
            ->setType(Application\RouteListFactory::class)
356
            ->setArguments([$config['routes']['presentersRoot'], $config['routes']['autoRebuild'], $config['cacheDir']])
357
            ->addSetup('$service->setModule(?)', [$config['routes']['module']])
358
            ->addSetup('$service->setPrefix(?)', [$config['routes']['prefix']]);
359
360
        $container->addDefinition($this->prefix('cachedRouteListFactory'))
361
            ->setType(Application\CachedRouteListFactory::class)
362
            ->setArguments([$config['routes']['presentersRoot'], $this->prefix('@routeListFactory')]);
363
364
        $statement = new Statement(
365
            'offsetSet',
366
            [
367
                null,
368
                new Statement($this->prefix('@cachedRouteListFactory') . '::create')
369
            ]
370
        );
371
        // jak to sakra funguje?
372
        // -> init routeru v hlavnim nette, pripadne tady a pak vyuziti s doplnenim parametru, co jsou v config neonu
373
        $this->startLocalRouter($container, $config);
374
        if ($config['routes']['generateAtStart']) {
375
            /** @var ServiceDefinition $def */
376
            $def = $container->getDefinition('router');
377
            $setup = $def->getSetup();
378
            array_unshift($setup, $statement);
379
            /** @var ServiceDefinition $def */
380
            $def = $container->getDefinition('router');
381
            $def->setSetup($setup);
382
        } else {
383
            /** @var ServiceDefinition $def */
384
            $def = $container->getDefinition('router');
385
            $def->addSetup($statement);
386
        }
387
    }
388
389
    /**
390
     * @param ContainerBuilder $container
391
     * @param Conf $config
392
     * @return void
393
     */
394
    private function startLocalRouter(ContainerBuilder $container, array $config): void
395
    {
396
        try {
397
            $container->getDefinition('router');
398
        } catch (MissingServiceException) {
399
            $container->addDefinition('router')
400
                ->setType(Application\Routes\ResourceRoute::class)
401
                ->setArguments([
402
                    $config['resourceRoute']['mask'] ?? '',
403
                    $config['resourceRoute']['metadata'] ?? [],
404
                    $config['resourceRoute']['flags'] ?? Application\IResourceRouter::CRUD
405
                ])
406
            ;
407
        }
408
    }
409
410
    /**
411
     * @param ContainerBuilder $container
412
     * @param Conf $config
413
     * @return void
414
     */
415
    private function loadResourceRoutePanel(ContainerBuilder $container, array $config): void
416
    {
417
        $container->addDefinition($this->prefix('panel'))
418
            ->setType(Diagnostics\ResourceRouterPanel::class)
419
            ->setArguments([$config['security']['privateKey'], $config['security']['requestTimeKey'] ?? 'timestamp'])
420
            ->addSetup('\Tracy\Debugger::getBar()->addPanel(?)', ['@self']);
421
422
        /** @var ServiceDefinition $serviceDef */
423
        $serviceDef = $container->getDefinition('application');
424
        $serviceDef->addSetup('$service->onStartup[] = ?', [[$this->prefix('@panel'), 'getTab']]);
425
    }
426
427
    /**
428
     * Before compile
429
     */
430
    public function beforeCompile(): void
431
    {
432
        $container = $this->getContainerBuilder();
433
434
        /** @var ServiceDefinition $resourceConverter */
435
        $resourceConverter = $container->getDefinition($this->prefix('resourceConverter'));
436
        $services = $container->findByTag(self::CONVERTER_TAG);
437
438
        foreach ($services as $service => $args) {
439
            $resourceConverter->addSetup('$service->addConverter(?)', ['@' . $service]);
440
        }
441
    }
442
}
443