Total Complexity | 41 |
Total Lines | 431 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like Shell often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Shell, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
57 | class Shell |
||
58 | { |
||
59 | public const STDIN_DESCRIPTOR = 0; |
||
60 | public const STDOUT_DESCRIPTOR = 1; |
||
61 | public const STDERR_DESCRIPTOR = 2; |
||
62 | |||
63 | public const STATE_READY = 0; |
||
64 | public const STATE_STARTED = 1; |
||
65 | public const STATE_CLOSED = 2; |
||
66 | public const STATE_TERMINATED = 3; |
||
67 | |||
68 | /** |
||
69 | * Whether to wait for the process to finish or return instantly |
||
70 | * @var bool |
||
71 | */ |
||
72 | protected bool $async = false; |
||
73 | |||
74 | /** |
||
75 | * The command to execute |
||
76 | * @var string |
||
77 | */ |
||
78 | protected string $command = ''; |
||
79 | |||
80 | /** |
||
81 | * Current working directory |
||
82 | * @var string|null |
||
83 | */ |
||
84 | protected ?string $cwd = null; |
||
85 | |||
86 | /** |
||
87 | * The list of descriptors to pass to process |
||
88 | * @var array<int, array<int, string>> |
||
89 | */ |
||
90 | protected array $descriptors = []; |
||
91 | |||
92 | /** |
||
93 | * List of environment variables |
||
94 | * @var array<string, mixed>|null |
||
95 | */ |
||
96 | protected ?array $env = null; |
||
97 | |||
98 | /** |
||
99 | * The process exit code |
||
100 | * @var int|null |
||
101 | */ |
||
102 | protected ?int $exitCode = null; |
||
103 | |||
104 | /** |
||
105 | * The path for input stream |
||
106 | * @var string|null |
||
107 | */ |
||
108 | protected ?string $input = null; |
||
109 | |||
110 | /** |
||
111 | * Others options to pass to process |
||
112 | * @var array<string, mixed> |
||
113 | */ |
||
114 | protected array $options = []; |
||
115 | |||
116 | /** |
||
117 | * Pointers to standard input/output/error |
||
118 | * @var array<int, resource> |
||
119 | */ |
||
120 | protected array $pipes = []; |
||
121 | |||
122 | /** |
||
123 | * The actual process resource returned |
||
124 | * @var resource|false |
||
125 | */ |
||
126 | protected $process; |
||
127 | |||
128 | /** |
||
129 | * The process start time in Unix timestamp |
||
130 | * @var float |
||
131 | */ |
||
132 | protected float $startTime = 0; |
||
133 | |||
134 | /** |
||
135 | * Default timeout for the process in seconds with microseconds |
||
136 | * @var float|null |
||
137 | */ |
||
138 | protected ?float $timeout = null; |
||
139 | |||
140 | /** |
||
141 | * The status list of the process |
||
142 | * @var array<string, mixed> |
||
143 | */ |
||
144 | protected array $status = []; |
||
145 | |||
146 | /** |
||
147 | * The current process status |
||
148 | * @var int |
||
149 | */ |
||
150 | protected int $state = self::STATE_READY; |
||
151 | |||
152 | /** |
||
153 | * Create new instance |
||
154 | */ |
||
155 | public function __construct() |
||
156 | { |
||
157 | if (!function_exists('proc_open')) { |
||
158 | throw new RuntimeException( |
||
159 | 'The "proc_open" could not be found in your PHP setup' |
||
160 | ); |
||
161 | } |
||
162 | } |
||
163 | |||
164 | /** |
||
165 | * Set the command to be executed |
||
166 | * @param string $command |
||
167 | * @return $this |
||
168 | */ |
||
169 | public function setCommand(string $command): self |
||
170 | { |
||
171 | $this->command = $command; |
||
172 | |||
173 | return $this; |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Set the input stream information |
||
178 | * @param string|null $input |
||
179 | * @return $this |
||
180 | */ |
||
181 | public function setInput(?string $input): self |
||
182 | { |
||
183 | $this->input = $input; |
||
184 | return $this; |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Whether the process is running |
||
189 | * @return bool |
||
190 | */ |
||
191 | public function isRunning(): bool |
||
192 | { |
||
193 | if ($this->state !== self::STATE_STARTED) { |
||
194 | return false; |
||
195 | } |
||
196 | |||
197 | $this->updateStatus(); |
||
198 | |||
199 | return $this->status['running']; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Kill the process |
||
204 | * @return void |
||
205 | */ |
||
206 | public function kill(): void |
||
207 | { |
||
208 | if (is_resource($this->process)) { |
||
209 | proc_terminate($this->process); |
||
210 | } |
||
211 | |||
212 | $this->state = self::STATE_TERMINATED; |
||
213 | } |
||
214 | |||
215 | /** |
||
216 | * Stop the process |
||
217 | * @return int|null |
||
218 | */ |
||
219 | public function stop(): ?int |
||
220 | { |
||
221 | $this->closePipes(); |
||
222 | |||
223 | if (is_resource($this->process)) { |
||
224 | proc_close($this->process); |
||
225 | } |
||
226 | |||
227 | $this->state = self::STATE_CLOSED; |
||
228 | $this->exitCode = $this->status['exitcode']; |
||
229 | |||
230 | return $this->exitCode; |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Set options used for process execution |
||
235 | * @param string|null $cwd |
||
236 | * @param array<string, mixed>|null $env |
||
237 | * @param float|null $timeout |
||
238 | * @param array<string, mixed> $options |
||
239 | * @return $this |
||
240 | */ |
||
241 | public function setOptions( |
||
242 | ?string $cwd = null, |
||
243 | ?array $env = [], |
||
244 | ?float $timeout = null, |
||
245 | array $options = [] |
||
246 | ): self { |
||
247 | $this->cwd = $cwd; |
||
248 | $this->env = $env; |
||
249 | $this->timeout = $timeout; |
||
250 | $this->options = $options; |
||
251 | |||
252 | return $this; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * Execute the process |
||
257 | * @param bool $async |
||
258 | * @return $this |
||
259 | */ |
||
260 | public function execute(bool $async = false): self |
||
261 | { |
||
262 | if ($this->isRunning()) { |
||
263 | throw new RuntimeException(sprintf( |
||
264 | 'Process [%s] already running', |
||
265 | $this->command |
||
266 | )); |
||
267 | } |
||
268 | |||
269 | $this->descriptors = $this->getDescriptors(); |
||
270 | $this->startTime = microtime(true); |
||
|
|||
271 | |||
272 | $this->process = proc_open( |
||
273 | $this->command, |
||
274 | $this->descriptors, |
||
275 | $this->pipes, |
||
276 | $this->cwd, |
||
277 | $this->env, |
||
278 | $this->options |
||
279 | ); |
||
280 | |||
281 | $this->writeInput(); |
||
282 | |||
283 | if (!is_resource($this->process)) { |
||
284 | throw new RuntimeException(sprintf( |
||
285 | 'Bad program [%s] could not be started', |
||
286 | $this->command |
||
287 | )); |
||
288 | } |
||
289 | |||
290 | $this->state = self::STATE_STARTED; |
||
291 | |||
292 | $this->updateStatus(); |
||
293 | |||
294 | $this->async = $async; |
||
295 | |||
296 | if ($this->async) { |
||
297 | $this->setOutputStreamNonBlocking(); |
||
298 | } else { |
||
299 | $this->wait(); |
||
300 | } |
||
301 | |||
302 | return $this; |
||
303 | } |
||
304 | |||
305 | /** |
||
306 | * Return the process current state |
||
307 | * @return int |
||
308 | */ |
||
309 | public function getState(): int |
||
310 | { |
||
311 | return $this->state; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Return the process command error output |
||
316 | * @return string |
||
317 | */ |
||
318 | public function getErrorOutput(): string |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * Return the process command output |
||
334 | * @return string |
||
335 | */ |
||
336 | public function getOutput(): string |
||
337 | { |
||
338 | $output = stream_get_contents($this->pipes[self::STDOUT_DESCRIPTOR]); |
||
339 | |||
340 | if ($output === false) { |
||
341 | throw new RuntimeException(sprintf( |
||
342 | 'Can not get process [%s] error output', |
||
343 | $this->command |
||
344 | )); |
||
345 | } |
||
346 | |||
347 | return $output; |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Return the process exit code |
||
352 | * @return int|null |
||
353 | */ |
||
354 | public function getExitCode(): ?int |
||
355 | { |
||
356 | $this->updateStatus(); |
||
357 | |||
358 | return $this->exitCode; |
||
359 | } |
||
360 | |||
361 | /** |
||
362 | * Return the process ID |
||
363 | * @return int|null |
||
364 | */ |
||
365 | public function getProcessId(): ?int |
||
366 | { |
||
367 | return $this->isRunning() ? $this->status['pid'] : null; |
||
368 | } |
||
369 | |||
370 | /** |
||
371 | * Return the descriptors to be used later |
||
372 | * @return array<int, array<int, string>> |
||
373 | */ |
||
374 | protected function getDescriptors(): array |
||
375 | { |
||
376 | $out = $this->isWindows() |
||
377 | ? ['pipe', 'w'] // ['file', 'NUL', 'w'] |
||
378 | : ['pipe', 'w']; |
||
379 | |||
380 | return [ |
||
381 | self::STDIN_DESCRIPTOR => ['pipe', 'r'], |
||
382 | self::STDOUT_DESCRIPTOR => $out, |
||
383 | self::STDERR_DESCRIPTOR => $out, |
||
384 | ]; |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Whether the current Os is Windows |
||
389 | * @return bool |
||
390 | */ |
||
391 | protected function isWindows(): bool |
||
392 | { |
||
393 | return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; |
||
394 | } |
||
395 | |||
396 | /** |
||
397 | * Write to input stream |
||
398 | * @return void |
||
399 | */ |
||
400 | protected function writeInput(): void |
||
401 | { |
||
402 | if ($this->input !== null) { |
||
403 | fwrite($this->pipes[self::STDIN_DESCRIPTOR], $this->input); |
||
404 | } |
||
405 | } |
||
406 | |||
407 | /** |
||
408 | * Update the process status |
||
409 | * @return void |
||
410 | */ |
||
411 | protected function updateStatus(): void |
||
412 | { |
||
413 | if ($this->state !== self::STATE_STARTED) { |
||
414 | return; |
||
415 | } |
||
416 | |||
417 | if (is_resource($this->process)) { |
||
418 | $status = proc_get_status($this->process); |
||
419 | if ($status === false) { |
||
420 | throw new RuntimeException(sprintf( |
||
421 | 'Can not get process [%s] status information', |
||
422 | $this->command |
||
423 | )); |
||
424 | } |
||
425 | |||
426 | $this->status = $status; |
||
427 | |||
428 | if ($this->status['running'] === false && $this->exitCode === null) { |
||
429 | $this->exitCode = $this->status['exitcode']; |
||
430 | } |
||
431 | } |
||
432 | } |
||
433 | |||
434 | /** |
||
435 | * Close the process pipes |
||
436 | * @return void |
||
437 | */ |
||
438 | protected function closePipes(): void |
||
439 | { |
||
440 | fclose($this->pipes[self::STDIN_DESCRIPTOR]); |
||
441 | fclose($this->pipes[self::STDOUT_DESCRIPTOR]); |
||
442 | fclose($this->pipes[self::STDERR_DESCRIPTOR]); |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * Waiting the process to finish |
||
447 | * @return int|null |
||
448 | */ |
||
449 | protected function wait(): ?int |
||
457 | } |
||
458 | |||
459 | /** |
||
460 | * Check for process execution timeout |
||
461 | * @return void |
||
462 | */ |
||
463 | protected function checkTimeout(): void |
||
477 | )); |
||
478 | } |
||
479 | } |
||
480 | |||
481 | /** |
||
482 | * Set the running process to asynchronous |
||
483 | * @return bool |
||
484 | */ |
||
485 | protected function setOutputStreamNonBlocking(): bool |
||
488 | } |
||
489 | } |
||
490 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.