1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Recca0120\LaravelTracy\Panels; |
4
|
|
|
|
5
|
|
|
use DateTime; |
6
|
|
|
use PDO; |
7
|
|
|
|
8
|
|
|
class Helper |
9
|
|
|
{ |
10
|
|
|
/** |
11
|
|
|
* KEYWORDS1. |
12
|
|
|
* |
13
|
|
|
* @var string |
14
|
|
|
*/ |
15
|
|
|
const KEYWORDS1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE'; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* KEYWORDS2. |
19
|
|
|
* |
20
|
|
|
* @var string |
21
|
|
|
*/ |
22
|
|
|
const KEYWORDS2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE'; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Returns syntax highlighted SQL command. |
26
|
|
|
* |
27
|
|
|
* |
28
|
|
|
* @param string $sql |
29
|
|
|
* @param array $bindings |
30
|
|
|
* @param \PDO $pdo |
31
|
|
|
* @return string |
32
|
|
|
*/ |
33
|
3 |
|
public static function hightlight($sql, array $bindings = [], PDO $pdo = null) |
34
|
|
|
{ |
35
|
|
|
// insert new lines |
36
|
3 |
|
$sql = " $sql "; |
37
|
3 |
|
$sql = preg_replace('#(?<=[\\s,(])('.static::KEYWORDS1.')(?=[\\s,)])#i', "\n\$1", $sql); |
38
|
|
|
|
39
|
|
|
// reduce spaces |
40
|
3 |
|
$sql = preg_replace('#[ \t]{2,}#', ' ', $sql); |
41
|
|
|
|
42
|
|
|
// syntax highlight |
43
|
3 |
|
$sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8'); |
44
|
|
|
$sql = preg_replace_callback('#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])('.static::KEYWORDS1.')(?=[\\s,)])|(?<=[\\s,(=])('.static::KEYWORDS2.')(?=[\\s,)=])#is', function ($matches) { |
45
|
3 |
|
if (! empty($matches[1])) { // comment |
46
|
1 |
|
return '<em style="color:gray">'.$matches[1].'</em>'; |
47
|
3 |
|
} elseif (! empty($matches[2])) { // error |
48
|
1 |
|
return '<strong style="color:red">'.$matches[2].'</strong>'; |
49
|
3 |
|
} elseif (! empty($matches[3])) { // most important keywords |
50
|
3 |
|
return '<strong style="color:blue; text-transform: uppercase;">'.$matches[3].'</strong>'; |
51
|
1 |
|
} elseif (! empty($matches[4])) { // other keywords |
52
|
1 |
|
return '<strong style="color:green">'.$matches[4].'</strong>'; |
53
|
|
|
} |
54
|
3 |
|
}, $sql); |
55
|
|
|
|
56
|
|
|
$bindings = array_map(function ($binding) use ($pdo) { |
57
|
3 |
|
if (is_array($binding) === true) { |
58
|
|
|
$binding = implode(', ', array_map(function ($value) { |
59
|
1 |
|
return is_string($value) === true ? htmlspecialchars('\''.$value.'\'', ENT_NOQUOTES, 'UTF-8') : $value; |
60
|
1 |
|
}, $binding)); |
61
|
|
|
|
62
|
1 |
|
return htmlspecialchars('('.$binding.')', ENT_NOQUOTES, 'UTF-8'); |
63
|
|
|
} |
64
|
|
|
|
65
|
3 |
|
if (is_string($binding) === true && (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $binding) || preg_last_error())) { |
66
|
|
|
return '<i title="Length '.strlen($binding).' bytes"><binary></i>'; |
67
|
|
|
} |
68
|
|
|
|
69
|
3 |
|
if (is_string($binding) === true) { |
70
|
3 |
|
$text = htmlspecialchars($pdo ? $pdo->quote($binding) : '\''.$binding.'\'', ENT_NOQUOTES, 'UTF-8'); |
71
|
|
|
|
72
|
3 |
|
return '<span title="Length '.strlen($text).' characters">'.$text.'</span>'; |
73
|
|
|
} |
74
|
|
|
|
75
|
1 |
|
if (is_resource($binding) === true) { |
76
|
1 |
|
$type = get_resource_type($binding); |
77
|
1 |
|
if ($type === 'stream') { |
78
|
1 |
|
$info = stream_get_meta_data($binding); |
79
|
|
|
} |
80
|
|
|
|
81
|
1 |
|
return '<i'.(isset($info['uri']) ? ' title="'.htmlspecialchars($info['uri'], ENT_NOQUOTES, 'UTF-8').'"' : null) |
82
|
1 |
|
.'><'.htmlspecialchars($type, ENT_NOQUOTES, 'UTF-8').' resource></i>'; |
83
|
|
|
} |
84
|
|
|
|
85
|
1 |
|
if ($binding instanceof DateTime) { |
86
|
1 |
|
return htmlspecialchars('\''.$binding->format('Y-m-d H:i:s').'\'', ENT_NOQUOTES, 'UTF-8'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return htmlspecialchars($binding, ENT_NOQUOTES, 'UTF-8'); |
90
|
3 |
|
}, $bindings); |
91
|
3 |
|
$sql = str_replace(['%', '?'], ['%%', '%s'], $sql); |
92
|
|
|
|
93
|
3 |
|
return '<div><code>'.nl2br(trim(vsprintf($sql, $bindings))).'</code></div>'; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* perform quer analysis hint. |
98
|
|
|
* |
99
|
|
|
* @param string $sql |
100
|
|
|
* @param string $version |
101
|
|
|
* @param float $driver |
102
|
|
|
* @return array |
103
|
|
|
*/ |
104
|
2 |
|
public static function performQueryAnalysis($sql, $version = null, $driver = null) |
105
|
|
|
{ |
106
|
2 |
|
$hints = []; |
107
|
2 |
|
if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $sql)) { |
108
|
2 |
|
$hints[] = 'Use <code>SELECT *</code> only if you need all columns from table'; |
109
|
|
|
} |
110
|
2 |
|
if (preg_match('/ORDER BY RAND()/i', $sql)) { |
111
|
1 |
|
$hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can. |
112
|
|
|
You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work">read this</a> |
113
|
|
|
or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function">this</a>'; |
114
|
|
|
} |
115
|
2 |
|
if (strpos($sql, '!=') !== false) { |
116
|
1 |
|
$hints[] = 'The <code>!=</code> operator is not standard. Use the <code><></code> operator to test for inequality instead.'; |
117
|
|
|
} |
118
|
2 |
|
if (stripos($sql, 'WHERE') === false) { |
119
|
1 |
|
$hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended'; |
120
|
|
|
} |
121
|
2 |
|
if (preg_match('/LIMIT\\s/i', $sql) && stripos($sql, 'ORDER BY') === false) { |
122
|
1 |
|
$hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan'; |
123
|
|
|
} |
124
|
2 |
|
if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $sql, $matches)) { |
125
|
1 |
|
$hints[] = 'An argument has a leading wildcard character: <code>'.$matches[1].'</code>. |
126
|
|
|
The predicate with this argument is not sargable and cannot use an index if one exists.'; |
127
|
|
|
} |
128
|
2 |
|
if ($version < 5.5 && $driver === 'mysql') { |
129
|
2 |
|
if (preg_match('/\\sIN\\s*\\(\\s*SELECT/i', $sql)) { |
130
|
1 |
|
$hints[] = '<code>IN()</code> and <code>NOT IN()</code> subqueries are poorly optimized in that MySQL version : '.$version. |
131
|
1 |
|
'. MySQL executes the subquery as a dependent subquery for each row in the outer query'; |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
2 |
|
return $hints; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* explain sql. |
140
|
|
|
* |
141
|
|
|
* @param PDO $pdo |
142
|
|
|
* @param string $sql |
143
|
|
|
* @param array $bindings |
144
|
|
|
* @return array |
145
|
|
|
*/ |
146
|
1 |
|
public static function explain(PDO $pdo, $sql, $bindings = []) |
147
|
|
|
{ |
148
|
1 |
|
$explains = []; |
149
|
1 |
|
if (preg_match('#\s*\(?\s*SELECT\s#iA', $sql)) { |
150
|
1 |
|
$statement = $pdo->prepare('EXPLAIN '.$sql); |
151
|
1 |
|
$statement->execute($bindings); |
152
|
1 |
|
$explains = $statement->fetchAll(PDO::FETCH_CLASS); |
153
|
|
|
} |
154
|
|
|
|
155
|
1 |
|
return $explains; |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|