Completed
Push — master ( 292728...4a826b )
by Neomerx
02:38
created

ExecuteCommandTrait   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
dl 0
loc 257
ccs 66
cts 66
cp 1
rs 10
c 0
b 0
f 0

13 Methods

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