Failed Conditions
Push — master ( 40cff2...53ac18 )
by Alexander
10:35 queued 08:36
created

StatementProfiler::normalizeStatement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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
	protected $ignoredDuplicateStatements = array();
73
74
	/**
75
	 * Console IO.
76
	 *
77
	 * @var ConsoleIO
78
	 */
79
	private $_io;
80
81
	/**
82
	 * Debug mode.
83
	 *
84
	 * @var boolean
85
	 */
86
	private $_debugMode = false;
87
88
	/**
89
	 * Debug backtrace options.
90
	 *
91
	 * @var integer
92
	 */
93
	private $_backtraceOptions;
94
95
	/**
96
	 * Creates statement profiler instance.
97
	 */
98 43
	public function __construct()
99
	{
100 43
		$this->logger = new NullLogger();
101 43
		$this->_backtraceOptions = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? DEBUG_BACKTRACE_IGNORE_ARGS : 0;
102 43
	}
103
104
	/**
105
	 * Sets IO.
106
	 *
107
	 * @param ConsoleIO $io Console IO.
108
	 *
109
	 * @return void
110
	 */
111 3
	public function setIO(ConsoleIO $io = null)
112
	{
113 3
		$this->_io = $io;
114 3
		$this->_debugMode = isset($io) && $io->isVerbose();
115 3
	}
116
117
	/**
118
	 * Adds statement to ignore list.
119
	 *
120
	 * @param string $statement The SQL query statement.
121
	 *
122
	 * @return void
123
	 */
124 24
	public function ignoreDuplicateStatement($statement)
125
	{
126 24
		$this->ignoredDuplicateStatements[] = $this->normalizeStatement($statement);
127 24
	}
128
129
	/**
130
	 * Turns the profiler on and off.
131
	 *
132
	 * @param boolean $active True to turn on, false to turn off.
133
	 *
134
	 * @return void
135
	 */
136 37
	public function setActive($active)
137
	{
138 37
		$this->active = (bool)$active;
139 37
	}
140
141
	/**
142
	 * Is the profiler active?
143
	 *
144
	 * @return boolean
145
	 */
146 31
	public function isActive()
147
	{
148 31
		return (bool)$this->active;
149
	}
150
151
	/**
152
	 * Toggle duplicate statement tracker.
153
	 *
154
	 * @param boolean $track Duplicate statement tracker status.
155
	 *
156
	 * @return void
157
	 */
158 28
	public function trackDuplicates($track)
159
	{
160 28
		$this->trackDuplicates = (bool)$track;
161 28
	}
162
163
	/**
164
	 * Adds a profile entry.
165
	 *
166
	 * @param float  $duration    The query duration.
167
	 * @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
	 *
171
	 * @return void
172
	 * @throws \PDOException When duplicate statement is detected.
173
	 */
174 17
	public function addProfile(
175
		$duration,
176
		$function,
177
		$statement,
178
		array $bind_values = array()
179
	) {
180 17
		if ( !$this->isActive() || $function === 'prepare' || !$statement ) {
181 3
			return;
182
		}
183
184 14
		$normalized_statement = $this->normalizeStatement($statement);
185 14
		$profile_key = $this->createProfileKey($normalized_statement, $bind_values);
186
187 14
		if ( $this->trackDuplicates
188 14
			&& !in_array($normalized_statement, $this->ignoredDuplicateStatements)
189 14
			&& isset($this->profiles[$profile_key])
190
		) {
191 2
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
192 2
			$error_msg = 'Duplicate statement:' . PHP_EOL . $substituted_normalized_statement;
193
194 2
			throw new \PDOException($error_msg);
195
		}
196
197 14
		$this->profiles[$profile_key] = array(
198 14
			'duration' => $duration,
199 14
			'function' => $function,
200 14
			'statement' => $statement,
201 14
			'bind_values' => $bind_values,
202
		);
203
204 14
		if ( $this->_debugMode ) {
205 2
			$trace = debug_backtrace($this->_backtraceOptions);
206
207
			do {
208 2
				$trace_line = array_shift($trace);
209 2
			} while ( $trace && strpos($trace_line['file'], 'aura/sql') !== false );
210
211 2
			$runtime = sprintf('%01.2f', $duration);
212 2
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
213 2
			$trace_file = substr($trace_line['file'], strpos($trace_line['file'], '/src/')) . ':' . $trace_line['line'];
214 2
			$this->_io->writeln(array(
215 2
				'',
216 2
				'<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 2
				'<debug>[db origin]: ' . $trace_file . '</debug>',
218
			));
219
		}
220 14
	}
221
222
	/**
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 14
	public function removeProfile($statement, array $bind_values = array())
231
	{
232 14
		if ( !$this->isActive() ) {
233 1
			return;
234
		}
235
236 13
		$normalized_statement = $this->normalizeStatement($statement);
237 13
		unset($this->profiles[$this->createProfileKey($normalized_statement, $bind_values)]);
238 13
	}
239
240
	/**
241
	 * Normalizes statement.
242
	 *
243
	 * @param string $statement The SQL query statement.
244
	 *
245
	 * @return string
246
	 */
247 34
	protected function normalizeStatement($statement)
248
	{
249 34
		return preg_replace('/\s+/', ' ', $statement);
250
	}
251
252
	/**
253
	 * Creates profile key.
254
	 *
255
	 * @param string $normalized_statement The normalized SQL query statement.
256
	 * @param array  $bind_values          The values bound to the statement.
257
	 *
258
	 * @return string
259
	 */
260 26
	protected function createProfileKey($normalized_statement, array $bind_values = array())
261
	{
262 26
		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
	 * @param array  $bind_values          The values bound to the statement.
270
	 *
271
	 * @return string
272
	 */
273 4
	protected function substituteParameters($normalized_statement, array $bind_values = array())
274
	{
275 4
		arsort($bind_values);
276
277 4
		foreach ( $bind_values as $param_name => $param_value ) {
278 4
			if ( is_array($param_value) ) {
279 1
				$param_value = '"' . implode('","', $param_value) . '"';
280
			}
281
			else {
282 4
				$param_value = '"' . $param_value . '"';
283
			}
284
285 4
			$normalized_statement = str_replace(':' . $param_name, $param_value, $normalized_statement);
286
		}
287
288 4
		return $normalized_statement;
289
	}
290
291
	/**
292
	 * Returns all the profile entries.
293
	 *
294
	 * @return array
295
	 */
296 13
	public function getProfiles()
297
	{
298 13
		return $this->profiles;
299
	}
300
301
	/**
302
	 * Reset all the profiles
303
	 *
304
	 * @return void
305
	 */
306 1
	public function resetProfiles()
307
	{
308 1
		$this->profiles = array();
309 1
	}
310
311
	/**
312
	 * @inheritDoc
313
	 */
314 1
	public function getLogger()
315
	{
316 1
		return $this->logger;
317
	}
318
319
	/**
320
	 * @inheritDoc
321
	 */
322 1
	public function getLogLevel()
323
	{
324 1
		return $this->logLevel;
325
	}
326
327
	/**
328
	 * @inheritDoc
329
	 */
330 1
	public function setLogLevel($logLevel)
331
	{
332 1
		$this->logLevel = $logLevel;
333 1
	}
334
335
	/**
336
	 * @inheritDoc
337
	 */
338 1
	public function getLogFormat()
339
	{
340 1
		return $this->logFormat;
341
	}
342
343
	/**
344
	 * @inheritDoc
345
	 */
346 1
	public function setLogFormat($logFormat)
347
	{
348 1
		$this->logFormat = $logFormat;
349 1
	}
350
351
	/**
352
	 * @inheritDoc
353
	 */
354 17
	public function start($function)
355
	{
356
357 17
	}
358
359
	/**
360
	 * @inheritDoc
361
	 */
362 17
	public function finish($statement = null, array $values = [])
363
	{
364
365 17
	}
366
367
}
368