Passed
Push — master ( 3968e4...9fbfbc )
by Melech
13:11 queued 11:31
created

checkCommandNameForTypo()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
nc 6
nop 2
dl 0
loc 19
rs 9.9666
c 1
b 0
f 0
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\Middleware\RouteNotMatched;
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\Input\Contract\InputContract;
18
use Valkyrja\Cli\Interaction\Message\Answer;
19
use Valkyrja\Cli\Interaction\Message\Contract\AnswerContract;
20
use Valkyrja\Cli\Interaction\Message\NewLine;
21
use Valkyrja\Cli\Interaction\Message\Question;
22
use Valkyrja\Cli\Interaction\Output\Contract\OutputContract;
23
use Valkyrja\Cli\Middleware\Contract\RouteNotMatchedMiddlewareContract;
24
use Valkyrja\Cli\Middleware\Handler\Contract\RouteNotMatchedHandlerContract;
25
use Valkyrja\Cli\Routing\Collection\Contract\CollectionContract;
26
use Valkyrja\Cli\Routing\Data\Contract\RouteContract;
27
use Valkyrja\Cli\Routing\Dispatcher\Contract\RouterContract;
28
29
use function array_filter;
30
use function array_key_first;
31
use function array_map;
32
use function similar_text;
33
34
class CheckCommandForTypoMiddleware implements RouteNotMatchedMiddlewareContract
35
{
36
    protected RouteContract|null $matchedRoute = null;
37
38
    public function __construct(
39
        protected RouterContract $router,
40
        protected CollectionContract $collection
41
    ) {
42
    }
43
44
    /**
45
     * @inheritDoc
46
     */
47
    #[Override]
48
    public function routeNotMatched(InputContract $input, OutputContract $output, RouteNotMatchedHandlerContract $handler): OutputContract
49
    {
50
        $routeOrOutput = $this->checkCommandNameForTypo($input, $output);
51
52
        if ($routeOrOutput instanceof RouteContract) {
53
            $output = $this->router->dispatch(
54
                input: $input->withCommandName($routeOrOutput->getName())
55
            );
56
        }
57
58
        return $handler->routeNotMatched($input, $output);
59
    }
60
61
    /**
62
     * Check the command name from the input for a typo.
63
     */
64
    protected function checkCommandNameForTypo(InputContract $input, OutputContract $output): RouteContract|OutputContract
65
    {
66
        $name = $input->getCommandName();
67
68
        $commands = [];
69
70
        foreach ($this->collection->all() as $command) {
71
            similar_text($command->getName(), $name, $percent);
72
73
            if ($percent >= 60) {
74
                $commands[] = $command;
75
            }
76
        }
77
78
        if ($commands !== []) {
79
            return $this->askToRunSimilarCommands($output, $commands);
80
        }
81
82
        return $output;
83
    }
84
85
    /**
86
     * Ask the user if they want to run similar commands.
87
     *
88
     * @param RouteContract[] $commands The list of commands
89
     */
90
    protected function askToRunSimilarCommands(OutputContract $output, array $commands): RouteContract|OutputContract
91
    {
92
        $commandNames = array_map(static fn (RouteContract $command) => $command->getName(), $commands);
93
94
        $output = $output
95
            ->withAddedMessages(
96
                new NewLine(),
97
                new Question(
98
                    'Did you mean to run one of the following commands?',
99
                    fn (OutputContract $output, AnswerContract $answer): OutputContract => $this->questionCallback(
100
                        output: $output,
101
                        answer: $answer,
102
                        commands: $commands
103
                    ),
104
                    new Answer(
105
                        defaultResponse: 'no',
106
                        allowedResponses: $commandNames
107
                    ),
108
                ),
109
            )
110
            ->writeMessages();
111
112
        return $this->matchedRoute
113
            ?? $output;
114
    }
115
116
    /**
117
     * @param RouteContract[] $commands The list of commands
118
     */
119
    protected function questionCallback(OutputContract $output, AnswerContract $answer, array $commands): OutputContract
120
    {
121
        $response           = $answer->getUserResponse();
122
        $this->matchedRoute = $response !== 'no'
123
            ? $this->getMatchedRoute(commands: $commands, response: $response)
124
            : null;
125
126
        return $output;
127
    }
128
129
    /**
130
     * @param RouteContract[] $commands The list of commands
131
     */
132
    protected function getMatchedRoute(array $commands, string $response): RouteContract|null
133
    {
134
        $matchedRoutes = array_filter(
135
            $commands,
136
            static fn (RouteContract $command): bool => $command->getName() === $response
137
        );
138
139
        if ($matchedRoutes !== []) {
140
            return $matchedRoutes[array_key_first($matchedRoutes)];
141
        }
142
143
        return null;
144
    }
145
}
146