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

в 6:45, , рубрики: 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 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 действительно поражают.

index.php

<?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();
}

IndexController.php

<?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';
  }
}

EntityForm.php

<?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();
  }
}

Users.php

<?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;
}

form.volt

<h2>Test form in Volt</h2>
<hr>
{{ myform.renderform() }}
<hr>

Автор: olegcorner

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js