«vivo, presto, prestissimo...»
О Phalcon пока еще мало материалов, но фреймворк достаточно интересный и заслуживающий внимания. Одно из интересных решений Phalcon — расширенные возможности по использованию аннотаций. Парсер написан на C, работает очень быстро. Позволяет легко и непринужденно перенести часть настроек (чуть ли не большую часть) из конфигурационных файлов в код.
Часть I. Vivo (Быстро).
Назначение маршрутов в Phalcon довольно «творчеcкая» задача.
Многие примеры пестрят разными способами назначения маршрутов.
На Хабре даже проскочил пример использования файлов xml…
И это в то время, когда многие фреймворки предлагают маршрутизацию посредством аннотаций.
А что же Phalcon?
Phalcon скромно и тихо указывает в документации, что возможна маршрутизация на аннотациях.
И что для этого авторы Phalcon просто создали парсер аннотаций на C.
Впервые!
Что нужно для включения аннотаций?
Всего ничего.
В bootstrap файле внедряем сервис аннотаций, всего несколько строк кода. И все.
...
//set routers
$di->set('router', function() {
$router = new PhalconMvcRouterAnnotations(false);
$router->removeExtraSlashes(true);
$router->setUriSource(PhalconMvcRouter::URI_SOURCE_SERVER_REQUEST_URI);
$router->addResource('Index');
$router->notFound([
"controller" => "index",
"action" => "page404"
]);
return $router;
});
...
Теперь в маршруты (в том числе с префиксами), указываем прямо в контроллере.
...
/**
* @Post("/create")
*/
public function createAction()
{
/...
}
...
Более подробно маршрутизация (типы запросов, пареметры) описана в документации.
Часть II. Presto (Быстро, насколько возможно)
Маршрутизация на аннотациях — это, конечно, хорошо. Но нам еще в проектах приходится иметь дело с данными. И здесь можно заметить одну особенность Phalcon.
При работе с базой данных он делает обычно 3 запроса.
1-й проверяет наличие таблицы в базе.
2-й получает метаданные таблицы.
3-й непосредственно запрос.
Не знаю, насколько это правильно, не буду спорить. Но несколько неудобно.
Нам же 3 запроса ни к чему. Нам хотелось бы иметь метаданные таблицы где-то в загашнике.
В кэше, например.
И Phalcon того же мнения. Поэтому предлагает сохранять метаданные в кэше. При этом можно использовать аннотации.
Опять DI, опять botstrap:
...
//Set a models manager
$di->set('modelsManager', new PhalconMvcModelManager());
//Set the models cache service
$di->set('modelsCache', function() {
//Cache data for one day by default
$frontCache = new PhalconCacheFrontendData([
"lifetime" => 86400
]);
$cache = new PhalconCacheBackendMemcache($frontCache, [
"host" => "localhost",
"port" => "11211",
'persistent' => TRUE,
]);
return $cache;
});
$di->set('modelsMetadata', function() {
// Create a meta-data manager with APC
//$metaData = new PhalconMvcModelMetaDataApc([
// "lifetime" => 86400,
// "prefix" => "general-phsql"
//]);
$metaData = new PhalconMvcModelMetaDataMemory([
'prefix' => 'general',
]);
$metaData->setStrategy(new StrategyAnnotations());
return $metaData;
});
...
После чего, метаданные, считанные единожды, хранятся в кэш.
На время разработки мы можем переключиться на хранение в памяти.
Здесь, в принципе, все. Опять же, более подробно в документации.
Что же, ничего удивительного в этих механизмах нет. За исключением того, что работает это очень быстро. Быстро так, насколько это может быть достигнуто в компоненте, созданном на C. То есть, практически незаметно.
Но эти механизмы есть и у других фрейморков.
Нам же хочется чего-то вкусненького, какой-то изюминки…
Часть III. Prestissimo (Еще быстрее).
Наверное, не секрет, что при проэктировании сайта приходится писать админку.
А это формы, формы, и еще раз формы. Много форм. А это утомляет…
Хочется автоматизации.
Из чего состоит форма? Конечно же, ключевой элемент — это тэг input.
Он, как правило, имеет тип type, длину length, шаблон заполнения pattern и т.д.
И все это для каждой формы нужно указывать… Для каждого поля таблицы…
Хочется автоматизации. При этом не хочется писать много кода.
А описать универсальную форму для любой сущности.
И здесь нам опять пригодятся аннотации. Парсер, опять же, на C.
Phalcon предлагает компонент PhalconFormsForm.
Возьмем простейшую таблицу Users. Ее модель:
<?php namespace FrontendModel;
class Users extends PhalconMvcModel
{
/**
* @Primary
* @Identity
* @Column(type="integer", nullable=false)
* @FormOptions(type=hidden)
*/
public $id;
/**
* @Column(type="string", nullable=false)
* @FormOptions(type=text, length=32)
*/
public $name;
/**
* @Column(type="integer", nullable=false)
* @FormOptions(type=email)
*/
public $email;
/**
* @Column(type="integer", nullable=false)
* @FormOptions(type=text, length=9, pattern='[0-9]{9}')
*/
public $indcode;
}
Здесь мы применяем собственную аннотацию, где указываем нужные нам для построения формы опции.
Да, эту аннотацию мы придумали сейчас, сами, для применения в конкретном случае.
У нас это @FormOptions. В атрибуте type мы указываем тип поля, необходимый нам для input.
Phalcon предлагает следующие типы PhalconFormsElement :
PhalconFormsElementCheck
PhalconFormsElementDate
PhalconFormsElementEmail
PhalconFormsElementFile
PhalconFormsElementHidden
PhalconFormsElementNumeric
PhalconFormsElementPassword
PhalconFormsElementSelect
PhalconFormsElementSubmit
PhalconFormsElementText
PhalconFormsElementTextArea
Более, чем достаточно.
Осталось дело за малым…
Нужно как-то «научить» Phalcon распознавать наши аннотации…
Нет ничего проще!
bootstrap — включаем парсер аннотаций.
...
//Annotations
$di->set('annotations', function() {
return new PhalconAnnotationsAdapterMemory();
});
...
В процессе разработки можно использовать адаптер памяти,
в производстве можно переключиться на хранение в файлах, APC, XCache.
Теперь создаем класс формы для любой сущности.
<?php
use PhalconFormsForm,
PhalconFormsElementSubmit as Submit;
class EntityForm extends Form
{
public $fields = [];
private $classprefix = '\Phalcon\Forms\Element\';
public $action;
/**
* @param object $model, action
*/
public function initialize($model, $action)
{
$this->action = $action;
//Заполняем поля формы данными из модели
$object = $model;
$this->setEntity($object);
//Получаем атрибуты модели
$attributes = $this->modelsMetadata->getAttributes($object);
// Получаем аннотации из модели
$metadata = $this->annotations->get($object);
// Считыаем аннотацию @FormOptions
foreach ( $attributes as $attribute ) {
$this->fields[$attribute] = $metadata
->getPropertiesAnnotations()[$attribute]
->get('FormOptions')
->getArguments();
}
// Создаем поля формы с учетом видимости
foreach ($this->fields as $field => $type) {
$fieldtype = array_shift($type); // атрибут type в аннотации нам более не нужен
$fieldclass = $this->classprefix.$fieldtype;
$this->add(new $fieldclass($field, $type));
//устанавливаем label если поле не скрыто
if ( $fieldtype !== 'hidden') {
$this->get($field)->setLabel($this->get($field)->getName());
}
}
// Добавляем кнопку отправки
$this->add(new Submit('submit',[
'value' => 'Send',
]));
}
public function renderform()
{
echo $this->tag->form([
$this->action,
'id' => 'actorform',
]);
//fill form tags
foreach ($this as $element) {
// collect messages
$messages = $this->getMessagesFor($element->getName());
if (count($messages)) {
// each element render
echo '<div class="messages">';
foreach ($messages as $message) {
echo $message;
}
echo '</div>';
}
echo '<div>';
echo '<label for="', $element->getName(), '">', $element->getLabel(), '</label>';
echo $element;
echo '</div>';
}
echo $this->tag->endForm();
}
}
Здесь, при инициализации класса EntityForm мы считываем метаданные переданного объекта и его аннотации.
После этого внедряем все необходимые поля в форму.
Функция renderform просто выводит нашу форму в браузер.
Вернемся в контроллер, и создадим действие вывода формы:
...
/**
* @Get("/form")
*/
public function formAction()
{
$myform = new EntityForm(new Users(), 'create');
$this->view->setVars([
'myform' => $myform,
]);
}
...
и получателя:
...
/**
* @Post("/create")
*/
public function createAction()
{
echo '<pre>';
var_dump($_POST);
echo '</pre>';
}
...
Остается только в шаблоне вывода (Volt) вывести форму:
<b>{{ myform.renderform() }}</b>
Вот и все.
Конечно же, необходимо добавить в класс формы CSRF-защиту, валидацию данных, сохранение.
Но задача этой статьи показать простоту и удобство использования аннотаций в Phalcon.
Эти возможности предоставлены нам благодаря мощному и быстрому парсеру аннотаций PhalconPHP.
И, когда начинаешь использовать Phalcon, понимаешь, что он действительно быстр.
И не только при выводе «Hello, world!».
Скорость и удобство работы с Phalcon действительно поражают.
<?php
use PhalconMvcViewEngineVolt;
use PhalconMvcModelMetaDataStrategyAnnotations as StrategyAnnotations;
try {
//Register an autoloader
$loader = new PhalconLoader();
$loader->registerDirs([
'../app/controllers/',
'../app/models/',
'../app/forms/'
]);
$loader->registerNamespaces([
'Frontend\Model' => __DIR__.'/../app/models/',
]);
$loader->register();
//Create a DI
$di = new PhalconDIFactoryDefault();
//Set a models manager
$di->set('modelsManager', new PhalconMvcModelManager());
//Set the models cache service
$di->set('modelsCache', function() {
//Cache data for one day by default
$frontCache = new PhalconCacheFrontendData([
"lifetime" => 86400
]);
$cache = new PhalconCacheBackendMemcache($frontCache, [
"host" => "localhost",
"port" => "11211",
'persistent' => TRUE,
]);
return $cache;
});
$di->set('modelsMetadata', function() {
// Create a meta-data manager with APC
//$metaData = new PhalconMvcModelMetaDataApc([
// "lifetime" => 86400,
// "prefix" => "general-phsql"
//]);
$metaData = new PhalconMvcModelMetaDataMemory([
'prefix' => 'general',
]);
$metaData->setStrategy(new StrategyAnnotations());
return $metaData;
});
//SQL profiler
$di->set('profiler', function(){
return new PhalconDbProfiler();
}, true);
//set database connection
$di->set('db', function() use ($di) {
$eventsManager = new PhalconEventsManager();
//Get a shared instance of the DbProfiler
$profiler = $di->getProfiler();
//Listen all the database events
$eventsManager->attach('db', function($event, $connection) use ($profiler) {
if ($event->getType() == 'beforeQuery') {
$profiler->startProfile($connection->getSQLStatement());
}
if ($event->getType() == 'afterQuery') {
$profiler->stopProfile();
}
});
$connection = new PhalconDbAdapterPdoMysql([
"host" => "localhost",
"username" => "root",
"password" => "12345",
"dbname" => "general"
]);
//Assign the eventsManager to the db adapter instance
$connection->setEventsManager($eventsManager);
return $connection;
});
//Register Volt as a service
$di->set('voltService', function($view, $di) {
$volt = new Volt($view, $di);
$volt->setOptions([
"compiledPath" => "../app/cache/",
]);
return $volt;
});
//Setting up the view component
$di->set('view', function(){
$view = new PhalconMvcView();
$view->setViewsDir('../app/views/');
$view->registerEngines([
".volt" => 'voltService'
]);
return $view;
});
//Create Form manager
$di->set('forms', function() {
$forms = new PhalconFormsManager();
return $forms;
});
$di->set('session', function() use($di) {
$session = new PhalconSessionAdapterFiles();
$session->setoptions([
'uniqueId' => 'privatRsc',
]);
$session->start();
return $session;
});
//set routers
$di->set('router', function() {
$router = new PhalconMvcRouterAnnotations(false);
$router->removeExtraSlashes(true);
$router->setUriSource(PhalconMvcRouter::URI_SOURCE_SERVER_REQUEST_URI);
$router->addResource('Index');
$router->notFound([
"controller" => "index",
"action" => "page404"
]);
return $router;
});
//Annotations
$di->set('annotations', function() {
return new PhalconAnnotationsAdapterMemory();
});
//Handle the request
$application = new PhalconMvcApplication($di);
echo $application->handle()->getContent();
} catch(PhalconException $e) {
echo "PhalconException: ", $e->getMessage();
}
<?php
use FrontendModelUsers as Users;
/**
* @RoutePrefix("")
**/
class IndexController extends PhalconMvcController
{
/**
* @Get("/")
*/
public function indexAction()
{
echo <h3>Index Action</h3>;
}
/**
* @Get("/form")
*/
public function formAction()
{
$myform = new EntityForm(new Users(), 'create');
$this->view->setVars([
'myform' => $myform,
]);
}
/**
* @Post("/create")
*/
public function createAction()
{
echo '<pre>';
var_dump($_POST);
echo '</pre>';
}
/**
* @Get("/page404")
*/
public function page404Action()
{
echo '404 - route not found';
}
}
<?php
use PhalconFormsForm,
PhalconFormsElementSubmit as Submit;
class EntityForm extends Form
{
public $fields = [];
private $classprefix = '\Phalcon\Forms\Element\';
public $action;
/**
* @param object $model, action
*/
public function initialize($model, $action)
{
$this->action = $action;
//Set fields options from annotations
$object = $model;
$this->setEntity($object);
$attributes = $this->modelsMetadata->getAttributes($object);
$metadata = $this->annotations->get($object);
foreach ( $attributes as $attribute ) {
$this->fields[$attribute] = $metadata
->getPropertiesAnnotations()[$attribute]
->get('FormOptions')
->getArguments();
}
foreach ($this->fields as $field => $type) {
$fieldtype = array_shift($type);
$fieldclass = $this->classprefix.$fieldtype;
$this->add(new $fieldclass($field, $type));
if ( $fieldtype !== 'hidden') {
$this->get($field)->setLabel($this->get($field)->getName());
}
}
$this->add(new Submit('submit',[
'value' => 'Send',
]));
}
public function renderform()
{
echo $this->tag->form([
$this->action,
'id' => 'actorform',
]);
//fill form tags
foreach ($this as $element) {
// collect messages
$messages = $this->getMessagesFor($element->getName());
if (count($messages)) {
// each element render
echo '<div class="messages">';
foreach ($messages as $message) {
echo $message;
}
echo '</div>';
}
echo '<div>';
echo '<label for="', $element->getName(), '">', $element->getLabel(), '</label>';
echo $element;
echo '</div>';
}
echo $this->tag->endForm();
}
}
<?php namespace FrontendModel;
class Users extends PhalconMvcModel
{
/**
* @Primary
* @Identity
* @Column(type="integer", nullable=false)
* @FormOptions(type=hidden)
*/
public $id;
/**
* @Column(type="string", nullable=false)
* @FormOptions(type=text, length=32)
*/
public $name;
/**
* @Column(type="integer", nullable=false)
* @FormOptions(type=email)
*/
public $email;
/**
* @Column(type="integer", nullable=false)
* @FormOptions(type=text, length=9, pattern='[0-9]{9}')
*/
public $indcode;
}
<h2>Test form in Volt</h2>
<hr>
{{ myform.renderform() }}
<hr>
Автор: olegcorner