Failed Conditions
Push — master ( b317d8...4dca63 )
by Alexander
02:29
created

StatementProfiler::substituteParameters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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

196
				'<debug>[db, ' . round(/** @scrutinizer ignore-type */ $runtime, 2) . 's]: ' . $substituted_normalized_statement . '</debug>',
Loading history...
197 2
				'<debug>[db origin]: ' . $trace_file . '</debug>',
198 2
			));
199
		}
200
	}
201
202
	/**
203
	 * Removes a profile entry.
204
	 *
205
	 * @param string $statement   The SQL query statement.
206
	 * @param array  $bind_values The values bound to the statement.
207
	 *
208
	 * @return void
209
	 */
210 14
	public function removeProfile($statement, array $bind_values = array())
211
	{
212 14
		if ( !$this->isActive() ) {
213 1
			return;
214
		}
215
216 13
		$normalized_statement = $this->normalizeStatement($statement);
217 13
		unset($this->profiles[$this->createProfileKey($normalized_statement, $bind_values)]);
218
	}
219
220
	/**
221
	 * Normalizes statement.
222
	 *
223
	 * @param string $statement The SQL query statement.
224
	 *
225
	 * @return string
226
	 */
227 34
	protected function normalizeStatement($statement)
228
	{
229 34
		return preg_replace('/\s+/', ' ', $statement);
230
	}
231
232
	/**
233
	 * Creates profile key.
234
	 *
235
	 * @param string $normalized_statement The normalized SQL query statement.
236
	 * @param array  $bind_values          The values bound to the statement.
237
	 *
238
	 * @return string
239
	 */
240 26
	protected function createProfileKey($normalized_statement, array $bind_values = array())
241
	{
242 26
		return md5('statement:' . $normalized_statement . ';bind_values:' . serialize($bind_values));
243
	}
244
245
	/**
246
	 * Substitutes parameters in the statement.
247
	 *
248
	 * @param string $normalized_statement The normalized SQL query statement.
249
	 * @param array  $bind_values          The values bound to the statement.
250
	 *
251
	 * @return string
252
	 */
253 4
	protected function substituteParameters($normalized_statement, array $bind_values = array())
254
	{
255 4
		arsort($bind_values);
256
257 4
		foreach ( $bind_values as $param_name => $param_value ) {
258 4
			if ( is_array($param_value) ) {
259 1
				$param_value = '"' . implode('","', $param_value) . '"';
260
			}
261
			else {
262 4
				$param_value = '"' . $param_value . '"';
263
			}
264
265 4
			$normalized_statement = str_replace(':' . $param_name, $param_value, $normalized_statement);
266
		}
267
268 4
		return $normalized_statement;
269
	}
270
271
	/**
272
	 * Returns all the profile entries.
273
	 *
274
	 * @return array
275
	 */
276 13
	public function getProfiles()
277
	{
278 13
		return $this->profiles;
279
	}
280
281
	/**
282
	 * Reset all the profiles
283
	 *
284
	 * @return void
285
	 */
286 1
	public function resetProfiles()
287
	{
288 1
		$this->profiles = array();
289
	}
290
291
}
292