Completed
Push — develop ( da0857...da75a0 )
by Neomerx
03:18
created

ntainerConfigurators()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 0
cp 0
rs 9.9332
c 0
b 0
f 0
cc 2
crap 6
1
<?php namespace Limoncello\Commands;
2
3
/**
4
 * Copyright 2015-2018 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Closure;
20
use Composer\Command\BaseCommand;
21
use Exception;
22
use Limoncello\Commands\Traits\CommandSerializationTrait;
23
use Limoncello\Commands\Traits\CommandTrait;
24
use Limoncello\Commands\Wrappers\DataArgumentWrapper;
25
use Limoncello\Commands\Wrappers\DataOptionWrapper;
26
use Limoncello\Common\Reflection\CheckCallableTrait;
27
use Limoncello\Common\Reflection\ClassIsTrait;
28
use Limoncello\Contracts\Application\ApplicationConfigurationInterface;
29
use Limoncello\Contracts\Application\CacheSettingsProviderInterface;
30
use Limoncello\Contracts\Commands\IoInterface;
31
use Limoncello\Contracts\Commands\RoutesConfiguratorInterface;
32
use Limoncello\Contracts\Commands\RoutesInterface;
33
use Limoncello\Contracts\Container\ContainerInterface as LimoncelloContainerInterface;
34
use Limoncello\Contracts\Exceptions\ThrowableHandlerInterface;
35
use Limoncello\Contracts\FileSystem\FileSystemInterface;
36
use Psr\Container\ContainerInterface as PsrContainerInterface;
37
use ReflectionException;
38
use Symfony\Component\Console\Input\InputInterface;
39
use Symfony\Component\Console\Output\OutputInterface;
40
41
/**
42
 * @package Limoncello\Commands
43
 */
44
class LimoncelloCommand extends BaseCommand
45
{
46
    use CommandTrait, CommandSerializationTrait, ClassIsTrait;
47
48
    /**
49
     * @var string
50
     */
51
    private $description;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
52
53
    /**
54
     * @var string
55
     */
56
    private $help;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
57
58
    /**
59
     * @var array
60
     */
61
    private $arguments;
62
63
    /**
64
     * @var array
65
     */
66
    private $options;
67
68
    /**
69 4
     * @var callable|array
70
     */
71
    private $callable;
72
73
    /**
74
     * @param string $name
75
     * @param string $description
76
     * @param string $help
77 4
     * @param array  $arguments
78 4
     * @param array  $options
79 4
     * @param array  $callable
80 4
     */
81 4
    public function __construct(
82
        string $name,
83
        string $description,
84
        string $help,
85 4
        array $arguments,
86
        array $options,
87
        array $callable
88
    ) {
89
        $this->description = $description;
90
        $this->help        = $help;
91 4
        $this->arguments   = $arguments;
92
        $this->options     = $options;
93 4
        $this->callable    = $callable;
94
95
        // it is important to call the parent constructor after
96 4
        // data init as it calls `configure` method.
97 4
        parent::__construct($name);
98
    }
99 4
100 3
    /**
101 3
     * @inheritdoc
102
     */
103
    public function configure()
104 4
    {
105 3
        parent::configure();
106 3
107 3
        $this
108 3
            ->setDescription($this->description)
109 3
            ->setHelp($this->help);
110 3
111 3
        foreach ($this->arguments as $data) {
112
            $arg = new DataArgumentWrapper($data);
113
            $this->addArgument($arg->getName(), $arg->getMode(), $arg->getDescription(), $arg->getDefault());
114
        }
115
116
        foreach ($this->options as $data) {
117
            $opt = new DataOptionWrapper($data);
118
            $this->addOption(
119
                $opt->getName(),
120
                $opt->getShortcut(),
121
                $opt->getMode(),
122
                $opt->getDescription(),
123 3
                $opt->getDefault()
124
            );
125 3
        }
126
    }
127
128 3
    /** @noinspection PhpMissingParentCallCommonInspection
129 2
     * @inheritdoc
130 2
     *
131 2
     * @throws Exception
132
     *
133 1
     * @SuppressWarnings(PHPMD.ElseExpression)
134 1
     */
135
    public function execute(InputInterface $input, OutputInterface $output)
136 1
    {
137
        // This method does bootstrap for every command (e.g. configure containers)
138 1
        // and then calls the actual command handler.
139 1
140 1
        $container =  null;
141 1
142
        try {
143 1
            $container = $this->createContainer($this->getComposer());
0 ignored issues
show
Bug introduced by
It seems like $this->getComposer() can be null; however, createContainer() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
144
            assert($container instanceof LimoncelloContainerInterface);
145
146 2
            // At this point we have probably only partly configured container and we need to read from it
147
            // CLI route setting in order to fully configure it and then run the command with middleware.
148
            // However, when we read anything from it, it changes its state so we are not allowed to add
149
            // anything to it (technically we can but in some cases it might cause an exception).
150
            // So, what's the solution? We clone the container, read from the clone everything we need,
151
            // and then continue with the original unchanged container.
152
            $routesFolder = null;
153
            if (true) {
0 ignored issues
show
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
154
                $containerClone = clone $container;
155
156
                /** @var CacheSettingsProviderInterface $provider */
157
                $provider  = $container->get(CacheSettingsProviderInterface::class);
158
                $appConfig = $provider->getApplicationConfiguration();
159
160
                $routesFolder = $appConfig[ApplicationConfigurationInterface::KEY_ROUTES_FOLDER] ?? null;
161
162
                /** @var FileSystemInterface $files */
163
                assert(
164
                    ($files = $containerClone->get(FileSystemInterface::class)) !== null &&
165
                    $routesFolder !== null && $files->exists($routesFolder) === true,
166
                    'Routes folder must be defined in application settings.'
167
                );
168
169
                unset($containerClone);
170
            }
171
172
            [$configurators, $middleware] =
0 ignored issues
show
Bug introduced by
The variable $configurators does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $middleware does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
173
                $this->readExtraContainerConfiguratorsAndMiddleware($routesFolder, $this->getName());
174
175
            $this->executeContainerConfigurators($configurators, $container);
176
177
            $handler = $this->buildExecutionChain($middleware, $this->callable, $container);
178
179
            // finally go through all middleware and execute command handler
180
            // (container has to be the same (do not send as param), but middleware my wrap IO (send as param)).
181
            call_user_func($handler, $this->wrapIo($input, $output));
182
        } catch (Exception $exception) {
183
            if ($container !== null && $container->has(ThrowableHandlerInterface::class) === true) {
184
                /** @var ThrowableHandlerInterface $handler */
185
                $handler  = $container->get(ThrowableHandlerInterface::class);
186
                $response = $handler->createResponse($exception, $container);
187
188
                $output->writeln((string)$response->getBody());
189
            } else {
190
                $message = $exception->getMessage();
191
                $file    = $exception->getFile();
192
                $line    = $exception->getLine();
193
                $trace   = $exception->getTraceAsString();
194
195
                $output->writeln("$message at $file#$line" . PHP_EOL . $trace);
196
            }
197
198
            throw $exception;
199
        }
200
    }
201
202
    /**
203
     * @param string $routesFolder
204
     * @param string $commandName
205
     *
206
     * @return array
207
     *
208
     * @throws ReflectionException
209
     */
210
    private function readExtraContainerConfiguratorsAndMiddleware(string $routesFolder, string $commandName): array
211
    {
212
        $routesFilter = new class ($commandName) implements RoutesInterface
213
        {
214
            use CheckCallableTrait;
215
216
            /** @var array */
217
            private $middleware = [];
218
219
            /** @var array */
220
            private $configurators = [];
221
222
            /** @var string */
223
            private $commandName;
224
225
            /**
226
             * @param string $commandName
227
             */
228
            public function __construct(string $commandName)
229
            {
230
                $this->commandName = $commandName;
231
            }
232
233
            /**
234
             * @inheritdoc
235
             */
236
            public function addGlobalMiddleware(array $middleware): RoutesInterface
237
            {
238
                assert($this->checkMiddlewareCallables($middleware) === true);
239
240
                $this->middleware = array_merge($this->middleware, $middleware);
241
242
                return $this;
243
            }
244
245
            /**
246
             * @inheritdoc
247
             */
248
            public function addGlobalContainerConfigurators(array $configurators): RoutesInterface
249
            {
250
                assert($this->checkConfiguratorCallables($configurators) === true);
251
252
                $this->configurators = array_merge($this->configurators, $configurators);
253
254
                return $this;
255
            }
256
257
            /**
258
             * @inheritdoc
259
             */
260
            public function addCommandMiddleware(string $name, array $middleware): RoutesInterface
261
            {
262
                assert($this->checkMiddlewareCallables($middleware) === true);
263
264
                if ($this->commandName === $name) {
265
                    $this->middleware = array_merge($this->middleware, $middleware);
266
                }
267
268
                return $this;
269
            }
270
271
            /**
272
             * @inheritdoc
273
             */
274
            public function addCommandContainerConfigurators(string $name, array $configurators): RoutesInterface
275
            {
276
                assert($this->checkConfiguratorCallables($configurators) === true);
277
278
                if ($this->commandName === $name) {
279
                    $this->configurators = array_merge($this->configurators, $configurators);
280
                }
281
282
                return $this;
283
            }
284
285
            /**
286
             * @return array
287
             */
288
            public function getMiddleware(): array
289
            {
290
                return $this->middleware;
291
            }
292
293
            /**
294
             * @return array
295
             */
296
            public function getConfigurators(): array
297
            {
298
                return $this->configurators;
299
            }
300
301
            /**
302
             * @param array $mightBeConfigurators
303
             *
304
             * @return bool
305
             */
306
            private function checkConfiguratorCallables(array $mightBeConfigurators): bool
307
            {
308
                $result = true;
309
310
                foreach ($mightBeConfigurators as $mightBeCallable) {
311
                    $result = $result === true &&
312
                        $this->checkPublicStaticCallable(
313
                            $mightBeCallable,
314
                            [LimoncelloContainerInterface::class],
315
                            'void'
316
                        );
317
                }
318
319
                return $result;
320
            }
321
322
            /**
323
             * @param array $mightBeMiddleware
324
             *
325
             * @return bool
326
             */
327
            private function checkMiddlewareCallables(array $mightBeMiddleware): bool
328
            {
329
                $result = true;
330
331
                foreach ($mightBeMiddleware as $mightBeCallable) {
332
                    $result = $result === true && $this->checkPublicStaticCallable(
333
                        $mightBeCallable,
334
                        [IoInterface::class, Closure::class, PsrContainerInterface::class],
335
                        'void'
336
                    );
337
                }
338
339
                return $result;
340
            }
341
        };
342
343
        foreach (static::selectClasses($routesFolder, RoutesConfiguratorInterface::class) as $class) {
344
            /** @var RoutesConfiguratorInterface $class */
345
            $class::configureRoutes($routesFilter);
346
        }
347
348
        return [$routesFilter->getConfigurators(), $routesFilter->getMiddleware()];
349
    }
350
351
    /**
352
     * @param callable[]                   $configurators
353
     * @param LimoncelloContainerInterface $container
354
     *
355
     * @return void
356
     */
357
    private function executeContainerConfigurators(array $configurators, LimoncelloContainerInterface $container): void
358
    {
359
        foreach ($configurators as $configurator) {
360
            call_user_func($configurator, $container);
361
        }
362
    }
363
364
    /**
365
     * @param array                 $middleware
366
     * @param callable              $command
367
     * @param PsrContainerInterface $container
368
     *
369
     * @return Closure
370
     */
371
    private function buildExecutionChain(
372
        array $middleware,
373
        callable $command,
374
        PsrContainerInterface $container
375
    ): Closure {
376
        $next = function (IoInterface $inOut) use ($command, $container): void
377
        {
378
            call_user_func($command, $container, $inOut);
379
        };
380
381
        for ($index = count($middleware) - 1; $index >= 0; $index--) {
382
            $currentMiddleware = $middleware[$index];
383
            $next = function(IoInterface $inOut) use ($currentMiddleware, $next, $container): void
384
            {
385
                call_user_func($currentMiddleware, $inOut, $next, $container);
386
            };
387
        }
388
389
        return $next;
390
    }
391
}
392