Контроллеры диспетчеризации

Phalcon\Mvc\Dispatcher является компонентом, ответственным за создание экземпляров контроллеров и выполнением необходимых действий над ними в приложении MVC. Понимание его работы и возможностей помогает нам получить больше от услуг, предоставляемых инфраструктурой.

Цикл диспетчеризации

Это важный процесс, который имеет много общего с самим потоком MVC, особенно с частью контроллера. Работа происходит внутри диспетчера контроллера. Файлы контроллера считываются, загружаются и создаются экземпляры. Затем выполняются необходимые действия. Если действие перенаправляет поток на другой контроллер / действие, диспетчер контроллера запускается снова. Чтобы лучше проиллюстрировать это, в следующем примере показан приблизительно процесс, выполняемый в Phalcon\Mvc\Dispatcher:

<?php

// Цикл диспетчеризации
while (!$finished) {
    $finished = true;

    $controllerClass = $controllerName . 'Controller';

    // Создание экземпляра класса контроллера с помощью автозагрузчиков
    $controller = new $controllerClass();

    // Выполнить действие
    call_user_func_array(
        [
            $controller,
            $actionName . 'Action'
        ],
        $params
    );

    // '$finished' должна быть перезагружена, чтобы проверить, был ли поток передан другому контроллеру
    $finished = true;
}

В приведенном выше коде отсутствуют валидации, фильтры и дополнительные проверки, но он демонстрирует нормальный ход работы диспетчера.

События Цикла Диспетчеризации

Phalcon\Mvc\Dispatcher может отправлять события EventsManager , если он присутствует. События запускаются с помощью  типа dispatch. Некоторые события при возврате boolean false могут остановить активную операцию. Поддерживаются следующие события:

Имя событияСрабатываетМожет остановить работу?Запускает
beforeDispatchLoop Срабатывает перед входом в цикл диспетчеризации. На этом этапе диспетчер не знает, существует ли контроллер или выполняемые действия. Диспетчер знает только информацию, передаваемую маршрутизатором. да Слушатели
beforeDispatch Срабатывает после входа в цикл диспетчеризации. На этом этапе диспетчер не знает, существует ли контроллер или выполняемые действия. Диспетчер знает только информацию, передаваемую маршрутизатором. да Слушатели
beforeExecuteRoute Срабатывает перед выполнением контроллера / метода действия. На этом этапе диспетчер инициализирует контроллер и знает, существует ли действие. да Слушатели / Контроллеры
initialize Разрешить глобально инициализировать контроллер в запросе нет Контроллеры
afterExecuteRoute Срабатывает после выполнения контроллера / метода действия. Поскольку операция не может быть остановлена, используйте это событие только для очистки после выполнения действия нет Слушатели / Контроллеры
beforeNotFoundAction Срабатывает, когда действие не найдено в контроллере да Слушатели
beforeException Срабатывает до того, как диспетчер выдаст любое исключение да Слушатели
afterDispatch Срабатывает после выполнения контроллера / метода действия. Поскольку операция не может быть остановлена, используйте это событие только для очистки после выполнения действия да Слушатели
afterDispatchLoop Срабатывает после выхода из цикла отправки нет Слушатели
afterBinding Срабатывает после привязки моделей, но перед выполнением маршрута да Слушатели / Контроллеры

Учебник INVO показывает, как использовать преимущества диспетчеризации событий, реализующих фильтр безопасности с Acl.

В следующем примере показано, как присоединить прослушиватели к этому компоненту:

<?php

use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

$di->set(
    'dispatcher',
    function () {
        // Создание event-менеджер
        $eventsManager = new EventsManager();

        // Присоединение прослушивателя для типа 'dispatch'
        $eventsManager->attach(
            'dispatch',
            function (Event $event, $dispatcher) {
                // ...
            }
        );

        $dispatcher = new MvcDispatcher();

        // Привязать eventManager к компоненту вида
        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    },
    true
);

Созданный экземпляр контроллера автоматически выступает в качестве прослушивателя событий dispatch, поэтому можно реализовать методы в качестве обратных вызовов:

<?php

use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;

class PostsController extends Controller
{
    public function beforeExecuteRoute(Dispatcher $dispatcher)
    {
        // Выполняется перед каждым найденным действием
    }

    public function afterExecuteRoute(Dispatcher $dispatcher)
    {
        // Выполняется после каждого действия
    }
}

Переадресация на другие действия

Цикл диспетчеризации позволяет перенаправить поток выполнения на другой контроллер / действие. Это очень полезно, чтобы проверить, если пользователь может получить доступ к определенной функции, перенаправлять пользователей на другие экраны или просто повторно использовать код.

<?php

use Phalcon\Mvc\Controller;

class PostsController extends Controller
{
    public function indexAction()
    {

    }

    public function saveAction($year, $postTitle)
    {
        // ... Сохранить некоторый продукт и перенаправьтте пользователя

        // Прямой поток к действию индекса
        $this->dispatcher->forward(
            [
                'controller' => 'posts',
                'action'     => 'index',
            ]
        );
    }
}

Имейте в виду, что создание forward (дальше) -это не то же самое, что перенаправление HTTP. Хотя они, видимо, получили тот же результат. forward не перезагружает текущую страницу, все перенаправление происходит в одном запросе, в то время как перенаправление HTTP требует двух запросов для завершения процесса.

Дополнительные примеры пересылки:

<?php

// Прямой поток к другому действию в текущем контроллере
$this->dispatcher->forward(
    [
        'action' => 'search'
    ]
);

// Прямой поток к другому действию в текущем контроллере
// передача параметров
$this->dispatcher->forward(
    [
        'action' => 'search',
        'params' => [1, 2, 3]
    ]
);

Действие forward принимает следующие параметры:

ПараметрОписание
controller Действительное имя контроллера для пересылки.
action Действительное имя действия для пересылки.
params Массив параметров для действия.
namespace Действительное имя пространства имен, в котором находится контроллер.

Использование событий диспетчера

Вы можете использовать событие диспетчера dispatcher::beforeForward , чтобы изменить модули и перенаправить их проще и «чище»:

<?php

use Phalcon\Di;
use Phalcon\Events\Manager;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Events\Event;

$di = new Di();

$modules = [
  'backend' => [
      'className' => 'App\Backend\Bootstrap',
      'path'      => '/app/Modules/Backend/Bootstrap.php',
      'metadata'  => [
          'controllersNamespace' => 'App\Backend\Controllers',
      ],
  ],
];

$manager = new Manager();

$manager->attach(
  'dispatch:beforeForward',
  function (Event $event, Dispatcher $dispatcher, array $forward) use ($modules) {
      $metadata = $modules[$forward['module']]['metadata'];
      $dispatcher->setModuleName($forward['module']);
      $dispatcher->setNamespaceName($metadata['controllersNamespace']);
  }
);

$dispatcher = new Dispatcher();
$dispatcher->setDI($di);
$dispatcher->setEventsManager($manager);
$di->set('dispatcher', $dispatcher);
$dispatcher->forward(
  [
      'module'     => 'backend',
      'controller' => 'posts',
      'action'     => 'index',
  ]
);

echo $dispatcher->getModuleName(); // будет отображаться правильно 'backend' 

Подготовка параметров

Благодаря точкам перехвата, предоставленным Phalcon\Mvc\Dispatcherr, вы можете легко адаптировать приложение к любой схеме URL; то есть вы можете захотеть, чтобы ваши URL-адреса выглядели следующим образом: http://example.com/controller/key1/value1/key2/value. Поскольку параметры передаются с порядком их определения в URL-адресе для действий, вы можете преобразовать их, чтобы принять желаемую схему:

<?php

use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

$di->set(
    'dispatcher',
    function () {
        // Создать EventManager
        $eventsManager = new EventsManager();

        // Прикрепите слушателя
        $eventsManager->attach(
            'dispatch:beforeDispatchLoop',
            function (Event $event, $dispatcher) {
                $params = $dispatcher->getParams();

                $keyParams = [];

                // Используйте нечетные параметры как ключи и даже значения
                foreach ($params as $i => $value) {
                    if ($i & 1) {
                        // Previous param
                        $key = $params[$i - 1];

                        $keyParams[$key] = $value;
                    }
                }

                // Параметры переопределения
                $dispatcher->setParams($keyParams);
            }
        );

        $dispatcher = new MvcDispatcher();

        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    }
);

Если желаемая схема: http://example.com/controller/key1:value1/key2:value, необходим следующий код:

<?php

use Phalcon\Dispatcher;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

$di->set(
    'dispatcher',
    function () {
        // Создать EventManager
        $eventsManager = new EventsManager();

        // Прикрепить слушателя
        $eventsManager->attach(
            'dispatch:beforeDispatchLoop',
            function (Event $event, $dispatcher) {
                $params = $dispatcher->getParams();

                $keyParams = [];

                // Разнесение каждого параметра по парам ключ, значение
                foreach ($params as $number => $value) {
                    $parts = explode(':', $value);

                    $keyParams[$parts[0]] = $parts[1];
                }

                // Переопределение параметров
                $dispatcher->setParams($keyParams);
            }
        );

        $dispatcher = new MvcDispatcher();

        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    }
);

Получение Параметров

Когда маршрут предоставляет именованные параметры, вы можете получить их в контроллере, представлении или любом другом компоненте, который расширяет Phalcon\Di\Injectable.

<?php

use Phalcon\Mvc\Controller;

class PostsController extends Controller
{
    public function indexAction()
    {

    }

    public function saveAction()
    {
        // Получить заголовок записи, переданный в URL
        // как параметр или подготовленный в событии
        $title = $this->dispatcher->getParam('title');

        // Получить год записи, переданный в URL как параметр
        // или подготовленный в событии, также отфильтровав его
        $year = $this->dispatcher->getParam('year', 'int');

        // ...
    }
}

Подготовка действий

Вы также можете определить произвольную схему действий перед before циклом отправки.

Имена действий в верблюжем стиле.

Если исходный URL-адрес: http://example.com/admin/products/show-latest-products, и, например, вы хотите, параметры его отбражались в верблюжем стиле  show-latest-products как ShowLatestProducts, то необходим следующий код:

<?php

use Phalcon\Text;
use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

$di->set(
    'dispatcher',
    function () {
        // Создать EventManager
        $eventsManager = new EventsManager();

        // действие Camelize 
        $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

Вы можете удалить его перед отправкой контроллера / комбинации действий:

<?php

use Phalcon\Mvc\Dispatcher as MvcDispatcher;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

$di->set(
    'dispatcher',
    function () {
        // Создать EventManager
        $eventsManager = new EventsManager();

        // Удалите расширение перед отправкой
        $eventsManager->attach(
            'dispatch:beforeDispatchLoop',
            function (Event $event, $dispatcher) {
                $action = $dispatcher->getActionName();

                // Удалить расширение
                $action = preg_replace('/\.php$/', '', $action);

                // Заменить действие
                $dispatcher->setActionName($action);
            }
        );

        $dispatcher = new MvcDispatcher();

        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    }
);

Внедрение экземпляров модели

В этом примере разработчик хочет проверить параметры, которые получит действие, чтобы динамически внедрить экземпляры модели.

Контроллер выглядит так:

<?php

use Phalcon\Mvc\Controller;

class PostsController extends Controller
{
    /**
     * Shows posts
     *
     * @param \Posts $post
     */
    public function showAction(Posts $post)
    {
        $this->view->post = $post;
    }
}

Метод showAction получает экземпляр модели \Posts, разработчик может проверить это перед отправкой действия, подготавливающего параметр соответствующим образом:

<?php

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 () {
        // Создание EventsManager
        $eventsManager = new EventsManager();

        $eventsManager->attach(
            'dispatch:beforeDispatchLoop',
            function (Event $event, $dispatcher) {
                // Возможное имя класса контроллера
                $controllerName = $dispatcher->getControllerClass();

                // Possible method name
                $actionName = $dispatcher->getActiveMethod();

                try {
                    // Получить отражение для метода, который будет выполняться
                    $reflection = new ReflectionMethod($controllerName, $actionName);

                    $parameters = $reflection->getParameters();

                    // Проверить параметры
                    foreach ($parameters as $parameter) {
                        // Получить ожидаемое имя модели
                        $className = $parameter->getClass()->name;

                        // Проверить, ожидает ли параметр экземпляр модели
                        if (is_subclass_of($className, Model::class)) {
                            $model = $className::findFirstById($dispatcher->getParams()[0]);

                            // Переопределение параметров экземпляром модели
                            $dispatcher->setParams([$model]);
                        }
                    }
                } catch (Exception $e) {
                    // Произошло исключение, возможно, класс или действие не существует?
                }
            }
        );

        $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;

Он также вводит новый интерфейс 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
{
    /**
     * Показывает сообщения
     *
     * @param Posts $post
     */
    public function showAction(Posts $post)
    {
        $this->view->post = $post;
    }
}

Обработка Не Найденных Исключений

С помощью EventsManager можно вставить точку подключения до того, как диспетчер выдаст исключение, если комбинация контроллер / действие не найдена:

<?php

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 () {
        // Создание EventsManager
        $eventsManager = new EventsManager();

        // Прикрепить слушатель
        $eventsManager->attach(
            'dispatch:beforeException',
            function (Event $event, $dispatcher, Exception $exception) {
                // Обрабатывать 404 исключения
                if ($exception instanceof DispatchException) {
                    $dispatcher->forward(
                        [
                            'controller' => 'index',
                            'action'     => 'show404',
                        ]
                    );

                    return false;
                }

                // Альтернативного способа, контроллера или действия не существует
                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();

        // Привязка EventsManager к диспетчеру
        $dispatcher->setEventsManager($eventsManager);

        return $dispatcher;
    }
);

Конечно, этот метод может быть перемещен на независимые классы плагина, позволяя более чем одному классу выполнять действия, когда исключение создается в цикле диспетчеризации:

<?php

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)
    {
        // Действие ошибка по умолчанию
        $action = 'show503';

        // Обрабатывать 404 исключения
        if ($exception instanceof DispatchException) {
            $action = 'show404';
        }

        $dispatcher->forward(
            [
                'controller' => 'index',
                'action'     => $action,
            ]
        );

        return false;
    }
}

Реализация собственного диспетчера

Интерфейс Phalcon\Mvc\DispatcherInterface должен быть реализован для создания вашего собственного диспетчера, заменяющего тот, который предоставляется Phalcon.