Phalcon PHP фрейморк. Работа с аннотациями

«vivo, presto, prestissimo...»

О Phalcon пока еще мало материалов, но фреймворк достаточно интересный и заслуживающий внимания. Одно из интересных решений Phalcon — расширенные возможности по использованию аннотаций. Парсер написан на C, работает очень быстро. Позволяет легко и непринужденно перенести часть настроек (чуть ли не большую часть) из конфигурационных файлов в код.



Часть I. Vivo (Быстро).


Назначение маршрутов в Phalcon довольно «творчеcкая» задача.
Многие примеры пестрят разными способами назначения маршрутов.
На Хабре даже проскочил пример использования файлов xml…
И это в то время, когда многие фреймворки предлагают маршрутизацию посредством аннотаций.
А что же Phalcon?
Phalcon скромно и тихо указывает в документации, что возможна маршрутизация на аннотациях.
И что для этого авторы Phalcon просто создали парсер аннотаций на C.
Впервые!
Что нужно для включения аннотаций?
Всего ничего.
В bootstrap файле внедряем сервис аннотаций, всего несколько строк кода. И все.
...
	//set routers

	$di->set('router', function() {
		$router = new \Phalcon\Mvc\Router\Annotations(false);
        $router->removeExtraSlashes(true);
        $router->setUriSource(\Phalcon\Mvc\Router::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 \Phalcon\Mvc\Model\Manager());

	//Set the models cache service
	$di->set('modelsCache', function() {

		//Cache data for one day by default
		$frontCache = new \Phalcon\Cache\Frontend\Data([
							"lifetime" => 86400
		]);
		$cache = new \Phalcon\Cache\Backend\Memcache($frontCache, [
											"host" => "localhost",
											"port" => "11211",
											'persistent' => TRUE,
		]);
		return $cache;
	});
	
	$di->set('modelsMetadata', function() {

		// Create a meta-data manager with APC
		//$metaData = new \Phalcon\Mvc\Model\MetaData\Apc([
		//         "lifetime" => 86400,
		//         "prefix"   => "general-phsql"
		//]);
		$metaData = new \Phalcon\Mvc\Model\MetaData\Memory([
											'prefix'  => 'general',
        ]);
		$metaData->setStrategy(new StrategyAnnotations());
		return $metaData;
	});
...

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

Здесь, в принципе, все. Опять же, более подробно в документации.

Что же, ничего удивительного в этих механизмах нет. За исключением того, что работает это очень быстро. Быстро так, насколько это может быть достигнуто в компоненте, созданном на C. То есть, практически незаметно.
Но эти механизмы есть и у других фрейморков.
Нам же хочется чего-то вкусненького, какой-то изюминки…

Часть III. Prestissimo (Еще быстрее).


Наверное, не секрет, что при проэктировании сайта приходится писать админку.
А это формы, формы, и еще раз формы. Много форм. А это утомляет…
Хочется автоматизации.
Из чего состоит форма? Конечно же, ключевой элемент — это тэг input.
Он, как правило, имеет тип type, длину length, шаблон заполнения pattern и т.д.
И все это для каждой формы нужно указывать… Для каждого поля таблицы…
Хочется автоматизации. При этом не хочется писать много кода.
А описать универсальную форму для любой сущности.
И здесь нам опять пригодятся аннотации. Парсер, опять же, на C.
Phalcon предлагает компонент Phalcon\Forms\Form.
Возьмем простейшую таблицу Users. Ее модель:
<?php namespace Frontend\Model;

class Users extends \Phalcon\Mvc\Model
{
  /**
   * @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 предлагает следующие типы Phalcon\Forms\Element :
Phalcon\Forms\Element\Check
Phalcon\Forms\Element\Date
Phalcon\Forms\Element\Email
Phalcon\Forms\Element\File
Phalcon\Forms\Element\Hidden
Phalcon\Forms\Element\Numeric
Phalcon\Forms\Element\Password
Phalcon\Forms\Element\Select
Phalcon\Forms\Element\Submit
Phalcon\Forms\Element\Text
Phalcon\Forms\Element\TextArea

Более, чем достаточно.
Осталось дело за малым…
Нужно как-то «научить» Phalcon распознавать наши аннотации…
Нет ничего проще!
bootstrap — включаем парсер аннотаций.
...
	//Annotations
	$di->set('annotations', function() {
		return new \Phalcon\Annotations\Adapter\Memory();
	}); 
...

В процессе разработки можно использовать адаптер памяти,
в производстве можно переключиться на хранение в файлах, APC, XCache.
Теперь создаем класс формы для любой сущности.
Класс формы
<?php
use Phalcon\Forms\Form,
    \Phalcon\Forms\Element\Submit 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) вывести форму:
{{ myform.renderform() }}
Вот и все.
Конечно же, необходимо добавить в класс формы CSRF-защиту, валидацию данных, сохранение.
Но задача этой статьи показать простоту и удобство использования аннотаций в Phalcon.
Эти возможности предоставлены нам благодаря мощному и быстрому парсеру аннотаций PhalconPHP.
И, когда начинаешь использовать Phalcon, понимаешь, что он действительно быстр.
И не только при выводе «Hello, world!».
Скорость и удобство работы с Phalcon действительно поражают.
index.php
<?php
use Phalcon\Mvc\View\Engine\Volt;
use Phalcon\Mvc\Model\MetaData\Strategy\Annotations as StrategyAnnotations;
try {
	//Register an autoloader
	$loader = new \Phalcon\Loader();
	$loader->registerDirs([
				'../app/controllers/',
				'../app/models/',
				'../app/forms/'
	]);
	$loader->registerNamespaces([
				'Frontend\\Model'  => __DIR__.'/../app/models/',
    ]);
	$loader->register();
	//Create a DI
	$di = new \Phalcon\DI\FactoryDefault();
  
	//Set a models manager
	$di->set('modelsManager', new \Phalcon\Mvc\Model\Manager());

	//Set the models cache service
	$di->set('modelsCache', function() {

		//Cache data for one day by default
		$frontCache = new \Phalcon\Cache\Frontend\Data([
							"lifetime" => 86400
		]);
		$cache = new \Phalcon\Cache\Backend\Memcache($frontCache, [
											"host" => "localhost",
											"port" => "11211",
											'persistent' => TRUE,
		]);
		return $cache;
	});
	
	$di->set('modelsMetadata', function() {

		// Create a meta-data manager with APC
		//$metaData = new \Phalcon\Mvc\Model\MetaData\Apc([
		//         "lifetime" => 86400,
		//         "prefix"   => "general-phsql"
		//]);
		$metaData = new \Phalcon\Mvc\Model\MetaData\Memory([
											'prefix'  => 'general',
        ]);
		$metaData->setStrategy(new StrategyAnnotations());
		return $metaData;
	});
 
	//SQL profiler
	$di->set('profiler', function(){
		return new \Phalcon\Db\Profiler();
    }, true);
	//set database connection
	$di->set('db', function() use ($di) {
		$eventsManager = new \Phalcon\Events\Manager();

		//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 \Phalcon\Db\Adapter\Pdo\Mysql([
											"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 \Phalcon\Mvc\View();
		$view->setViewsDir('../app/views/');
		$view->registerEngines([
						".volt" => 'voltService'
        ]);
		return $view;
	});

	//Create Form manager
	$di->set('forms', function() {
		$forms = new \Phalcon\Forms\Manager();
		return $forms;
	});

	$di->set('session', function() use($di) {
		$session = new Phalcon\Session\Adapter\Files();
        $session->setoptions([
					'uniqueId'  => 'privatRsc',
        ]);
        $session->start();
        return $session;
	});

	//set routers

	$di->set('router', function() {
		$router = new \Phalcon\Mvc\Router\Annotations(false);
        $router->removeExtraSlashes(true);
        $router->setUriSource(\Phalcon\Mvc\Router::URI_SOURCE_SERVER_REQUEST_URI);
        $router->addResource('Index', "/");
        $router->notFound([
						"controller"	=> "index",
						"action"		=> "page404"
		]);
        return $router;
	});
	
	//Annotations
	$di->set('annotations', function() {
		return new \Phalcon\Annotations\Adapter\Memory();
	}); 


	//Handle the request
	$application = new \Phalcon\Mvc\Application($di);

	echo $application->handle()->getContent();

} catch(\Phalcon\Exception $e) {
	echo "PhalconException: ", $e->getMessage();
}


IndexController.php
<?php

use \Frontend\Model\Users as Users;

/**
 * @RoutePrefix("/")
 **/

class IndexController extends \Phalcon\Mvc\Controller
{
  /**
   * @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';
  }
}


EntityForm.php
<?php
use Phalcon\Forms\Form,
    \Phalcon\Forms\Element\Submit 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();
  }
}


Users.php
<?php namespace Frontend\Model;

class Users extends \Phalcon\Mvc\Model
{
  /**
   * @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;
}


form.volt
<h2>Test form in Volt</h2>
<hr>
{{ myform.renderform() }}
<hr>
Метки:
  • +5
  • 11,2k
  • 9
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 9
  • –3
    Когда же аннотации станут частью языка? Больно видеть, как извращаются пхпшники, лишь бы не отставать от других языков
    • 0

      А в некоторых языках с аннотациями, даже анонимок нет.
      • +2
        Вообще если задуматься, аннотации — большое зло, это «чёрные ящики кода» которые непонятно откуда и как вызываются и непонятно как разбирать ошибки связанные с ними, да и вообще сам процесс смешивания кода или данных с комментариями говорит о каких-то проблемах с возможностями языков программирования.
        • 0
          удаленный комментарий.
      • 0
        А как обстоят дела с проверкой на существование аннотации? Проверяются ли типы переданных аргументов в аннотацию? Можно ли написать свою аннотацию?
        • 0
          В данной статье как раз указано о создании своей аннотации. Можно было ее назвать «inputAttributes», к примеру. Проверка на наличие аннотации есть в компоненте.класс Phalcon\Annotations\Collection метод boolean has (string $name)
          • +1
            Посмотрел внимательнее, нашел. Но в итоге у нас динамические аннотации, если я например ошибусь в названии аннотации (например напишу @FormOption вместо @FormOptions), то узнаю про это в самый последний момент, парсер не укажет — что такая аннотация не объявлена.
        • +1
          По-моему, писать инструкции для выполнения программы в комментариях — ЛДП.
          • +2
            Ипонскийбох! Convention over comments в частности для правил маршрутизации на практике — очень неудобно. По URL неясно, куда роутится запрос, и, не глядя в код, неясно, как будет выглядеть URL. То есть настроить может только сотрудник, близко знакомый с кодом, то есть программист.

            С моей точки зрения проектировать систему надо так, чтобы в случае кастомизации продукта (в частности, необходимость реконфигурация URL) или технических неполадках (в частности, если рабочие URL вдруг стали выдавать 404) специально обученный человек знал, какой конфиг поправить.

            Рассказывать админам о архитектуре проекта непроизводительно, а тем более учить его PHP, чтобы он понимал source code.

            Я про крупные проекты. В небольших проектах, вполне возможно, управляет всем один тестировщикоадминопрограммист и он всё знает.

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