Реальные запросы в phalconphp

C самого первого дня использования phalconphp меня начал волновать вопрос: какие запросы реально приходят на выполнение в базу данных mysql от phalconphp.
Официальную документацию я прочел от корки до корки. В ней в разделе Logging Low-Level SQL Statements (название раздела звучит очень обнадеживающе) предлагается повесить обработчик на событие beforeQuery базы данных и из этого события записывать low-lewel запросы в лог. Дальше — больше: прямо сразу в следующем же разделе «Profiling SQL Statements» нам предлагают использовать профайлер, который запишет наш запрос в стек и засечет время начала выполнения запроса, в обработчике события afterQuery останавливаем профайл. И после формирования страницы пробегаемся по всем профайлам и записываем все в лог.
Как же велико было мое разочарование когда я увидел лог:

SELECT `users`.`id`, `users`.`email`, `users`.`name` FROM `users` WHERE `users`.`email` LIKE :email: LIMIT :2;
INSERT INTO `users` (`email`,`name`) VALUES (?, ?);

Какой же это б**ть low-lewel sql?? Прям lower больше уже некуда %)
Такое положение вещей меня не устраивает, поэтому пришлось придумывать решение. Спешу поделиться тем, что у меня получилось.
Все сервисы, кроме роутера для удобства я инициализирую в одном файле app/config/services.php

< ?php 
/**
 * @return \Phalcon\DI\FactoryDefault
 */
function getDi() {
  static $di = null;
  if (!$di) {
    $di = new \Phalcon\DI\FactoryDefault();
  }
  return $di;
}
getDi()->setShared('config', include __DIR__ . '/config.php');
getDi()->setShared('logger', function() {
  $logger = new \Phalcon\Logger\Adapter\File(__DIR__ . '/../logs/' . date('Y-m-d') . '.log');
  return $logger;
});
getDi()->setShared('profiler', '\Phalcon\Db\Profiler');
getDi()->setShared('db', function() {
  $db = new \Phalcon\Db\Adapter\Pdo\Mysql(array(
    "host"   => getDi()->getConfig()->database->host,
    "username" => getDi()->getConfig()->database->username,
    "password" => getDi()->getConfig()->database->password,
    "dbname"   => getDi()->getConfig()->database->dbname,
    "charset"  => getDi()->getConfig()->database->charset,
    "options"  => array(
      PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES' . getDi()->getConfig()->database->charset,
    )
  ));
  getDi()->getEventsManager()->attach('db', function($event, $db) {
    $sql = $db->getSQLStatement();
    // service questions for clarification of structure of a database disturb me
    // you can remove the following block to see all queries
    if (strpos($sql, 'DESCRIBE') === 0) {
      return true;
    }
    static $profileStarted = false;
    if ($event->getType() == 'beforeQuery') {
      // Just in case we will deny queries on change and cleaning of a database.
      if (preg_match('/(drop|alter|truncate) /i', $sql, $operation)) {
        throw new \Exception('Operation '.$operation[1].' was denied', 405);
      }
      if ($profileStarted) {
        getDi()->getProfiler()->stopProfile();
      }
      $vars = $db->getSQLVariables();
      $keys       = array();
      $values     = array();
      $myKeys     = array();
      $pregMyKeys = array();
      if ($vars) {
        foreach ($vars as $placeHolder=>$var) {
          // fill array of placeholders
          if (is_string($placeHolder)) {
            $keys[] = '/:'.ltrim($placeHolder, ':').'/';
          } else {
            $keys[] = '/[\?]/';
          }
          $myKeys[] = '::myKey'.(++$num).'::';
          $pregMyKeys[] = '/::myKey'.$num.'::/';
          // fill array of values
          // It makes sense to use RawValue only in INSERT and UPDATE queries and only as values
          // in all other cases it will be inserted as a quoted string
          if ((strpos($sql, 'INSERT') === 0 || strpos($sql, 'UPDATE') === 0) && $var instanceof \Phalcon\Db\RawValue) {
            $var = $var->getValue();
          } elseif (is_null($var)) {
            $var = 'NULL';
          } elseif (is_numeric($var)) {
            $var = $var;
          } else {
            $var = getDi()->getDb()->escapeString(mb_substr($var, 0, 500));
          }
          $values[] = $var;
        }
        $sql = preg_replace($keys, $myKeys, $sql, 1);
        $sql = preg_replace($pregMyKeys, $values, $sql, 1);
      }
      $sql = preg_replace('/ (WHERE|FROM|LEFT JOIN|SET|GROUP BY|HAVING|ORDER BY|LIMIT)/', PHP_EOL . str_repeat(' ', 8) . '$1', $sql);
      $sql = PHP_EOL . str_repeat(' ', 8) . $sql;
      $memoryUsage = round(memory_get_peak_usage(true)/1024/1024, 2).'MB';
      getDi()->getProfiler()->startProfile($memoryUsage.' '.$sql);
      $profileStarted = true;
    }
    if ($event->getType() == 'afterQuery') {
      getDi()->getProfiler()->stopProfile();
      $profileStarted = false;
    }
  });
  $db->setEventsManager(getDi()->getEventsManager());
  return $db;
});

Подключаем все необходимые сервисы, подключаем ивент-менеджер к базе данных и непосредственно перед запросом заменяем все плейсхолдеры на соответствующие им значения с учетом типов значений, после чего у нас есть тот запрос, что уходит на выполнение в базу данных.
Сначала попробовал заменять ключи сразу на значения, но, тогда, если значение — строка и в ней есть по-какой-то причине знаки вопросов, то замена происходит коряво, вместо замены последующих плейсхолдеров в искомом SQLStatement значения подставляются внутрь уже вставленной строки. Ничего лучше, чем сначала заменить исходные плейсхолдеры на свои, вида «::myKey{Num}::», а потом уже заменить эти плейсхолдеры на реальные зачения — на скорую руку не придумалось. Решение работает, поэтому дальше заморачиваться не стал. Если знаете решение изящнее — предложите, буду благодарен.

Ну и осталось теперь сохранить все профайлы в лог в bootstrap файле public/index.php

< ?php
try {
  if (!isset($_GET['_url']) || $_GET['_url'] == '/index.html') {
    $_GET['_url'] = '/';
    $_SERVER['REQUEST_URI'] = '/index.php?_url=/';
  }
  include __DIR__ . '/../app/config/services.php';
  $application = new \Phalcon\Mvc\Application();
  $application->setDI(getDi());
  getDi()->getProfiler()->startProfile($_SERVER['REQUEST_METHOD'].' '.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
  getDi()->getProfiler()->stopProfile();
  echo $application->handle()->getContent();
} catch (Exception $e) {
  include 'error.php';
}
foreach (getDi()->getProfiler()->getProfiles() as $profile) {
  $time = sprintf('%01.4f', $profile->getTotalElapsedSeconds());
  getDi()->getLogger()->info($time.' sec: '.$profile->getSQLStatement());
}
$time = sprintf('%01.4f', getDi()->getProfiler()->getTotalElapsedSeconds());
getDi()->getLogger()->info($time.' sec: total' . PHP_EOL . PHP_EOL);

Сначала создаем профайл, котором запоминаем метод текущего запроса и запрошенный адрес и уже после формирования всей страницы пробегаем по всем итемам в профайлере и записываем в лог: время обработки каждого запроса, сам (настоящий low-lewel) запрос и потребляемую сайтом память на момент выполнения запроса.
Наконец-то теперь в логах именно те запросы, что приходят в базу данных. Их уже можно скопировать в phpmyadmin, поиграться с параметрами, проанализировать и при необзодимости внести правки в код. Надеюсь, что кому-то еще это решение будет полезным.

Тэг base и jquery tabs

Сегодня мне вынес мозг стандартный плагин jquery. Казалось бы, какие с ним-то могут быть трудности, давно все сколько-нибудь возможные баги выловлены, все пользуются, у всех все нормально.
У меня, в принципе, тоже. Было. До сегодняшнего дня.
На странице уже есть один tabs с тремя вкладками, контент в которые загружаются по аяксу. Все работает как положено.
И тут понадобился мне переключатель для двух табов. Аякс был не нужен, всего лишь нужно было, чтобы открывался по клику на вкладку див со сгенерированным яваскриптом графиком. Один график желательно, чтобы всегда был под рукой, а другой связанный с ним по смыслу, должен быть легкодоступным. То есть строим график в первой вкладке после загрузки страницы, а второй только после того, как открыли вторую вкладку по ивенту activate. Все довольно банально, делов на пару минут.
«А вот хрен тебе» — сказал мне плагин… И загрузил и вставил мне перед первым графиком главную страницу сайта.
Думаю, что это за хрень такая? Лезу проверять: ссылки в списке указывают на якоря, совпадают с идэшниками у дивов. Структура идентичная указанной в примере на jqueryui. На всякий случай вставляю в href идэшники копипастом, чтобы исключить ошибку по этой причине.
Обновляю страницу — перед контентом первой вкладки загружается аяксом главная страница сайта ( Смотрю в дебагер: в контейнере добавлены к моим двум дивам еще два дива, в один из которых загружена главная страница сайта.
Копипащу со страницы примера html код

tab1
tab2

их рыбу заменил на текст покороче, чтобы глаза не мозолил.
И что бы вы думали? Перед tab1 загрузилась главная страница.
Добавляю в li аттрибут aria-controls с указателями на ид дивов-контейнеров, дополнительные дивы перестали создаваться, главная страница загружается в первую вкладку и заменяет собой контент, который там был %)
Копирую исоlный код страницы в отдельный файл оставляю только head со стилями, только div#tabs в body и script с загрузкой jquery и jquery из cdn яндекса. Все тот же идиотский прикол…
Сношу к хренам все стили, заменяю загрузку скриптов на cdn jquery. Нифига..
Остается только несколько записей в head, кодировка страницы, метатеги, автор, ссылки на иконку сайта, стандартная все, вроде, лабуда. Начинаю сносить уже и все оставшееся по одной строчке, потому что хрен его уже знает, что бы еще сделать, бустраповский табулятор не вариант вешать, нет нужных мне плюшек, допиливать неохота, кодить свой тем более лень.
И тут: о чудо! Табулятор заработал как надо. И во всем был виноват какой бы вы думали тэг?
Правильно,


Как только проблема вылечилась, сразу и понятно стало, как так получилось. ссылка <li><a href="#tabs-1" из-за тега base выглядит для скрипта как href="http://mysite.ru/#tabs-1", которую он и пытается загрузить в заботливо созданный им новый контейнер.
Тэг base добавил из-за того, что тема была покупная, пути к картинкам в некоторых js и css файлах были прописаны относительные, но без предваряющего ./ Добавление тега base проблему с такими путями решило, но, как оказалось добавило более интересную. Пришлось убрать <base и поправить пути к картинкам. Надо только будет просмотреть логи на предмет не найденных путей и поправить неправильные пропущенные.

git ––force push

Если  вам, как и мне, вдруг, приспичило сделать pull текущей ветки с перезаписью всех измененных файлов, то у меня есть рецепт. Лично для меня такая надобность актуальна: база данных распухла до размеров больше гигабайта и локально тестировать сайт уже не представляется возможным. WAMP натуральным образом дохнет  в корчах при обращении к сайту, поэтому я настроил netbeans на выгрузку файлов при сохранении. Делаю изменения и тестирую прямо на сервере на тестовом поддомене. Коммичу и пушу изменения в реп и кое-что дорабатываю с часик дома, после чего по приходе на работу вполне успешно делаю пулл на рабочем компьютере. Проблема возникала в плане выгрузки всех актуальных изменений на сервер. Делать синхронизацию средствами netbeans по мне слишком муторно. При попытке сделать git pull — он вполне обоснованно начинал ругаться фразами типа:

error: Your local changes to '*****' would be overwritten by merge.  Aborting.
Please, commit your changes or stash them before you can merge.

Просит типа сделать коммит сделанных изменений. Но делать коммит на сервере в такой ситуации, как выяснилось, откровенно хреновая идея. Можно поиметь кучу геморроя с разрешением конфликтов, что собственно у меня и произошло. Делать это на сервере без нормального редактора для разрешения конфликтов, сказать, что проблематично — это будет слишком мягко сказано, да и в принципе — все конфликты совершенно идиотские: одни и те же изменения оказываются в разных коммитах.
Гугление «git —force push» показало, (подумалось, что должно же быть что-то подобное), что такой опции в гите нет, но навело на хреновую идею, высказанную на паре сайтов — сделать git hard reset head, перед git pull. Не вздумайте повторить.
Это было даже худшей идеей, чем сделать коммит, этот долбаный ресет снес к фигам мне все файлы из заигноренных директорий: все загруженные пользователями файлы и сгенерированные отчеты. Как же хорошо, что до этого я догадался заархивировать все файлы проекта и положить в директорию выше проекта.

Хочу сказать что гуглил и мучился я достаточно долго прежде чем набрел на нужный рецепт. Нужная мне команда выглядит так:

git checkout -- .

Она возвращает все измененные файлы проекта в индексе git к состоянию последнего успешного pull. После чего совершенно спокойно можно сделать git pull. Все директории и файлы на сервере не внесенные в индекс git останутся нетронутыми. Именно то, что мне так давно было нужно.
git status выдает, что:

# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.

этим он говорит, что последний коммит был сделан не на сервере, что мы получили его на сервер, запустив pull. То есть состояние проекта на один коммит впереди, чем локальный репозиторий проекта на сервере. Это нормально.
Ну и покажет, если есть файлы не в индексе:

# Untracked files:
#   (use "git add ..." to include in what will be committed)

Это повод задуматься о внесении дополнений в .gitignore

Новые вагоны РЖД

Ехал в  плацкартном вагоне поезда 001М «Россия».

Впечатления довольно неоднозначные.

Самый актуальный вопрос — это, конечно же, как там с розетками? Поэтому на него отвечу в первую очередь.
Cитуация с розетками очень печальная, вагон новый, но потребности в розетках никто в нем учесть и не подумал. В вагоне 2 розетки на боковых нижних местах в 3 и 7 секции и одна между туалетами, розетки внутри вагона заняты непрерывно, довольно тяжело вклиниться туда со своей зарядкой. А стоять у туалетов довольно мучительно, в старом вагоне можно присесть на крышку ящика для мусора, в окно поглазеть, в новом же вагоне придется стоять, либо бросать телефон без присмотра, окно зачем-то убрали. И розетку у бойлера зачем-то убрали. Есть еще 2 розетки в туалетах (по одной на каждый туалет).
Ходят слухи о невозможности снабдить вагон большим количеством розеток, но при этом в новых купейных вагонах розетка есть в каждом купе под столом, то есть 9 розеток только в вагоне и плюс к этому розетки в туалетах и туалетном тамбуре.

Вагон нового образца, 2 био-туалета, на стоянках не закрываются, что несомненный плюс, оба находятся в конце вагона. Дверь ведущая в тамбур, где находятся туалеты полностью прозрачная, затонированная, выглядит почти стильно. Над дверью этого тамбура висит табло, которое показывает температуру на улице, в вагоне, свободны ли туалеты, текущее время, правда перевести часы за 2 недели с момента перевода часов на зимнее время никто не удосужился, так что показывали они всю дорогу время на час больше действительного.

На унитазах в туалетах страшненькие пластиковые крышки. Рядом оповещение, что с ногами забираться на унитаз нельзя. Есть коробки для бумажных подкладок на крышку, которые всю дорогу пустовали, так что, наверняка, большинство наплевало на предупреждение и залезали на него с ногами. Место для бумажных рулонных полотенец есть, но пустовало всю дорогу. Не знаю с чем это связано, но случалось ездить не на фирменных поездах, и в них были и подкладки и полотенца. Для меня непонятно, что дает повод называть поезд фирменным. Убирались в вагоне один раз в сутки более чем небрежно, так что было очень пыльно.

В оодном туалете вода бежала настолько тоненькой струйкой, что помыть руки в нем было проблематично, поэтому лично я всегда стремился попасть во второй туалет. Правда в нем опасно болтался унитаз, он был почему-то откручен от основы. Когда большинство людей спало можно было один туалет использовать по прямому предназначению и после перейти во второй, чтобы умыться и почистить зубы.

Обновили систему подачи воды — нажимаешь на кран и секунд 10 вода течет, потом автоматически перекрывается до следующего нажатия. Решение довольно сомнительное: смачиваешь зубную щетку и потом 10 секунд стоишь смотришь, как она попусту выливается в раковину, перекрыть досрочно не получается — я пробовал поднимать кнопку: не помогло 🙂

Иногда вода текла приятно теплая, иногда совершенно ледяная, системы я не понял.

Сам вагон — сомнительно улучшенный вариант предыдущей модификации. В более старых вагонах все практически так же, только для обивки использовался материал типа кожзаменителя, матрас чересчур активно елозил по этой обивке, на новой матерчатой обшивке такого эффекта уже не наблюдается — куда положил матрас — там он всю дорогу и лежит, но есть огромное «НО»! Новая обивка очень маркая, все сидения в грязных пятнах и собирают на себя все нитки, перья, пыль, которые после сидения оказываются на одежде. Так что и не знаю что лучше.

В минусы отнесу отсутствие окна в тамбуре у туалета, раньше там можно было постоять и поглазеть на природу. Теперь стоять уже негде. Летом, конечно можно постоять в тамбуре для курения или у выхода из вагона — но зимой там конкретный дубак.

Запрет курения в вагонах — это прямо праздник какой-то, в вагоне теперь нет такой ужасной вони как раньше. И теперь, получается, в каждом вагоне осталось бесполезное место — тамбур для курения. Теперь это просто коридорчик к кладовкам проводников.

Основной плюс — это, конечно, наличие кондиционера. Всю дорогу была комфортная температура: прохладно, но не настолько, чтобы мерзнуть. Впервые после дороги желание помыться не было единственной мыслью.

Отслеживание почтовых отправлений

LOGO-RPПочта России продолжает удивлять. После первого отслеживания посылки по номеру трека решил подписаться на уведомления по email, чтобы меньше мучать их систему трекинга своими запросами.

Правда ни одного уведомления так и не дождался. Получил посылку и думать уже про это забыл. И тут вдруг внезапно приходит письмо в конце октября: Ваша посылка прибыла в место вручения 21 августа — можете забирать 🙂