Автозавершение кода для phalconphp

Хотелось бы иметь простой доступ к списку методов некоторых сервисов и передаваемых в них параметров не заглядывая в документацию.
Конечно, из любого контроллера можно обратиться к стандартному сервису через переменную с соответствующим названием $this->tag, $this->url, автодополнение срабатывает как положено в любом нормальном IDE. Как настроить автодополнение для phalconphp для Netbeans?
Но в случае использования своих сервисов IDE уже молчит. Да и возникает необходимость обращаться как к своим, так и к стандартным сервисам phalcon не только из контроллеров.
В моделях возникает необходимость добавить методы получения данных из соответствующих таблиц, в случаях, когда критерии выборки из таблицы сложные, в этом случае неправильно пихать такой код в контроллер, тем более, когда получаем эти данные с такими критериями в нескольких местах одного, а то и нескольких контроллеров. При этом может возникнуть необходимость обратиться к методам сервисов: базы данных, acl.
К примеру, надо получить список задач:
проверяем, чтобы ответственный за выполнение задачи или постановщик совпадал с ИД текущего пользователя или он входил в список наблюдателей задачи;
обнуляем поля таблицы, для просмотра которых пользователей не имеет прав (обусловлено использованием dataTables — данные передаются по аякс и в данных должны присутствовать все столбцы).
Получается — из модели мы как минимум должны обратиться к методам сервисов auth (предпочитаю использовать отдельный сервис для данных пользователя, который умеет инициализироваться по ключу из кук, запомнить авторизацию в куках и забыть при выходе) и acl (\Phalcon\Acl\Adapter), в некоторых случаях еще возникает необходимость вызвать методы адаптера базы данных (\Phalcon\Db\Adapter\Pdo) или модель-менеджера (\Phalcon\Mvc\Model\Manager).
Можно, конечно, воспользоваться конструкцией вида:

\Phalcon\DI::getDefault()->getModelManager()
\Phalcon\DI::getDefault()->getAuth()
# или 
\Phalcon\DI::getDefault()->get('modelManager')
\Phalcon\DI::getDefault()->get('auth')

Но, вот незадача — в обоих случаях уже надо будет лезть в документацию, чтобы узнать точное название нужного метода и порядок передаваемых параметров. А хотелось бы, чтобы IDE сразу выдало подсказку для всех возможных методов.
Для начала: я предпочитаю при инициализации сервисов объявить глобальную функцию getDi():

/**
 * @return \Phalcon\DI\FactoryDefault
 */
function getDi() {
	static $di = null;
	if (!$di) {
		$di = new \Phalcon\DI\FactoryDefault();
	}
	return $di;
}

потому как не люблю лишнего нагромождения кода, которое создаст использование конструкции \Phalcon\DI::getDefault().
Далее, создаем папочку в проекте, к примеру my-phalcon.
И копируем туда phalcon-devtools\ide\{номер версии}\Phalcon\DI.php.
Вносим нужные изменения в этот файлик — добавляем вызов недостающих там сервисов:

namespace Phalcon {
	...
	class DI implements \Phalcon\DiInterface {
		...
		/**
		 * @return \Phalcon\Security
		 */
		public function getSecurity() {}
		/**
		 * @return \Handy\Models\Auth
		 */
		public function getAuth() {}
		/**
		 * @return \Phalcon\Acl\Adapter\Memory
		 */
		public function getAcl() {}
		/**
		 * @return \Phalcon\Db\Adapter\Pdo\Mysql
		 */
		public function getDb() {}
		/**
		 * @return \Phalcon\Tag
		 */
		public function getTag() {}
		...
		# ваши сервисы
		...

И, вуаля: автозавершение для всех доступных методов, всех нужных сервисов работает!
my-phalcon-di

fetchAll и bindTypes в phalconphp

Согласно документации описание метода fetchAll класса Phalcon\Db\Adapter\Pdo\Mysql выглядит так:

public array fetchAll (
    string $sqlQuery,  
    [int $fetchMode],
    [unknown $placeholders]
) inherited from Phalcon\Db\Adapter

подготовил запрос, включил туда

LIMIT :offset, :limit

массив bind:

$bind = array(
    'offset'=>$offset,
    'limit'=>$limit
);

Вызываю fetchAll

getDi()->getDb()->fetchAll(
    $sql, 
    \Phalcon\Db::FETCH_ASSOC, 
    $bind
);

— и на тебе:
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘ ‘0’, ’25’ ‘
Похоже целочисленные значения подставились в запрос как строки.
Пробую явно привести значения к целочисленному типу:

$bind = array(
    'offset'=>intval($offset), 
    'limit'=>intval($limit)
);

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

public array fetchAll (
    string $sqlQuery
    [, int $fetchMode]
    [, array $placeholders]
    [, array $types]
) inherited from Phalcon\Db\Adapter

Стоило лишь добавить в вызов четвертый параметр

$types = array(
    'limit' => \Phalcon\Db\Column::BIND_PARAM_INT,
    'offset' => \Phalcon\Db\Column::BIND_PARAM_INT
);
getDi()->getDb()->fetchAll(
    $sql,
    \Phalcon\Db::FETCH_ASSOC,
    $bind,
    $types
);

и все заработало.

Отчего падает phalconphp?

Буду дополнять список ситуаций, из-за которых phalcon падает без объяснения причин.

Просто отдает пустую страницу, если:

  1. Попытаться дать доступ к ресурсу, вызвав \Phalcon\Acl::allow, если ресурс или роль еще не добавлены

Реальные запросы в 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, поиграться с параметрами, проанализировать и при необзодимости внести правки в код. Надеюсь, что кому-то еще это решение будет полезным.

Value of field ’email’ must have a valid e-mail format

По мнению разработчиков phalcon видимо поле в таблице с названием email обязано содержать значения с валидными email, невзирая на тип поля в таблице, в модель при генерации автоматически добавляется метод валидации.

Uncaught exception ‘Exception’

И это в конструкции вида

try {
	throw new Exception();
} catch (Exception $e) {

}

Так вышло из-за действия namespace, в catch скрипт пытается отловить исключение класса \ТекущийNamespace\Exception невзирая на то, что такого класса не существует в природе, проверка на него все-таки производится.
И при выбросе стандартного исключения и при его отлове лучше всегда явно указывать нужное namespace.

try {
	throw new \Exception();
} catch (\Exception $e) {

}

Column doesn’t belong to any of the selected models

Phalcon\Mvc\Model\Exception: Column ‘XXXX’ doesn’t belong to any of the selected models (1)
Вдруг кому пригодится — вы ошиблись с указанием названия столбца при передаче $parameters для одного из методов модели, ошиблись с рЕгистром символов, пропустили букву или наоборот — лишних символов добавили.

Main Layout в мультимодульном приложении phalconphp

C одномодульным приложением все просто, Action View — app/views/controller/, Main Layout — app/views/index.phtml.
С мультимодульным возникает проблема с использованием общего Main Layout для всего приложения. как решить?

Не работает фильтр format в phalconphp

phalconphp formatПонадобилось вывести целую часть дробного числа в представлении. Из встроенных фильтров Volt подходящий:
«format Форматирует строку, используя sprintf»

{{price|format('%d')}}

«Фиг вам» — ответил phalcon и вывел цену вместе с дробной частью.
UPD: Пришел ответ на issue, оказывается фильтр format следует использовать отличным от других фильтров способом, разработчики не парились особо с шаблонизатором и слизали его с TWIG

{{'%d'|format(price)}}

меня ситуация не устраивает →