Issues (1474)

framework/Shell/TShellApplication.php (11 issues)

1
<?php
2
3
/**
4
 * TShellApplication class file
5
 *
6
 * @author Qiang Xue <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Shell;
12
13
use Prado\Prado;
14
use Prado\Shell\Actions\TActiveRecordAction;
15
use Prado\Shell\Actions\THelpAction;
16
use Prado\Shell\Actions\TFlushCachesAction;
17
use Prado\Shell\Actions\TPhpShellAction;
18
use Prado\Shell\Actions\TWebServerAction;
19
use Prado\IO\ITextWriter;
20
use Prado\IO\TStdOutWriter;
21
use Prado\Shell\TShellWriter;
22
use Prado\TPropertyValue;
23
24
/**
25
 * TShellApplication class.
26
 *
27
 * TShellApplication is the base class for developing command-line PRADO
28
 * tools that share the same configurations as their Web application counterparts.
29
 *
30
 * A typical usage of TShellApplication in a command-line PHP script is as follows:
31
 * ```php
32
 * require 'path/to/vendor/autoload.php';
33
 * $application=new TShellApplication('path/to/application.xml');
34
 * $application->run($_SERVER);
35
 * // perform command-line tasks here
36
 * ```
37
 *
38
 * Since the application instance has access to all configurations, including
39
 * path aliases, modules and parameters, the command-line script has nearly the same
40
 * accessibility to resources as the PRADO Web applications.
41
 *
42
 * @author Qiang Xue <[email protected]>
43
 * @author Brad Anderson <[email protected]> shell refactor
44
 * @since 3.1.0
45
 */
46
class TShellApplication extends \Prado\TApplication
47
{
48
	/** @var bool tells the application to be in quiet mode, levels [0..1], default 0, */
49
	private $_quietMode = 0;
50
51
	/**
52
	 * @var array<\Prado\Shell\TShellAction> cli shell Application commands. Modules can add their own command
53
	 */
54
	private $_actions = [];
55
56
	/**
57
	 * @var TShellWriter output writer.
58
	 */
59
	protected $_outWriter;
60
61
	/**
62
	 * @var array<string, callable> application command options and property set callable
63
	 */
64
	protected $_options = [];
65
66
	/**
67
	 * @var array<string, string> application command optionAliases of the short letter(s) and option name
68
	 */
69
	protected $_optionAliases = [];
70
71
	/**
72
	 * @var array<array> The option help text and help values
73
	 */
74
	protected $_optionsData = [];
75
76
	/**
77
	 * @var bool is the application help printed
78
	 */
79
	protected $_helpPrinted = false;
80
81
	/**
82
	 * @var string[] arguments to the application
83
	 */
84
	private $_arguments;
85
86
	/**
87
	 * Runs the application.
88
	 * This method overrides the parent implementation by initializing
89
	 * application with configurations specified when it is created.
90
	 * @param null|array<string> $args
91
	 */
92
	public function run($args = null)
93
	{
94
		array_shift($args);
0 ignored issues
show
It seems like $args can also be of type null; however, parameter $array of array_shift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

94
		array_shift(/** @scrutinizer ignore-type */ $args);
Loading history...
95
		$this->_arguments = $args;
96
		$this->detectShellLanguageCharset();
97
98
		$this->_outWriter = new TShellWriter(new TStdOutWriter());
99
100
		$this->registerOption('quiet', [$this, 'setQuietMode'], 'Quiets the output to <level> [1..3], default 1 (when specified)', '=<level>');
101
		$this->registerOptionAlias('q', 'quiet');
102
103
		$this->attachEventHandler('onInitComplete', [$this, 'processArguments'], 20);
0 ignored issues
show
20 of type integer is incompatible with the type Prado\numeric|null expected by parameter $priority of Prado\TComponent::attachEventHandler(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
		$this->attachEventHandler('onInitComplete', [$this, 'processArguments'], /** @scrutinizer ignore-type */ 20);
Loading history...
104
105
		parent::run();
106
	}
107
108
	/**
109
	 * This takes the shell LANG and sets the HTTP_ACCEPT_LANGUAGE/HTTP_ACCEPT_CHARSET
110
	 * for the application to do I18N.
111
	 * @since 4.2.0
112
	 */
113
	private function detectShellLanguageCharset()
114
	{
115
		if (isset($_SERVER['LANG'])) {
116
			$lang = $_SERVER['LANG'];
117
			$pos = strpos($lang, '.');
118
			if ($pos !== false) {
119
				$_SERVER['HTTP_ACCEPT_CHARSET'] = substr($lang, $pos + 1);
120
				$lang = substr($lang, 0, $pos);
121
			}
122
			$_SERVER['HTTP_ACCEPT_LANGUAGE'] = $lang;
123
		}
124
	}
125
126
	/**
127
	 * This checks if shell environment is from a system CronTab.
128
	 * @return bool is the shell environment in crontab
129
	 * @since 4.2.2
130
	 */
131
	public static function detectCronTabShell()
132
	{
133
		return php_sapi_name() == 'cli' && (!($term = getenv('TERM')) || $term == 'unknown');
134
	}
135
136
	/**
137
	 * This processes the arguments entered into the cli.  This is processed after
138
	 * the application is initialized and modules can
139
	 * @param object $sender
140
	 * @param mixed $param
141
	 * @since 4.2.0
142
	 */
143
	public function processArguments($sender, $param)
144
	{
145
		$this->installShellActions();
146
147
		$options = $this->_options;
148
		$aliases = $this->_optionAliases;
149
		$skip = false;
0 ignored issues
show
The assignment to $skip is dead and can be removed.
Loading history...
150
		foreach ($this->_arguments as $i => $arg) {
151
			$arg = explode('=', $arg, 2);
152
			$processed = false;
153
			foreach ($options as $option => $setMethod) {
154
				$option = '--' . $option;
155
				if ($arg[0] === $option) {
156
					call_user_func($setMethod, $arg[1] ?? '');
157
					unset($this->_arguments[$i]);
158
					break;
159
				}
160
			}
161
			if (!$processed) {
162
				foreach ($aliases as $alias => $_option) {
163
					$alias = '-' . $alias;
164
					if (isset($options[$_option]) && $arg[0] === $alias) {
165
						call_user_func($options[$_option], $arg[1] ?? '');
166
						unset($this->_arguments[$i]);
167
						break;
168
					}
169
				}
170
			}
171
		}
172
		$this->_arguments = array_values($this->_arguments);
173
	}
174
175
	/**
176
	 * Installs the shell actions.
177
	 * @since 4.3.0
178
	 */
179
	public function installShellActions()
180
	{
181
		$this->addShellActionClass(TFlushCachesAction::class);
182
		$this->addShellActionClass(THelpAction::class);
183
		$this->addShellActionClass(TPhpShellAction::class);
184
		$this->addShellActionClass(TActiveRecordAction::class);
185
186
		$app = Prado::getApplication();
187
		if ($app->getMode() === \Prado\TApplicationMode::Debug || TPropertyValue::ensureBoolean($app->getParameters()[TWebServerAction::DEV_WEBSERVER_PARAM])) {
188
			$this->addShellActionClass(TWebServerAction::class);
189
		}
190
	}
191
192
	/**
193
	 * Runs the requested service.
194
	 * @since 4.2.0
195
	 */
196
	public function runService()
197
	{
198
		$args = $this->_arguments;
199
200
		$outWriter = $this->_outWriter;
201
		$valid = false;
202
203
		$this->printGreeting($outWriter);
204
		foreach ($this->_actions as $class => $action) {
205
			if (($method = $action->isValidAction($args)) !== null) {
206
				$action->setWriter($outWriter);
207
				$this->processActionArguments($args, $action, $method);
208
				$m = 'action' . str_replace('-', '', $method);
209
				if (method_exists($action, $m)) {
210
					$valid |= call_user_func([$action, $m], $args);
211
				} else {
212
					$outWriter->writeError("$method is not an available command");
213
					$valid = true;
214
				}
215
				break;
216
			}
217
		}
218
		if (!$valid && $this->_quietMode === 0) {
0 ignored issues
show
The condition $this->_quietMode === 0 is always false.
Loading history...
219
			$this->printHelp($outWriter);
220
		}
221
	}
222
223
	/**
224
	 * This processes the arguments entered into the cli
225
	 * @param array $args
226
	 * @param TShellAction $action
227
	 * @param string $method
228
	 * @since 4.2.0
229
	 */
230
	public function processActionArguments(&$args, $action, $method)
231
	{
232
		$options = $action->options($method);
233
		$aliases = $action->optionAliases();
234
		$skip = false;
0 ignored issues
show
The assignment to $skip is dead and can be removed.
Loading history...
235
		if (!$options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
$options is an empty array, thus ! $options is always true.
Loading history...
236
			return;
237
		}
238
		$keys = array_flip($options);
239
		foreach ($args as $i => $arg) {
240
			$arg = explode('=', $arg);
241
			$processed = false;
242
			foreach ($options as $_option) {
243
				$option = '--' . $_option;
244
				if ($arg[0] === $option) {
245
					$action->$_option = $arg[1] ?? '';
246
					$processed = true;
247
					unset($args[$i]);
248
					break;
249
				}
250
			}
251
			if (!$processed) {
252
				foreach ($aliases as $alias => $_option) {
253
					$alias = '-' . $alias;
254
					if (isset($keys[$_option]) && $arg[0] === $alias) {
255
						$action->$_option = $arg[1] ?? '';
256
						unset($args[$i]);
257
						break;
258
					}
259
				}
260
			}
261
		}
262
		$args = array_values($args);
263
	}
264
265
266
	/**
267
	 * Flushes output to shell.
268
	 * @param bool $continueBuffering whether to continue buffering after flush if buffering was active
269
	 * @since 4.2.0
270
	 */
271
	public function flushOutput($continueBuffering = true)
272
	{
273
		$this->_outWriter->flush();
274
		if (!$continueBuffering) {
275
			$this->_outWriter = null;
276
		}
277
	}
278
279
	/**
280
	 * @param string $class action class name
281
	 * @since 4.2.0
282
	 */
283
	public function addShellActionClass($class)
284
	{
285
		$this->_actions[is_array($class) ? $class['class'] : $class] = Prado::createComponent($class);
0 ignored issues
show
The condition is_array($class) is always false.
Loading history...
286
	}
287
288
	/**
289
	 * @return \Prado\Shell\TShellAction[] the shell actions for the application
290
	 * @since 4.2.0
291
	 */
292
	public function getShellActions()
293
	{
294
		return $this->_actions;
295
	}
296
297
298
	/**
299
	 * This registers shell command line options and the setter callback
300
	 * @param string $name name of the option at the command line
301
	 * @param callable $setCallback the callback to set the property
302
	 * @param string $description Short description of the option
303
	 * @param string $values value after the option, eg "=<level>"
304
	 * @since 4.2.0
305
	 */
306
	public function registerOption($name, $setCallback, $description = '', $values = '')
307
	{
308
		$this->_options[$name] = $setCallback;
309
		$this->_optionsData[$name] = [TPropertyValue::ensureString($description), TPropertyValue::ensureString($values)];
310
	}
311
312
313
	/**
314
	 * This registers shell command line option aliases and linked variable
315
	 * @param string $alias the short command
316
	 * @param string $name the command name
317
	 * @since 4.2.0
318
	 */
319
	public function registerOptionAlias($alias, $name)
320
	{
321
		$this->_optionAliases[$alias] = $name;
322
	}
323
324
	/**
325
	 * @return \Prado\Shell\TShellWriter the writer for the class
326
	 * @since 4.2.0
327
	 */
328
	public function getWriter(): TShellWriter
329
	{
330
		return $this->_outWriter;
331
	}
332
333
	/**
334
	 * @param \Prado\Shell\TShellWriter $writer the writer for the class
335
	 * @since 4.2.0
336
	 */
337
	public function setWriter(TShellWriter $writer)
338
	{
339
		$this->_outWriter = $writer;
340
	}
341
342
	/**
343
	 * @return int the writer for the class, default 0
344
	 * @since 4.2.0
345
	 */
346
	public function getQuietMode(): int
347
	{
348
		return $this->_quietMode;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_quietMode returns the type boolean which is incompatible with the type-hinted return integer.
Loading history...
349
	}
350
351
	/**
352
	 * @param int $quietMode the writer for the class, [0..3]
353
	 * @since 4.2.0
354
	 */
355
	public function setQuietMode($quietMode)
356
	{
357
		$this->_quietMode = ($quietMode === '' ? 1 : min(max((int) $quietMode, 0), 3));
0 ignored issues
show
The condition $quietMode === '' is always false.
Loading history...
358
	}
359
360
361
	/**
362
	 * @param mixed $outWriter
363
	 * @since 4.2.0
364
	 */
365
	public function printGreeting($outWriter)
366
	{
367
		if (!$this->_helpPrinted && $this->_quietMode === 0) {
0 ignored issues
show
The condition $this->_quietMode === 0 is always false.
Loading history...
368
			$outWriter->write("  Command line tools for Prado " . Prado::getVersion() . ".", TShellWriter::DARK_GRAY);
369
			$outWriter->writeLine();
370
			$outWriter->flush();
371
			$this->_helpPrinted = true;
372
		}
373
	}
374
375
376
	/**
377
	 * Print command line help, default action.
378
	 * @param mixed $outWriter
379
	 * @since 4.2.0
380
	 */
381
	public function printHelp($outWriter)
382
	{
383
		$this->printGreeting($outWriter);
384
385
		$outWriter->write("usage: ");
386
		$outWriter->writeLine("php prado-cli.php command[/action] <parameter> [optional]", [TShellWriter::BLUE, TShellWriter::BOLD]);
387
		$outWriter->writeLine();
388
		$outWriter->writeLine("example: prado-cli http");
389
		$outWriter->writeLine("example: php prado-cli.php cache/flush-all");
390
		$outWriter->writeLine("example: prado-cli help");
391
		$outWriter->writeLine("example: prado-cli cron/tasks");
392
		$outWriter->writeLine();
393
		$outWriter->writeLine("The following options are available:");
394
		$outWriter->writeLine(str_pad("  -d=<folder>", 20) . " Loads the configuration.xml/php from <folder>");
395
		foreach ($this->_options as $option => $callable) {
396
			$data = $this->_optionsData[$option];
397
			$outWriter->writeLine(str_pad(" --{$option}{$data[1]}", 20) . ' ' . $data[0]);
398
		}
399
		foreach ($this->_optionAliases as $alias => $option) {
400
			$data = $this->_optionsData[$option] ?? ['', ''];
401
			$outWriter->writeLine(str_pad("  -{$alias}{$data[1]}", 20) . " is an alias for --" . $option);
402
		}
403
		$outWriter->writeLine();
404
		$outWriter->writeLine("The following commands are available:");
405
		foreach ($this->_actions as $action) {
406
			$action->setWriter($outWriter);
407
			$outWriter->writeLine($action->renderHelp());
408
		}
409
		$outWriter->writeLine("To see the help of each command, enter:");
410
		$outWriter->writeLine();
411
		$outWriter->writeLine("  prado-cli help <command-name>");
412
		$outWriter->writeLine();
413
	}
414
}
415