Passed
Push — master ( 5cdab4...3f5646 )
by Alexander
02:30
created

Command::_doRun()   B

Complexity

Conditions 6
Paths 35

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 37
ccs 23
cts 23
cp 1
rs 8.9297
cc 6
nc 35
nop 1
crap 6
1
<?php
2
/**
3
 * This file is part of the SVN-Buddy library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/svn-buddy
9
 */
10
11
namespace ConsoleHelpers\SVNBuddy\Repository\Connector;
12
13
14
use ConsoleHelpers\ConsoleKit\ConsoleIO;
15
use ConsoleHelpers\SVNBuddy\Cache\CacheManager;
16
use ConsoleHelpers\SVNBuddy\Exception\RepositoryCommandException;
17
use ConsoleHelpers\SVNBuddy\Process\IProcessFactory;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Process\Exception\ProcessFailedException;
20
use Symfony\Component\Process\Exception\ProcessTimedOutException;
21
use Symfony\Component\Process\Process;
22
23
class Command
24
{
25
26
	const IDLE_TIMEOUT = 60; // 1 minute.
27
28
	/**
29
	 * Process factory.
30
	 *
31
	 * @var IProcessFactory
32
	 */
33
	private $_processFactory;
34
35
	/**
36
	 * Command line.
37
	 *
38
	 * @var array
39
	 */
40
	private $_commandLine;
41
42
	/**
43
	 * Console IO.
44
	 *
45
	 * @var ConsoleIO
46
	 */
47
	private $_io;
48
49
	/**
50
	 * Cache manager.
51
	 *
52
	 * @var CacheManager
53
	 */
54
	private $_cacheManager;
55
56
	/**
57
	 * Cache duration.
58
	 *
59
	 * @var mixed
60
	 */
61
	private $_cacheDuration;
62
63
	/**
64
	 * Text that when different from cached will invalidate the cache.
65
	 *
66
	 * @var string
67
	 */
68
	private $_cacheInvalidator;
69
70
	/**
71
	 * Overwrites cached value regardless of it's expiration/invalidation settings.
72
	 *
73
	 * @var boolean
74
	 */
75
	private $_cacheOverwrite = false;
76
77
	/**
78
	 * Indicates whether idle timeout recovery is enabled.
79
	 *
80
	 * @var boolean
81
	 */
82
	private $_idleTimeoutRecovery = false;
83
84
	/**
85
	 * Creates a command instance.
86
	 *
87
	 * @param array           $command_line    Command line.
88
	 * @param ConsoleIO       $io              Console IO.
89
	 * @param CacheManager    $cache_manager   Cache manager.
90
	 * @param IProcessFactory $process_factory Process factory.
91
	 */
92 69
	public function __construct(
93
		array $command_line,
94
		ConsoleIO $io,
95
		CacheManager $cache_manager,
96
		IProcessFactory $process_factory
97
	) {
98 69
		$this->_commandLine = $command_line;
99 69
		$this->_io = $io;
100 69
		$this->_cacheManager = $cache_manager;
101 69
		$this->_processFactory = $process_factory;
102
	}
103
104
	/**
105
	 * Set cache invalidator.
106
	 *
107
	 * @param string $invalidator Invalidator.
108
	 *
109
	 * @return self
110
	 */
111 16
	public function setCacheInvalidator($invalidator)
112
	{
113 16
		$this->_cacheInvalidator = $invalidator;
114
115 16
		return $this;
116
	}
117
118
	/**
119
	 * Set cache duration.
120
	 *
121
	 * @param mixed $duration Duration (seconds if numeric OR whatever "strtotime" accepts).
122
	 *
123
	 * @return self
124
	 */
125 39
	public function setCacheDuration($duration)
126
	{
127 39
		$this->_cacheDuration = $duration;
128
129 39
		return $this;
130
	}
131
132
	/**
133
	 * Set cache overwrite.
134
	 *
135
	 * @param boolean $cache_overwrite Cache replace.
136
	 *
137
	 * @return self
138
	 */
139 4
	public function setCacheOverwrite($cache_overwrite)
140
	{
141 4
		$this->_cacheOverwrite = $cache_overwrite;
142
143 4
		return $this;
144
	}
145
146
	/**
147
	 * Set idle timeout recovery.
148
	 *
149
	 * @param boolean $idle_timeout_recovery Idle timeout recovery.
150
	 *
151
	 * @return self
152
	 */
153 2
	public function setIdleTimeoutRecovery($idle_timeout_recovery)
154
	{
155 2
		$this->_idleTimeoutRecovery = $idle_timeout_recovery;
156
157 2
		return $this;
158
	}
159
160
	/**
161
	 * Runs the command.
162
	 *
163
	 * @param callable|null $callback Callback.
164
	 *
165
	 * @return string|\SimpleXMLElement
166
	 */
167 69
	public function run($callback = null)
168
	{
169 69
		$output = null;
170 69
		$cache_key = $this->_getCacheKey();
171
172 69
		if ( $cache_key ) {
173 47
			if ( $this->_cacheOverwrite ) {
174 2
				$this->_cacheManager->deleteCache($cache_key, $this->_cacheDuration);
175
			}
176
			else {
177 45
				$output = $this->_cacheManager->getCache($cache_key, $this->_cacheInvalidator, $this->_cacheDuration);
178
			}
179
180 47
			if ( isset($output) ) {
181 31
				if ( $this->_io->isVerbose() ) {
182
					$this->_io->writeln('<debug>[svn, cached]: ' . $this . '</debug>');
183
				}
184
185 31
				if ( $this->_io->isDebug() ) {
186
					$this->_io->writeln($output, OutputInterface::OUTPUT_RAW);
187
				}
188
189 31
				if ( is_callable($callback) ) {
190 6
					call_user_func($callback, Process::OUT, $output);
0 ignored issues
show
Bug introduced by
It seems like $callback can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, 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

190
					call_user_func(/** @scrutinizer ignore-type */ $callback, Process::OUT, $output);
Loading history...
191
				}
192
			}
193
		}
194
195 69
		if ( !isset($output) ) {
196 38
			$output = $this->_doRun($callback);
197
198 35
			if ( $cache_key ) {
199 16
				$this->_cacheManager->setCache($cache_key, $output, $this->_cacheInvalidator, $this->_cacheDuration);
200
			}
201
		}
202
203 66
		if ( in_array('--xml', $this->_commandLine) ) {
204 16
			return simplexml_load_string($output);
205
		}
206
207 50
		return $output;
208
	}
209
210
	/**
211
	 * Returns cache key for a command.
212
	 *
213
	 * @return string
214
	 */
215 69
	private function _getCacheKey()
216
	{
217 69
		if ( $this->_cacheInvalidator || $this->_cacheDuration ) {
218 47
			$command_string = (string)$this;
219
220 47
			if ( preg_match(Connector::URL_REGEXP, $command_string, $regs) ) {
221 17
				return $regs[2] . $regs[3] . $regs[4] . '/command:' . $command_string;
222
			}
223
224 30
			return 'misc/command:' . $command_string;
225
		}
226
227 22
		return '';
228
	}
229
230
	/**
231
	 * Runs the command.
232
	 *
233
	 * @param callable|null $callback Callback.
234
	 *
235
	 * @return string
236
	 * @throws RepositoryCommandException When command execution failed.
237
	 * @throws ProcessTimedOutException When process timed-out with general timeout type.
238
	 */
239 38
	private function _doRun($callback = null)
240
	{
241 38
		$process = $this->_processFactory->createProcess($this->_commandLine, self::IDLE_TIMEOUT);
242 38
		$command_string = (string)$this;
243
244
		try {
245 38
			if ( $this->_io->isVerbose() ) {
246 1
				$this->_io->writeln('');
247 1
				$this->_io->write('<debug>[svn, ' . date('H:i:s') . '... ]: ' . $command_string . '</debug>');
248
249 1
				$start = microtime(true);
250 1
				$process->mustRun($callback);
251
252 1
				$runtime = sprintf('%01.2f', microtime(true) - $start);
253 1
				$this->_io->write(
254 1
					"\033[2K\r" . '<debug>[svn, ' . round($runtime, 2) . 's]: ' . $command_string . '</debug>'
0 ignored issues
show
Bug introduced by
$runtime of type string is incompatible with the type double|integer expected by parameter $num of round(). ( Ignorable by Annotation )

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

254
					"\033[2K\r" . '<debug>[svn, ' . round(/** @scrutinizer ignore-type */ $runtime, 2) . 's]: ' . $command_string . '</debug>'
Loading history...
255 1
				);
256 1
				$this->_io->writeln('');
257
			}
258
			else {
259 37
				$process->mustRun($callback);
260
			}
261
262 33
			return $this->getProcessOutput($process);
263
		}
264 5
		catch ( ProcessTimedOutException $e ) {
265
			// This happens for "svn log --use-merge-history ..." command when we've got all the output already.
266 4
			if ( $this->_idleTimeoutRecovery && $e->isIdleTimeout() ) {
267 2
				return $this->getProcessOutput($process);
268
			}
269
270 2
			throw $e;
271
		}
272 1
		catch ( ProcessFailedException $e ) {
273 1
			throw new RepositoryCommandException(
274 1
				$command_string,
275 1
				$process->getErrorOutput()
276 1
			);
277
		}
278
	}
279
280
	/**
281
	 * Returns process output.
282
	 *
283
	 * @param Process $process Process.
284
	 *
285
	 * @return string
286
	 */
287 35
	protected function getProcessOutput(Process $process)
288
	{
289 35
		$output = (string)$process->getOutput();
290
291 35
		if ( $this->_io->isDebug() ) {
292 1
			$this->_io->writeln($output, OutputInterface::OUTPUT_RAW);
293
		}
294
295 35
		return $output;
296
	}
297
298
	/**
299
	 * Runs an svn command and displays output in real time.
300
	 *
301
	 * @param array $replacements Replacements for the output.
302
	 *
303
	 * @return string
304
	 */
305 2
	public function runLive(array $replacements = array())
306
	{
307 2
		return $this->run($this->_createLiveOutputCallback($replacements));
308
	}
309
310
	/**
311
	 * Creates "live output" callback.
312
	 *
313
	 * @param array $replacements Replacements for the output.
314
	 *
315
	 * @return callable
316
	 */
317 2
	private function _createLiveOutputCallback(array $replacements = array())
318
	{
319 2
		$io = $this->_io;
320
321 2
		$replace_froms = array_keys($replacements);
322 2
		$replace_tos = array_values($replacements);
323
324 2
		return function ($type, $buffer) use ($io, $replace_froms, $replace_tos) {
325 2
			foreach ( $replace_froms as $index => $replace_from ) {
326 2
				$replace_to = $replace_tos[$index];
327
328 2
				if ( substr($replace_from, 0, 1) === '/' && substr($replace_from, -1, 1) === '/' ) {
329 2
					$buffer = preg_replace($replace_from, $replace_to, $buffer);
330
				}
331
				else {
332 2
					$buffer = str_replace($replace_from, $replace_to, $buffer);
333
				}
334
			}
335
336 2
			if ( $type === Process::ERR ) {
337 1
				$buffer = '<error>ERR:</error> ' . $buffer;
338
			}
339
340 2
			$io->write($buffer);
341 2
		};
342
	}
343
344
	/**
345
	 * Returns a string representation of a command.
346
	 *
347
	 * @return string
348
	 */
349 69
	public function __toString()
350
	{
351 69
		return implode(' ', $this->_commandLine);
352
	}
353
354
}
355