Управление контроллерами
Phalcon\Mvc\Dispatcher — это компонент, отвечающий за создание контроллеров и выполнение необходимых действий над ними в приложении MVC. Понимание его работы и возможностей помогает нам получить больше от предоставляемых фреймворком сервисов.
Цикл обработки запроса
Это важный процесс, который имеет много общего с самим циклом MVC, особенно с частью контроллера. Работа происходит внутри диспетчера контроллера. Файлы контроллеров читаются, загружаются и создаются экземпляры. Затем выполняются необходимые действия. Если действие перенаправляет поток в другой контроллер/действие, диспетчер контроллера запускается снова. Для лучшей иллюстрации следующий пример демонстрирует приблизительно процесс, выполняемый в Phalcon\Mvc\Dispatcher:
// Dispatch loop
while (!$finished) {
$finished = true;
$controllerClass = $controllerName . "Controller";
// Instantiating the controller class via autoloaders
$controller = new $controllerClass();
// Execute the action
call_user_func_array(
[
$controller,
$actionName . "Action"
],
$params
);
// '$finished' should be reloaded to check if the flow was forwarded to another controller
$finished = true;
}
Приведённый выше код не содержит валидации, фильтров и дополнительных проверок, но он демонстрирует нормальный поток работы в диспетчере.
События цикла обработки запроса
Phalcon\Mvc\Dispatcher может отправлять события в EventsManager, если он присутствует. События вызываются с типом “dispatch”. Некоторые события при возвращении значения boolean false могут остановить активную операцию. Поддерживаются следующие события:
В руководстве INVO показано, как использовать события обработки запроса для реализации фильтра безопасности с помощью Acl
Следующий пример демонстрирует, как прикрепить слушатели к этому компоненту:
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an event manager
$eventsManager = new EventsManager();
// Attach a listener for type "dispatch"
$eventsManager->attach(
"dispatch",
function (Event $event, $dispatcher) {
// ...
}
);
$dispatcher = new MvcDispatcher();
// Bind the eventsManager to the view component
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
},
true
);
Созданный экземпляр контроллера автоматически действует как слушатель событий обработки запроса, поэтому вы можете реализовать методы в качестве обратных вызовов:
use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;
class PostsController extends Controller
{
public function beforeExecuteRoute(Dispatcher $dispatcher)
{
// Executed before every found action
}
public function afterExecuteRoute(Dispatcher $dispatcher)
{
// Executed after every found action
}
}
Примечание
Методы слушателей событий принимают объект Phalcon\Events\Event в качестве первого параметра — методы в контроллерах этого не делают.
Перенаправление на другие действия
Цикл обработки запроса позволяет перенаправлять поток выполнения в другой контроллер/действие. Это очень полезно для проверки доступа пользователя к определённым опциям, перенаправления пользователей на другие страницы или просто повторного использования кода.
use Phalcon\Mvc\Controller;
class PostsController extends Controller
{
public function indexAction()
{
}
public function saveAction($year, $postTitle)
{
// ... Store some product and forward the user
// Forward flow to the index action
$this->dispatcher->forward(
[
"controller" => "posts",
"action" => "index",
]
);
}
}
Обратите внимание, что «перенаправление» не то же самое, что HTTP-перенаправление. Хотя они, по-видимому, дают одинаковый результат. «Перенаправление» не перезагружает текущую страницу, всё перенаправление происходит в рамках одного запроса, в то время как HTTP-перенаправление требует двух запросов для завершения процесса.
Дополнительные примеры перенаправлений:
// Forward flow to another action in the current controller
$this->dispatcher->forward(
[
"action" => "search"
]
);
// Forward flow to another action in the current controller
// passing parameters
$this->dispatcher->forward(
[
"action" => "search",
"params" => [1, 2, 3]
]
);
Действие перенаправления принимает следующие параметры:
| Параметр | Описание |
|---|---|
| controller | Действительное имя контроллера для перенаправления. |
| action | Действительное имя действия для перенаправления. |
| params | Массив параметров для действия |
| namespace | Действительное имя пространства имён, к которому относится контроллер. |
Подготовка параметров
Благодаря точкам подключения, предоставляемым Phalcon\Mvc\Dispatcher, вы можете легко адаптировать своё приложение к любой схеме URL:
Например, вы хотите, чтобы ваши URL выглядели так: http://example.com/controller/key1/value1/key2/value
Параметры по умолчанию передаются в действия в том виде, в котором они поступают в URL, вы можете преобразовать их в желаемую схему:
use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
// Attach a listener
$eventsManager->attach(
"dispatch:beforeDispatchLoop",
function (Event $event, $dispatcher) {
$params = $dispatcher->getParams();
$keyParams = [];
// Use odd parameters as keys and even as values
foreach ($params as $i => $value) {
if ($i & 1) {
// Previous param
$key = $params[$i - 1];
$keyParams[$key] = $value;
}
}
// Override parameters
$dispatcher->setParams($keyParams);
}
);
$dispatcher = new MvcDispatcher();
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Если желаемая схема такая: http://example.com/controller/key1:value1/key2:value, необходим следующий код:
use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
// Attach a listener
$eventsManager->attach(
"dispatch:beforeDispatchLoop",
function (Event $event, $dispatcher) {
$params = $dispatcher->getParams();
$keyParams = [];
// Explode each parameter as key,value pairs
foreach ($params as $number => $value) {
$parts = explode(":", $value);
$keyParams[$parts[0]] = $parts[1];
}
// Override parameters
$dispatcher->setParams($keyParams);
}
);
$dispatcher = new MvcDispatcher();
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Получение параметров
Когда маршрут предоставляет именованные параметры, вы можете получить их в контроллере, представлении или любом другом компоненте, который расширяет Phalcon\Di\Injectable.
use Phalcon\Mvc\Controller;
class PostsController extends Controller
{
public function indexAction()
{
}
public function saveAction()
{
// Get the post's title passed in the URL as parameter
// or prepared in an event
$title = $this->dispatcher->getParam("title");
// Get the post's year passed in the URL as parameter
// or prepared in an event also filtering it
$year = $this->dispatcher->getParam("year", "int");
// ...
}
}
Подготовка действий
Вы также можете определить произвольную схему для действий перед их обработкой.
Преобразование имён действий в camelCase
Если исходный URL: http://example.com/admin/products/show-latest-products, и, например, вы хотите преобразовать ‘show-latest-products’ в ‘ShowLatestProducts’, необходим следующий код:
use Phalcon\Text;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
// Camelize actions
$eventsManager->attach(
"dispatch:beforeDispatchLoop",
function (Event $event, $dispatcher) {
$dispatcher->setActionName(
Text::camelize($dispatcher->getActionName())
);
}
);
$dispatcher = new MvcDispatcher();
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Удаление устаревших расширений
Если исходный URL всегда содержит расширение ‘.php’:
http://example.com/admin/products/show-latest-products.php http://example.com/admin/products/index.php
Вы можете удалить его перед обработкой сочетания контроллер/действие:
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
// Remove extension before dispatch
$eventsManager->attach(
"dispatch:beforeDispatchLoop",
function (Event $event, $dispatcher) {
$action = $dispatcher->getActionName();
// Remove extension
$action = preg_replace("/\.php$/", "", $action);
// Override action
$dispatcher->setActionName($action);
}
);
$dispatcher = new MvcDispatcher();
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Вставка экземпляров моделей
В этом примере разработчик хочет проверить параметры, которые получит действие, чтобы динамически вставить экземпляры моделей.
Контроллер выглядит так:
use Phalcon\Mvc\Controller;
class PostsController extends Controller
{
/**
* Shows posts
*
* @param \Posts $post
*/
public function showAction(Posts $post)
{
$this->view->post = $post;
}
}
Метод «showAction» получает экземпляр модели Posts, разработчик мог проверить это перед обработкой действия, подготовив параметр соответственно:
use Exception;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
use ReflectionMethod;
$di->set(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
$eventsManager->attach(
"dispatch:beforeDispatchLoop",
function (Event $event, $dispatcher) {
// Possible controller class name
$controllerName = $dispatcher->getControllerClass();
// Possible method name
$actionName = $dispatcher->getActiveMethod();
try {
// Get the reflection for the method to be executed
$reflection = new ReflectionMethod($controllerName, $actionName);
$parameters = $reflection->getParameters();
// Check parameters
foreach ($parameters as $parameter) {
// Get the expected model name
$className = $parameter->getClass()->name;
// Check if the parameter expects a model instance
if (is_subclass_of($className, Model::class)) {
$model = $className::findFirstById($dispatcher->getParams()[0]);
// Override the parameters by the model instance
$dispatcher->setParams([$model]);
}
}
} catch (Exception $e) {
// An exception has occurred, maybe the class or action does not exist?
}
}
);
$dispatcher = new MvcDispatcher();
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Приведённый выше пример упрощён для учебных целей. Разработчик может улучшить его, чтобы вставлять любые типы зависимостей или моделей в действия перед их выполнением.
Начиная с версии 3.1.x, диспетчер также имеет возможность обрабатывать это внутренне для всех моделей, передаваемых в действие контроллера, используя Phalcon\Mvc\Model\Binder.
use Phalcon\Mvc\Dispatcher; use Phalcon\Mvc\Model\Binder; $dispatcher = new Dispatcher(); $dispatcher->setModelBinder(new Binder()); return $dispatcher;
Поскольку объект Binder использует внутренне API рефлексии, который может быть ресурсоёмким, существует возможность задать кэш. Это можно сделать, используя второй аргумент вsetModelBinder(), который также может принимать имя сервиса или просто передав экземпляр кэша в конструкторBinder.
Он также вводит новый интерфейс Phalcon\Mvc\Model\Binder\BindableInterface, который позволяет определять модели контроллеров, связанные с ними, чтобы разрешить привязку моделей в базовых контроллерах.
Например, у вас есть базовый контроллер CrudController, от которого наследуется ваш PostsController. Ваш CrudController выглядит примерно так:
use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Model;
class CrudController extends Controller
{
/**
* Show action
*
* @param Model $model
*/
public function showAction(Model $model)
{
$this->view->model = $model;
}
}
В вашем PostsController вам нужно определить, с какой моделью связан контроллер. Это делается путём реализации Phalcon\Mvc\Model\Binder\BindableInterface, который добавит метод getModelName(), из которого вы можете вернуть имя модели. Он может вернуть строку с одним именем модели или ассоциативный массив, где ключ — имя параметра.
use Phalcon\Mvc\Model\Binder\BindableInterface;
use Models\Posts;
class PostsController extends CrudController implements BindableInterface
{
public static function getModelName()
{
return Posts::class;
}
}
Объявив модель, связанную с PostsController, диспетчер может проверить контроллер на наличие метода getModelName() перед передачей определённой модели в родительское действие show.
Если структура вашего проекта не использует родительский контроллер, вы, конечно, всё ещё можете связать модель напрямую с действием контроллера:
use Phalcon\Mvc\Controller;
use Models\Posts;
class PostsController extends Controller
{
/**
* Shows posts
*
* @param Posts $post
*/
public function showAction(Posts $post)
{
$this->view->post = $post;
}
}
В настоящее время диспетчер будет использовать только первичный ключ моделей для выполненияfindFirst(). Пример маршрута для вышеуказанного был бы /posts/show/{1}
Обработка исключений "Не найдено"
С помощью EventsManager можно вставить точку подключения перед тем, как диспетчер выбросит исключение, когда сочетание контроллер/действие не найдено:
use Exception;
use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;
use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
$di->setShared(
"dispatcher",
function () {
// Create an EventsManager
$eventsManager = new EventsManager();
// Attach a listener
$eventsManager->attach(
"dispatch:beforeException",
function (Event $event, $dispatcher, Exception $exception) {
// Handle 404 exceptions
if ($exception instanceof DispatchException) {
$dispatcher->forward(
[
"controller" => "index",
"action" => "show404",
]
);
return false;
}
// Alternative way, controller or action doesn't exist
switch ($exception->getCode()) {
case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
case Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
$dispatcher->forward(
[
"controller" => "index",
"action" => "show404",
]
);
return false;
}
}
);
$dispatcher = new MvcDispatcher();
// Bind the EventsManager to the dispatcher
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
Конечно, этот метод может быть перемещён в независимые классы плагинов, позволяя нескольким классам принимать действия, когда в цикле обработки запроса возникает исключение:
use Exception;
use Phalcon\Events\Event;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
class ExceptionsPlugin
{
public function beforeException(Event $event, Dispatcher $dispatcher, Exception $exception)
{
// Default error action
$action = "show503";
// Handle 404 exceptions
if ($exception instanceof DispatchException) {
$action = "show404";
}
$dispatcher->forward(
[
"controller" => "index",
"action" => $action,
]
);
return false;
}
}
Оповещения о событиях ‘beforeException’ отправляются только для исключений, созданных диспетчером, и исключений, созданных в исполняемом действии. Исключения, созданные в слушателях или событиях контроллера, перенаправляются в последнюю структуру try/catch.
Реализация собственного диспетчера
Интерфейс Phalcon\Mvc\DispatcherInterface необходимо реализовать для создания собственного диспетчера, заменяя предоставляемый Phalcon.
© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/dispatching.html