调度控制器Dispatching Controllers

:doc:`Phalcon\Mvc\Dispatcher <../api/Phalcon_Mvc_Dispatcher>`是MVC应用中负责实例化 控制器和执行在这些控制器上必要动作的组件。理解它的操作和能力将能帮助我们获得更多Phalcon框架提供的服务。

Phalcon\Mvc\Dispatcher is the component responsible for instantiating controllers and executing the required actions on them in an MVC application. Understanding its operation and capabilities helps us get more out of the services provided by the framework.

循环调度The Dispatch Loop

在MVC流中,这是一个重要的处理环节,特别对于控制器这部分。这些处理 发生在控制调度器中。控制器的文件将会被依次读取、加载和实例化。然后指定的action将会被执行。 如果一个动作将这个流转发给了另一个控制器/动作,控制调度器将会再次启动。为了更好 解释这一点,以下示例怡到好处地说明了在 Phalcon\Mvc\Dispatcher 中的处理过程:

This is an important process that has much to do with the MVC flow itself, especially with the controller part. The work occurs within the controller dispatcher. The controller files are read, loaded, and instantiated. Then the required actions are executed. If an action forwards the flow to another controller/action, the controller dispatcher starts again. To better illustrate this, the following example shows approximately the process performed within Phalcon\Mvc\Dispatcher:

  1. <?php
  2. //Dispatch loop
  3. while (!$finished) {
  4. $finished = true;
  5. $controllerClass = $controllerName . "Controller";
  6. //Instantiating the controller class via autoloaders
  7. $controller = new $controllerClass();
  8. // Execute the action
  9. call_user_func_array(array($controller, $actionName . "Action"), $params);
  10. // '$finished' should be reloaded to check if the flow
  11. // was forwarded to another controller
  12. $finished = true;
  13. }

上面的代码缺少了验证,过滤器和额外的检查,但它演示了在调度器中正常的操作流。

The code above lacks validations, filters and additional checks, but it demonstrates the normal flow of operation in the dispatcher.

循环调度事件Dispatch Loop Events

Phalcon\Mvc\Dispatcher 可以发送事件给当前的 EventsManager 。 事件会以“dispatch”类型被所触发。当返回false时有些事件可以终止当前激活的操作。已支持的事件如下:

Phalcon\Mvc\Dispatcher is able to send events to an EventsManager if it is present. Events are triggered using the type “dispatch”. Some events when returning boolean false could stop the active operation. The following events are supported:

Event NameTriggeredCan stop operation?Triggered on
beforeDispatchLoopTriggered before entering in the dispatch loop. At this point the dispatcher don’t know if the controller or the actions to be executed exist. The Dispatcher only knows the information passed by the Router.YesListeners
beforeDispatchTriggered after entering in the dispatch loop. At this point the dispatcher don’t know if the controller or the actions to be executed exist. The Dispatcher only knows the information passed by the Router.YesListeners
beforeExecuteRouteTriggered before executing the controller/action method. At this point the dispatcher has been initialized the controller and know if the action exist.YesListeners/Controllers
initializeAllow to globally initialize the controller in the requestNoControllers
afterExecuteRouteTriggered after executing the controller/action method. As operation cannot be stopped, only use this event to make clean up after execute the actionNoListeners/Controllers
beforeNotFoundActionTriggered when the action was not found in the controllerYesListeners
beforeExceptionTriggered before the dispatcher throws any exceptionYesListeners
afterDispatchTriggered after executing the controller/action method. As operation cannot be stopped, only use this event to make clean up after execute the actionYesListeners
afterDispatchLoopTriggered after exiting the dispatch loopNoListeners

:doc:`INVO <tutorial-invo>`这篇导读说明了如何从通过结合 Acl 实现的一个安全过滤器中获得事件调度的好处

The INVO tutorial shows how to take advantage of dispatching events implementing a security filter with Acl

以下例子演示了如何将侦听者绑定到组件上:

The following example demonstrates how to attach listeners to this component:

  1. <?php
  2. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  3. use Phalcon\Events\Manager as EventsManager;
  4. $di->set('dispatcher', function(){
  5. //Create an event manager
  6. $eventsManager = new EventsManager();
  7. //Attach a listener for type "dispatch"
  8. $eventsManager->attach("dispatch", function($event, $dispatcher) {
  9. //...
  10. });
  11. $dispatcher = new MvcDispatcher();
  12. //Bind the eventsManager to the view component
  13. $dispatcher->setEventsManager($eventsManager);
  14. return $dispatcher;
  15. }, true);

一个实例化的控制器会自动作为事件调度的侦听者,所以你可以实现回调函数:

An instantiated controller automatically acts as a listener for dispatch events, so you can implement methods as callbacks:

  1. <?php
  2. use Phalcon\Mvc\Controller;
  3. class PostsController extends Controller
  4. {
  5. public function beforeExecuteRoute($dispatcher)
  6. {
  7. // Executed before every found action
  8. }
  9. public function afterExecuteRoute($dispatcher)
  10. {
  11. // Executed after every found action
  12. }
  13. }

转发到其他动作Forwarding to other actions

循环调度允许我们转发执行流到另一个控制器/动作。这对于检查用户是否可以 访问页面,将用户重定向到其他屏幕或简单地代码重用都非常有用。

The dispatch loop allows us to forward the execution flow to another controller/action. This is very useful to check if the user can access to certain options, redirect users to other screens or simply reuse code.

  1. <?php
  2. use Phalcon\Mvc\Controller;
  3. class PostsController extends Controller
  4. {
  5. public function indexAction()
  6. {
  7. }
  8. public function saveAction($year, $postTitle)
  9. {
  10. // .. store some product and forward the user
  11. // Forward flow to the index action
  12. $this->dispatcher->forward(array(
  13. "controller" => "post",
  14. "action" => "index"
  15. ));
  16. }
  17. }

请注意制造一个“forward”并不等同于制造一个HTTP的重定向。尽管这两者表面上最终效果都一样。 “forward”不会重新加载当前页面,全部的重定向都只发生在一个请求里面,而HTTP重定向则需要两次请求 才能完成这个流程。

Keep in mind that making a “forward” is not the same as making an HTTP redirect. Although they apparently got the same result. The “forward” doesn’t reload the current page, all the redirection occurs in a single request, while the HTTP redirect needs two requests to complete the process.

更多转发示例:

More forwarding examples:

  1. <?php
  2. // Forward flow to another action in the current controller
  3. $this->dispatcher->forward(array(
  4. "action" => "search"
  5. ));
  6. // Forward flow to another action in the current controller
  7. // passing parameters
  8. $this->dispatcher->forward(array(
  9. "action" => "search",
  10. "params" => array(1, 2, 3)
  11. ));

一个转发的动作可以接受以下参数:

A forward action accepts the following parameters:

ParameterTriggered
controllerA valid controller name to forward to.
actionA valid action name to forward to.
paramsAn array of parameters for the action
namespaceA valid namespace name where the controller is part of

准备参数Preparing Parameters

因为 :doc:`Phalcon\Mvc\Dispatcher <../api/Phalcon_Mvc_Dispatcher>`提供的钩子函数, 你可以简单地调整你的应用来匹配URL格式: 例如,你想把你的URL看起来像这样:http://example.com/controller/key1/value1/key2/value 默认下,参数会按URL传递的顺序传给对应的动作,你可以按期望来转换他们:

Thanks to the hooks points provided by Phalcon\Mvc\Dispatcher you can easily adapt your application to any URL schema:

For example, you want your URLs look like: http://example.com/controller/key1/value1/key2/value

Parameters by default are passed as they come in the URL to actions, you can transform them to the desired schema:

  1. <?php
  2. use Phalcon\Dispatcher;
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5. $di->set('dispatcher', function() {
  6. //Create an EventsManager
  7. $eventsManager = new EventsManager();
  8. //Attach a listener
  9. $eventsManager->attach("dispatch:beforeDispatchLoop", function($event, $dispatcher) {
  10. $keyParams = array();
  11. $params = $dispatcher->getParams();
  12. //Use odd parameters as keys and even as values
  13. foreach ($params as $number => $value) {
  14. if ($number & 1) {
  15. $keyParams[$params[$number - 1]] = $value;
  16. }
  17. }
  18. //Override parameters
  19. $dispatcher->setParams($keyParams);
  20. });
  21. $dispatcher = new MvcDispatcher();
  22. $dispatcher->setEventsManager($eventsManager);
  23. return $dispatcher;
  24. });

如果期望的链接是这样: http://example.com/controller/key1:value1/key2:value,那么就需要以下这样的代码:

If the desired schema is: http://example.com/controller/key1:value1/key2:value, the following code is required:

  1. <?php
  2. use Phalcon\Dispatcher;
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5. $di->set('dispatcher', function() {
  6. //Create an EventsManager
  7. $eventsManager = new EventsManager();
  8. //Attach a listener
  9. $eventsManager->attach("dispatch:beforeDispatchLoop", function($event, $dispatcher) {
  10. $keyParams = array();
  11. $params = $dispatcher->getParams();
  12. //Explode each parameter as key,value pairs
  13. foreach ($params as $number => $value) {
  14. $parts = explode(':', $value);
  15. $keyParams[$parts[0]] = $parts[1];
  16. }
  17. //Override parameters
  18. $dispatcher->setParams($keyParams);
  19. });
  20. $dispatcher = new MvcDispatcher();
  21. $dispatcher->setEventsManager($eventsManager);
  22. return $dispatcher;
  23. });

获取参数Getting Parameters

当路由提供了命名的参数变量,你就可以在控制器、视图或者任何一个继承了 :doc:`Phalcon\DI\Injectable <../api/Phalcon_DI_Injectable>`的组件中获得这些参数。

When a route provides named parameters you can receive them in a controller, a view or any other component that extends Phalcon\DI\Injectable.

  1. <?php
  2. use Phalcon\Mvc\Controller;
  3. class PostsController extends Controller
  4. {
  5. public function indexAction()
  6. {
  7. }
  8. public function saveAction()
  9. {
  10. // Get the post's title passed in the URL as parameter
  11. // or prepared in an event
  12. $title = $this->dispatcher->getParam("title");
  13. // Get the post's year passed in the URL as parameter
  14. // or prepared in an event also filtering it
  15. $year = $this->dispatcher->getParam("year", "int");
  16. }
  17. }

准备行动Preparing actions

你也可以为动作定义一个调度前的映射表。

You can also define an arbitrary schema for actions before be dispatched.

转换动作名Camelize action names

如果原始链接是:http://example.com/admin/products/show-latest-products, 例如你想把’show-latest-products’转换成’ShowLatestProducts’, 需要以下代码:

If the original URL is: http://example.com/admin/products/show-latest-products, and for example you want to camelize ‘show-latest-products’ to ‘ShowLatestProducts’, the following code is required:

  1. <?php
  2. use Phalcon\Text;
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5. $di->set('dispatcher', function() {
  6. //Create an EventsManager
  7. $eventsManager = new EventsManager();
  8. //Camelize actions
  9. $eventsManager->attach("dispatch:beforeDispatchLoop", function($event, $dispatcher) {
  10. $dispatcher->setActionName(Text::camelize($dispatcher->getActionName()));
  11. });
  12. $dispatcher = new MvcDispatcher();
  13. $dispatcher->setEventsManager($eventsManager);
  14. return $dispatcher;
  15. });

删除遗留的扩展名Remove legacy extensions

如果原始链接总是包含一个’.php’扩展名:

If the original URL always contains a ‘.php’ extension:

http://example.com/admin/products/show-latest-products.php http://example.com/admin/products/index.php

你可以在调度对应的控制器/动作组前将它删除:

You can remove it before dispatch the controller/action combination:

  1. <?php
  2. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  3. use Phalcon\Events\Manager as EventsManager;
  4. $di->set('dispatcher', function() {
  5. //Create an EventsManager
  6. $eventsManager = new EventsManager();
  7. //Remove extension before dispatch
  8. $eventsManager->attach("dispatch:beforeDispatchLoop", function($event, $dispatcher) {
  9. //Remove extension
  10. $action = preg_replace('/\.php$/', '', $dispatcher->getActionName());
  11. //Override action
  12. $dispatcher->setActionName($action);
  13. });
  14. $dispatcher = new MvcDispatcher();
  15. $dispatcher->setEventsManager($eventsManager);
  16. return $dispatcher;
  17. });

注入模型实例Inject model instances

在这个实例中,开发人员想要观察动作接收到的参数以便可以动态注入模型实例。

In this example, the developer wants to inspect the parameters that an action will receive in order to dynamically inject model instances.

控制器看起来像这样:

The controller looks like:

  1. <?php
  2. use Phalcon\Mvc\Controller;
  3. class PostsController extends Controller
  4. {
  5. /**
  6. * Shows posts
  7. *
  8. * @param \Posts $post
  9. */
  10. public function showAction(Posts $post)
  11. {
  12. $this->view->post = $post;
  13. }
  14. }

‘showAction’方法接收到一个 Posts 模型的实例,开发人员可以在调度动作和准备映射参数前进行观察:

Method ‘showAction’ receives an instance of the model Posts, the developer could inspect this before dispatch the action preparing the parameter accordingly:

  1. <?php
  2. use Phalcon\Text;
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5. $di->set('dispatcher', function() {
  6. //Create an EventsManager
  7. $eventsManager = new EventsManager();
  8. $eventsManager->attach("dispatch:beforeDispatchLoop", function($event, $dispatcher) {
  9. //Possible controller class name
  10. $controllerName = Text::camelize($dispatcher->getControllerName()) . 'Controller';
  11. //Possible method name
  12. $actionName = $dispatcher->getActionName() . 'Action';
  13. try {
  14. //Get the reflection for the method to be executed
  15. $reflection = new \ReflectionMethod($controllerName, $actionName);
  16. //Check parameters
  17. foreach ($reflection->getParameters() as $parameter) {
  18. //Get the expected model name
  19. $className = $parameter->getClass()->name;
  20. //Check if the parameter expects a model instance
  21. if (is_subclass_of($className, 'Phalcon\Mvc\Model')) {
  22. $model = $className::findFirstById($dispatcher->getParams()[0]);
  23. //Override the parameters by the model instance
  24. $dispatcher->setParams(array($model));
  25. }
  26. }
  27. } catch (\Exception $e) {
  28. //An exception has occurred, maybe the class or action does not exist?
  29. }
  30. });
  31. $dispatcher = new MvcDispatcher();
  32. $dispatcher->setEventsManager($eventsManager);
  33. return $dispatcher;
  34. });

上面示例出于学术目的已经作了简化。 开发人员可以在执行动作前注入任何类型的依赖或者模型,以进行提高和强化。

The above example has been simplified for academic purposes. A developer can improve it to inject any kind of dependency or model in actions before be executed.

处理 Not-Found 错误Handling Not-Found Exceptions

使用 EventsManager ,可以在调度器找不到对应的控制器/动作组时而抛出异常前,插入一个钩子:

Using the EventsManager it’s possible to insert a hook point before the dispatcher throws an exception when the controller/action combination wasn’t found:

  1. <?php
  2. use Phalcon\Dispatcher;
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5. use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
  6. $di->set('dispatcher', function() {
  7. //Create an EventsManager
  8. $eventsManager = new EventsManager();
  9. //Attach a listener
  10. $eventsManager->attach("dispatch:beforeException", function($event, $dispatcher, $exception) {
  11. //Handle 404 exceptions
  12. if ($exception instanceof DispatchException) {
  13. $dispatcher->forward(array(
  14. 'controller' => 'index',
  15. 'action' => 'show404'
  16. ));
  17. return false;
  18. }
  19. //Alternative way, controller or action doesn't exist
  20. if ($event->getType() == 'beforeException') {
  21. switch ($exception->getCode()) {
  22. case \Phalcon\Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
  23. case \Phalcon\Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
  24. $dispatcher->forward(array(
  25. 'controller' => 'index',
  26. 'action' => 'show404'
  27. ));
  28. return false;
  29. }
  30. }
  31. });
  32. $dispatcher = new \Phalcon\Mvc\Dispatcher();
  33. //Bind the EventsManager to the dispatcher
  34. $dispatcher->setEventsManager($eventsManager);
  35. return $dispatcher;
  36. }, true);

当然,这个方法也可以移至独立的插件类中,使得在循环调度产生异常时可以有超过一个类执行需要的动作:

Of course, this method can be moved onto independent plugin classes, allowing more than one class take actions when an exception is produced in the dispatch loop:

  1. <?php
  2. use Phalcon\Events\Event;
  3. use Phalcon\Mvc\Dispatcher;
  4. use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
  5. class ExceptionsPlugin
  6. {
  7. public function beforeException(Event $event, Dispatcher $dispatcher, $exception)
  8. {
  9. //Handle 404 exceptions
  10. if ($exception instanceof DispatchException) {
  11. $dispatcher->forward(array(
  12. 'controller' => 'index',
  13. 'action' => 'show404'
  14. ));
  15. return false;
  16. }
  17. //Handle other exceptions
  18. $dispatcher->forward(array(
  19. 'controller' => 'index',
  20. 'action' => 'show503'
  21. ));
  22. return false;
  23. }
  24. }

仅仅当异常产生于调度器或者异常产生于被执行的动作时才会通知’beforeException’里面的事件。 侦听者或者控制器事件中产生的异常则会重定向到最近的try/catch。

Only exceptions produced by the dispatcher and exceptions produced in the executed action are notified in the ‘beforeException’ events. Exceptions produced in listeners or controller events are redirected to the latest try/catch.

自定义调度器Implementing your own Dispatcher

为了创建自定义调度器,必须实现 :doc:`Phalcon\Mvc\DispatcherInterface <../api/Phalcon_Mvc_DispatcherInterface>`接口, 从而替换Phalcon框架默认提供的调度器。

The Phalcon\Mvc\DispatcherInterface interface must be implemented to create your own dispatcher replacing the one provided by Phalcon.