1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Doctrine\Bundle\DoctrineBundle\Twig; |
4
|
|
|
|
5
|
|
|
use SqlFormatter; |
6
|
|
|
use Symfony\Component\VarDumper\Cloner\Data; |
7
|
|
|
use Twig_Extension; |
8
|
|
|
use Twig_SimpleFilter; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* This class contains the needed functions in order to do the query highlighting |
12
|
|
|
*/ |
13
|
|
|
class DoctrineExtension extends Twig_Extension |
14
|
|
|
{ |
15
|
|
|
/** |
16
|
|
|
* Number of maximum characters that one single line can hold in the interface |
17
|
|
|
* |
18
|
|
|
* @var int |
19
|
|
|
*/ |
20
|
|
|
private $maxCharWidth = 100; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Define our functions |
24
|
|
|
* |
25
|
|
|
* @return Twig_SimpleFilter[] |
26
|
|
|
*/ |
27
|
|
|
public function getFilters() |
28
|
|
|
{ |
29
|
|
|
return [ |
30
|
|
|
new Twig_SimpleFilter('doctrine_minify_query', [$this, 'minifyQuery'], ['deprecated' => true]), |
31
|
|
|
new Twig_SimpleFilter('doctrine_pretty_query', [$this, 'formatQuery'], ['is_safe' => ['html']]), |
32
|
|
|
new Twig_SimpleFilter('doctrine_replace_query_parameters', [$this, 'replaceQueryParameters']), |
33
|
|
|
]; |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* Get the possible combinations of elements from the given array |
38
|
|
|
* |
39
|
|
|
* @param array $elements |
40
|
|
|
* @param int $combinationsLevel |
41
|
|
|
* |
42
|
|
|
* @return array |
43
|
|
|
*/ |
44
|
|
|
private function getPossibleCombinations(array $elements, $combinationsLevel) |
45
|
|
|
{ |
46
|
|
|
$baseCount = count($elements); |
47
|
|
|
$result = []; |
48
|
|
|
|
49
|
|
|
if ($combinationsLevel === 1) { |
50
|
|
|
foreach ($elements as $element) { |
51
|
|
|
$result[] = [$element]; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
return $result; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
$nextLevelElements = $this->getPossibleCombinations($elements, $combinationsLevel - 1); |
58
|
|
|
|
59
|
|
|
foreach ($nextLevelElements as $nextLevelElement) { |
60
|
|
|
$lastElement = $nextLevelElement[$combinationsLevel - 2]; |
61
|
|
|
$found = false; |
62
|
|
|
|
63
|
|
|
foreach ($elements as $key => $element) { |
64
|
|
|
if ($element === $lastElement) { |
65
|
|
|
$found = true; |
66
|
|
|
continue; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
if ($found !== true || $key >= $baseCount) { |
70
|
|
|
continue; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
$tmp = $nextLevelElement; |
74
|
|
|
$newCombination = array_slice($tmp, 0); |
75
|
|
|
$newCombination[] = $element; |
76
|
|
|
$result[] = array_slice($newCombination, 0); |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
return $result; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Shrink the values of parameters from a combination |
85
|
|
|
* |
86
|
|
|
* @param array $parameters |
87
|
|
|
* @param array $combination |
88
|
|
|
* |
89
|
|
|
* @return string |
90
|
|
|
*/ |
91
|
|
|
private function shrinkParameters(array $parameters, array $combination) |
92
|
|
|
{ |
93
|
|
|
array_shift($parameters); |
94
|
|
|
$result = ''; |
95
|
|
|
|
96
|
|
|
$maxLength = $this->maxCharWidth; |
97
|
|
|
$maxLength -= count($parameters) * 5; |
98
|
|
|
$maxLength /= count($parameters); |
99
|
|
|
|
100
|
|
|
foreach ($parameters as $key => $value) { |
101
|
|
|
$isLarger = false; |
102
|
|
|
|
103
|
|
|
if (strlen($value) > $maxLength) { |
104
|
|
|
$value = wordwrap($value, $maxLength, "\n", true); |
105
|
|
|
$value = explode("\n", $value); |
106
|
|
|
$value = $value[0]; |
107
|
|
|
|
108
|
|
|
$isLarger = true; |
109
|
|
|
} |
110
|
|
|
$value = self::escapeFunction($value); |
111
|
|
|
|
112
|
|
|
if (! is_numeric($value)) { |
113
|
|
|
$value = substr($value, 1, -1); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
if ($isLarger) { |
117
|
|
|
$value .= ' [...]'; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$result .= ' ' . $combination[$key] . ' ' . $value; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
return trim($result); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Attempt to compose the best scenario minified query so that a user could find it without expanding it |
128
|
|
|
* |
129
|
|
|
* @param string $query |
130
|
|
|
* @param array $keywords |
131
|
|
|
* @param int $required |
132
|
|
|
* |
133
|
|
|
* @return string |
134
|
|
|
*/ |
135
|
|
|
private function composeMiniQuery($query, array $keywords, $required) |
136
|
|
|
{ |
137
|
|
|
// Extract the mandatory keywords and consider the rest as optional keywords |
138
|
|
|
$mandatoryKeywords = array_splice($keywords, 0, $required); |
139
|
|
|
|
140
|
|
|
$combinations = []; |
141
|
|
|
$combinationsCount = count($keywords); |
142
|
|
|
|
143
|
|
|
// Compute all the possible combinations of keywords to match the query for |
144
|
|
|
while ($combinationsCount > 0) { |
145
|
|
|
$combinations = array_merge($combinations, $this->getPossibleCombinations($keywords, $combinationsCount)); |
146
|
|
|
$combinationsCount--; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
// Try and match the best case query pattern |
150
|
|
|
foreach ($combinations as $combination) { |
151
|
|
|
$combination = array_merge($mandatoryKeywords, $combination); |
152
|
|
|
|
153
|
|
|
$regexp = implode('(.*) ', $combination) . ' (.*)'; |
154
|
|
|
$regexp = '/^' . $regexp . '/is'; |
155
|
|
|
|
156
|
|
|
if (preg_match($regexp, $query, $matches)) { |
157
|
|
|
return $this->shrinkParameters($matches, $combination); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
// Try and match the simplest query form that contains only the mandatory keywords |
162
|
|
|
$regexp = implode(' (.*)', $mandatoryKeywords) . ' (.*)'; |
163
|
|
|
$regexp = '/^' . $regexp . '/is'; |
164
|
|
|
|
165
|
|
|
if (preg_match($regexp, $query, $matches)) { |
166
|
|
|
return $this->shrinkParameters($matches, $mandatoryKeywords); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
// Fallback in case we didn't managed to find any good match (can we actually have that happen?!) |
170
|
|
|
return substr($query, 0, $this->maxCharWidth); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Minify the query |
175
|
|
|
* |
176
|
|
|
* @param string $query |
177
|
|
|
* |
178
|
|
|
* @return string |
179
|
|
|
*/ |
180
|
|
|
public function minifyQuery($query) |
181
|
|
|
{ |
182
|
|
|
$result = ''; |
183
|
|
|
$keywords = []; |
184
|
|
|
$required = 1; |
185
|
|
|
|
186
|
|
|
// Check if we can match the query against any of the major types |
187
|
|
|
switch (true) { |
188
|
|
View Code Duplication |
case stripos($query, 'SELECT') !== false: |
|
|
|
|
189
|
|
|
$keywords = ['SELECT', 'FROM', 'WHERE', 'HAVING', 'ORDER BY', 'LIMIT']; |
190
|
|
|
$required = 2; |
191
|
|
|
break; |
192
|
|
|
|
193
|
|
View Code Duplication |
case stripos($query, 'DELETE') !== false: |
|
|
|
|
194
|
|
|
$keywords = ['DELETE', 'FROM', 'WHERE', 'ORDER BY', 'LIMIT']; |
195
|
|
|
$required = 2; |
196
|
|
|
break; |
197
|
|
|
|
198
|
|
View Code Duplication |
case stripos($query, 'UPDATE') !== false: |
|
|
|
|
199
|
|
|
$keywords = ['UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT']; |
200
|
|
|
$required = 2; |
201
|
|
|
break; |
202
|
|
|
|
203
|
|
View Code Duplication |
case stripos($query, 'INSERT') !== false: |
|
|
|
|
204
|
|
|
$keywords = ['INSERT', 'INTO', 'VALUE', 'VALUES']; |
205
|
|
|
$required = 2; |
206
|
|
|
break; |
207
|
|
|
|
208
|
|
|
// If there's no match so far just truncate it to the maximum allowed by the interface |
209
|
|
|
default: |
210
|
|
|
$result = substr($query, 0, $this->maxCharWidth); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
// If we had a match then we should minify it |
214
|
|
|
if ($result === '') { |
215
|
|
|
$result = $this->composeMiniQuery($query, $keywords, $required); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
return $result; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Escape parameters of a SQL query |
223
|
|
|
* DON'T USE THIS FUNCTION OUTSIDE ITS INTENDED SCOPE |
224
|
|
|
* |
225
|
|
|
* @internal |
226
|
|
|
* |
227
|
|
|
* @param mixed $parameter |
228
|
|
|
* |
229
|
|
|
* @return string |
|
|
|
|
230
|
|
|
*/ |
231
|
|
|
public static function escapeFunction($parameter) |
232
|
|
|
{ |
233
|
|
|
$result = $parameter; |
234
|
|
|
|
235
|
|
|
switch (true) { |
236
|
|
|
// Check if result is non-unicode string using PCRE_UTF8 modifier |
237
|
|
|
case is_string($result) && ! preg_match('//u', $result): |
238
|
|
|
$result = '0x' . strtoupper(bin2hex($result)); |
239
|
|
|
break; |
240
|
|
|
|
241
|
|
|
case is_string($result): |
242
|
|
|
$result = "'" . addslashes($result) . "'"; |
243
|
|
|
break; |
244
|
|
|
|
245
|
|
|
case is_array($result): |
246
|
|
|
foreach ($result as &$value) { |
247
|
|
|
$value = static::escapeFunction($value); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$result = implode(', ', $result); |
251
|
|
|
break; |
252
|
|
|
|
253
|
|
|
case is_object($result): |
254
|
|
|
$result = addslashes((string) $result); |
255
|
|
|
break; |
256
|
|
|
|
257
|
|
|
case $result === null: |
258
|
|
|
$result = 'NULL'; |
259
|
|
|
break; |
260
|
|
|
|
261
|
|
|
case is_bool($result): |
262
|
|
|
$result = $result ? '1' : '0'; |
263
|
|
|
break; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
return $result; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Return a query with the parameters replaced |
271
|
|
|
* |
272
|
|
|
* @param string $query |
273
|
|
|
* @param array|Data $parameters |
274
|
|
|
* |
275
|
|
|
* @return string |
276
|
|
|
*/ |
277
|
|
|
public function replaceQueryParameters($query, $parameters) |
278
|
|
|
{ |
279
|
|
|
if ($parameters instanceof Data) { |
280
|
|
|
// VarDumper < 3.3 compatibility layer |
281
|
|
|
$parameters = method_exists($parameters, 'getValue') ? $parameters->getValue(true) : $parameters->getRawData(); |
|
|
|
|
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
$i = 0; |
285
|
|
|
|
286
|
|
|
if (! array_key_exists(0, $parameters) && array_key_exists(1, $parameters)) { |
287
|
|
|
$i = 1; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
return preg_replace_callback( |
291
|
|
|
'/\?|((?<!:):[a-z0-9_]+)/i', |
292
|
|
|
static function ($matches) use ($parameters, &$i) { |
293
|
|
|
$key = substr($matches[0], 1); |
294
|
|
|
if (! array_key_exists($i, $parameters) && ($key === false || ! array_key_exists($key, $parameters))) { |
295
|
|
|
return $matches[0]; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$value = array_key_exists($i, $parameters) ? $parameters[$i] : $parameters[$key]; |
299
|
|
|
$result = DoctrineExtension::escapeFunction($value); |
300
|
|
|
$i++; |
301
|
|
|
|
302
|
|
|
return $result; |
303
|
|
|
}, |
304
|
|
|
$query |
305
|
|
|
); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Formats and/or highlights the given SQL statement. |
310
|
|
|
* |
311
|
|
|
* @param string $sql |
312
|
|
|
* @param bool $highlightOnly If true the query is not formatted, just highlighted |
313
|
|
|
* |
314
|
|
|
* @return string |
315
|
|
|
*/ |
316
|
|
|
public function formatQuery($sql, $highlightOnly = false) |
317
|
|
|
{ |
318
|
|
|
SqlFormatter::$pre_attributes = 'class="highlight highlight-sql"'; |
319
|
|
|
SqlFormatter::$quote_attributes = 'class="string"'; |
320
|
|
|
SqlFormatter::$backtick_quote_attributes = 'class="string"'; |
321
|
|
|
SqlFormatter::$reserved_attributes = 'class="keyword"'; |
322
|
|
|
SqlFormatter::$boundary_attributes = 'class="symbol"'; |
323
|
|
|
SqlFormatter::$number_attributes = 'class="number"'; |
324
|
|
|
SqlFormatter::$word_attributes = 'class="word"'; |
325
|
|
|
SqlFormatter::$error_attributes = 'class="error"'; |
326
|
|
|
SqlFormatter::$comment_attributes = 'class="comment"'; |
327
|
|
|
SqlFormatter::$variable_attributes = 'class="variable"'; |
328
|
|
|
|
329
|
|
|
if ($highlightOnly) { |
330
|
|
|
$html = SqlFormatter::highlight($sql); |
331
|
|
|
$html = preg_replace('/<pre class=".*">([^"]*+)<\/pre>/Us', '\1', $html); |
332
|
|
|
} else { |
333
|
|
|
$html = SqlFormatter::format($sql); |
334
|
|
|
$html = preg_replace('/<pre class="(.*)">([^"]*+)<\/pre>/Us', '<div class="\1"><pre>\2</pre></div>', $html); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return $html; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Get the name of the extension |
342
|
|
|
* |
343
|
|
|
* @return string |
344
|
|
|
*/ |
345
|
|
|
public function getName() |
346
|
|
|
{ |
347
|
|
|
return 'doctrine_extension'; |
348
|
|
|
} |
349
|
|
|
} |
350
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.