Passed
Push — master ( 6ce998...91641e )
by Fabio
05:04
created

TWebServerAction::actionServe()   C

Complexity

Conditions 16
Paths 168

Size

Total Lines 79
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 50
c 1
b 0
f 0
nc 168
nop 1
dl 0
loc 79
rs 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * TWebServerAction class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Shell\Actions;
11
12
use Prado\Prado;
13
use Prado\Shell\TShellAction;
14
use Prado\Shell\TShellApplication;
15
use Prado\TPropertyValue;
16
use Prado\Util\Helpers\TProcessHelper;
17
18
/**
19
 * TWebServerAction class
20
 *
21
 * This class serves the application with the built-in PHP testing web server.
22
 *
23
 * When no network address is specified, the web server will listen on the default
24
 * 127.0.0.1 network interface and on port 8080.  The application is accessible on
25
 * the machine web browser at web address "http://127.0.0.1:8080/".
26
 *
27
 * The command option `--address=localhost` can specify the network address both
28
 * with and without a port.  A port can also be specified with the network address,
29
 * eg `--address=localhost:8777`.
30
 *
31
 * if the machine "hosts" file maps a domain name to 127.0.0.1/localhost, then that
32
 * domain name can be specified with `--address=testdomain.com` and is accessible
33
 * on the machine web browser at "http://testdomain.com/".  In this example, testdomain.com
34
 * maps to 127.0.0.1 in the system's "hosts" file.
35
 *
36
 * The network address can be changed to IPv6 with the command line option `--ipv6`.
37
 * To serve pages on all network addresses, including any internet IP address, include
38
 * the option `--all`.  These options only work when there is no address specified.
39
 *
40
 * The command line option `--port=8777` can be used to change the port; in this example
41
 * to port 8777.  Ports 1023 and below are typically reserved for application and
42
 * system use and cannot be specified without administration access.
43
 *
44
 * To have more than one worker (to handle multiple requests), specify the `--workers=8`
45
 * command option (with the number of page workers need for you).  In this example,
46
 * eight concurrent workers are created.
47
 *
48
 * The TWebServerAction is only available when the application is in "Debug" mode.
49
 * In other Application modes, this action can be enabled with the Prado Application
50
 * Parameter "Prado:PhpWebServer" set to "true". eg. Within the Application configuration:
51
 *
52
 * ```xml
53
 * <parameters>
54
 *     <parameter id="Prado:PhpWebServer" value="true" />
55
 * </parameters>
56
 * ```
57
 *
58
 * @author Brad Anderson <[email protected]>
59
 * @since 4.2.3
60
 */
61
class TWebServerAction extends TShellAction
62
{
63
	public const DEV_WEBSERVER_ENV = 'PRADO_DEV_WEBSERVER';
64
	public const WORKERS_ENV = 'PHP_CLI_SERVER_WORKERS';
65
66
	public const DEV_WEBSERVER_PARAM = 'Prado:PhpWebServer';
67
68
	protected $action = 'http';
69
	protected $methods = ['serve'];
70
	protected $parameters = [[]];
71
	protected $optional = [['router-filepath']];
72
	protected $description = [
73
		'Provides a Test PHP Web Server to serve the application.',
74
		'Runs a PHP Web Server after Initializing the Application.'];
75
76
	/** @var bool Listen on all network addresses assigned to the computer, when one is not provided.  Default false. */
77
	private bool $_all = false;
78
79
	/** @var ?string The specific address to listen on.  Default null. */
80
	private ?string $_address = null;
81
82
	/** @var int The port to listen on, default 8080 */
83
	private int $_port = 8080;
84
85
	/** @var bool Use a direct ip v6 address, when one is not provided.  Default false. */
86
	private bool $_ipv6 = false;
87
88
	/** @var int the number of workers for the Web Server, default 1. */
89
	private int $_workers = 1;
90
91
	/**
92
	 * This option is only used when no network interface is specified.
93
	 * @return bool Respond on all network addresses, default false.
94
	 */
95
	public function getAll(): bool
96
	{
97
		return $this->_all;
98
	}
99
100
	/**
101
	 * This option is only used when no network interface is specified.  When called
102
	 * without a $value (null or ''), All is set to true.
103
	 * @param mixed $value
104
	 * @return bool Respond on all network addresses.
105
	 * @return static The current object.
106
	 */
107
	public function setAll($value): static
108
	{
109
		if ($value === null || $value === '') {
110
			$this->_all = true;
111
		} else {
112
			$this->_all = TPropertyValue::ensureBoolean($value);
113
		}
114
		return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Prado\Shell\Actions\TWebServerAction which is incompatible with the documented return type boolean.
Loading history...
115
	}
116
117
	/**
118
	 * Gets the network address to serve pages from.  When no network address is specified
119
	 * then this will return the proper network address based upon {@see self::getIpv6()}
120
	 * and {@see self::getAll()}.
121
	 * @return string The network address to serve pages.
122
	 */
123
	public function getAddress(): string
124
	{
125
		if(!$this->_address) {
126
			if ($this->getIpv6()) {
127
				if ($this->getAll()) {
128
					return '[::0]';
129
				} else {
130
					return 'localhost';
131
				}
132
			} else {
133
				if ($this->getAll()) {
134
					return '0.0.0.0';
135
				} else {
136
					return '127.0.0.1';
137
				}
138
			}
139
		}
140
		return $this->_address;
141
	}
142
143
	/**
144
	 * @param ?string $address The network address to serve pages from.
145
	 * @return static The current object.
146
	 */
147
	public function setAddress($address): static
148
	{
149
		if ($address) {
150
			$address = TPropertyValue::ensureString($address);
151
			$port = null;
152
153
			if (($address[0] ?? '') === '[') {
154
				if ($pos = strrpos($address, ']')) {
155
					if ($pos = strrpos($address, ':', $pos)) {
156
						$port = substr($address, $pos + 1);
157
					}
158
				}
159
			} else {
160
				if (($pos = strrpos($address, ':')) !== false) {
161
					$port = substr($address, $pos + 1);
162
				}
163
			}
164
165
			if (is_numeric($port)) {
166
				$this->_port = (int) $port;
167
				$address = substr($address, 0, $pos);
168
			}
169
			$this->_address = $address;
170
		} else {
171
			$this->_address = null;
172
		}
173
174
		return $this;
175
	}
176
177
	/**
178
	 * @return int The port to serve pages, default 8080.
179
	 */
180
	public function getPort(): int
181
	{
182
		return $this->_port;
183
	}
184
185
	/**
186
	 * @param null|int|string $port The port to serve pages.
187
	 * @return static The current object.
188
	 */
189
	public function setPort($port): static
190
	{
191
		$this->_port = TPropertyValue::ensureInteger($port);
192
193
		return $this;
194
	}
195
196
	/**
197
	 * @return bool Use the local IPv6 network address, default false.
198
	 */
199
	public function getIpv6(): bool
200
	{
201
		return $this->_ipv6;
202
	}
203
204
	/**
205
	 * When called without a $value (null or ''), Ipv6 is set to true.
206
	 * @param null|bool|string $ipv6 Use the ipv6 local network address.
207
	 * @return static The current object.
208
	 */
209
	public function setIpv6($ipv6): static
210
	{
211
		if ($ipv6 === null || $ipv6 === '') {
212
			$ipv6 = true;
213
		}
214
		$this->_ipv6 = TPropertyValue::ensureBoolean($ipv6);
215
216
		return $this;
217
	}
218
219
	/**
220
	 * @return int The number of web server requests workers, default 1.
221
	 */
222
	public function getWorkers(): int
223
	{
224
		return $this->_workers;
225
	}
226
227
	/**
228
	 * When called without a value (null or '') then the number of workers is set to 8.
229
	 * @param null|int|string $value The number of web server requests workers.
230
	 * @return static The current object.
231
	 */
232
	public function setWorkers($value): static
233
	{
234
		if ($value === null || $value === '') {
235
			$this->_workers = 8;
236
		} else {
237
			$this->_workers = max(1, TPropertyValue::ensureInteger($value));
238
		}
239
240
		return $this;
241
	}
242
243
	/**
244
	 * Properties for the action set by parameter.
245
	 * @param string $methodID the action being executed
246
	 * @return array properties for the $actionID
247
	 */
248
	public function options($methodID): array
249
	{
250
		if ($methodID === 'serve') {
251
			return ['address', 'port', 'workers', 'ipv6', 'all'];
252
		}
253
		return [];
254
	}
255
256
	/**
257
	 * Aliases for the properties to be set by parameter.  'i' is for 'interface'.
258
	 * @return array<string, string> alias => property for the $actionID
259
	 */
260
	public function optionAliases(): array
261
	{
262
		return ['a' => 'address', 'p' => 'port', 'w' => 'workers', '6' => 'ipv6', 'i' => 'all'];
263
	}
264
265
266
	/**
267
	 * This runs the PHP Development Web Server.
268
	 * @param array $args parameters
269
	 * @return bool
270
	 */
271
	public function actionServe($args)
272
	{
273
		array_shift($args);
274
275
		$env = getenv();
276
		$env[static::DEV_WEBSERVER_ENV] = '1';
277
278
		if (($workers = $this->getWorkers()) > 1) {
279
			$env[static::WORKERS_ENV] = $workers;
280
		}
281
282
		$address = $this->getAddress();
283
		$port = $this->getPort();
284
		$documentRoot = dirname($_SERVER['SCRIPT_FILENAME']);
285
286
		if ($router = array_shift($args)) {
287
			if ($r = realpath($router)) {
288
				$router = $r;
289
			}
290
		}
291
292
		$app = Prado::getApplication();
293
		$quiet = ($app instanceof TShellApplication) ? $app->getQuietMode() : 0;
294
295
		$writer = $this->getWriter();
296
297
		if (!is_dir($documentRoot)) {
298
			if ($quiet !== 3) {
299
				$writer->writeError("Document root \"$documentRoot\" does not exist.");
300
				$writer->flush();
301
			}
302
			return true;
303
		}
304
305
		if ($this->isAddressTaken($address, $port)) {
306
			if ($quiet !== 3) {
307
				$writer->writeError("http://$address is taken by another process.");
308
				$writer->flush();
309
			}
310
			return true;
311
		}
312
313
		if ($router !== null && !file_exists($router)) {
314
			if ($quiet !== 3) {
315
				$writer->writeError("Routing file \"$router\" does not exist.");
316
				$writer->flush();
317
			}
318
			return true;
319
		}
320
321
		$nullFile = null;
322
		if ($quiet >= 2) {
323
			$nullFile = TProcessHelper::isSystemWindows() ? 'NUL' : '/dev/null';
324
			$descriptors = [STDIN, ['file', $nullFile, 'w'], ['file', $nullFile, 'w']];
325
		} else {
326
			$writer->writeline();
327
			$writer->write("Document root is \"{$documentRoot}\"\n");
328
			if ($router) {
329
				$writer->write("Routing file is \"$router\"\n");
330
			}
331
			$writer->writeline();
332
			$writer->write("To quit press CTRL-C or COMMAND-C.\n");
333
			$writer->flush();
334
335
			$descriptors = [STDIN, STDOUT, STDERR];
336
		}
337
338
		$command = $this->generateCommand($address . ':' . $port, $documentRoot, $router);
339
		$cwd = null;
340
341
		$process = proc_open($command, $descriptors, $pipes, $cwd, $env);
342
		proc_close($process);
343
344
		if ($nullFile) {
345
			fclose($descriptors[1]);
346
			fclose($descriptors[2]);
347
		}
348
349
		return true;
350
	}
351
352
	/**
353
	 * @param string $address The web server address and port.
354
	 * @param string $documentRoot The path of the application.
355
	 * @param ?string $router The router file
356
	 * @return array
357
	 */
358
	public function generateCommand(string $address, string $documentRoot, $router): array
359
	{
360
		return TProcessHelper::filterCommand(array_merge(['@php', '-S', $address, '-t', $documentRoot], $router ? [$router] : []));
361
	}
362
363
364
	/**
365
	 * This checks if a specific hostname and port are currently being used in the system
366
	 * @param string $hostname server address
367
	 * @param int $port server port
368
	 * @return bool if address is already in use
369
	 */
370
	protected function isAddressTaken($hostname, $port)
371
	{
372
		$fp = @fsockopen($hostname, $port, $errno, $errstr, 3);
373
		if ($fp === false) {
374
			return false;
375
		}
376
		fclose($fp);
377
		return true;
378
	}
379
}
380