Passed
Branch master (e894a3)
by Melech
15:39 queued 01:19
created

Console::addParsedCommand()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 12
nc 3
nop 2
dl 0
loc 25
rs 9.2222
c 0
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\Console;
15
16
use InvalidArgumentException;
17
use Valkyrja\Config\Constant\ConfigKeyPart;
18
use Valkyrja\Console\Contract\Console as Contract;
19
use Valkyrja\Console\Event\CommandDispatched;
20
use Valkyrja\Console\Event\CommandDispatching;
21
use Valkyrja\Console\Exception\CommandNotFound;
22
use Valkyrja\Console\Input\Contract\Input;
23
use Valkyrja\Console\Model\Contract\Command;
24
use Valkyrja\Console\Output\Contract\Output;
25
use Valkyrja\Console\Support\Provider;
26
use Valkyrja\Container\Contract\Container;
27
use Valkyrja\Dispatcher\Contract\Dispatcher;
28
use Valkyrja\Dispatcher\Exception\InvalidClosureException;
29
use Valkyrja\Dispatcher\Exception\InvalidDispatchCapabilityException;
30
use Valkyrja\Dispatcher\Exception\InvalidFunctionException;
31
use Valkyrja\Dispatcher\Exception\InvalidMethodException;
32
use Valkyrja\Dispatcher\Exception\InvalidPropertyException;
33
use Valkyrja\Dispatcher\Validator\Contract\Validator;
34
use Valkyrja\Event\Contract\Dispatcher as Events;
35
use Valkyrja\Path\Parser\Contract\Parser;
36
use Valkyrja\Support\Provider\ProvidersAwareTrait;
37
38
use function preg_match;
39
40
/**
41
 * Class Console.
42
 *
43
 * @author Melech Mizrachi
44
 *
45
 * @psalm-import-type ParsedPath from Parser
46
 *
47
 * @phpstan-import-type ParsedPath from Parser
48
 */
49
class Console implements Contract
50
{
51
    use ProvidersAwareTrait {
52
        ProvidersAwareTrait::register as traitRegister;
53
    }
54
55
    /**
56
     * The run method to call within command handlers.
57
     *
58
     * @var non-empty-string
59
     */
60
    public const string RUN_METHOD = 'run';
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 60 at column 24
Loading history...
61
62
    /**
63
     * The commands.
64
     *
65
     * @var array<string, Command>
66
     */
67
    protected static array $commands = [];
68
69
    /**
70
     * The command paths.
71
     *
72
     * @var array<non-empty-string, string>
73
     */
74
    protected static array $paths = [];
75
76
    /**
77
     * The commands by name.
78
     *
79
     * @var array<string, string>
80
     */
81
    protected static array $namedCommands = [];
82
83
    /**
84
     * Console constructor.
85
     */
86
    public function __construct(
87
        protected Container $container,
88
        protected Dispatcher $dispatcher,
89
        protected Validator $validator,
90
        protected Events $events,
91
        protected Parser $pathParser,
92
        protected Config $config,
93
        protected bool $debug = false
94
    ) {
95
    }
96
97
    /**
98
     * Add a new command.
99
     *
100
     * @param Command $command The command
101
     *
102
     * @throws InvalidDispatchCapabilityException
103
     * @throws InvalidFunctionException
104
     * @throws InvalidMethodException
105
     * @throws InvalidPropertyException
106
     * @throws InvalidClosureException
107
     *
108
     * @return void
109
     */
110
    public function addCommand(Command $command): void
111
    {
112
        $command->setMethod($command->getMethod() ?? static::RUN_METHOD);
113
114
        $this->validator->dispatch($command);
115
116
        $this->addParsedCommand($command, $this->pathParser->parse((string) $command->getPath()));
117
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122
    public function getCommand(string $name): Command|null
123
    {
124
        return $this->hasCommand($name)
125
            ? self::$commands[self::$namedCommands[$name]]
126
            : null;
127
    }
128
129
    /**
130
     * @inheritDoc
131
     */
132
    public function hasCommand(string $name): bool
133
    {
134
        return isset(self::$namedCommands[$name]);
135
    }
136
137
    /**
138
     * @inheritDoc
139
     */
140
    public function removeCommand(string $name): void
141
    {
142
        if ($this->hasCommand($name)) {
143
            unset(self::$commands[self::$namedCommands[$name]], self::$namedCommands[$name]);
144
        }
145
    }
146
147
    /**
148
     * @inheritDoc
149
     */
150
    public function inputCommand(Input $input): Command
151
    {
152
        return $this->matchCommand($input->getStringArguments());
153
    }
154
155
    /**
156
     * @inheritDoc
157
     */
158
    public function matchCommand(string $path): Command
159
    {
160
        // If the path matches a set command path
161
        if (isset(self::$commands[$path])) {
162
            return self::$commands[$path];
163
        }
164
165
        $command = null;
166
167
        // Otherwise iterate through the commands and attempt to match via regex
168
        foreach (self::$paths as $regex => $commandPath) {
169
            // If the preg match is successful, we've found our command!
170
            if (preg_match($regex, $path, $matches)) {
171
                // Check if this command is provided
172
                if ($this->isDeferred($commandPath)) {
173
                    // Initialize the provided command
174
                    $this->publishProvided($commandPath);
175
                }
176
177
                // Clone the command to avoid changing the one set in the master
178
                // array
179
                $command = clone self::$commands[$commandPath];
180
                // The first match is the path itself
181
                unset($matches[0]);
182
183
                // Set the matches
184
                $command->setMatches($matches);
185
186
                break;
187
            }
188
        }
189
190
        // If a command was not found
191
        if ($command === null) {
192
            // Throw a not found exception
193
            throw new CommandNotFound('The command ' . $path . ' not found.');
194
        }
195
196
        return $command;
197
    }
198
199
    /**
200
     * @inheritDoc
201
     */
202
    public function dispatch(Input $input, Output $output): int
203
    {
204
        $command = $this->inputCommand($input);
205
206
        if ($input->hasOption('-h') || $input->hasOption('--help')) {
207
            $command->setMethod('help');
208
        }
209
210
        if ($input->hasOption('-V') || $input->hasOption('--version')) {
211
            $command->setMethod('version');
212
        }
213
214
        return $this->dispatchCommand($command);
215
    }
216
217
    /**
218
     * @inheritDoc
219
     */
220
    public function dispatchCommand(Command $command): int
221
    {
222
        // Trigger an event before dispatching
223
        $this->events->dispatchByIdIfHasListeners(CommandDispatching::class, [$command]);
224
225
        // Dispatch the command
226
        /** @var int $exitCode */
227
        $exitCode = $this->dispatcher->dispatch($command, $command->getMatches());
228
229
        // Trigger an event after dispatching
230
        $this->events->dispatchByIdIfHasListeners(CommandDispatched::class, [$command, $exitCode]);
231
232
        return $exitCode;
233
    }
234
235
    /**
236
     * @inheritDoc
237
     */
238
    public function all(): array
239
    {
240
        // Iterate through all the command providers to set any deferred commands
241
        foreach ($this->deferred as $provided => $provider) {
242
            // Initialize the provided command
243
            $this->publishProvided($provided);
244
        }
245
246
        return self::$commands;
247
    }
248
249
    /**
250
     * @inheritDoc
251
     */
252
    public function set(Command ...$commands): void
253
    {
254
        self::$commands = [];
255
256
        foreach ($commands as $command) {
257
            $path = $command->getPath();
258
259
            if ($path === null || $path === '') {
260
                throw new InvalidArgumentException('Path must be valid');
261
            }
262
263
            self::$commands[$path] = $command;
264
        }
265
    }
266
267
    /**
268
     * @inheritDoc
269
     */
270
    public function getNamedCommands(): array
271
    {
272
        return self::$namedCommands;
273
    }
274
275
    /**
276
     * @inheritDoc
277
     */
278
    public function register(string $provider, bool $force = false): void
279
    {
280
        // Do the default registration of the service provider
281
        $this->traitRegister($provider, $force);
282
283
        /** @var class-string<Provider> $provider */
284
        // Get the commands names provided
285
        $commands = $provider::commands();
286
287
        // Iterate through the provided commands
288
        foreach ($provider::provides() as $key => $provided) {
289
            // Parse the provided path
290
            $parsedPath = $this->pathParser->parse($provided);
291
292
            // Set the path and regex in the paths list
293
            self::$paths[$parsedPath[ConfigKeyPart::REGEX]] = $provided;
294
            // Set the path and command in the named commands list
295
            self::$namedCommands[$commands[$key]] = $provided;
296
        }
297
    }
298
299
    /**
300
     * Add a parsed command.
301
     *
302
     * @param Command    $command       The command
303
     * @param ParsedPath $parsedCommand The parsed command
304
     *
305
     * @return void
306
     */
307
    protected function addParsedCommand(Command $command, array $parsedCommand): void
308
    {
309
        // Set the properties
310
        $command->setRegex($parsedCommand['regex']);
311
        $command->setParams($parsedCommand['params']);
312
        $command->setSegments($parsedCommand['segments']);
313
314
        $path  = $command->getPath();
315
        $regex = $command->getRegex();
316
317
        if ($path === null || $path === '' || $regex === null || $regex === '') {
318
            throw new InvalidArgumentException('Invalid command provided.');
319
        }
320
321
        // Set the command in the commands list
322
        self::$commands[$path] = $command;
323
        // Set the command in the commands paths list
324
        self::$paths[$regex] = $path;
325
326
        $name = $command->getName();
327
328
        // If the command has a name
329
        if ($name !== null) {
330
            // Set in the named commands list to find it more easily later
331
            self::$namedCommands[$name] = $path;
332
        }
333
    }
334
}
335