Completed
Push — master ( 33e43e...f3adf8 )
by Mark
07:54 queued 07:43
created

CommandRunner::runCommand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 0
loc 8
rs 10
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\Exception\StopException;
20
use Cake\Core\ConsoleApplicationInterface;
21
use Cake\Core\HttpApplicationInterface;
22
use Cake\Core\PluginApplicationInterface;
23
use Cake\Event\EventDispatcherInterface;
24
use Cake\Event\EventDispatcherTrait;
25
use Cake\Event\EventManager;
26
use Cake\Routing\Router;
27
use Cake\Utility\Inflector;
28
use InvalidArgumentException;
29
use RuntimeException;
30
31
/**
32
 * Run CLI commands for the provided application.
33
 */
34
class CommandRunner implements EventDispatcherInterface
35
{
36
    /**
37
     * Alias methods away so we can implement proxying methods.
38
     */
39
    use EventDispatcherTrait {
40
        eventManager as private _eventManager;
41
        getEventManager as private _getEventManager;
42
        setEventManager as private _setEventManager;
43
    }
44
45
    /**
46
     * The application console commands are being run for.
47
     *
48
     * @var \Cake\Core\ConsoleApplicationInterface
49
     */
50
    protected $app;
51
52
    /**
53
     * The application console commands are being run for.
54
     *
55
     * @var \Cake\Console\CommandFactoryInterface
56
     */
57
    protected $factory;
58
59
    /**
60
     * The root command name. Defaults to `cake`.
61
     *
62
     * @var string
63
     */
64
    protected $root;
65
66
    /**
67
     * Alias mappings.
68
     *
69
     * @var array
70
     */
71
    protected $aliases = [];
72
73
    /**
74
     * Constructor
75
     *
76
     * @param \Cake\Core\ConsoleApplicationInterface $app The application to run CLI commands for.
77
     * @param string $root The root command name to be removed from argv.
78
     * @param \Cake\Console\CommandFactoryInterface|null $factory Command factory instance.
79
     */
80
    public function __construct(ConsoleApplicationInterface $app, $root = 'cake', CommandFactoryInterface $factory = null)
81
    {
82
        $this->app = $app;
83
        $this->root = $root;
84
        $this->factory = $factory ?: new CommandFactory();
85
        $this->aliases = [
86
            '--version' => 'version',
87
            '--help' => 'help',
88
            '-h' => 'help',
89
        ];
90
    }
91
92
    /**
93
     * Replace the entire alias map for a runner.
94
     *
95
     * Aliases allow you to define alternate names for commands
96
     * in the collection. This can be useful to add top level switches
97
     * like `--version` or `-h`
98
     *
99
     * ### Usage
100
     *
101
     * ```
102
     * $runner->setAliases(['--version' => 'version']);
103
     * ```
104
     *
105
     * @param array $aliases The map of aliases to replace.
106
     * @return $this
107
     */
108
    public function setAliases(array $aliases)
109
    {
110
        $this->aliases = $aliases;
111
112
        return $this;
113
    }
114
115
    /**
116
     * Run the command contained in $argv.
117
     *
118
     * Use the application to do the following:
119
     *
120
     * - Bootstrap the application
121
     * - Create the CommandCollection using the console() hook on the application.
122
     * - Trigger the `Console.buildCommands` event of auto-wiring plugins.
123
     * - Run the requested command.
124
     *
125
     * @param array $argv The arguments from the CLI environment.
126
     * @param \Cake\Console\ConsoleIo $io The ConsoleIo instance. Used primarily for testing.
127
     * @return int The exit code of the command.
128
     * @throws \RuntimeException
129
     */
130
    public function run(array $argv, ConsoleIo $io = null)
131
    {
132
        $this->bootstrap();
133
134
        $commands = new CommandCollection([
135
            'version' => VersionCommand::class,
136
            'help' => HelpCommand::class,
137
        ]);
138
        $commands = $this->app->console($commands);
139
        $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...
140
141
        if ($this->app instanceof PluginApplicationInterface) {
142
            $commands = $this->app->pluginConsole($commands);
143
        }
144
        $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...
145
        $this->dispatchEvent('Console.buildCommands', ['commands' => $commands]);
146
        $this->loadRoutes();
147
148
        if (empty($argv)) {
149
            throw new RuntimeException("Cannot run any commands. No arguments received.");
150
        }
151
        // Remove the root executable segment
152
        array_shift($argv);
153
154
        $io = $io ?: new ConsoleIo();
155
156
        list($name, $argv) = $this->longestCommandName($commands, $argv);
157
        $name = $this->resolveName($commands, $io, $name);
158
159
        $result = Command::CODE_ERROR;
160
        $shell = $this->getShell($io, $commands, $name);
161
        if ($shell instanceof Shell) {
162
            $result = $this->runShell($shell, $argv);
163
        }
164
        if ($shell instanceof Command) {
165
            $result = $this->runCommand($shell, $argv, $io);
166
        }
167
168
        if ($result === null || $result === true) {
169
            return Command::CODE_SUCCESS;
170
        }
171
        if (is_int($result)) {
172
            return $result;
173
        }
174
175
        return Command::CODE_ERROR;
176
    }
177
178
    /**
179
     * Application bootstrap wrapper.
180
     *
181
     * Calls `bootstrap()` and `events()` if application implements `EventApplicationInterface`.
182
     * After the application is bootstrapped and events are attached, plugins are bootstrapped
183
     * and have their events attached.
184
     *
185
     * @return void
186
     */
187
    protected function bootstrap()
188
    {
189
        $this->app->bootstrap();
190
        if ($this->app instanceof PluginApplicationInterface) {
191
            $this->app->pluginBootstrap();
192
        }
193
    }
194
195
    /**
196
     * Check the created CommandCollection
197
     *
198
     * @param mixed $commands The CommandCollection to check, could be anything though.
199
     * @param string $method The method that was used.
200
     * @return void
201
     * @throws \RuntimeException
202
     * @deprecated 3.6.0 This method should be replaced with return types in 4.x
203
     */
204
    protected function checkCollection($commands, $method)
205
    {
206
        if (!($commands instanceof CommandCollection)) {
207
            $type = getTypeName($commands);
208
            throw new RuntimeException(
209
                "The application's `{$method}` method did not return a CommandCollection." .
210
                " Got '{$type}' instead."
211
            );
212
        }
213
    }
214
215
    /**
216
     * Get the application's event manager or the global one.
217
     *
218
     * @return \Cake\Event\EventManagerInterface
219
     */
220
    public function getEventManager()
221
    {
222
        if ($this->app instanceof PluginApplicationInterface) {
223
            return $this->app->getEventManager();
224
        }
225
226
        return EventManager::instance();
227
    }
228
229
    /**
230
     * Get/set the application's event manager.
231
     *
232
     * If the application does not support events and this method is used as
233
     * a setter, an exception will be raised.
234
     *
235
     * @param \Cake\Event\EventManager|null $events The event manager to set.
236
     * @return \Cake\Event\EventManager|$this
237
     * @deprecated 3.6.0 Will be removed in 4.0
238
     */
239 View Code Duplication
    public function eventManager(EventManager $events = null)
240
    {
241
        deprecationWarning('eventManager() is deprecated. Use getEventManager()/setEventManager() instead.');
242
        if ($events === null) {
243
            return $this->getEventManager();
244
        }
245
246
        return $this->setEventManager($events);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->setEventManager($events); (Cake\Console\CommandRunner) is incompatible with the return type declared by the interface Cake\Event\EventDispatcherInterface::eventManager of type Cake\Event\EventManager.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
247
    }
248
249
    /**
250
     * Get/set the application's event manager.
251
     *
252
     * If the application does not support events and this method is used as
253
     * a setter, an exception will be raised.
254
     *
255
     * @param \Cake\Event\EventManager $events The event manager to set.
256
     * @return $this
257
     */
258 View Code Duplication
    public function setEventManager(EventManager $events)
259
    {
260
        if ($this->app instanceof PluginApplicationInterface) {
261
            $this->app->setEventManager($events);
262
263
            return $this;
264
        }
265
266
        throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
267
    }
268
269
    /**
270
     * Get the shell instance for a given command name
271
     *
272
     * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
273
     * @param \Cake\Console\CommandCollection $commands The command collection to find the shell in.
274
     * @param string $name The command name to find
275
     * @return \Cake\Console\Shell|\Cake\Console\Command
276
     */
277
    protected function getShell(ConsoleIo $io, CommandCollection $commands, $name)
278
    {
279
        $instance = $commands->get($name);
280
        if (is_string($instance)) {
281
            $instance = $this->createShell($instance, $io);
282
        }
283
        if ($instance instanceof Shell) {
284
            $instance->setRootName($this->root);
285
        }
286
        if ($instance instanceof Command) {
287
            $instance->setName("{$this->root} {$name}");
288
        }
289
        if ($instance instanceof CommandCollectionAwareInterface) {
290
            $instance->setCommandCollection($commands);
291
        }
292
293
        return $instance;
294
    }
295
296
    /**
297
     * Build the longest command name that exists in the collection
298
     *
299
     * Build the longest command name that matches a
300
     * defined command. This will traverse a maximum of 3 tokens.
301
     *
302
     * @param \Cake\Console\CommandCollection $commands The command collection to check.
303
     * @param array $argv The CLI arguments.
304
     * @return array An array of the resolved name and modified argv.
305
     */
306
    protected function longestCommandName($commands, $argv)
307
    {
308
        for ($i = 3; $i > 1; $i--) {
309
            $parts = array_slice($argv, 0, $i);
310
            $name = implode(' ', $parts);
311
            if ($commands->has($name)) {
312
                return [$name, array_slice($argv, $i)];
313
            }
314
        }
315
        $name = array_shift($argv);
316
317
        return [$name, $argv];
318
    }
319
320
    /**
321
     * Resolve the command name into a name that exists in the collection.
322
     *
323
     * Apply backwards compatible inflections and aliases.
324
     * Will step forward up to 3 tokens in $argv to generate
325
     * a command name in the CommandCollection. More specific
326
     * command names take precedence over less specific ones.
327
     *
328
     * @param \Cake\Console\CommandCollection $commands The command collection to check.
329
     * @param \Cake\Console\ConsoleIo $io ConsoleIo object for errors.
330
     * @param string $name The name
331
     * @return string The resolved class name
332
     */
333
    protected function resolveName($commands, $io, $name)
334
    {
335
        if (!$name) {
336
            $io->err('<error>No command provided. Choose one of the available commands.</error>', 2);
337
            $name = 'help';
338
        }
339
        if (isset($this->aliases[$name])) {
340
            $name = $this->aliases[$name];
341
        }
342
        if (!$commands->has($name)) {
343
            $name = Inflector::underscore($name);
344
        }
345
        if (!$commands->has($name)) {
346
            throw new RuntimeException(
347
                "Unknown command `{$this->root} {$name}`." .
348
                " Run `{$this->root} --help` to get the list of valid commands."
349
            );
350
        }
351
352
        return $name;
353
    }
354
355
    /**
356
     * Execute a Command class.
357
     *
358
     * @param \Cake\Console\Command $command The command to run.
359
     * @param array $argv The CLI arguments to invoke.
360
     * @param \Cake\Console\ConsoleIo $io The console io
361
     * @return int Exit code
362
     */
363
    protected function runCommand(Command $command, array $argv, ConsoleIo $io)
364
    {
365
        try {
366
            return $command->run($argv, $io);
367
        } catch (StopException $e) {
368
            return $e->getCode();
369
        }
370
    }
371
372
    /**
373
     * Execute a Shell class.
374
     *
375
     * @param \Cake\Console\Shell $shell The shell to run.
376
     * @param array $argv The CLI arguments to invoke.
377
     * @return int Exit code
378
     */
379
    protected function runShell(Shell $shell, array $argv)
380
    {
381
        try {
382
            $shell->initialize();
383
384
            return $shell->runCommand($argv, true);
0 ignored issues
show
Bug Compatibility introduced by
The expression $shell->runCommand($argv, true); of type integer|boolean|null adds the type boolean to the return on line 384 which is incompatible with the return type documented by Cake\Console\CommandRunner::runShell of type integer.
Loading history...
385
        } catch (StopException $e) {
386
            return $e->getCode();
387
        }
388
    }
389
390
    /**
391
     * The wrapper for creating shell instances.
392
     *
393
     * @param string $className Shell class name.
394
     * @param \Cake\Console\ConsoleIo $io The IO wrapper for the created shell class.
395
     * @return \Cake\Console\Shell|\Cake\Console\Command
396
     */
397
    protected function createShell($className, ConsoleIo $io)
398
    {
399
        $shell = $this->factory->create($className);
400
        if ($shell instanceof Shell) {
401
            $shell->setIo($io);
402
        }
403
404
        return $shell;
405
    }
406
407
    /**
408
     * Ensure that the application's routes are loaded.
409
     *
410
     * Console commands and shells often need to generate URLs.
411
     *
412
     * @return void
413
     */
414 View Code Duplication
    protected function loadRoutes()
415
    {
416
        $builder = Router::createRouteBuilder('/');
417
418
        if ($this->app instanceof HttpApplicationInterface) {
419
            $this->app->routes($builder);
420
        }
421
        if ($this->app instanceof PluginApplicationInterface) {
422
            $this->app->pluginRoutes($builder);
423
        }
424
    }
425
}
426