Failed Conditions
Pull Request — master (#142)
by Alexander
08:32
created

StatementProfiler::setIO()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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\Database;
12
13
14
use Aura\Sql\Profiler\ProfilerInterface;
15
use ConsoleHelpers\ConsoleKit\ConsoleIO;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\LogLevel;
18
use Psr\Log\NullLogger;
19
20
class StatementProfiler implements ProfilerInterface
21
{
22
23
	/**
24
	 * Is the profiler active?
25
	 *
26
	 * @var boolean
27
	 */
28
	protected $active = false;
29
30
	/**
31
	 * Log profile data through this interface.
32
	 *
33
	 * @var LoggerInterface
34
	 */
35
	protected $logger;
36
37
	/**
38
	 * The log level for all messages.
39
	 *
40
	 * @var string
41
	 * @see setLogLevel()
42
	 */
43
	protected $logLevel = LogLevel::DEBUG;
44
45
	/**
46
	 * Sets the format for the log message, with placeholders.
47
	 *
48
	 * @var string
49
	 * @see setLogFormat()
50
	 */
51
	protected $logFormat = '{function} ({duration} seconds): {statement} {backtrace}';
52
53
	/**
54
	 * Retained profiles.
55
	 *
56
	 * @var array
57
	 */
58
	protected $profiles = array();
59
60
	/**
61
	 * Track duplicate statements.
62
	 *
63
	 * @var boolean
64
	 */
65
	protected $trackDuplicates = true;
66
67
	/**
68
	 * Ignored duplicate statements.
69
	 *
70
	 * @var array
71
	 */
72 40
	protected $ignoredDuplicateStatements = array();
73
74 40
	/**
75 40
	 * Console IO.
76
	 *
77
	 * @var ConsoleIO
78
	 */
79
	private $_io;
80
81
	/**
82
	 * Debug mode.
83
	 *
84 3
	 * @var boolean
85
	 */
86 3
	private $_debugMode = false;
87 3
88 3
	/**
89
	 * Debug backtrace options.
90
	 *
91
	 * @var integer
92
	 */
93
	private $_backtraceOptions;
94
95
	/**
96
	 * Creates statement profiler instance.
97 24
	 */
98
	public function __construct()
99 24
	{
100 24
		$this->logger = new NullLogger();
101
		$this->_backtraceOptions = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? DEBUG_BACKTRACE_IGNORE_ARGS : 0;
102
	}
103
104
	/**
105
	 * Sets IO.
106
	 *
107
	 * @param ConsoleIO $io Console IO.
108
	 *
109 37
	 * @return void
110
	 */
111 37
	public function setIO(ConsoleIO $io = null)
112 37
	{
113
		$this->_io = $io;
114
		$this->_debugMode = isset($io) && $io->isVerbose();
115
	}
116
117
	/**
118
	 * Adds statement to ignore list.
119 36
	 *
120
	 * @param string $statement The SQL query statement.
121 36
	 *
122
	 * @return void
123
	 */
124
	public function ignoreDuplicateStatement($statement)
125
	{
126
		$this->ignoredDuplicateStatements[] = $this->normalizeStatement($statement);
127
	}
128
129
	/**
130
	 * Turns the profiler on and off.
131 28
	 *
132
	 * @param boolean $active True to turn on, false to turn off.
133 28
	 *
134 28
	 * @return void
135
	 */
136
	public function setActive($active)
137
	{
138
		$this->active = (bool)$active;
139
	}
140
141
	/**
142
	 * Is the profiler active?
143
	 *
144
	 * @return boolean
145
	 */
146
	public function isActive()
147 34
	{
148
		return (bool)$this->active;
149
	}
150
151
	/**
152
	 * Toggle duplicate statement tracker.
153 34
	 *
154 20
	 * @param boolean $track Duplicate statement tracker status.
155
	 *
156
	 * @return void
157 31
	 */
158 31
	public function trackDuplicates($track)
159
	{
160 31
		$this->trackDuplicates = (bool)$track;
161 31
	}
162 31
163
	/**
164 2
	 * Adds a profile entry.
165 2
	 *
166
	 * @param float  $duration    The query duration.
167 2
	 * @param string $function    The PDO method that made the entry.
168
	 * @param string $statement   The SQL query statement.
169
	 * @param array  $bind_values The values bound to the statement.
170 31
	 *
171 31
	 * @return void
172 31
	 * @throws \PDOException When duplicate statement is detected.
173 31
	 */
174 31
	public function addProfile(
175
		$duration,
176
		$function,
177 31
		$statement,
178 2
		array $bind_values = array()
179
	) {
180
		if ( !$this->isActive() || $function === 'prepare' || !$statement ) {
181 2
			return;
182 2
		}
183
184 2
		$normalized_statement = $this->normalizeStatement($statement);
185 2
		$profile_key = $this->createProfileKey($normalized_statement, $bind_values);
186 2
187 2
		if ( $this->trackDuplicates
188 2
			&& !in_array($normalized_statement, $this->ignoredDuplicateStatements)
189 2
			&& isset($this->profiles[$profile_key])
190 2
		) {
191
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
192
			$error_msg = 'Duplicate statement:' . PHP_EOL . $substituted_normalized_statement;
193 31
194
			throw new \PDOException($error_msg);
195
		}
196
197
		$this->profiles[$profile_key] = array(
198
			'duration' => $duration,
199
			'function' => $function,
200
			'statement' => $statement,
201
			'bind_values' => $bind_values,
202
		);
203 14
204
		if ( $this->_debugMode ) {
205 14
			$trace = debug_backtrace($this->_backtraceOptions);
206 1
207
			do {
208
				$trace_line = array_shift($trace);
209 13
			} while ( $trace && strpos($trace_line['file'], 'aura/sql') !== false );
210 13
211 13
			$runtime = sprintf('%01.2f', $duration);
212
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
213
			$trace_file = substr($trace_line['file'], strpos($trace_line['file'], '/src/')) . ':' . $trace_line['line'];
214
			$this->_io->writeln(array(
215
				'',
216
				'<debug>[db, ' . round($runtime, 2) . 's]: ' . $substituted_normalized_statement . '</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

216
				'<debug>[db, ' . round(/** @scrutinizer ignore-type */ $runtime, 2) . 's]: ' . $substituted_normalized_statement . '</debug>',
Loading history...
217
				'<debug>[db origin]: ' . $trace_file . '</debug>',
218
			));
219
		}
220 34
	}
221
222 34
	/**
223
	 * Removes a profile entry.
224
	 *
225
	 * @param string $statement   The SQL query statement.
226
	 * @param array  $bind_values The values bound to the statement.
227
	 *
228
	 * @return void
229
	 */
230
	public function removeProfile($statement, array $bind_values = array())
231
	{
232
		if ( !$this->isActive() ) {
233 31
			return;
234
		}
235 31
236
		$normalized_statement = $this->normalizeStatement($statement);
237
		unset($this->profiles[$this->createProfileKey($normalized_statement, $bind_values)]);
238
	}
239
240
	/**
241
	 * Normalizes statement.
242
	 *
243
	 * @param string $statement The SQL query statement.
244
	 *
245
	 * @return string
246 4
	 */
247
	protected function normalizeStatement($statement)
248 4
	{
249
		return preg_replace('/\s+/', ' ', $statement);
250 4
	}
251 4
252 1
	/**
253
	 * Creates profile key.
254
	 *
255 4
	 * @param string $normalized_statement The normalized SQL query statement.
256
	 * @param array  $bind_values          The values bound to the statement.
257
	 *
258 4
	 * @return string
259
	 */
260
	protected function createProfileKey($normalized_statement, array $bind_values = array())
261 4
	{
262
		return md5('statement:' . $normalized_statement . ';bind_values:' . serialize($bind_values));
263
	}
264
265
	/**
266
	 * Substitutes parameters in the statement.
267
	 *
268
	 * @param string $normalized_statement The normalized SQL query statement.
269 13
	 * @param array  $bind_values          The values bound to the statement.
270
	 *
271 13
	 * @return string
272
	 */
273
	protected function substituteParameters($normalized_statement, array $bind_values = array())
274
	{
275
		arsort($bind_values);
276
277
		foreach ( $bind_values as $param_name => $param_value ) {
278
			if ( is_array($param_value) ) {
279 1
				$param_value = '"' . implode('","', $param_value) . '"';
280
			}
281 1
			else {
282 1
				$param_value = '"' . $param_value . '"';
283
			}
284
285
			$normalized_statement = str_replace(':' . $param_name, $param_value, $normalized_statement);
286
		}
287
288
		return $normalized_statement;
289
	}
290
291
	/**
292
	 * Returns all the profile entries.
293
	 *
294
	 * @return array
295
	 */
296
	public function getProfiles()
297
	{
298
		return $this->profiles;
299
	}
300
301
	/**
302
	 * Reset all the profiles
303
	 *
304
	 * @return void
305
	 */
306
	public function resetProfiles()
307
	{
308
		$this->profiles = array();
309
	}
310
311
	/**
312
	 * @inheritDoc
313
	 */
314
	public function getLogger()
315
	{
316
		return $this->logger;
317
	}
318
319
	/**
320
	 * @inheritDoc
321
	 */
322
	public function getLogLevel()
323
	{
324
		return $this->logLevel;
325
	}
326
327
	/**
328
	 * @inheritDoc
329
	 */
330
	public function setLogLevel($logLevel)
331
	{
332
		$this->logLevel = $logLevel;
333
	}
334
335
	/**
336
	 * @inheritDoc
337
	 */
338
	public function getLogFormat()
339
	{
340
		return $this->logFormat;
341
	}
342
343
	/**
344
	 * @inheritDoc
345
	 */
346
	public function setLogFormat($logFormat)
347
	{
348
		$this->logFormat = $logFormat;
349
	}
350
351
	/**
352
	 * @inheritDoc
353
	 */
354
	public function start($function)
355
	{
356
357
	}
358
359
	/**
360
	 * @inheritDoc
361
	 */
362
	public function finish($statement = null, array $values = [])
363
	{
364
365
	}
366
367
}
368