Completed
Push — master ( 3797b2...10d496 )
by recca
02:03
created

DatabasePanel::getDatabaseDriver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Recca0120\LaravelTracy\Panels;
4
5
use PDO;
6
use DateTime;
7
use Exception;
8
9
class DatabasePanel extends AbstractSubscribePanel implements IAjaxPanel
10
{
11
    /**
12
     * $queries.
13
     *
14
     * @var array
15
     */
16
    protected $queries = [];
17
18
    /**
19
     * $totalTime.
20
     *
21
     * @var float
22
     */
23
    protected $totalTime = 0.0;
24
25
    /**
26
     * $counter.
27
     *
28
     * @var int
29
     */
30
    protected $counter = 0;
31
32
    /**
33
     * subscribe.
34
     *
35
     * @method subscribe
36
     */
37 2
    protected function subscribe()
38
    {
39 2
        $events = $this->laravel['events'];
40 2
        if (version_compare($this->laravel->version(), 5.2, '>=') === true) {
41
            $events->listen('Illuminate\Database\Events\QueryExecuted', function ($event) {
42 1
                $this->logQuery(
43 1
                    $event->sql,
44 1
                    $event->bindings,
45 1
                    $event->time,
46 1
                    $event->connectionName,
47 1
                    $event->connection->getPdo()
48 1
                );
49 1
            });
50 1
        } else {
51
            $events->listen('illuminate.query', function ($sql, $bindings, $time, $connectionName) {
52 1
                $this->logQuery(
53 1
                    $sql,
54 1
                    $bindings,
55 1
                    $time,
56 1
                    $connectionName,
57 1
                    $this->laravel['db']->connection($connectionName)->getPdo()
58 1
                );
59 1
            });
60
        }
61 2
    }
62
63
    /**
64
     * logQuery.
65
     *
66
     * @method logQuery
67
     *
68
     * @param string $sql
69
     * @param array  $bindings
70
     * @param int    $time
71
     * @param string $name
72
     * @param PDO    $pdo
73
     * @param string $driver
74
     *
75
     * @return static
76
     */
77 3
    public function logQuery($sql, $bindings = [], $time = 0, $name = null, PDO $pdo = null, $driver = 'mysql')
78
    {
79 3
        ++$this->counter;
80 3
        $this->totalTime += $time;
81 3
        $source = self::findSource();
82 3
        $editorLink = self::editorLink($source);
0 ignored issues
show
Bug introduced by
It seems like $source defined by self::findSource() on line 81 can also be of type null; however, Recca0120\LaravelTracy\P...ractPanel::editorLink() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
83 3
        $this->queries[] = [
84 3
            'sql' => $sql,
85 3
            'bindings' => $bindings,
86 3
            'time' => $time,
87 3
            'name' => $name,
88 3
            'pdo' => $pdo,
89 3
            'driver' => $driver,
90 3
            'source' => $source,
91 3
            'editorLink' => $editorLink,
92 3
            'formattedSql' => null,
93 3
            'fullSql' => null,
94
        ];
95
96 3
        return $this;
97
    }
98
99
    /**
100
     * prepare sql.
101
     *
102
     * @param string $sql
103
     * @param array  $bindings
104
     *
105
     * @return string
106
     */
107 4
    public static function prepareBindings($sql, $bindings = [])
108
    {
109
        $bindings = array_map(function ($binding) {
110 3
            if (is_array($binding) === true) {
111
                $binding = implode(',', array_map(function ($value) {
112 1
                    return is_string($value) === true ? htmlspecialchars('\''.$value.'\'', ENT_NOQUOTES, 'UTF-8') : $value;
113 1
                }, $binding));
114
115 1
                return htmlspecialchars('('.$binding.')', ENT_NOQUOTES, 'UTF-8');
116
            }
117
118 3
            if ($binding instanceof DateTime) {
119 1
                return htmlspecialchars('\''.$binding->format('Y-m-d H:i:s').'\'', ENT_NOQUOTES, 'UTF-8');
120
            }
121
122 3
            return is_string($binding) === true ? htmlspecialchars('\''.$binding.'\'', ENT_NOQUOTES, 'UTF-8') : $binding;
123 4
        }, $bindings);
124
125 4
        $sql = str_replace(['%', '?'], ['%%', '%s'], $sql);
126 4
        $sql = vsprintf($sql, $bindings);
127
128 4
        return $sql;
129
    }
130
131
    /**
132
     * explain sql.
133
     *
134
     * @param PDO   $pdo
135
     * @param string $sql
136
     * @param array  $bindings
137
     *
138
     * @return array
139
     */
140 1
    public static function explain(PDO $pdo, $sql, $bindings = [])
141
    {
142 1
        $explains = [];
143 1
        if (preg_match('#\s*\(?\s*SELECT\s#iA', $sql)) {
144 1
            $statement = $pdo->prepare('EXPLAIN '.$sql);
145 1
            $statement->execute($bindings);
146 1
            $explains = $statement->fetchAll(PDO::FETCH_CLASS);
147 1
        }
148
149 1
        return $explains;
150
    }
151
152
    /**
153
     * Returns syntax highlighted SQL command.
154
     *
155
     * @param string $sql
156
     * @param array  $params
157
     * @param \PDO   $connection
158
     *
159
     * @return string
160
     */
161 4
    public static function formatSql($sql, array $params = null, PDO $connection = null)
162
    {
163 4
        static $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';
164 4
        static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|[RI]?LIKE|REGEXP|TRUE|FALSE';
165
166
        // insert new lines
167 4
        $sql = " $sql ";
168 4
        $sql = preg_replace("#(?<=[\\s,(])($keywords1)(?=[\\s,)])#i", "\n\$1", $sql);
169
170
        // reduce spaces
171 4
        $sql = preg_replace('#[ \t]{2,}#', ' ', $sql);
172
173 4
        $sql = wordwrap($sql, 100);
174 4
        $sql = preg_replace('#([ \t]*\r?\n){2,}#', "\n", $sql);
175
176
        // syntax highlight
177 4
        $sql = htmlspecialchars($sql, ENT_IGNORE, 'UTF-8');
178
        $sql = preg_replace_callback("#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is", function ($matches) {
179 4
            if (! empty($matches[1])) { // comment
180 1
                return '<em style="color:gray">'.$matches[1].'</em>';
181 4
            } elseif (! empty($matches[2])) { // error
182 1
                return '<strong style="color:red">'.$matches[2].'</strong>';
183 4
            } elseif (! empty($matches[3])) { // most important keywords
184 4
                return '<strong style="color:blue; text-transform: uppercase;">'.$matches[3].'</strong>';
185 1
            } elseif (! empty($matches[4])) { // other keywords
186 1
                return '<strong style="color:green">'.$matches[4].'</strong>';
187
            }
188 4
        }, $sql);
189
190
        // parameters
191 4
        $sql = preg_replace_callback('#\?#', function () use ($params, $connection) {
192 1
            static $i = 0;
193 1
            if (! isset($params[$i])) {
194
                return '?';
195
            }
196 1
            $param = $params[$i++];
197 1
            if (is_string($param) && (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $param) || preg_last_error())) {
198
                return '<i title="Length '.strlen($param).' bytes">&lt;binary&gt;</i>';
199 1
            } elseif (is_string($param)) {
200 1
                $text = htmlspecialchars($connection ? $connection->quote($param) : '\''.$param.'\'', ENT_NOQUOTES, 'UTF-8');
201
202 1
                return '<span title="Length '.strlen($text).' characters">'.$text.'</span>';
203 1
            } elseif (is_resource($param)) {
204
                $type = get_resource_type($param);
205
                if ($type === 'stream') {
206
                    $info = stream_get_meta_data($param);
207
                }
208
209
                return '<i'.(isset($info['uri']) ? ' title="'.htmlspecialchars($info['uri'], ENT_NOQUOTES, 'UTF-8').'"' : null)
210
                    .'>&lt;'.htmlspecialchars($type, ENT_NOQUOTES, 'UTF-8').' resource&gt;</i> ';
211
            } else {
212 1
                $result = htmlspecialchars($param, ENT_NOQUOTES, 'UTF-8');
213
214 1
                return ($param instanceof DateTime) ? '\''.$result.'\'' : $result;
215
            }
216 4
        }, $sql);
217
218 4
        return '<pre class="dump">'.trim($sql)."</pre>\n";
219
    }
220
221
    /**
222
     * perform quer analysis hint.
223
     *
224
     * @param string $sql
225
     * @param string $version
226
     * @param float  $driver
227
     *
228
     * @return array
229
     */
230 2
    public static function performQueryAnalysis($sql, $version = null, $driver = null)
231
    {
232 2
        $hints = [];
233 2
        if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $sql)) {
234 2
            $hints[] = 'Use <code>SELECT *</code> only if you need all columns from table';
235 2
        }
236 2
        if (preg_match('/ORDER BY RAND()/i', $sql)) {
237 1
            $hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can.
238
                You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work">read this</a>
239
                or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function">this</a>';
240 1
        }
241 2
        if (strpos($sql, '!=') !== false) {
242 1
            $hints[] = 'The <code>!=</code> operator is not standard. Use the <code>&lt;&gt;</code> operator to test for inequality instead.';
243 1
        }
244 2
        if (stripos($sql, 'WHERE') === false) {
245 1
            $hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended';
246 1
        }
247 2
        if (preg_match('/LIMIT\\s/i', $sql) && stripos($sql, 'ORDER BY') === false) {
248 1
            $hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan';
249 1
        }
250 2
        if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $sql, $matches)) {
251 1
            $hints[] = 'An argument has a leading wildcard character: <code>'.$matches[1].'</code>.
252 1
                The predicate with this argument is not sargable and cannot use an index if one exists.';
253 1
        }
254 2
        if ($version < 5.5 && $driver === 'mysql') {
255 2
            if (preg_match('/\\sIN\\s*\\(\\s*SELECT/i', $sql)) {
256 1
                $hints[] = '<code>IN()</code> and <code>NOT IN()</code> subqueries are poorly optimized in that MySQL version : '.$version.
257 1
                    '. MySQL executes the subquery as a dependent subquery for each row in the outer query';
258 1
            }
259 2
        }
260
261 2
        return $hints;
262
    }
263
264
    /**
265
     * getAttributes.
266
     *
267
     * @method getAttributes
268
     *
269
     * @return array
270
     */
271 3
    protected function getAttributes()
272
    {
273 3
        $queries = [];
274 3
        foreach ($this->queries as $query) {
275 3
            $sql = $query['sql'];
276 3
            $bindings = $query['bindings'];
277 3
            $pdo = $query['pdo'];
278 3
            $driver = $query['driver'];
279 3
            $version = 0;
280
281 3
            $fullSql = self::prepareBindings($sql, $bindings);
282 3
            $formattedSql = self::formatSql($fullSql);
283
284 3
            $explains = [];
285 3
            $hints = [];
286 3
            if ($pdo instanceof PDO) {
287 2
                $driver = $this->getDatabaseDriver($pdo);
288 2
                if ($driver === 'mysql') {
289 1
                    $version = $this->getDatabaseVersion($pdo);
290 1
                    $explains = static::explain($pdo, $sql, $bindings);
291 1
                    $hints = static::performQueryAnalysis($fullSql, $version, $driver);
292 1
                }
293 2
            }
294
295 3
            $queries[] = array_merge($query, compact('fullSql', 'formattedSql', 'explains', 'hints', 'driver', 'version'));
296 3
        }
297
298
        return [
299 3
            'counter' => $this->counter,
300 3
            'totalTime' => $this->totalTime,
301 3
            'queries' => $queries,
302 3
        ];
303
    }
304
305
    /**
306
     * getDatabaseDriver.
307
     *
308
     * @param  Pdo    $pdo
309
     *
310
     * @return string
311
     */
312 2
    protected function getDatabaseDriver(Pdo $pdo)
313
    {
314
        try {
315 2
            $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
316 2
        } catch (Exception $e) {
317 1
            $driver = null;
318
        }
319
320 2
        return $driver;
321
    }
322
323
    /**
324
     * getDatabaseVersion.
325
     *
326
     * @param  Pdo    $pdo
327
     *
328
     * @return string
329
     */
330 1
    protected function getDatabaseVersion(Pdo $pdo)
331
    {
332
        try {
333 1
            $version = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
334 1
        } catch (Exception $e) {
335 1
            $version = 0;
336
        }
337
338 1
        return $version;
339
    }
340
}
341