Сегодня разберу какие изменения я внёс в модуль отладки от open4dev. Изменения простые и будут вполне понятны начинающим разработчикам. Чем меня не устраивал модуль отладки, я описывал в одном из постов.
Ключевые слова MySQL
Подсветка синтаксиса в модуле уже была. Вот она.
define('DEBUG_SQL_KEYWORDS', 'select|delete|update|count|as|from|and|order by|or|where|inner join|left join|right join|join|group by|having|on|asc|desc|limit');
Всё самое нужное уже есть. Можно сюда было загнать сразу все ключевые слова SQL, но время деньги, а ещё нужно пост писать. Так что я ограничился только тем, что сразу бросилось в глаза при анализе главной страницы Опенкарта с включённым модулем.
Находим 45 строку и меняем её на такую.
define('DEBUG_SQL_KEYWORDS', 'select|delete|update|count|as|from|and|order by|or|where|distinct|replace|into|date_sub|now|interval|hour|sum|lcase|set|inner join|left join|right join|join|group by|having|on|asc|desc|limit');
Я добавил несколько ключевых слов и встроенных функций. Результат на картинке ниже.

Добавление строки вызова
Как я уже рассказывал, делается через замечательный back_trace(). Для начала нужно добавить в класс QueryDebug поле для хранения строки. Находим такой участок кода.
class QueryDebug {
private $queries = array();
public function add($sql, $duration = 'not_defined') {
$this->queries[] = array('text' => $sql, 'duration' => $duration);
}
И превращаем его.
class QueryDebug {
private $queries = array();
public function add($sql, $duration = 'not_defined', $backtrace = array()) {
$this->queries[] = array('text' => $sql, 'duration' => $duration, 'backtrace' => $backtrace);
}
Этим мы научили объект QueryDebug сохранять массив $backtrace в массиве отчётах о запросах $queies[]. Теперь нужно поправить вызов метода add(). Его можно найти дальше в модуле.
<!-- Wrap the query operation to count the time took and add it to our class (< 2.2.0.0)-->
<operation>
<ignoreif>$this->adaptor->query</ignoreif>
<search><![CDATA[return $this->db->query($sql);]]></search>
<add position="replace"><![CDATA[
$time_start = microtime(true);
$result = $this->db->query($sql);
$time_end = microtime(true);
$this->queryDebug->add($sql, ($time_end - $time_start));
return $result;
]]></add>
</operation>
Этот участок модуля для ocmod отвечает за замену строки возврата результата запроса к БД в опенкарте младше 2.2.0.0, на наш, который перехватывает запрос и сохраняет его в лог, попутно считая время выполнения. Добавим в лог информацию о стеке вызовов вызвав debug_backtrace() внутри этого метода.
<!-- Wrap the query operation to count the time took and add it to our class (< 2.2.0.0)-->
<operation>
<ignoreif>$this->adaptor->query</ignoreif>
<search><![CDATA[return $this->db->query($sql);]]></search>
<add position="replace"><![CDATA[
$time_start = microtime(true);
$result = $this->db->query($sql);
$time_end = microtime(true);
$this->queryDebug->add($sql, ($time_end - $time_start),debug_backtrace());
return $result;
]]></add>
</operation>
В опенкарте старше 2.2.0.0 вызов метода query() немного изменился и модуль это учитывает. Отдельная ветка изменений для свежего opencart 2.3.
Было:
<!-- Wrap the query operation to count the time took and add it to our class (>= 2.2.0.0)-->
<operation>
<ignoreif>$this->db->query</ignoreif>
<search><![CDATA[return $this->adaptor->query($sql, $params);]]></search>
<add position="replace"><![CDATA[
$time_start = microtime(true);
$result = $this->adaptor->query($sql, $params);
$time_end = microtime(true);
$this->queryDebug->add($sql, ($time_end - $time_start));
return $result;
]]></add>
</operation>
Стало после изменений в нашем коде.
<!-- Wrap the query operation to count the time took and add it to our class (>= 2.2.0.0)-->
<operation>
<ignoreif>$this->db->query</ignoreif>
<search><![CDATA[return $this->adaptor->query($sql, $params);]]></search>
<add position="replace"><![CDATA[
$time_start = microtime(true);
$result = $this->adaptor->query($sql, $params);
$time_end = microtime(true);
$this->queryDebug->add($sql, ($time_end - $time_start),debug_backtrace());
return $result;
]]></add>
</operation>
На этом изменения движка закончены. Пора приступить к выводу полезной информации.
Сортировка и вывод запросов SQL
Определимся, что нужно для вывода. Для начала неплохо, что бы файл вызова с указанием строки находился под SQL запросом. Потом нужна, конечно же, кнопка для группировки по файлам и неплохо бы сохранить аутентичные стили.
Стили самое простое — с них и начнём. Находим строку с определением стиля для вывода запросов.
$out .= ".query { white-space: normal; background-color: #efefef; margin-left: 90px; padding: 4px; border: 1px solid #bbbbbb; } " ."\n";
и просто добавляем к определнию стиля свой, пусть будет называться .line
$out .= ".query,.line { white-space: normal; background-color: #efefef; margin-left: 90px; padding: 4px; border: 1px solid #bbbbbb; } " ."\n";
Со стилями разобрались, давайте добавим кнопку. Находим строку с объявлениями кнопок.
$out .= '
Queries: ' . count($this->queries) . ', Total time: ' . number_format($total_time * 1000, 3) . ' ms [
By time] [
By id]
';
И добавляем новую кнопку [By line]
$out .= '
Queries: ' . count($this->queries) . ', Total time: ' . number_format($total_time * 1000, 3) . ' ms [
By time] [
By id] [
By line]
';
Ну вот. Кнопка на месте. Теперь нужно вывести саму строку с указанием файла и номера строки. Не забываем про css класс .line.
Находим
$out .= '
' . $count . ' (' . number_format($q['duration'] * 1000, 3) . ') ' . '
' . $sql . '
';
Меняем на
$out .= '
' . $count . ' (' . number_format($q['duration'] * 1000, 3) . ') ' . '
' . $sql . '
'.$q['backtrace'][0]['file'].':'.$q['backtrace'][0]['line'].'
';
Из кода видно как собирается строка с указанием файла и линии вызова. Теперь нужно добавить обработчик на нашу кнопку и отсортировать результаты. Всё очень просто.
Для начала находим хвости JS кода.
$out .= ' $(".queries.qrow").remove(); ' . "\n";
$out .= ' $(".queries").append(sorted); ' . "\n";
$out .= " return false; \n";
$out .= "}); \n";
А теперь перед ним вешаем наш обработчик.
$out .= '$("#query-sort-line").click(function(e) {' ."\n";
$out .= " e.stopPropagation(); \n";
$out .= " var sorted = $('.qrow').sort( function(a, b) { \n";
$out .= " var d = 0;\n";
$out .= " var t1 = $(a).find('.line').prop('innerHTML');\n";
$out .= " var file1 = t1.slice(0,t1.lastIndexOf(':'))\n";
$out .= " var file1Line = t1.slice(t1.lastIndexOf(':'))\n";
$out .= " var t1id = $(a).find('.count').prop('id').replace('c', '') || 0;\n";
$out .= " var t2 = $(b).find('.line').prop('innerHTML');\n";
$out .= " var file2 = t2.slice(0,t2.lastIndexOf(':'))\n";
$out .= " var file2Line = t2.slice(t2.lastIndexOf(':'));\n";
$out .= " var t2id = $(b).find('.count').prop('id').replace('c', '') || 0;\n";
$out .= " d = file1.localeCompare(file2);\n";
$out .= " if (d==0){\n";
$out .= " d = file1Line.localeCompare(file2Line);\n";
$out .= " if (d==0){\n";
$out .= " d = t1id - t2id;\n";
$out .= " };\n";
$out .= " };\n";
$out .= " return d;\n";
$out .= " });\n";
Выглядит страшно и ужасно, но работает надёжно. Сначала получаем нужные нам для последующий сортировки параметры: путь к файлу, номер строки, порядковый номер вызова. Сначала сравниваем имена файлов, если они совпали, значит сравниваем номер строки вызова. Если же и номер совпадает, то просто сортируем по ID. Это позволяет быстро оценить масштабы бедствия.
Вывод максимального объёма выделенной памяти
В добавок ко всему выше написанному можно в две строчки добавить вывод. Добавить к стилю .total, стиль .memory
$out .= ".total,.memory { font-weight: bold; margin: 0px 10px 10px 95px; border-bottom: 1px solid #bbbbbb;} " ."\n";
А перед выводом кнопок сделать вывод memory_get_peak_usage().
$out .= '
Total memory used: '.round(memory_get_peak_usage()/1024/1024,2).' MB
';
Репозиторий проекта на github — Opencart-debug-database-queries