1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 2.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Console;
16:
17: use Cake\Console\Exception\MissingShellException;
18: use Cake\Console\Exception\StopException;
19: use Cake\Core\App;
20: use Cake\Core\Configure;
21: use Cake\Core\Exception\Exception;
22: use Cake\Core\Plugin;
23: use Cake\Log\Log;
24: use Cake\Shell\Task\CommandTask;
25: use Cake\Utility\Inflector;
26:
27: /**
28: * Shell dispatcher handles dispatching cli commands.
29: *
30: * Consult /bin/cake.php for how this class is used in practice.
31: */
32: class ShellDispatcher
33: {
34:
35: /**
36: * Contains arguments parsed from the command line.
37: *
38: * @var array
39: */
40: public $args = [];
41:
42: /**
43: * List of connected aliases.
44: *
45: * @var array
46: */
47: protected static $_aliases = [];
48:
49: /**
50: * Constructor
51: *
52: * The execution of the script is stopped after dispatching the request with
53: * a status code of either 0 or 1 according to the result of the dispatch.
54: *
55: * @param array $args the argv from PHP
56: * @param bool $bootstrap Should the environment be bootstrapped.
57: */
58: public function __construct($args = [], $bootstrap = true)
59: {
60: set_time_limit(0);
61: $this->args = (array)$args;
62:
63: $this->addShortPluginAliases();
64:
65: if ($bootstrap) {
66: $this->_initEnvironment();
67: }
68: }
69:
70: /**
71: * Add an alias for a shell command.
72: *
73: * Aliases allow you to call shells by alternate names. This is most
74: * useful when dealing with plugin shells that you want to have shorter
75: * names for.
76: *
77: * If you re-use an alias the last alias set will be the one available.
78: *
79: * ### Usage
80: *
81: * Aliasing a shell named ClassName:
82: *
83: * ```
84: * $this->alias('alias', 'ClassName');
85: * ```
86: *
87: * Getting the original name for a given alias:
88: *
89: * ```
90: * $this->alias('alias');
91: * ```
92: *
93: * @param string $short The new short name for the shell.
94: * @param string|null $original The original full name for the shell.
95: * @return string|false The aliased class name, or false if the alias does not exist
96: */
97: public static function alias($short, $original = null)
98: {
99: $short = Inflector::camelize($short);
100: if ($original) {
101: static::$_aliases[$short] = $original;
102: }
103:
104: return isset(static::$_aliases[$short]) ? static::$_aliases[$short] : false;
105: }
106:
107: /**
108: * Clear any aliases that have been set.
109: *
110: * @return void
111: */
112: public static function resetAliases()
113: {
114: static::$_aliases = [];
115: }
116:
117: /**
118: * Run the dispatcher
119: *
120: * @param array $argv The argv from PHP
121: * @param array $extra Extra parameters
122: * @return int The exit code of the shell process.
123: */
124: public static function run($argv, $extra = [])
125: {
126: $dispatcher = new ShellDispatcher($argv);
127:
128: return $dispatcher->dispatch($extra);
129: }
130:
131: /**
132: * Defines current working environment.
133: *
134: * @return void
135: * @throws \Cake\Core\Exception\Exception
136: */
137: protected function _initEnvironment()
138: {
139: if (!$this->_bootstrap()) {
140: $message = "Unable to load CakePHP core.\nMake sure Cake exists in " . CAKE_CORE_INCLUDE_PATH;
141: throw new Exception($message);
142: }
143:
144: if (function_exists('ini_set')) {
145: ini_set('html_errors', '0');
146: ini_set('implicit_flush', '1');
147: ini_set('max_execution_time', '0');
148: }
149:
150: $this->shiftArgs();
151: }
152:
153: /**
154: * Initializes the environment and loads the CakePHP core.
155: *
156: * @return bool Success.
157: */
158: protected function _bootstrap()
159: {
160: if (!Configure::read('App.fullBaseUrl')) {
161: Configure::write('App.fullBaseUrl', 'http://localhost');
162: }
163:
164: return true;
165: }
166:
167: /**
168: * Dispatches a CLI request
169: *
170: * Converts a shell command result into an exit code. Null/True
171: * are treated as success. All other return values are an error.
172: *
173: * @param array $extra Extra parameters that you can manually pass to the Shell
174: * to be dispatched.
175: * Built-in extra parameter is :
176: * - `requested` : if used, will prevent the Shell welcome message to be displayed
177: * @return int The cli command exit code. 0 is success.
178: */
179: public function dispatch($extra = [])
180: {
181: try {
182: $result = $this->_dispatch($extra);
183: } catch (StopException $e) {
184: return $e->getCode();
185: }
186: if ($result === null || $result === true) {
187: return Shell::CODE_SUCCESS;
188: }
189: if (is_int($result)) {
190: return $result;
191: }
192:
193: return Shell::CODE_ERROR;
194: }
195:
196: /**
197: * Dispatch a request.
198: *
199: * @param array $extra Extra parameters that you can manually pass to the Shell
200: * to be dispatched.
201: * Built-in extra parameter is :
202: * - `requested` : if used, will prevent the Shell welcome message to be displayed
203: * @return bool|int|null
204: * @throws \Cake\Console\Exception\MissingShellMethodException
205: */
206: protected function _dispatch($extra = [])
207: {
208: $shell = $this->shiftArgs();
209:
210: if (!$shell) {
211: $this->help();
212:
213: return false;
214: }
215: if (in_array($shell, ['help', '--help', '-h'])) {
216: $this->help();
217:
218: return true;
219: }
220: if (in_array($shell, ['version', '--version'])) {
221: $this->version();
222:
223: return true;
224: }
225:
226: $Shell = $this->findShell($shell);
227:
228: $Shell->initialize();
229:
230: return $Shell->runCommand($this->args, true, $extra);
231: }
232:
233: /**
234: * For all loaded plugins, add a short alias
235: *
236: * This permits a plugin which implements a shell of the same name to be accessed
237: * Using the shell name alone
238: *
239: * @return array the resultant list of aliases
240: */
241: public function addShortPluginAliases()
242: {
243: $plugins = Plugin::loaded();
244:
245: $io = new ConsoleIo();
246: $task = new CommandTask($io);
247: $io->setLoggers(false);
248: $list = $task->getShellList() + ['app' => []];
249: $fixed = array_flip($list['app']) + array_flip($list['CORE']);
250: $aliases = $others = [];
251:
252: foreach ($plugins as $plugin) {
253: if (!isset($list[$plugin])) {
254: continue;
255: }
256:
257: foreach ($list[$plugin] as $shell) {
258: $aliases += [$shell => $plugin];
259: if (!isset($others[$shell])) {
260: $others[$shell] = [$plugin];
261: } else {
262: $others[$shell] = array_merge($others[$shell], [$plugin]);
263: }
264: }
265: }
266:
267: foreach ($aliases as $shell => $plugin) {
268: if (isset($fixed[$shell])) {
269: Log::write(
270: 'debug',
271: "command '$shell' in plugin '$plugin' was not aliased, conflicts with another shell",
272: ['shell-dispatcher']
273: );
274: continue;
275: }
276:
277: $other = static::alias($shell);
278: if ($other) {
279: $other = $aliases[$shell];
280: if ($other !== $plugin) {
281: Log::write(
282: 'debug',
283: "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$other'",
284: ['shell-dispatcher']
285: );
286: }
287: continue;
288: }
289:
290: if (isset($others[$shell])) {
291: $conflicts = array_diff($others[$shell], [$plugin]);
292: if (count($conflicts) > 0) {
293: $conflictList = implode("', '", $conflicts);
294: Log::write(
295: 'debug',
296: "command '$shell' in plugin '$plugin' was not aliased, conflicts with '$conflictList'",
297: ['shell-dispatcher']
298: );
299: }
300: }
301:
302: static::alias($shell, "$plugin.$shell");
303: }
304:
305: return static::$_aliases;
306: }
307:
308: /**
309: * Get shell to use, either plugin shell or application shell
310: *
311: * All paths in the loaded shell paths are searched, handles alias
312: * dereferencing
313: *
314: * @param string $shell Optionally the name of a plugin
315: * @return \Cake\Console\Shell A shell instance.
316: * @throws \Cake\Console\Exception\MissingShellException when errors are encountered.
317: */
318: public function findShell($shell)
319: {
320: $className = $this->_shellExists($shell);
321: if (!$className) {
322: $shell = $this->_handleAlias($shell);
323: $className = $this->_shellExists($shell);
324: }
325:
326: if (!$className) {
327: throw new MissingShellException([
328: 'class' => $shell,
329: ]);
330: }
331:
332: return $this->_createShell($className, $shell);
333: }
334:
335: /**
336: * If the input matches an alias, return the aliased shell name
337: *
338: * @param string $shell Optionally the name of a plugin or alias
339: * @return string Shell name with plugin prefix
340: */
341: protected function _handleAlias($shell)
342: {
343: $aliased = static::alias($shell);
344: if ($aliased) {
345: $shell = $aliased;
346: }
347:
348: $class = array_map('Cake\Utility\Inflector::camelize', explode('.', $shell));
349:
350: return implode('.', $class);
351: }
352:
353: /**
354: * Check if a shell class exists for the given name.
355: *
356: * @param string $shell The shell name to look for.
357: * @return string|bool Either the classname or false.
358: */
359: protected function _shellExists($shell)
360: {
361: $class = App::className($shell, 'Shell', 'Shell');
362: if (class_exists($class)) {
363: return $class;
364: }
365:
366: return false;
367: }
368:
369: /**
370: * Create the given shell name, and set the plugin property
371: *
372: * @param string $className The class name to instantiate
373: * @param string $shortName The plugin-prefixed shell name
374: * @return \Cake\Console\Shell A shell instance.
375: */
376: protected function _createShell($className, $shortName)
377: {
378: list($plugin) = pluginSplit($shortName);
379: $instance = new $className();
380: $instance->plugin = trim($plugin, '.');
381:
382: return $instance;
383: }
384:
385: /**
386: * Removes first argument and shifts other arguments up
387: *
388: * @return mixed Null if there are no arguments otherwise the shifted argument
389: */
390: public function shiftArgs()
391: {
392: return array_shift($this->args);
393: }
394:
395: /**
396: * Shows console help. Performs an internal dispatch to the CommandList Shell
397: *
398: * @return void
399: */
400: public function help()
401: {
402: $this->args = array_merge(['command_list'], $this->args);
403: $this->dispatch();
404: }
405:
406: /**
407: * Prints the currently installed version of CakePHP. Performs an internal dispatch to the CommandList Shell
408: *
409: * @return void
410: */
411: public function version()
412: {
413: $this->args = array_merge(['command_list', '--version'], $this->args);
414: $this->dispatch();
415: }
416: }
417: