Completed
Push — master ( a04d7d...af82d7 )
by Alexander
03:22
created

StatementProfiler::trackDuplicates()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
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
	/**
70
	 * Creates statement profiler instance.
71
	 */
72 40
	public function __construct()
73
	{
74 40
		$this->_backtraceOptions = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? DEBUG_BACKTRACE_IGNORE_ARGS : 0;
75 40
	}
76
77
	/**
78
	 * Sets IO.
79
	 *
80
	 * @param ConsoleIO $io Console IO.
81
	 *
82
	 * @return void
83
	 */
84 3
	public function setIO(ConsoleIO $io = null)
85
	{
86 3
		$this->_io = $io;
87 3
		$this->_debugMode = isset($io) && $io->isVerbose();
88 3
	}
89
90
	/**
91
	 * Adds statement to ignore list.
92
	 *
93
	 * @param string $statement The SQL query statement.
94
	 *
95
	 * @return void
96
	 */
97 23
	public function ignoreDuplicateStatement($statement)
98
	{
99 23
		$this->ignoredDuplicateStatements[] = $this->normalizeStatement($statement);
100 23
	}
101
102
	/**
103
	 * Turns the profiler on and off.
104
	 *
105
	 * @param boolean $active True to turn on, false to turn off.
106
	 *
107
	 * @return void
108
	 */
109 37
	public function setActive($active)
110
	{
111 37
		$this->active = (bool)$active;
112 37
	}
113
114
	/**
115
	 * Is the profiler active?
116
	 *
117
	 * @return boolean
118
	 */
119 37
	public function isActive()
120
	{
121 37
		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 28
	public function trackDuplicates($track)
132
	{
133 28
		$this->trackDuplicates = (bool)$track;
134 28
	}
135
136
	/**
137
	 * Adds a profile entry.
138
	 *
139
	 * @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
	 * @param array  $bind_values The values bound to the statement.
143
	 *
144
	 * @return void
145
	 * @throws \PDOException When duplicate statement is detected.
146
	 */
147 35
	public function addProfile(
148
		$duration,
149
		$function,
150
		$statement,
151
		array $bind_values = array()
152
	) {
153 35
		if ( !$this->isActive() || $function === 'prepare' || !$statement ) {
154 21
			return;
155
		}
156
157 32
		$normalized_statement = $this->normalizeStatement($statement);
158 32
		$profile_key = $this->createProfileKey($normalized_statement, $bind_values);
159
160 32
		if ( $this->trackDuplicates
161 32
			&& !in_array($normalized_statement, $this->ignoredDuplicateStatements)
162 32
			&& isset($this->profiles[$profile_key])
163 32
		) {
164 2
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
165 2
			$error_msg = 'Duplicate statement:' . PHP_EOL . $substituted_normalized_statement;
166
167 2
			throw new \PDOException($error_msg);
168
		}
169
170 32
		$this->profiles[$profile_key] = array(
171 32
			'duration' => $duration,
172 32
			'function' => $function,
173 32
			'statement' => $statement,
174 32
			'bind_values' => $bind_values,
175
		);
176
177 32
		if ( $this->_debugMode ) {
178 2
			$trace = debug_backtrace($this->_backtraceOptions);
179
180
			do {
181 2
				$trace_line = array_shift($trace);
182 2
			} while ( $trace && strpos($trace_line['file'], 'aura/sql') !== false );
183
184 2
			$runtime = sprintf('%01.2f', $duration);
185 2
			$substituted_normalized_statement = $this->substituteParameters($normalized_statement, $bind_values);
186 2
			$trace_file = substr($trace_line['file'], strpos($trace_line['file'], '/src/')) . ':' . $trace_line['line'];
187 2
			$this->_io->writeln(array(
188 2
				'',
189 2
				'<debug>[db, ' . round($runtime, 2) . 's]: ' . $substituted_normalized_statement . '</debug>',
190 2
				'<debug>[db origin]: ' . $trace_file . '</debug>',
191 2
			));
192 2
		}
193 32
	}
194
195
	/**
196
	 * Removes a profile entry.
197
	 *
198
	 * @param string $statement   The SQL query statement.
199
	 * @param array  $bind_values The values bound to the statement.
200
	 *
201
	 * @return void
202
	 */
203 14
	public function removeProfile($statement, array $bind_values = array())
204
	{
205 14
		if ( !$this->isActive() ) {
206 1
			return;
207
		}
208
209 13
		$normalized_statement = $this->normalizeStatement($statement);
210 13
		unset($this->profiles[$this->createProfileKey($normalized_statement, $bind_values)]);
211 13
	}
212
213
	/**
214
	 * Normalizes statement.
215
	 *
216
	 * @param string $statement The SQL query statement.
217
	 *
218
	 * @return string
219
	 */
220 34
	protected function normalizeStatement($statement)
221
	{
222 34
		return preg_replace('/\s+/', ' ', $statement);
223
	}
224
225
	/**
226
	 * Creates profile key.
227
	 *
228
	 * @param string $normalized_statement The normalized SQL query statement.
229
	 * @param array  $bind_values          The values bound to the statement.
230
	 *
231
	 * @return string
232
	 */
233 32
	protected function createProfileKey($normalized_statement, array $bind_values = array())
234
	{
235 32
		return md5('statement:' . $normalized_statement . ';bind_values:' . serialize($bind_values));
236
	}
237
238
	/**
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 4
	protected function substituteParameters($normalized_statement, array $bind_values = array())
247
	{
248 4
		arsort($bind_values);
249
250 4
		foreach ( $bind_values as $param_name => $param_value ) {
251 4
			if ( is_array($param_value) ) {
252 1
				$param_value = '"' . implode('","', $param_value) . '"';
253 1
			}
254
			else {
255 4
				$param_value = '"' . $param_value . '"';
256
			}
257
258 4
			$normalized_statement = str_replace(':' . $param_name, $param_value, $normalized_statement);
259 4
		}
260
261 4
		return $normalized_statement;
262
	}
263
264
	/**
265
	 * Returns all the profile entries.
266
	 *
267
	 * @return array
268
	 */
269 14
	public function getProfiles()
270
	{
271 14
		return $this->profiles;
272
	}
273
274
	/**
275
	 * Reset all the profiles
276
	 *
277
	 * @return void
278
	 */
279 2
	public function resetProfiles()
280
	{
281 2
		$this->profiles = array();
282 2
	}
283
284
}
285