anonymous//src/ExecuteCommandTrait.php$0   A
last analyzed

Complexity

Total Complexity 15

Size/Duplication

Total Lines 130
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 15
lcom 1
cbo 1
dl 0
loc 130
ccs 34
cts 34
cp 1
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Commands;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Closure;
22
use Limoncello\Common\Reflection\CheckCallableTrait;
23
use Limoncello\Common\Reflection\ClassIsTrait;
24
use Limoncello\Contracts\Application\ApplicationConfigurationInterface;
25
use Limoncello\Contracts\Application\CacheSettingsProviderInterface;
26
use Limoncello\Contracts\Commands\IoInterface;
27
use Limoncello\Contracts\Commands\RoutesConfiguratorInterface;
28
use Limoncello\Contracts\Commands\RoutesInterface;
29
use Limoncello\Contracts\Container\ContainerInterface as LimoncelloContainerInterface;
30
use Limoncello\Contracts\FileSystem\FileSystemInterface;
31
use Psr\Container\ContainerInterface as PsrContainerInterface;
32
use ReflectionException;
33
use function assert;
34
use function array_merge;
35
use function call_user_func;
36
use function count;
37
38
/**
39
 * Code for command execution is separated from the main code to get rid of a dependency from Composer.
40
 * This code could be executed independently in tests without composer dependency.
41
 *
42
 * @package Limoncello\Commands
43
 *
44
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
45
 */
46
trait ExecuteCommandTrait
47
{
48
    use ClassIsTrait;
49
50
    /**
51
     * @param string                       $name
52
     * @param callable                     $handler
53
     * @param IoInterface                  $inOut
54
     * @param LimoncelloContainerInterface $container
55
     *
56 2
     * @return void
57
     *
58
     * @throws ReflectionException
59
     *
60
     * @SuppressWarnings(PHPMD.ElseExpression)
61
     */
62
    public function executeCommand(
63
        string $name,
64
        callable $handler,
65
        IoInterface $inOut,
66
        LimoncelloContainerInterface $container
67
    ): void {
68
        // This method does bootstrap for every command (e.g. configure containers)
69
        // and then calls the actual command handler.
70
71 2
        // At this point we have probably only partly configured container and we need to read from it
72 2
        // CLI route setting in order to fully configure it and then run the command with middleware.
73 2
        // However, when we read anything from it, it changes its state so we are not allowed to add
74
        // anything to it (technically we can but in some cases it might cause an exception).
75
        // So, what's the solution? We clone the container, read from the clone everything we need,
76 2
        // and then continue with the original unchanged container.
77 2
        $routesPath = null;
78
        if (true) {
79 2
            $containerClone = clone $container;
80 2
81
            /** @var CacheSettingsProviderInterface $provider */
82
            $provider  = $container->get(CacheSettingsProviderInterface::class);
83 2
            $appConfig = $provider->getApplicationConfiguration();
84 2
85 2
            $routesFolder = $appConfig[ApplicationConfigurationInterface::KEY_ROUTES_FOLDER] ?? '';
86 2
            $routesMask   = $appConfig[ApplicationConfigurationInterface::KEY_ROUTES_FILE_MASK] ?? '';
87 2
88
            /** @var FileSystemInterface $files */
89
            assert(
90 2
                ($files = $containerClone->get(FileSystemInterface::class)) !== null &&
91
                empty($routesFolder) === false && empty($routesMask) === false &&
92 2
                $files->exists($routesFolder) === true,
93
                'Routes folder and mask must be defined in application settings.'
94
            );
95
96 2
            unset($containerClone);
97
98 2
            $routesPath = $routesFolder . DIRECTORY_SEPARATOR . $routesMask;
99
        }
100 2
101
        [$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...
102
            = $this->readExtraContainerConfiguratorsAndMiddleware($routesPath, $name);
103
104 2
        $this->executeContainerConfigurators($configurators, $container);
105
106
        $handler = $this->buildExecutionChain($middleware, $handler, $container);
107
108
        // finally go through all middleware and execute command handler
109
        // (container has to be the same (do not send as param), but middleware my wrap IO (send as param)).
110
        call_user_func($handler, $inOut);
111
    }
112
113
    /**
114
     * @param string $routesFolder
115
     * @param string $commandName
116
     *
117 2
     * @return array
118
     *
119
     * @throws ReflectionException
120
     *
121
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
122
     */
123
    private function readExtraContainerConfiguratorsAndMiddleware(string $routesFolder, string $commandName): array
124
    {
125
        $routesFilter = new class ($commandName) implements RoutesInterface
126
        {
127
            use CheckCallableTrait;
128
129
            /** @var array */
130
            private $middleware = [];
131
132
            /** @var array */
133
            private $configurators = [];
134
135
            /** @var string */
136
            private $commandName;
137 2
138
            /**
139
             * @param string $commandName
140
             */
141
            public function __construct(string $commandName)
142
            {
143
                $this->commandName = $commandName;
144
            }
145 2
146
            /**
147 2
             * @inheritdoc
148
             */
149 2
            public function addGlobalMiddleware(array $middleware): RoutesInterface
150
            {
151
                assert($this->checkMiddlewareCallables($middleware) === true);
152
153
                $this->middleware = array_merge($this->middleware, $middleware);
154
155
                return $this;
156
            }
157 2
158
            /**
159 2
             * @inheritdoc
160
             */
161 2
            public function addGlobalContainerConfigurators(array $configurators): RoutesInterface
162
            {
163
                assert($this->checkConfiguratorCallables($configurators) === true);
164
165
                $this->configurators = array_merge($this->configurators, $configurators);
166
167
                return $this;
168
            }
169 2
170
            /**
171 2
             * @inheritdoc
172 1
             */
173
            public function addCommandMiddleware(string $name, array $middleware): RoutesInterface
174
            {
175 2
                assert($this->checkMiddlewareCallables($middleware) === true);
176
177
                if ($this->commandName === $name) {
178
                    $this->middleware = array_merge($this->middleware, $middleware);
179
                }
180
181
                return $this;
182
            }
183 2
184
            /**
185 2
             * @inheritdoc
186 1
             */
187
            public function addCommandContainerConfigurators(string $name, array $configurators): RoutesInterface
188
            {
189 2
                assert($this->checkConfiguratorCallables($configurators) === true);
190
191
                if ($this->commandName === $name) {
192
                    $this->configurators = array_merge($this->configurators, $configurators);
193
                }
194
195
                return $this;
196
            }
197 2
198
            /**
199
             * @return array
200
             */
201
            public function getMiddleware(): array
202
            {
203
                return $this->middleware;
204
            }
205 2
206
            /**
207
             * @return array
208
             */
209
            public function getConfigurators(): array
210
            {
211
                return $this->configurators;
212
            }
213
214
            /**
215 2
             * @param array $mightBeConfigurators
216
             *
217 2
             * @return bool
218 2
             */
219 2
            private function checkConfiguratorCallables(array $mightBeConfigurators): bool
220 2
            {
221 2
                $result = true;
222 2
223
                foreach ($mightBeConfigurators as $mightBeCallable) {
224
                    $result = $result === true &&
225
                        $this->checkPublicStaticCallable(
226 2
                            $mightBeCallable,
227
                            [LimoncelloContainerInterface::class],
228
                            'void'
229
                        );
230
                }
231
232
                return $result;
233
            }
234
235
            /**
236 2
             * @param array $mightBeMiddleware
237
             *
238 2
             * @return bool
239 2
             */
240 2
            private function checkMiddlewareCallables(array $mightBeMiddleware): bool
241 2
            {
242 2
                $result = true;
243
244
                foreach ($mightBeMiddleware as $mightBeCallable) {
245
                    $result = $result === true && $this->checkPublicStaticCallable(
246 2
                        $mightBeCallable,
247
                        [IoInterface::class, Closure::class, PsrContainerInterface::class],
248
                        'void'
249
                    );
250 2
                }
251
252 2
                return $result;
253
            }
254
        };
255 2
256
        foreach (static::selectClasses($routesFolder, RoutesConfiguratorInterface::class) as $class) {
257
            /** @var RoutesConfiguratorInterface $class */
258
            $class::configureRoutes($routesFilter);
259
        }
260
261
        return [$routesFilter->getConfigurators(), $routesFilter->getMiddleware()];
262
    }
263
264 2
    /**
265
     * @param callable[]                   $configurators
266 2
     * @param LimoncelloContainerInterface $container
267 2
     *
268
     * @return void
269
     */
270
    private function executeContainerConfigurators(array $configurators, LimoncelloContainerInterface $container): void
271
    {
272
        foreach ($configurators as $configurator) {
273
            call_user_func($configurator, $container);
274
        }
275
    }
276
277
    /**
278 2
     * @param array                 $middleware
279
     * @param callable              $command
280
     * @param PsrContainerInterface $container
281
     *
282
     * @return Closure
283
     */
284 2
    private function buildExecutionChain(
285 2
        array $middleware,
286
        callable $command,
287 2
        PsrContainerInterface $container
288 2
    ): Closure {
289
        $next = function (IoInterface $inOut) use ($command, $container): void {
290 2
            call_user_func($command, $container, $inOut);
291 2
        };
292
293
        for ($index = count($middleware) - 1; $index >= 0; $index--) {
294 2
            $currentMiddleware = $middleware[$index];
295
            $next              = function (IoInterface $inOut) use ($currentMiddleware, $next, $container): void {
296
                call_user_func($currentMiddleware, $inOut, $next, $container);
297
            };
298
        }
299
300
        return $next;
301
    }
302
}
303