Issues (1)

src/Command.php (1 issue)

Severity
1
<?php
2
/**
3
 * @Author : a.zinovyev
4
 * @Package: rsync
5
 * @License: http://www.opensource.org/licenses/mit-license.php
6
 */
7
8
namespace xobotyi\rsync;
9
10
11
/**
12
 * Class Command
13
 *
14
 * @package xobotyi\rsync
15
 */
16
abstract class Command
17
{
18
    /**
19
     * @var array
20
     */
21
    protected $OPTIONS_LIST = [];
22
    /**
23
     * @var string
24
     */
25
    private $cwd = './';
26
    /**
27
     * @var string
28
     */
29
    private $executable;
30
    /**
31
     * @var int|null
32
     */
33
    private $exitCode;
34
    /**
35
     * @var array
36
     */
37
    private $options = [];
38
    /**
39
     * @var array
40
     */
41
    private $parameters = [];
42
    /**
43
     * @var string
44
     */
45
    private $stderr;
46
    /**
47
     * @var string
48
     */
49
    private $stdout;
50
51
    /**
52
     * Command constructor.
53
     *
54
     * @param string $executable
55
     * @param string $cwd
56
     *
57
     * @throws \xobotyi\rsync\Exception\Command
58
     */
59
    public function __construct(string $executable, string $cwd = './') {
60
        $this->setExecutable($executable)
61
             ->setCWD($cwd);
62
    }
63
64
    /**
65
     * Check if given variable can be casted to the string
66
     *
67
     * @param $var
68
     *
69
     * @return bool
70
     */
71
    private static function isStringable(&$var) :bool {
72
        return (\is_string($var) || \is_numeric($var) || (\is_object($var) && \method_exists($var, '__toString')));
73
    }
74
75
    /**
76
     *
77
     *
78
     * @param array  $arrayToStore
79
     * @param string $option
80
     * @param        $value
81
     *
82
     * @throws \xobotyi\rsync\Exception\Command
83
     */
84
    private static function StoreOption(array &$arrayToStore, string $option, $value) :void {
85
        if ($value === true) {
0 ignored issues
show
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
86
        }
87
        else if ($value === false) {
88
            unset($arrayToStore[$option]);
89
90
            return;
91
        }
92
        else if (is_array($value)) {
93
            foreach ($value as &$val) {
94
                if (!self::isStringable($val)) {
95
                    throw new Exception\Command("Option {$option} has non-stringable element");
96
                }
97
            }
98
        }
99
        else if (!self::isStringable($value)) {
100
            throw new Exception\Command("Option {$option} got non-stringable value");
101
        }
102
103
        $arrayToStore[$option] = $value;
104
    }
105
106
    /**
107
     * Check if given path is an executable file
108
     *
109
     * @param string $exec path to executable
110
     *
111
     * @return bool
112
     */
113
    public static function isExecutable(string $exec) :bool {
114
        if (substr(strtolower(php_uname('s')), 0, 3) === 'win') {
115
            if (strpos($exec, '/') !== false || strpos($exec, '\\') !== false) {
116
                $exec = dirname($exec);
117
                $exec = ($exec ? $exec . ':' : '') . basename($exec);
118
            }
119
120
            exec('where' . ' /Q ' . escapeshellcmd($exec), $output, $code);
121
122
            return $code === 0;
123
        }
124
125
        return (bool)shell_exec('which' . ' ' . escapeshellcmd($exec));
126
    }
127
128
    /**
129
     * Build the command
130
     *
131
     * @return string
132
     */
133
    public function __toString() :string {
134
        $options    = $this->getOptionsString();
135
        $parameters = $this->getParametersString();
136
137
        return $this->executable . ($options ?: '') . ($parameters ?: '');
138
    }
139
140
    /**
141
     * Build the options string
142
     *
143
     * @return string
144
     */
145
    public function getOptionsString() :string {
146
        if (empty($this->options)) {
147
            return '';
148
        }
149
150
        $shortOptions = $longOptions = $parametrizedOptions = '';
151
152
        foreach ($this->options as $opt => $value) {
153
            $option       = $this->OPTIONS_LIST[$opt]['option'];
154
            $isLongOption = strlen($option) > 1;
155
156
            if (!($this->OPTIONS_LIST[$opt]['argument'] ?? false)) {
157
                $isLongOption
158
                    ? $longOptions .= ' --' . $option
159
                    : $shortOptions .= $option;
160
161
                continue;
162
            }
163
164
            $option = ($isLongOption ? ' --' : ' -') . $option;
165
166
            if ($this->OPTIONS_LIST[$opt]['repeatable'] ?? false) {
167
                foreach ($value as $val) {
168
                    $parametrizedOptions .= $option . ' ' . escapeshellarg($val);
169
                }
170
171
                continue;
172
            }
173
174
            $parametrizedOptions .= $option . ' ' . escapeshellarg($value);
175
        }
176
177
        $shortOptions        = $shortOptions ?: '';
178
        $longOptions         = $longOptions ?: '';
179
        $parametrizedOptions = $parametrizedOptions ?: '';
180
181
        return ($shortOptions ? ' -' . $shortOptions : '')
182
               . $longOptions
183
               . $parametrizedOptions;
184
    }
185
186
    /**
187
     * Build the parameters string
188
     *
189
     * @return string
190
     */
191
    public function getParametersString() :string {
192
        if (empty($this->parameters)) {
193
            return '';
194
        }
195
196
        $parametersStr = '';
197
198
        foreach ($this->parameters as $value) {
199
            $parametersStr .= ' ' . $value;
200
        }
201
202
        return $parametersStr;
203
    }
204
205
    /**
206
     * Add single parameter to the string. New parameter will be appended to the end.
207
     *
208
     * @param $parameter
209
     *
210
     * @return $this
211
     * @throws \xobotyi\rsync\Exception\Command
212
     */
213
    public function addParameter($parameter) {
214
        if (!self::isStringable($parameter)) {
215
            throw new Exception\Command("Got non-stringable parameter");
216
        }
217
218
        $this->parameters[] = $parameter;
219
220
        return $this;
221
    }
222
223
    /**
224
     * Empty the parameters.
225
     *
226
     * @return \xobotyi\rsync\Command
227
     */
228
    public function clearParameters() :self {
229
        $this->parameters = [];
230
231
        return $this;
232
    }
233
234
    /**
235
     * Execute the command by opening the child process.
236
     *
237
     * @return \xobotyi\rsync\Command
238
     * @throws \xobotyi\rsync\Exception\Command
239
     */
240
    public function execute() :self {
241
        $this->exitCode = 1;    // exit 0 on ok
242
        $this->stdout   = '';   // output of the command
243
        $this->stderr   = '';   // errors during execution
244
245
        $descriptor = [
246
            0 => ["pipe", "r"],    // stdin is a pipe that the child will read from
247
            1 => ["pipe", "w"],    // stdout is a pipe that the child will write to
248
            2 => ["pipe", "w"]     // stderr is a pipe
249
        ];
250
251
        $proc = proc_open((string)$this, $descriptor, $pipes, $this->cwd);
252
253
        if ($proc === false) {
254
            throw new Exception\Command("Unable to execute command '{$this}'");
255
        }
256
257
        $this->stdout = trim(stream_get_contents($pipes[1]));
258
        $this->stderr = trim(stream_get_contents($pipes[2]));
259
260
        fclose($pipes[0]);
261
        fclose($pipes[1]);
262
        fclose($pipes[2]);
263
264
        $this->exitCode = proc_close($proc);
265
266
        return $this;
267
    }
268
269
    /**
270
     * Return the current working directory.
271
     *
272
     * @return string
273
     */
274
    public function getCWD() :string {
275
        return $this->cwd;
276
    }
277
278
    /**
279
     * Set the current working directory. Will be used during the command execution.
280
     *
281
     * @param string $cwd CWD path
282
     *
283
     * @return \xobotyi\rsync\Command
284
     */
285
    public function setCWD(string $cwd) :self {
286
        $this->cwd = $cwd;
287
288
        return $this;
289
    }
290
291
    /**
292
     * Get the executable path
293
     *
294
     * @return string
295
     */
296
    public function getExecutable() :string {
297
        return $this->executable;
298
    }
299
300
    /**
301
     * Set the executable path
302
     *
303
     * @param string $executable
304
     *
305
     * @return $this
306
     * @throws \xobotyi\rsync\Exception\Command
307
     */
308
    public function setExecutable(string $executable) {
309
        if (!($executable = \trim($executable))) {
310
            throw new Exception\Command("Executable path must be a valuable string");
311
        }
312
313
        if (!self::isExecutable($executable)) {
314
            throw new Exception\Command("{$executable} is not executable");
315
        }
316
317
        $this->executable = $executable;
318
319
        return $this;
320
    }
321
322
    /**
323
     * Return the last command execution exitCode, null if command hasn't been executed yet
324
     *
325
     * @return string|null
326
     */
327
    public function getExitCode() :?string {
328
        return $this->exitCode;
329
    }
330
331
    /**
332
     * Get the options array
333
     *
334
     * @return array
335
     */
336
    public function getOptions() :array {
337
        return $this->options;
338
    }
339
340
    /**
341
     * Set the bunch of option
342
     *
343
     * @param array $options an array of options, where array keys are options names and array values are options values
344
     *
345
     * @return $this
346
     * @throws \xobotyi\rsync\Exception\Command
347
     */
348
    public function setOptions(array $options) {
349
        foreach ($options as $option => $value) {
350
            $this->setOption($option, $value);
351
        }
352
353
        return $this;
354
    }
355
356
    /**
357
     * Set the command's option.
358
     *
359
     * @param string $optName option name (see the constants list for options names and its descriptions)
360
     * @param mixed  $val     option value, by default is true, if has false value - option wil be removed from result
361
     *                        command.
362
     *
363
     * @return $this
364
     * @throws \xobotyi\rsync\Exception\Command
365
     */
366
    public function setOption(string $optName, $val = true) {
367
        if (!($this->OPTIONS_LIST[$optName] ?? false)) {
368
            throw new Exception\Command("Option {$optName} is not supported");
369
        }
370
371
        if (!is_bool($val) && !($this->OPTIONS_LIST[$optName]['argument'] ?? false)) {
372
            throw new Exception\Command("Option {$optName} can not have any argument");
373
        }
374
375
        if (is_array($val) && !($this->OPTIONS_LIST[$optName]['repeatable'] ?? false)) {
376
            throw new Exception\Command("Option {$optName} is not repeatable (its value cant be an array)");
377
        }
378
379
        self::StoreOption($this->options, $optName, $val);
380
381
        return $this;
382
    }
383
384
    /**
385
     * Return the parameters of command
386
     *
387
     * @return array
388
     */
389
    public function getParameters() :array {
390
        return $this->parameters;
391
    }
392
393
    /**
394
     * Set the bunch of command parameters
395
     *
396
     * @param array $parameters array of strings to set as command parameters, each string will be appended to the end
397
     *                          of command
398
     *
399
     * @return $this
400
     * @throws \xobotyi\rsync\Exception\Command
401
     */
402
    public function setParameters(array $parameters) {
403
        $params = [];
404
        foreach ($parameters as &$value) {
405
            if (!self::isStringable($value)) {
406
                throw new Exception\Command("Got non-stringable parameter");
407
            }
408
409
            $params[] = (string)$value;
410
        }
411
412
        $this->parameters = $params;
413
414
        return $this;
415
    }
416
417
    /**
418
     * Return the last command execution stderr, null if command hasn't been executed yet
419
     *
420
     * @return string|null
421
     */
422
    public function getStderr() :?string {
423
424
        return $this->stderr;
425
    }
426
427
    /**
428
     * Return the last command execution stdout, null if command hasn't been executed yet
429
     *
430
     * @return string|null
431
     */
432
    public function getStdout() :?string {
433
        return $this->stdout;
434
    }
435
}