Сегодня разберу какие изменения я внёс в модуль отладки от 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 .= ' ';
И добавляем новую кнопку [By line]
$out .= ' ';
Ну вот. Кнопка на месте. Теперь нужно вывести саму строку с указанием файла и номера строки. Не забываем про 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