Completed
Push — develop ( 64ad43...c5a4cf )
by Neomerx
03:58
created

ExecuteCommandTrait   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 250
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 23
dl 0
loc 250
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 46 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 callable                     $handler
46
     * @param IoInterface                  $inOut
47
     * @param LimoncelloContainerInterface $container
48
     *
49
     * @throws ReflectionException
50
     *
51
     * @SuppressWarnings(PHPMD.ElseExpression)
52
     */
53 2
    public function executeCommand(callable $handler, IoInterface $inOut, LimoncelloContainerInterface $container): void
54
    {
55
        // This method does bootstrap for every command (e.g. configure containers)
56
        // and then calls the actual command handler.
57
58
        // At this point we have probably only partly configured container and we need to read from it
59
        // CLI route setting in order to fully configure it and then run the command with middleware.
60
        // However, when we read anything from it, it changes its state so we are not allowed to add
61
        // anything to it (technically we can but in some cases it might cause an exception).
62
        // So, what's the solution? We clone the container, read from the clone everything we need,
63
        // and then continue with the original unchanged container.
64 2
        $routesPath = null;
65 2
        if (true) {
0 ignored issues
show
Bug introduced by
Avoid IF statements that are always true or false
Loading history...
66 2
            $containerClone = clone $container;
67
68
            /** @var CacheSettingsProviderInterface $provider */
69 2
            $provider  = $container->get(CacheSettingsProviderInterface::class);
70 2
            $appConfig = $provider->getApplicationConfiguration();
71
72 2
            $routesFolder = $appConfig[ApplicationConfigurationInterface::KEY_ROUTES_FOLDER] ?? '';
73 2
            $routesMask   = $appConfig[ApplicationConfigurationInterface::KEY_ROUTES_FILE_MASK] ?? '';
74
75
            /** @var FileSystemInterface $files */
76 2
            assert(
77 2
                ($files = $containerClone->get(FileSystemInterface::class)) !== null &&
78 2
                empty($routesFolder) === false && empty($routesMask) === false &&
79 2
                $files->exists($routesFolder) === true,
80 2
                'Routes folder and mask must be defined in application settings.'
81
            );
82
83 2
            unset($containerClone);
84
85 2
            $routesPath = $routesFolder . DIRECTORY_SEPARATOR . $routesMask;
86
        }
87
88
        [$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...
89 2
            = $this->readExtraContainerConfiguratorsAndMiddleware($routesPath, $this->getName());
0 ignored issues
show
Bug introduced by
It seems like getName() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
90
91 2
        $this->executeContainerConfigurators($configurators, $container);
92
93 2
        $handler = $this->buildExecutionChain($middleware, $handler, $container);
94
95
        // finally go through all middleware and execute command handler
96
        // (container has to be the same (do not send as param), but middleware my wrap IO (send as param)).
97 2
        call_user_func($handler, $inOut);
98
    }
99
100
    /**
101
     * @param string $routesFolder
102
     * @param string $commandName
103
     *
104
     * @return array
105
     *
106
     * @throws ReflectionException
107
     *
108
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
109
     */
110 2
    private function readExtraContainerConfiguratorsAndMiddleware(string $routesFolder, string $commandName): array
111
    {
112
        $routesFilter = new class ($commandName) implements RoutesInterface
113
        {
114
            use CheckCallableTrait;
115
116
            /** @var array */
117
            private $middleware = [];
118
119
            /** @var array */
120
            private $configurators = [];
121
122
            /** @var string */
123
            private $commandName;
124
125
            /**
126
             * @param string $commandName
127
             */
128
            public function __construct(string $commandName)
129
            {
130 2
                $this->commandName = $commandName;
131
            }
132
133
            /**
134
             * @inheritdoc
135
             */
136
            public function addGlobalMiddleware(array $middleware): RoutesInterface
137
            {
138 2
                assert($this->checkMiddlewareCallables($middleware) === true);
139
140 2
                $this->middleware = array_merge($this->middleware, $middleware);
141
142 2
                return $this;
143
            }
144
145
            /**
146
             * @inheritdoc
147
             */
148
            public function addGlobalContainerConfigurators(array $configurators): RoutesInterface
149
            {
150 2
                assert($this->checkConfiguratorCallables($configurators) === true);
151
152 2
                $this->configurators = array_merge($this->configurators, $configurators);
153
154 2
                return $this;
155
            }
156
157
            /**
158
             * @inheritdoc
159
             */
160
            public function addCommandMiddleware(string $name, array $middleware): RoutesInterface
161
            {
162 2
                assert($this->checkMiddlewareCallables($middleware) === true);
163
164 2
                if ($this->commandName === $name) {
165 1
                    $this->middleware = array_merge($this->middleware, $middleware);
166
                }
167
168 2
                return $this;
169
            }
170
171
            /**
172
             * @inheritdoc
173
             */
174
            public function addCommandContainerConfigurators(string $name, array $configurators): RoutesInterface
175
            {
176 2
                assert($this->checkConfiguratorCallables($configurators) === true);
177
178 2
                if ($this->commandName === $name) {
179 1
                    $this->configurators = array_merge($this->configurators, $configurators);
180
                }
181
182 2
                return $this;
183
            }
184
185
            /**
186
             * @return array
187
             */
188
            public function getMiddleware(): array
189
            {
190 2
                return $this->middleware;
191
            }
192
193
            /**
194
             * @return array
195
             */
196
            public function getConfigurators(): array
197
            {
198 2
                return $this->configurators;
199
            }
200
201
            /**
202
             * @param array $mightBeConfigurators
203
             *
204
             * @return bool
205
             */
206
            private function checkConfiguratorCallables(array $mightBeConfigurators): bool
207
            {
208 2
                $result = true;
209
210 2
                foreach ($mightBeConfigurators as $mightBeCallable) {
211 2
                    $result = $result === true &&
212 2
                        $this->checkPublicStaticCallable(
213 2
                            $mightBeCallable,
214 2
                            [LimoncelloContainerInterface::class],
215 2
                            'void'
216
                        );
217
                }
218
219 2
                return $result;
220
            }
221
222
            /**
223
             * @param array $mightBeMiddleware
224
             *
225
             * @return bool
226
             */
227
            private function checkMiddlewareCallables(array $mightBeMiddleware): bool
228
            {
229 2
                $result = true;
230
231 2
                foreach ($mightBeMiddleware as $mightBeCallable) {
232 2
                    $result = $result === true && $this->checkPublicStaticCallable(
233 2
                        $mightBeCallable,
234 2
                        [IoInterface::class, Closure::class, PsrContainerInterface::class],
235 2
                        'void'
236
                    );
237
                }
238
239 2
                return $result;
240
            }
241
        };
242
243 2
        foreach (static::selectClasses($routesFolder, RoutesConfiguratorInterface::class) as $class) {
244
            /** @var RoutesConfiguratorInterface $class */
245 2
            $class::configureRoutes($routesFilter);
246
        }
247
248 2
        return [$routesFilter->getConfigurators(), $routesFilter->getMiddleware()];
249
    }
250
251
    /**
252
     * @param callable[]                   $configurators
253
     * @param LimoncelloContainerInterface $container
254
     *
255
     * @return void
256
     */
257 2
    private function executeContainerConfigurators(array $configurators, LimoncelloContainerInterface $container): void
258
    {
259 2
        foreach ($configurators as $configurator) {
260 2
            call_user_func($configurator, $container);
261
        }
262
    }
263
264
    /**
265
     * @param array                 $middleware
266
     * @param callable              $command
267
     * @param PsrContainerInterface $container
268
     *
269
     * @return Closure
270
     */
271 2
    private function buildExecutionChain(
272
        array $middleware,
273
        callable $command,
274
        PsrContainerInterface $container
275
    ): Closure {
276
        $next = function (IoInterface $inOut) use ($command, $container): void {
277 2
            call_user_func($command, $container, $inOut);
278 2
        };
279
280 2
        for ($index = count($middleware) - 1; $index >= 0; $index--) {
281 2
            $currentMiddleware = $middleware[$index];
282
            $next              = function (IoInterface $inOut) use ($currentMiddleware, $next, $container): void {
283 2
                call_user_func($currentMiddleware, $inOut, $next, $container);
284 2
            };
285
        }
286
287 2
        return $next;
288
    }
289
}
290