Failed Conditions
Push — master ( e58a5b...a04d7d )
by Alexander
03:00
created

StatementProfiler::normalizeStatement()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
cc 1
eloc 2
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\ProfilerInterface;
15
use ConsoleHelpers\ConsoleKit\ConsoleIO;
16
17
class StatementProfiler implements ProfilerInterface
18
{
19
20
	/**
21
	 * Is the profiler active?
22
	 *
23
	 * @var boolean
24
	 */
25
	protected $active = false;
26
27
	/**
28
	 * Retained profiles.
29
	 *
30
	 * @var array
31
	 */
32
	protected $profiles = array();
33
34
	/**
35
	 * Track duplicate statements.
36
	 *
37
	 * @var boolean
38
	 */
39
	protected $trackDuplicates = true;
40
41
	/**
42
	 * Ignored duplicate statements.
43
	 *
44
	 * @var array
45
	 */
46
	protected $ignoredDuplicateStatements = array();
47
48
	/**
49
	 * Console IO.
50
	 *
51
	 * @var ConsoleIO
52
	 */
53
	private $_io;
54
55
	/**
56
	 * Debug mode.
57
	 *
58
	 * @var boolean
59
	 */
60
	private $_debugMode = false;
61
62
	/**
63
	 * Debug backtrace options.
64
	 *
65
	 * @var integer
66
	 */
67
	private $_backtraceOptions;
68
69 3
	/**
70
	 * Creates statement profiler instance.
71 3
	 */
72 3
	public function __construct()
73 3
	{
74
		$this->_backtraceOptions = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? DEBUG_BACKTRACE_IGNORE_ARGS : 0;
75
	}
76
77
	/**
78
	 * Sets IO.
79
	 *
80
	 * @param ConsoleIO $io Console IO.
81
	 *
82 116
	 * @return void
83
	 */
84 116
	public function setIO(ConsoleIO $io = null)
85 116
	{
86
		$this->_io = $io;
87
		$this->_debugMode = isset($io) && $io->isVerbose();
88
	}
89
90
	/**
91
	 * Adds statement to ignore list.
92
	 *
93
	 * @param string $statement The SQL query statement.
94 130
	 *
95
	 * @return void
96 130
	 */
97 130
	public function ignoreDuplicateStatement($statement)
98
	{
99
		$this->ignoredDuplicateStatements[] = $this->normalizeStatement($statement);
100
	}
101
102
	/**
103
	 * Turns the profiler on and off.
104 118
	 *
105
	 * @param boolean $active True to turn on, false to turn off.
106 118
	 *
107
	 * @return void
108
	 */
109
	public function setActive($active)
110
	{
111
		$this->active = (bool)$active;
112
	}
113
114
	/**
115
	 * Is the profiler active?
116 9
	 *
117
	 * @return boolean
118 9
	 */
119 9
	public function isActive()
120
	{
121
		return (bool)$this->active;
122
	}
123
124
	/**
125
	 * Toggle duplicate statement tracker.
126
	 *
127
	 * @param boolean $track Duplicate statement tracker status.
128
	 *
129
	 * @return void
130
	 */
131
	public function trackDuplicates($track)
132 116
	{
133
		$this->trackDuplicates = (bool)$track;
134
	}
135
136
	/**
137
	 * Adds a profile entry.
138 116
	 *
139 102
	 * @param float  $duration    The query duration.
140
	 * @param string $function    The PDO method that made the entry.
141
	 * @param string $statement   The SQL query statement.
142 113
	 * @param array  $bind_values The values bound to the statement.
143 113
	 *
144
	 * @return void
145 113
	 * @throws \PDOException When duplicate statement is detected.
146 113
	 */
147 113
	public function addProfile(
148 113
		$duration,
149 2
		$function,
150 2
		$statement,
151
		array $bind_values = array()
152 2
	) {
153
		if ( !$this->isActive() || $function === 'prepare' || !$statement ) {
154
			return;
155 113
		}
156 113
157 113
		$normalized_statement = $this->normalizeStatement($statement);
158 113
		$profile_key = $this->createProfileKey($normalized_statement, $bind_values);
159 113
160
		if ( $this->trackDuplicates
161
			&& !in_array($normalized_statement, $this->ignoredDuplicateStatements)
162 113
			&& isset($this->profiles[$profile_key])
163 2
		) {
164 2
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
165 2
			$error_msg = 'Duplicate statement:' . PHP_EOL . $substituted_normalized_statement;
166 2
167 2
			throw new \PDOException($error_msg);
168 2
		}
169 2
170 113
		$this->profiles[$profile_key] = array(
171
			'duration' => $duration,
172
			'function' => $function,
173
			'statement' => $statement,
174
			'bind_values' => $bind_values,
175
		);
176
177
		if ( $this->_debugMode ) {
178
			$trace = debug_backtrace($this->_backtraceOptions);
179
180 54
			do {
181
				$trace_line = array_shift($trace);
182 54
			} while ( $trace && strpos($trace_line['file'], 'aura/sql') !== false );
183 1
184
			$runtime = sprintf('%01.2f', $duration);
185
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
186 53
			$trace_file = substr($trace_line['file'], strpos($trace_line['file'], '/src/')) . ':' . $trace_line['line'];
187 53
			$this->_io->writeln(array(
188 53
				'',
189
				'<debug>[db, ' . round($runtime, 2) . 's]: ' . $substituted_normalized_statement . '</debug>',
190
				'<debug>[db origin]: ' . $trace_file . '</debug>',
191
			));
192
		}
193
	}
194
195
	/**
196
	 * Removes a profile entry.
197 127
	 *
198
	 * @param string $statement   The SQL query statement.
199 127
	 * @param array  $bind_values The values bound to the statement.
200
	 *
201
	 * @return void
202
	 */
203
	public function removeProfile($statement, array $bind_values = array())
204
	{
205
		if ( !$this->isActive() ) {
206
			return;
207
		}
208
209
		$normalized_statement = $this->normalizeStatement($statement);
210 113
		unset($this->profiles[$this->createProfileKey($normalized_statement, $bind_values)]);
211
	}
212 113
213
	/**
214
	 * Normalizes statement.
215
	 *
216
	 * @param string $statement The SQL query statement.
217
	 *
218
	 * @return string
219
	 */
220
	protected function normalizeStatement($statement)
221
	{
222
		return preg_replace('/\s+/', ' ', $statement);
223 4
	}
224
225 4
	/**
226
	 * Creates profile key.
227 4
	 *
228 4
	 * @param string $normalized_statement The normalized SQL query statement.
229 1
	 * @param array  $bind_values          The values bound to the statement.
230 1
	 *
231
	 * @return string
232 4
	 */
233
	protected function createProfileKey($normalized_statement, array $bind_values = array())
234
	{
235 4
		return md5('statement:' . $normalized_statement . ';bind_values:' . serialize($bind_values));
236 4
	}
237
238 4
	/**
239
	 * Substitutes parameters in the statement.
240
	 *
241
	 * @param string $normalized_statement The normalized SQL query statement.
242
	 * @param array  $bind_values          The values bound to the statement.
243
	 *
244
	 * @return string
245
	 */
246 14
	protected function substituteParameters($normalized_statement, array $bind_values = array())
247
	{
248 14
		arsort($bind_values);
249
250
		foreach ( $bind_values as $param_name => $param_value ) {
251
			if ( is_array($param_value) ) {
252
				$param_value = '"' . implode('","', $param_value) . '"';
253
			}
254
			else {
255
				$param_value = '"' . $param_value . '"';
256 2
			}
257
258 2
			$normalized_statement = str_replace(':' . $param_name, $param_value, $normalized_statement);
259 2
		}
260
261
		return $normalized_statement;
262
	}
263
264
	/**
265
	 * Returns all the profile entries.
266
	 *
267
	 * @return array
268
	 */
269
	public function getProfiles()
270
	{
271
		return $this->profiles;
272
	}
273
274
	/**
275
	 * Reset all the profiles
276
	 *
277
	 * @return void
278
	 */
279
	public function resetProfiles()
280
	{
281
		$this->profiles = array();
282
	}
283
284
}
285