Completed
Push — master ( 8e5a24...e91b96 )
by Mark
26s
created

CommandRunner::longestCommandName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
11
 * @link          http://cakephp.org CakePHP(tm) Project
12
 * @since         3.5.0
13
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Console;
16
17
use Cake\Command\HelpCommand;
18
use Cake\Command\VersionCommand;
19
use Cake\Console\CommandCollection;
20
use Cake\Console\CommandCollectionAwareInterface;
21
use Cake\Console\ConsoleIo;
22
use Cake\Console\Exception\StopException;
23
use Cake\Console\Shell;
24
use Cake\Core\ConsoleApplicationInterface;
25
use Cake\Core\HttpApplicationInterface;
26
use Cake\Core\PluginApplicationInterface;
27
use Cake\Event\EventDispatcherInterface;
28
use Cake\Event\EventDispatcherTrait;
29
use Cake\Event\EventManager;
30
use Cake\Routing\Router;
31
use Cake\Utility\Inflector;
32
use InvalidArgumentException;
33
use RuntimeException;
34
35
/**
36
 * Run CLI commands for the provided application.
37
 */
38
class CommandRunner implements EventDispatcherInterface
39
{
40
    /**
41
     * Alias methods away so we can implement proxying methods.
42
     */
43
    use EventDispatcherTrait {
44
        eventManager as private _eventManager;
45
        getEventManager as private _getEventManager;
46
        setEventManager as private _setEventManager;
47
    }
48
49
    /**
50
     * The application console commands are being run for.
51
     *
52
     * @var \Cake\Core\ConsoleApplicationInterface
53
     */
54
    protected $app;
55
56
    /**
57
     * The application console commands are being run for.
58
     *
59
     * @var \Cake\Console\CommandFactoryInterface
60
     */
61
    protected $factory;
62
63
    /**
64
     * The root command name. Defaults to `cake`.
65
     *
66
     * @var string
67
     */
68
    protected $root;
69
70
    /**
71
     * Alias mappings.
72
     *
73
     * @var array
74
     */
75
    protected $aliases = [];
76
77
    /**
78
     * Constructor
79
     *
80
     * @param \Cake\Core\ConsoleApplicationInterface $app The application to run CLI commands for.
81
     * @param string $root The root command name to be removed from argv.
82
     * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance.
83
     */
84
    public function __construct(ConsoleApplicationInterface $app, $root = 'cake', CommandFactoryInterface $factory = null)
85
    {
86
        $this->app = $app;
87
        $this->root = $root;
88
        $this->factory = $factory ?: new CommandFactory();
89
        $this->aliases = [
90
            '--version' => 'version',
91
            '--help' => 'help',
92
            '-h' => 'help',
93
        ];
94
    }
95
96
    /**
97
     * Replace the entire alias map for a runner.
98
     *
99
     * Aliases allow you to define alternate names for commands
100
     * in the collection. This can be useful to add top level switches
101
     * like `--version` or `-h`
102
     *
103
     * ### Usage
104
     *
105
     * ```
106
     * $runner->setAliases(['--version' => 'version']);
107
     * ```
108
     *
109
     * @param array $aliases The map of aliases to replace.
110
     * @return $this
111
     */
112
    public function setAliases(array $aliases)
113
    {
114
        $this->aliases = $aliases;
115
116
        return $this;
117
    }
118
119
    /**
120
     * Run the command contained in $argv.
121
     *
122
     * Use the application to do the following:
123
     *
124
     * - Bootstrap the application
125
     * - Create the CommandCollection using the console() hook on the application.
126
     * - Trigger the `Console.buildCommands` event of auto-wiring plugins.
127
     * - Run the requested command.
128
     *
129
     * @param array $argv The arguments from the CLI environment.
130
     * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing.
131
     * @return int The exit code of the command.
132
     * @throws \RuntimeException
133
     */
134
    public function run(array $argv, ConsoleIo $io = null)
135
    {
136
        $this->bootstrap();
137
138
        $commands = new CommandCollection([
139
            'version' => VersionCommand::class,
140
            'help' => HelpCommand::class,
141
        ]);
142
        $commands = $this->app->console($commands);
143
        $this->checkCollection($commands, 'console');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Console\CommandRunner::checkCollection() has been deprecated with message: 3.6.0 This method should be replaced with return types in 4.x

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
144
145
        if ($this->app instanceof PluginApplicationInterface) {
146
            $commands = $this->app->pluginConsole($commands);
147
        }
148
        $this->checkCollection($commands, 'pluginConsole');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Console\CommandRunner::checkCollection() has been deprecated with message: 3.6.0 This method should be replaced with return types in 4.x

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
149
        $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
150
        $this->loadRoutes();
151
152
        if (empty($argv)) {
153
            throw new RuntimeException("Cannot run any commands. No arguments received.");
154
        }
155
        // Remove the root executable segment
156
        array_shift($argv);
157
158
        $io = $io ?: new ConsoleIo();
159
160
        list($name, $argv) = $this->longestCommandName($commands, $argv);
161
        $name = $this->resolveName($commands, $io, $name);
162
163
        $result = Shell::CODE_ERROR;
164
        $shell = $this->getShell($io, $commands, $name);
165
        if ($shell instanceof Shell) {
166
            $result = $this->runShell($shell, $argv);
167
        }
168
        if ($shell instanceof Command) {
169
            $result = $shell->run($argv, $io);
170
        }
171
172
        if ($result === null || $result === true) {
173
            return Shell::CODE_SUCCESS;
174
        }
175
        if (is_int($result)) {
176
            return $result;
177
        }
178
179
        return Shell::CODE_ERROR;
180
    }
181
182
    /**
183
     * Application bootstrap wrapper.
184
     *
185
     * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`.
186
     * After the application is bootstrapped and events are attached, plugins are bootstrapped
187
     * and have their events attached.
188
     *
189
     * @return void
190
     */
191
    protected function bootstrap()
192
    {
193
        $this->app->bootstrap();
194
        if ($this->app instanceof PluginApplicationInterface) {
195
            $this->app->pluginBootstrap();
196
        }
197
    }
198
199
    /**
200
     * Check the created CommandCollection
201
     *
202
     * @param mixed $commands The CommandCollection to check, could be anything though.
203
     * @param string $method The method that was used.
204
     * @return void
205
     * @throws \RuntimeException
206
     * @deprecated 3.6.0 This method should be replaced with return types in 4.x
207
     */
208
    protected function checkCollection($commands, $method)
209
    {
210
        if (!($commands instanceof CommandCollection)) {
211
            $type = getTypeName($commands);
212
            throw new RuntimeException(
213
                "The application's `{$method}` method did not return a CommandCollection." .
214
                " Got '{$type}' instead."
215
            );
216
        }
217
    }
218
219
    /**
220
     * Get the application's event manager or the global one.
221
     *
222
     * @return \Cake\Event\EventManagerInterface
223
     */
224
    public function getEventManager()
225
    {
226
        if ($this->app instanceof PluginApplicationInterface) {
227
            return $this->app->getEventManager();
228
        }
229
230
        return EventManager::instance();
231
    }
232
233
    /**
234
     * Get/set the application's event manager.
235
     *
236
     * If the application does not support events and this method is used as
237
     * a setter, an exception will be raised.
238
     *
239
     * @param \Cake\Event\EventManager|null $events The event manager to set.
240
     * @return \Cake\Event\EventManager|$this
241
     * @deprecated 3.6.0 Will be removed in 4.0
242
     */
243 View Code Duplication
    public function eventManager(EventManager $events = null)
244
    {
245
        deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.');
246
        if ($events === null) {
247
            return $this->getEventManager();
248
        }
249
250
        return $this->setEventManager($events);
251
    }
252
253
    /**
254
     * Get/set the application's event manager.
255
     *
256
     * If the application does not support events and this method is used as
257
     * a setter, an exception will be raised.
258
     *
259
     * @param \Cake\Event\EventManager $events The event manager to set.
260
     * @return $this
261
     */
262 View Code Duplication
    public function setEventManager(EventManager $events)
263
    {
264
        if ($this->app instanceof PluginApplicationInterface) {
265
            $this->app->setEventManager($events);
266
267
            return $this;
268
        }
269
270
        throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
271
    }
272
273
    /**
274
     * Get the shell instance for a given command name
275
     *
276
     * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
277
     * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
278
     * @param string $name The command name to find
279
     * @return \Cake\Console\Shell|\Cake\Console\Command
280
     */
281
    protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
282
    {
283
        $instance = $commands->get($name);
284
        if (is_string($instance)) {
285
            $instance = $this->createShell($instance, $io);
286
        }
287
        if ($instance instanceof Shell) {
288
            $instance->setRootName($this->root);
289
        }
290
        if ($instance instanceof Command) {
291
            $instance->setName("{$this->root} {$name}");
292
        }
293
        if ($instance instanceof CommandCollectionAwareInterface) {
294
            $instance->setCommandCollection($commands);
295
        }
296
297
        return $instance;
298
    }
299
300
    /**
301
     * Build the longest command name that exists in the collection
302
     *
303
     * Build the longest command name that matches a
304
     * defined command. This will traverse a maximum of 3 tokens.
305
     *
306
     * @param \Cake\Console\CommandCollection $commands The command collection to check.
307
     * @param array $argv The CLI arguments.
308
     * @return array An array of the resolved name and modified argv.
309
     */
310
    protected function longestCommandName($commands, $argv)
311
    {
312
        for ($i = 3; $i > 1; $i--) {
313
            $parts = array_slice($argv, 0, $i);
314
            $name = implode(' ', $parts);
315
            if ($commands->has($name)) {
316
                return [$name, array_slice($argv, $i)];
317
            }
318
        }
319
        $name = array_shift($argv);
320
321
        return [$name, $argv];
322
    }
323
324
    /**
325
     * Resolve the command name into a name that exists in the collection.
326
     *
327
     * Apply backwards compatible inflections and aliases.
328
     * Will step forward up to 3 tokens in $argv to generate
329
     * a command name in the CommandCollection. More specific
330
     * command names take precedence over less specific ones.
331
     *
332
     * @param \Cake\Console\CommandCollection $commands The command collection to check.
333
     * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
334
     * @param string $name The name
335
     * @return string The resolved class name
336
     */
337
    protected function resolveName($commands, $io, $name)
338
    {
339
        if (!$name) {
340
            $io->err('<error>No command provided. Choose one of the available commands.</error>', 2);
341
            $name = 'help';
342
        }
343
        if (isset($this->aliases[$name])) {
344
            $name = $this->aliases[$name];
345
        }
346
        if (!$commands->has($name)) {
347
            $name = Inflector::underscore($name);
348
        }
349
        if (!$commands->has($name)) {
350
            throw new RuntimeException(
351
                "Unknown command `{$this->root} {$name}`." .
352
                " Run `{$this->root} --help` to get the list of valid commands."
353
            );
354
        }
355
356
        return $name;
357
    }
358
359
    /**
360
     * Execute a Shell class.
361
     *
362
     * @param \Cake\Console\Shell $shell The shell to run.
363
     * @param array $argv The CLI arguments to invoke.
364
     * @return int Exit code
365
     */
366
    protected function runShell(Shell $shell, array $argv)
367
    {
368
        try {
369
            $shell->initialize();
370
371
            return $shell->runCommand($argv, true);
372
        } catch (StopException $e) {
373
            return $e->getCode();
374
        }
375
    }
376
377
    /**
378
     * The wrapper for creating shell instances.
379
     *
380
     * @param string $className Shell class name.
381
     * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
382
     * @return \Cake\Console\Shell|\Cake\Console\Command
383
     */
384
    protected function createShell($className, ConsoleIo $io)
385
    {
386
        $shell = $this->factory->create($className);
387
        if ($shell instanceof Shell) {
388
            $shell->setIo($io);
389
        }
390
391
        return $shell;
392
    }
393
394
    /**
395
     * Ensure that the application's routes are loaded.
396
     *
397
     * Console commands and shells often need to generate URLs.
398
     *
399
     * @return void
400
     */
401 View Code Duplication
    protected function loadRoutes()
402
    {
403
        $builder = Router::createRouteBuilder('/');
404
405
        if ($this->app instanceof HttpApplicationInterface) {
406
            $this->app->routes($builder);
407
        }
408
        if ($this->app instanceof PluginApplicationInterface) {
409
            $this->app->pluginRoutes($builder);
410
        }
411
    }
412
}
413