Passed
Push — master ( 23a26d...cc7dc4 )
by Melech
04:05
created

Router::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 0
c 1
b 0
f 1
dl 0
loc 11
rs 10
cc 1
nc 1
nop 9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Cli\Routing;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override 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...
17
use Valkyrja\Cli\Interaction\Enum\ExitCode;
18
use Valkyrja\Cli\Interaction\Factory\Contract\OutputFactory;
19
use Valkyrja\Cli\Interaction\Input\Contract\Input;
20
use Valkyrja\Cli\Interaction\Message\Answer;
21
use Valkyrja\Cli\Interaction\Message\Banner;
22
use Valkyrja\Cli\Interaction\Message\Contract\Answer as AnswerContract;
23
use Valkyrja\Cli\Interaction\Message\ErrorMessage;
24
use Valkyrja\Cli\Interaction\Message\NewLine;
25
use Valkyrja\Cli\Interaction\Message\Question;
26
use Valkyrja\Cli\Interaction\Option\Option;
27
use Valkyrja\Cli\Interaction\Output\Contract\Output;
28
use Valkyrja\Cli\Middleware;
29
use Valkyrja\Cli\Middleware\Handler\Contract\CommandDispatchedHandler;
30
use Valkyrja\Cli\Middleware\Handler\Contract\CommandMatchedHandler;
31
use Valkyrja\Cli\Middleware\Handler\Contract\CommandNotMatchedHandler;
32
use Valkyrja\Cli\Middleware\Handler\Contract\ExitedHandler;
33
use Valkyrja\Cli\Middleware\Handler\Contract\ThrowableCaughtHandler;
34
use Valkyrja\Cli\Routing\Collection\Contract\Collection;
35
use Valkyrja\Cli\Routing\Command\HelpCommand;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Cli\Routing\Command\HelpCommand 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...
36
use Valkyrja\Cli\Routing\Contract\Router as Contract;
37
use Valkyrja\Cli\Routing\Data\Contract\Command;
38
use Valkyrja\Cli\Routing\Data\Option\HelpOptionParameter;
0 ignored issues
show
Bug introduced by
The type Valkyrja\Cli\Routing\Dat...ion\HelpOptionParameter 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...
39
use Valkyrja\Cli\Routing\Enum\ArgumentValueMode;
40
use Valkyrja\Cli\Routing\Exception\RuntimeException;
41
use Valkyrja\Container\Contract\Container;
42
use Valkyrja\Dispatcher\Contract\Dispatcher;
43
44
use function in_array;
45
46
/**
47
 * Class Router.
48
 *
49
 * @author Melech Mizrachi
50
 */
51
class Router implements Contract
52
{
53
    public function __construct(
54
        protected Container $container = new \Valkyrja\Container\Container(),
55
        protected Dispatcher $dispatcher = new \Valkyrja\Dispatcher\Dispatcher(),
56
        protected Collection $collection = new \Valkyrja\Cli\Routing\Collection\Collection(),
57
        protected OutputFactory $outputFactory = new \Valkyrja\Cli\Interaction\Factory\OutputFactory(),
58
        protected ThrowableCaughtHandler $throwableCaughtHandler = new Middleware\Handler\ThrowableCaughtHandler(),
59
        protected CommandMatchedHandler $commandMatchedHandler = new Middleware\Handler\CommandMatchedHandler(),
60
        protected CommandNotMatchedHandler $commandNotMatchedHandler = new Middleware\Handler\CommandNotMatchedHandler(),
61
        protected CommandDispatchedHandler $commandDispatchedHandler = new Middleware\Handler\CommandDispatchedHandler(),
62
        protected ExitedHandler $exitedHandler = new Middleware\Handler\ExitedHandler(),
63
    ) {
64
    }
65
66
    /**
67
     * @inheritDoc
68
     */
69
    #[Override]
70
    public function dispatch(Input $input): Output
71
    {
72
        // Attempt to match the command
73
        $matchedCommand = $this->attemptToMatchCommand($input);
74
75
        // If the command was not matched an output returned
76
        if ($matchedCommand instanceof Output) {
77
            // Dispatch RouteNotMatchedMiddleware
78
            return $this->commandNotMatchedHandler->commandNotMatched(
79
                input: $input,
80
                output: $matchedCommand
81
            );
82
        }
83
84
        return $this->dispatchCommand(
85
            input: $input,
86
            command: $matchedCommand
87
        );
88
    }
89
90
    /**
91
     * @inheritDoc
92
     */
93
    #[Override]
94
    public function dispatchCommand(Input $input, Command $command): Output
95
    {
96
        // The command has been matched
97
        $this->commandMatched($command);
98
99
        // Dispatch the RouteMatchedMiddleware
100
        $commandAfterMiddleware = $this->commandMatchedHandler->commandMatched(
101
            input: $input,
102
            command: $command
103
        );
104
105
        // If the return value after middleware is an output return it
106
        if ($commandAfterMiddleware instanceof Output) {
107
            return $commandAfterMiddleware;
108
        }
109
110
        // Set the command after middleware has potentially modified it in the service container
111
        $this->container->setSingleton(Command::class, $commandAfterMiddleware);
112
113
        $dispatch  = $commandAfterMiddleware->getDispatch();
114
        $arguments = $dispatch->getArguments();
115
116
        // Attempt to dispatch the route using any one of the callable options
117
        $output = $this->dispatcher->dispatch(
118
            dispatch: $dispatch,
119
            arguments: $arguments
120
        );
121
122
        if (! $output instanceof Output) {
123
            throw new RuntimeException('All commands must return an output');
124
        }
125
126
        return $this->commandDispatchedHandler->commandDispatched(
127
            input: $input,
128
            output: $output,
129
            command: $commandAfterMiddleware
130
        );
131
    }
132
133
    /**
134
     * Match a command, or a response if no command exists, from a given input.
135
     */
136
    protected function attemptToMatchCommand(Input $input): Command|Output
137
    {
138
        $commandName = $input->getCommandName();
139
140
        // Try to get the command
141
        $command = $this->collection->get(
142
            name: $commandName
143
        );
144
145
        // Return the command if it was found
146
        if ($command !== null) {
147
            if (
148
                $input->hasOption(HelpOptionParameter::NAME)
149
                || $input->hasOption(HelpOptionParameter::SHORT_NAME)
150
            ) {
151
                $command = $this->collection->get(name: HelpCommand::NAME);
152
                $input   = $input->withOptions(
153
                    new Option('command', $commandName),
154
                );
155
156
                if ($command === null) {
157
                    throw new RuntimeException('Help command does not exist');
158
                }
159
            }
160
161
            return $this->addParametersToCommand($input, $command);
162
        }
163
164
        $errorText = "Command `$commandName` was not found.";
165
166
        $output = $this->outputFactory
167
            ->createOutput(exitCode: ExitCode::ERROR)
168
            ->withMessages(
169
                new Banner(new ErrorMessage($errorText))
170
            );
171
172
        return $this->checkCommandNameForTypo($input, $output);
173
    }
174
175
    /**
176
     * Add the parameters from the input to the command.
177
     */
178
    protected function addParametersToCommand(Input $input, Command $command): Command
179
    {
180
        $command = $this->addArgumentsToCommand($input, $command);
181
182
        return $this->addOptionsToCommand($input, $command);
183
    }
184
185
    /**
186
     * Add the arguments from the input to the command.
187
     */
188
    protected function addArgumentsToCommand(Input $input, Command $command): Command
189
    {
190
        $arguments          = [...$input->getArguments()];
191
        $argumentParameters = $command->getArguments();
192
193
        foreach ($argumentParameters as $key => $argumentParameter) {
194
            $argumentParameterArguments = [];
195
196
            // Array arguments must be last, and will take up all the remaining arguments from the input
197
            if ($argumentParameter->getValueMode() === ArgumentValueMode::ARRAY) {
198
                $argumentParameterArguments = $arguments;
199
200
                $arguments = [];
201
            } elseif (isset($arguments[$key])) {
202
                // If not an array type then we should match each argument in order of appearance
203
                $argumentParameterArguments[] = $arguments[$key];
204
205
                unset($arguments[$key]);
206
            }
207
208
            $argumentParameters[$key] = $argumentParameter
209
                ->withArguments(...$argumentParameterArguments)
0 ignored issues
show
Bug introduced by
$argumentParameterArguments of type Valkyrja\Cli\Interaction...ent\Contract\Argument[] is incompatible with the type Valkyrja\Cli\Interaction...ument\Contract\Argument expected by parameter $arguments of Valkyrja\Cli\Routing\Dat...ameter::withArguments(). ( Ignorable by Annotation )

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

209
                ->withArguments(/** @scrutinizer ignore-type */ ...$argumentParameterArguments)
Loading history...
210
                ->validateValues();
211
        }
212
213
        return $command
214
            ->withArguments(...$argumentParameters);
215
    }
216
217
    /**
218
     * Add the options from the input to the command.
219
     */
220
    protected function addOptionsToCommand(Input $input, Command $command): Command
221
    {
222
        $options          = $input->getOptions();
223
        $optionParameters = [...$command->getOptions()];
224
225
        foreach ($optionParameters as $key => $optionParameter) {
226
            $optionParameterOptions = [];
227
228
            foreach ($options as $option) {
229
                // Add the option only if it matches the name or one of the short names
230
                if (
231
                    $optionParameter->getName() === $option->getName()
232
                    || in_array($option->getName(), $optionParameter->getShortNames(), true)
233
                ) {
234
                    $optionParameterOptions[] = $option;
235
                }
236
            }
237
238
            $optionParameters[$key] = $optionParameter
239
                ->withOptions(...$optionParameterOptions)
240
                ->validateValues();
241
        }
242
243
        return $command
244
            ->withOptions(...$optionParameters);
245
    }
246
247
    /**
248
     * Check the command name from the input for a typo.
249
     */
250
    protected function checkCommandNameForTypo(Input $input, Output $output): Command|Output
251
    {
252
        $name = $input->getCommandName();
253
254
        $commands = [];
255
256
        foreach ($this->collection->all() as $command) {
257
            similar_text($command->getName(), $name, $percent);
258
259
            if ($percent >= 60) {
260
                $commands[] = $command;
261
            }
262
        }
263
264
        if ($commands !== []) {
265
            return $this->askToRunSimilarCommands($output, $commands);
266
        }
267
268
        return $output;
269
    }
270
271
    /**
272
     * Ask the user if they want to run similar commands.
273
     *
274
     * @param Command[] $commands The list of commands
275
     */
276
    protected function askToRunSimilarCommands(Output $output, array $commands): Command|Output
277
    {
278
        $command = null;
279
280
        $commandNames = array_map(static fn (Command $command) => $command->getName(), $commands);
281
282
        $output = $output
283
            ->withAddedMessages(
284
                new NewLine(),
285
                new Question(
286
                    'Did you mean to run one of the following commands?',
287
                    function (Output $output, AnswerContract $answer) use (&$command, $commands): Output {
288
                        $response = $answer->getUserResponse();
289
                        $command  = $response !== 'no'
290
                            ? array_filter($commands, static fn (Command $command): bool => $command->getName() === $response)[0] ?? null
291
                            : null;
292
293
                        return $output;
294
                    },
295
                    new Answer(
296
                        defaultResponse: 'no',
297
                        allowedResponses: $commandNames
298
                    ),
299
                ),
300
            )
301
            ->writeMessages();
302
303
        return $command
304
            ?? $output;
305
    }
306
307
    /**
308
     * Do various stuff after the route has been matched.
309
     */
310
    protected function commandMatched(Command $command): void
311
    {
312
        $this->commandMatchedHandler->add(...$command->getCommandMatchedMiddleware());
313
        $this->commandDispatchedHandler->add(...$command->getCommandDispatchedMiddleware());
314
        $this->throwableCaughtHandler->add(...$command->getThrowableCaughtMiddleware());
315
        $this->exitedHandler->add(...$command->getExitedMiddleware());
316
317
        // Set the found command in the service container
318
        $this->container->setSingleton(Command::class, $command);
319
    }
320
}
321