调度控制器(Dispatching Controllers)

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

循环调度(The Dispatch Loop)

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

  1. <?php
  2.  
  3. // 循环调度
  4. while (!$finished) {
  5.  
  6. $finished = true;
  7.  
  8. $controllerClass = $controllerName . "Controller";
  9.  
  10. // 通过自动加载器实例化控制器类
  11. $controller = new $controllerClass();
  12.  
  13. // 执行 action
  14. call_user_func_array(array($controller, $actionName . "Action"), $params);
  15.  
  16. // $finished应该重新加载以检测MVC流
  17. // 是否转发给了另一个控制器
  18. $finished = true;
  19. }

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

执行 action 的过程中,可以通过 throw PhalconContinueException(‘提前完成 action’) 终止 action 的运行,而不影响后续的程序运行。

循环调度事件(Dispatch Loop Events)

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

事件名称 何时触发 此操作是否可终止? 触发于
beforeDispatchLoop 在进入循环调度前触发。此时,调度器不知道将要执行的控制器或者动作是否存在。调度器只知道路由传递过来的信息。 侦听者
beforeDispatch 在进入循环调度后触发。此时,调度器不知道将要执行的控制器或者动作是否存在。调度器只知道路由传递过来的信息。 侦听者
beforeExecuteRoute 在执行控制器/动作方法前触发。此时,调度器已经初始化了控制器并知道动作是否存在。 侦听者/控制器
initialize 允许在请求中全局初始化控制器。 控制器
afterExecuteRoute 在执行控制器/动作方法后触发。由于此操作不可终止,所以仅在执行动作后才使用此事件进行清理工作。 侦听者/控制器
beforeNotFoundAction 当控制器中的动作找不到时触发。 侦听者
beforeException 在调度器抛出任意异常前触发。 侦听者
afterDispatch 在执行控制器/动作方法后触发。由于此操作不可终止,所以仅在执行动作后才使用此事件进行清理工作。 侦听者
afterDispatchLoop 在退出循环调度后触发。 侦听者

INVO 这篇导读说明了如何从通过结合 Acl 实现的一个安全过滤器中获得事件调度的好处。

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5.  
  6. $di->set('dispatcher', function () {
  7.  
  8. // 创建一个事件管理
  9. $eventsManager = new EventsManager();
  10.  
  11. // 为“dispatch”类型附上一个侦听者
  12. $eventsManager->attach("dispatch", function ($event, $dispatcher) {
  13. // ...
  14. });
  15.  
  16. $dispatcher = new MvcDispatcher();
  17.  
  18. // 将$eventsManager绑定到视图组件
  19. $dispatcher->setEventsManager($eventsManager);
  20.  
  21. return $dispatcher;
  22.  
  23. }, true);

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Controller;
  4. use Phalcon\Mvc\Dispatcher;
  5.  
  6. class PostsController extends Controller
  7. {
  8. public function beforeExecuteRoute(Dispatcher $dispatcher)
  9. {
  10. // 在每一个找到的动作前执行
  11. }
  12.  
  13. public function afterExecuteRoute(Dispatcher $dispatcher)
  14. {
  15. // 在每一个找到的动作后执行
  16. }
  17. }

转发到其他动作(Forwarding to other actions)

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Controller;
  4.  
  5. class PostsController extends Controller
  6. {
  7. public function indexAction()
  8. {
  9.  
  10. }
  11.  
  12. public function saveAction($year, $postTitle)
  13. {
  14. // ... 储存一些产品并且转发用户
  15.  
  16. // 将流转发到index动作
  17. $this->dispatcher->forward(
  18. array(
  19. "controller" => "Post", // 区分大小写,相当于路由处理后的数据
  20. "action" => "index"
  21. )
  22. );
  23. }
  24. }

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

更多转发示例:

  1. <?php
  2.  
  3. // 将流转发到当前控制器的另一个动作
  4. $this->dispatcher->forward(
  5. array(
  6. "action" => "search"
  7. )
  8. );
  9.  
  10. // 将流转发到当前控制器的另一个动作
  11. // 传递参数
  12. $this->dispatcher->forward(
  13. array(
  14. "action" => "search",
  15. "params" => array(1, 2, 3)
  16. )
  17. );

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

参数 触发
controller 一个待转发且有效的控制器名字。
action 一个待转发且有效的动作名字。
params 一个传递给动作的数组参数。
namespace 一个控制器对应的命名空间名字。

准备参数(Preparing Parameters)

多得 Phalcon\Mvc\Dispatcher 提供的钩子函数, 你可以简单地调整你的应用来匹配URL格式:

例如,你想把你的URL看起来像这样:http://example.com/controller/key1/value1/key2/value

默认下,参数会按URL传递的顺序传给对应的动作,你可以按期望来转换他们:

  1. <?php
  2.  
  3. use Phalcon\Dispatcher;
  4. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  5. use Phalcon\Events\Manager as EventsManager;
  6.  
  7. $di->set('dispatcher', function () {
  8.  
  9. // 创建一个事件管理
  10. $eventsManager = new EventsManager();
  11.  
  12. // 附上一个侦听者
  13. $eventsManager->attach("dispatch:beforeDispatchLoop", function ($event, $dispatcher) {
  14.  
  15. $keyParams = array();
  16. $params = $dispatcher->getParams();
  17.  
  18. // 用奇数参数作key,用偶数作值
  19. foreach ($params as $number => $value) {
  20. if ($number & 1) {
  21. $keyParams[$params[$number - 1]] = $value;
  22. }
  23. }
  24.  
  25. // 重写参数
  26. $dispatcher->setParams($keyParams);
  27. });
  28.  
  29. $dispatcher = new MvcDispatcher();
  30. $dispatcher->setEventsManager($eventsManager);
  31.  
  32. return $dispatcher;
  33. });

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

  1. <?php
  2.  
  3. use Phalcon\Dispatcher;
  4. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  5. use Phalcon\Events\Manager as EventsManager;
  6.  
  7. $di->set('dispatcher', function () {
  8.  
  9. // 创建一个事件管理
  10. $eventsManager = new EventsManager();
  11.  
  12. // 附上一个侦听者
  13. $eventsManager->attach("dispatch:beforeDispatchLoop", function ($event, $dispatcher) {
  14.  
  15. $keyParams = array();
  16. $params = $dispatcher->getParams();
  17.  
  18. // 将每一个参数分解成key、值 对
  19. foreach ($params as $number => $value) {
  20. $parts = explode(':', $value);
  21. $keyParams[$parts[0]] = $parts[1];
  22. }
  23.  
  24. // 重写参数
  25. $dispatcher->setParams($keyParams);
  26. });
  27.  
  28. $dispatcher = new MvcDispatcher();
  29. $dispatcher->setEventsManager($eventsManager);
  30.  
  31. return $dispatcher;
  32. });

获取参数(Getting Parameters)

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Controller;
  4.  
  5. class PostsController extends Controller
  6. {
  7. public function indexAction()
  8. {
  9.  
  10. }
  11.  
  12. public function saveAction()
  13. {
  14. // 从URL传递过来的参数中获取title
  15. // 或者在一个事件中准备
  16. $title = $this->dispatcher->getParam("title");
  17.  
  18. // 从URL传递过来的参数中获取year
  19. // 或者在一个事件中准备并且进行过滤
  20. $year = $this->dispatcher->getParam("year", "int");
  21.  
  22. // ...
  23. }
  24. }

准备行动(Preparing actions)

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

转换动作名(Camelize action names)

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

  1. <?php
  2.  
  3. use Phalcon\Text;
  4. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  5. use Phalcon\Events\Manager as EventsManager;
  6.  
  7. $di->set('dispatcher', function () {
  8.  
  9. // 创建一个事件管理
  10. $eventsManager = new EventsManager();
  11.  
  12. // Camelize动作
  13. $eventsManager->attach("dispatch:beforeDispatchLoop", function ($event, $dispatcher) {
  14. $dispatcher->setActionName(Text::camelize($dispatcher->getActionName()));
  15. });
  16.  
  17. $dispatcher = new MvcDispatcher();
  18. $dispatcher->setEventsManager($eventsManager);
  19.  
  20. return $dispatcher;
  21. });

删除遗留的扩展名(Remove legacy extensions)

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

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

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  4. use Phalcon\Events\Manager as EventsManager;
  5.  
  6. $di->set('dispatcher', function () {
  7.  
  8. // 创建一个事件管理
  9. $eventsManager = new EventsManager();
  10.  
  11. // 在调度前删除扩展
  12. $eventsManager->attach("dispatch:beforeDispatchLoop", function ($event, $dispatcher) {
  13.  
  14. // 删除扩展
  15. $action = preg_replace('/\.php$/', '', $dispatcher->getActionName());
  16.  
  17. // 重写动作
  18. $dispatcher->setActionName($action);
  19. });
  20.  
  21. $dispatcher = new MvcDispatcher();
  22. $dispatcher->setEventsManager($eventsManager);
  23.  
  24. return $dispatcher;
  25. });

注入模型实例(Inject model instances)

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

控制器看起来像这样:

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

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  5. use Phalcon\Events\Manager as EventsManager;
  6.  
  7. $di->set('dispatcher', function () {
  8.  
  9. // 创建一个事件管理
  10. $eventsManager = new EventsManager();
  11.  
  12. $eventsManager->attach("dispatch:beforeDispatchLoop", function ($event, $dispatcher) {
  13.  
  14. // 可能的控制器类名
  15. $controllerName = $dispatcher->getControllerClass();
  16.  
  17. // 可能的方法名
  18. $actionName = $dispatcher->getActiveMethod();
  19.  
  20. try {
  21.  
  22. // 从反射中获取将要被执行的方法
  23. $reflection = new \ReflectionMethod($controllerName, $actionName);
  24.  
  25. // 参数检查
  26. foreach ($reflection->getParameters() as $parameter) {
  27.  
  28. // 获取期望的模型名字
  29. $className = $parameter->getClass()->name;
  30.  
  31. // 检查参数是否为模型的实例
  32. if (is_subclass_of($className, Model::class)) {
  33.  
  34. $model = $className::findFirstById($dispatcher->getParams()[0]);
  35.  
  36. // 根据模型实例重写参数
  37. $dispatcher->setParams(array($model));
  38. }
  39. }
  40.  
  41. } catch (\Exception $e) {
  42. // 异常触发,类或者动作不存在?
  43. }
  44.  
  45. });
  46.  
  47. $dispatcher = new MvcDispatcher();
  48. $dispatcher->setEventsManager($eventsManager);
  49.  
  50. return $dispatcher;
  51. });

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

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

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

  1. <?php
  2.  
  3. use Phalcon\Dispatcher;
  4. use Phalcon\Mvc\Dispatcher as MvcDispatcher;
  5. use Phalcon\Events\Manager as EventsManager;
  6. use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
  7.  
  8. $di->set('dispatcher', function () {
  9.  
  10. // 创建一个事件管理
  11. $eventsManager = new EventsManager();
  12.  
  13. // 附上一个侦听者
  14. $eventsManager->attach("dispatch:beforeException", function ($event, $dispatcher, $exception) {
  15.  
  16. // 处理404异常
  17. if ($exception instanceof DispatchException) {
  18. $dispatcher->forward(
  19. array(
  20. 'controller' => 'index',
  21. 'action' => 'show404'
  22. )
  23. );
  24.  
  25. return false;
  26. }
  27.  
  28. // 代替控制器或者动作不存在时的路径
  29. switch ($exception->getCode()) {
  30. case Dispatcher::EXCEPTION_HANDLER_NOT_FOUND:
  31. case Dispatcher::EXCEPTION_ACTION_NOT_FOUND:
  32. $dispatcher->forward(
  33. array(
  34. 'controller' => 'index',
  35. 'action' => 'show404'
  36. )
  37. );
  38.  
  39. return false;
  40. }
  41. });
  42.  
  43. $dispatcher = new MvcDispatcher();
  44.  
  45. // 将EventsManager绑定到调度器
  46. $dispatcher->setEventsManager($eventsManager);
  47.  
  48. return $dispatcher;
  49.  
  50. }, true);

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

  1. <?php
  2.  
  3. use Phalcon\Events\Event;
  4. use Phalcon\Mvc\Dispatcher;
  5. use Phalcon\Mvc\Dispatcher\Exception as DispatchException;
  6.  
  7. class ExceptionsPlugin
  8. {
  9. public function beforeException(Event $event, Dispatcher $dispatcher, $exception)
  10. {
  11. // 处理404异常
  12. if ($exception instanceof DispatchException) {
  13. $dispatcher->forward(array(
  14. 'controller' => 'index',
  15. 'action' => 'show404'
  16. ));
  17. return false;
  18. }
  19.  
  20. // 处理其他异常
  21. $dispatcher->forward(array(
  22. 'controller' => 'index',
  23. 'action' => 'show503'
  24. ));
  25.  
  26. return false;
  27. }
  28. }
仅仅当异常产生于调度器或者异常产生于被执行的动作时才会通知’beforeException’里面的事件。侦听者或者控制器事件中产生的异常则会重定向到最近的try/catch。

设置错误处理器(Set error handler)

没有匹配到对应的路由,我们通过 $router->notFound 来设置控制器,如果是无法加载对应的控制器,或者找不到对应的 Action,我们通过 $dispatch->setErrorHandler 来设置:

  1. <?php
  2.  
  3. $dispatcher->setErrorHandler('Error::index', Phalcon\Dispatcher::EXCEPTION_HANDLER_NOT_FOUND);
  4. $dispatcher->setErrorHandler('Error::index', Phalcon\Dispatcher::EXCEPTION_ACTION_NOT_FOUND);

控制器的驼峰转换(Camelize Controller)

Phalcon 默认执行控制器类名和命名空间的驼峰转换,我们可以关闭它:

  1. <?php
  2.  
  3. $dispatcher->camelizeController(false);
  4. $dispatcher->camelizeNamespace(false);

自定义调度器(Implementing your own Dispatcher)

为了创建自定义调度器,必须实现 Phalcon\Mvc\DispatcherInterface 接口,从而替换Phalcon框架默认提供的调度器。

原文: http://www.myleftstudio.com/reference/dispatching.html