Completed
Push — master ( d80a5b...04c87b )
by Anton
02:02
created

Command::StoreOption()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 6
nc 3
nop 3
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 string
36
     */
37
    private $optionValueAssigner = ' ';
38
    /**
39
     * @var array
40
     */
41
    private $options = [];
42
    /**
43
     * @var array
44
     */
45
    private $parameters = [];
46
    /**
47
     * @var string
48
     */
49
    private $stderr;
50
    /**
51
     * @var string
52
     */
53
    private $stdout;
54
55
    /**
56
     * Command constructor.
57
     *
58
     * @param string $executable
59
     * @param string $cwd
60
     * @param string $optionValueAssigner
61
     *
62
     * @throws \xobotyi\rsync\Exception\Command
63
     */
64
    public function __construct(string $executable, string $cwd = './', string $optionValueAssigner = ' ') {
65
        $this->setExecutable($executable)
66
             ->setCWD($cwd)
67
             ->setOptionValueAssigner($optionValueAssigner);
68
    }
69
70
    private static function StoreOption(array &$arrayToStore, string $option, $value) :void {
71
        if ($value === false) {
72
            unset($arrayToStore[$option]);
73
74
            return;
75
        }
76
77
        if ($value !== true && !is_array($value) && !self::isStringable($value)) {
78
            throw new Exception\Command("Option {$option} got non-stringable value");
79
        }
80
81
        $arrayToStore[$option] = $value;
82
    }
83
84
    /**
85
     * @return string
86
     */
87
    public function __toString() :string {
88
        $options    = $this->getOptionsString();
89
        $parameters = $this->getParametersString();
90
91
        return $this->executable . ($options ?: '') . ($parameters ?: '');
92
    }
93
94
    /**
95
     * @param $parameter
96
     *
97
     * @return $this
98
     * @throws \xobotyi\rsync\Exception\Command
99
     */
100
    public function addParameter($parameter) {
101
        if (!self::isStringable($parameter)) {
102
            throw new Exception\Command("Got non-stringable parameter");
103
        }
104
        $this->parameters[] = $parameter;
105
106
        return $this;
107
    }
108
109
    /**
110
     * @return \xobotyi\rsync\Command
111
     */
112
    public function clearParameters() :self {
113
        $this->parameters = [];
114
115
        return $this;
116
    }
117
118
    /**
119
     * @return \xobotyi\rsync\Command
120
     * @throws \xobotyi\rsync\Exception\Command
121
     */
122
    public function execute() :self {
123
        $this->exitCode = 1;    // exit 0 on ok
124
        $this->stdout   = '';   // output of the command
125
        $this->stderr   = '';   // errors during execution
126
127
        $descriptor = [
128
            0 => ["pipe", "r"],    // stdin is a pipe that the child will read from
129
            1 => ["pipe", "w"],    // stdout is a pipe that the child will write to
130
            2 => ["pipe", "w"]     // stderr is a pipe
131
        ];
132
133
        $proc = proc_open((string)$this, $descriptor, $pipes, $this->cwd);
134
135
        if ($proc === false) {
136
            throw new Exception\Command("Unable to execute command '{$this}'");
137
        }
138
139
        $this->stdout = trim(stream_get_contents($pipes[1]));
140
        $this->stderr = trim(stream_get_contents($pipes[2]));
141
142
        fclose($pipes[0]);
143
        fclose($pipes[1]);
144
        fclose($pipes[2]);
145
146
        $this->exitCode = proc_close($proc);
147
148
        return $this;
149
    }
150
151
    /**
152
     * @return string
153
     */
154
    public function getCWD() :string {
155
        return $this->cwd;
156
    }
157
158
    /**
159
     * @param string $cwd
160
     *
161
     * @return \xobotyi\rsync\Command
162
     */
163
    public function setCWD(string $cwd) :self {
164
        $this->cwd = $cwd;
165
166
        return $this;
167
    }
168
169
    /**
170
     * @return string
171
     */
172
    public function getExecutable() :string {
173
        return $this->executable;
174
    }
175
176
    /**
177
     * @param string $executable
178
     *
179
     * @return $this
180
     * @throws \xobotyi\rsync\Exception\Command
181
     */
182
    public function setExecutable(string $executable) {
183
        if (!($executable = \trim($executable))) {
184
            throw new Exception\Command("Executable path must be a valuable string");
185
        }
186
187
        if (!self::isExecutable($executable)) {
188
            throw new Exception\Command("{$executable} is not executable");
189
        }
190
191
        $this->executable = $executable;
192
193
        return $this;
194
    }
195
196
    /**
197
     * @return string|null
198
     */
199
    public function getExitCode() :?string {
200
        return $this->exitCode;
201
    }
202
203
    /**
204
     * @return string
205
     */
206
    public function getOptionValueAssigner() :string {
207
        return $this->optionValueAssigner;
208
    }
209
210
    /**
211
     * @param string $optionValueAssigner
212
     *
213
     * @return \xobotyi\rsync\Command
214
     */
215
    public function setOptionValueAssigner(string $optionValueAssigner) :self {
216
        $this->optionValueAssigner = $optionValueAssigner;
217
218
        return $this;
219
    }
220
221
    /**
222
     * @return array
223
     */
224
    public function getOptions() :array {
225
        return $this->options;
226
    }
227
228
    /**
229
     * @param array $options
230
     *
231
     * @return $this
232
     * @throws \xobotyi\rsync\Exception\Command
233
     */
234
    public function setOptions(array $options) {
235
        foreach ($options as $option => $value) {
236
            $this->setOption($option, $value);
237
        }
238
239
        return $this;
240
    }
241
242
    /**
243
     * @return string
244
     */
245
    public function getOptionsString() :string {
246
        if (empty($this->options)) {
247
            return '';
248
        }
249
250
        $shortOptions        = '';
251
        $longOptions         = '';
252
        $parametrizedOptions = '';
253
254
        foreach ($this->options as $opt => $value) {
255
            $option = $this->OPTIONS_LIST[$opt]['option'];
256
            if (!($this->OPTIONS_LIST[$opt]['argument'] ?? false)) {
257
                strlen($option) > 1
258
                    ? $longOptions .= ' --' . $option
259
                    : $shortOptions .= $option;
260
261
                continue;
262
            }
263
264
            if ($this->OPTIONS_LIST[$opt]['repeatable'] ?? false) {
265
                foreach ($value as $val) {
266
                    $parametrizedOptions .= (strlen($option) > 1 ? ' --' : ' -') . $option . $this->optionValueAssigner . escapeshellarg($val);
267
                }
268
269
                continue;
270
            }
271
272
            $parametrizedOptions .= (strlen($option) > 1 ? ' --' : ' -') . $option . $this->optionValueAssigner . escapeshellarg($value);
273
        }
274
275
        $shortOptions        = rtrim($shortOptions);
276
        $longOptions         = rtrim($longOptions);
277
        $parametrizedOptions = rtrim($parametrizedOptions);
278
279
        return ($shortOptions ? ' -' . $shortOptions : '') . ($longOptions ?: '') . ($parametrizedOptions ?: '');
280
    }
281
282
    /**
283
     * @return array
284
     */
285
    public function getParameters() :array {
286
        return $this->parameters;
287
    }
288
289
    /**
290
     * @param array $parameters
291
     *
292
     * @return $this
293
     * @throws \xobotyi\rsync\Exception\Command
294
     */
295
    public function setParameters(array $parameters) {
296
        $params = [];
297
        foreach ($parameters as &$value) {
298
            if (!self::isStringable($value)) {
299
                throw new Exception\Command("Got non-stringable parameter");
300
            }
301
302
            $params[] = (string)$value;
303
        }
304
305
        $this->parameters = $params;
306
307
        return $this;
308
    }
309
310
    /**
311
     * @return string
312
     */
313
    public function getParametersString() :string {
314
        if (empty($this->parameters)) {
315
            return '';
316
        }
317
318
        $parametersStr = '';
319
320
        foreach ($this->parameters as $value) {
321
            $parametersStr .= ' ' . $value;
322
        }
323
324
        return $parametersStr;
325
    }
326
327
    /**
328
     * @return string|null
329
     */
330
    public function getStderr() :?string {
331
332
        return $this->stderr;
333
    }
334
335
    /**
336
     * @return string|null
337
     */
338
    public function getStdout() :?string {
339
        return $this->stdout;
340
    }
341
342
    /**
343
     * @param string $exec
344
     *
345
     * @return bool
346
     */
347
    public static function isExecutable(string $exec) :bool {
348
        if (substr(strtolower(php_uname('s')), 0, 3) === 'win') {
349
            if (strpos($exec, '/') !== false || strpos($exec, '\\') !== false) {
350
                $exec = dirname($exec);
351
                $exec = ($exec ? $exec . ':' : '') . basename($exec);
352
            }
353
354
            exec('where' . ' /Q ' . escapeshellcmd($exec), $output, $code);
355
356
            return $code === 0;
357
        }
358
359
        return (bool)shell_exec('which' . ' ' . escapeshellcmd($exec));
360
    }
361
362
    /**
363
     * @param $var
364
     *
365
     * @return bool
366
     */
367
    private static function isStringable(&$var) :bool {
368
        return (\is_string($var) || \is_numeric($var) || (\is_object($var) && \method_exists($var, '__toString')));
369
    }
370
371
    /**
372
     * @param string $optName
373
     * @param mixed  $val
374
     *
375
     * @return $this
376
     * @throws \xobotyi\rsync\Exception\Command
377
     */
378
    public function setOption(string $optName, $val = true) {
379
        if (!($this->OPTIONS_LIST[$optName] ?? false)) {
380
            throw new Exception\Command("Option {$optName} is not supported");
381
        }
382
383
        if (!is_bool($val) && !($this->OPTIONS_LIST[$optName]['argument'] ?? false)) {
384
            throw new Exception\Command("Option {$optName} can not have any argument");
385
        }
386
387
        if (is_array($val)) {
388
            if (!($this->OPTIONS_LIST[$optName]['repeatable'] ?? false)) {
389
                throw new Exception\Command("Option {$optName} is not repeatable (its value cant be an array)");
390
            }
391
392
            foreach ($val as &$value) {
393
                if (!self::isStringable($value)) {
394
                    throw new Exception\Command("Option {$optName} got non-stringable value");
395
                }
396
            }
397
        }
398
399
        self::StoreOption($this->options, $optName, $val);
400
401
        return $this;
402
    }
403
}