🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 调度控制器 `Phalcon\Mvc\Dispatcher` 是负责实例化控制器并在MVC应用程序中对它们执行所需操作的组件。了解其操作和功能有助于我们从框架提供的服务中获得更多。 ## 调度循环 这是一个重要的过程,与MVC流本身有很大关系,特别是与控制器部分有关。工作发生在控制器调度程序中。读取,加载和实例化控制器文件。然后执行所需的操作。如果操作将流转发到另一个控制器/操作,则控制器调度程序再次启动。为了更好地说明这一点,以下示例显示了`Phalcon\Mvc\Dispatcher`中执行的大致过程: ```php <?php // 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`触发事件。返回布尔值`false`时的某些事件可能会停止活动操作。支持以下事件: | 事件名称 | 触发 | Can stop operation? | Triggered on | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | --------------------- | | beforeDispatchLoop | 在进入调度循环之前触发。此时,`Dispatcher`不知道控制器或要执行的动作是否存在。`Dispatcher`只知道路由器传递的信息。 | Yes | Listeners | | beforeDispatch | 进入调度循环后触发。此时,`Dispatcher`不知道控制器或要执行的动作是否存在。`Dispatcher`只知道路由器传递的信息 | Yes | Listeners | | beforeExecuteRoute | 在执行控制器/操作方法之前触发。此时,调度程序已初始化控制器并知道操作是否存在。 | Yes | Listeners/Controllers | | initialize | 在执行控制器/操作方法之前触发。此时,调度程序已初始化控制器并知道操作是否存在。 | No | Controllers | | afterExecuteRoute | 执行控制器/操作方法后触发。由于无法停止操作,因此仅在执行操作后使用此事件进行清理 | No | Listeners/Controllers | | beforeNotFoundAction |在控制器中找不到操作时触发 | Yes | Listeners | | beforeException | 在调度程序抛出任何异常之前触发 | Yes | Listeners | | afterDispatch |执行控制器/操作方法后触发。由于无法停止操作,因此仅在执行操作后使用此事件进行清理 | Yes | Listeners | | afterDispatchLoop | 退出调度循环后触发 | No | Listeners | | afterBinding | 模型绑定后但在执行路径之前触发 | Yes | Listeners/Controllers | `INVO`教程展示了如何利用使用`Acl`实现安全过滤器的调度事件。 以下示例演示如何将侦听器附加到此组件: ```php <?php 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 ); ``` 实例化的控制器自动充当调度事件的侦听器,因此您可以将方法实现为回调: ```php <?php 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 } } ``` >[warning] 事件侦听器上的方法接受`Phalcon\Events\Event`对象作为它们的第一个参数 - 控制器中的方法不接受。 ## 转发到其他动作 调度循环允许我们将执行流转发到另一个控制器/动作。这对于检查用户是否可以访问某些选项,将用户重定向到其他屏幕或仅重用代码非常有用。 ```php <?php 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', ] ); } } ``` 请记住,进行`forward`与进行HTTP重定向不同。虽然他们显然得到了相同的结果。`forward`不重新加载当前页面,所有重定向都发生在单个请求中,而HTTP重定向需要两个请求才能完成该过程。 更多转发示例: ```php <?php // 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] ] ); ``` `forward` 操作接受以下参数: | 参数 | 描述 | | ------------ | ------------------------------------------------------- | | `controller` | 要转发的有效控制器名称。 | | `action` | 要转发到的有效操作名称。 | | `params` | 操作的参数数组。 | | `namespace` | 控制器所属的有效命名空间名称。 | ### 使用事件管理器 您可以使用`dispatcher::beforeForward`事件来更改模块比重定向更容易和“更清洁”: ```php <?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(); // will display properly 'backend' ``` ## 准备参数 感谢`Phalcon\Mvc\Dispatcher`提供的挂钩点,您可以轻松地将您的应用程序调整为任何URL架构;即您可能希望您的网址如下所示:`http://example.com/controller/key1/value1/key2/value`。由于参数是按照在操作的URL中定义的顺序传递的,因此您可以将它们转换为采用所需的模式: ```php <?php 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`,则需要以下代码: ```php <?php 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`的组件中接收它们。 ```php <?php 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'); // ... } } ``` ## 准备动作 您还可以在调度循环之`前`为操作定义任意模式。 ### 操作名称驼峰化 如果原始网址为:`http://example.com/admin/products/show-latest-products`,例如,您希望将`show-latest-products`传递给`ShowLatestProducts`,则需要以下代码: ```php <?php 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`扩展名: ```php http://example.com/admin/products/show-latest-products.php http://example.com/admin/products/index.php ``` 您可以在调度控制器/操作组合之前将其删除: ```php <?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; } ); ``` ### 注入模型实例 在此示例中,开发人员希望检查操作将接收的参数,以便动态注入模型实例。 控制器看起来像: ```php <?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 <?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 () { // 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`在内部处理传递到控制器操作的所有模型。 ```php use Phalcon\Mvc\Dispatcher; use Phalcon\Mvc\Model\Binder; $dispatcher = new Dispatcher(); $dispatcher->setModelBinder(new Binder()); return $dispatcher; ``` >[warning] 由于Binder对象使用内部可能很重的Reflection Api,因此可以设置缓存。这可以通过在`setModelBinder()`中使用第二个参数来完成,该参数也可以接受服务名称或仅通过将缓存实例传递给Binder构造函数。 它还引入了一个新接口`Phalcon\Mvc\Model\Binder\BindableInterface` ,它允许您定义控制器关联模型,以允许模型在基本控制器中绑定。 例如,你有一个 `PostsController`继承于基础`CrudController`。你的`CrudController`看起来像这样: ```php 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()`方法,您可以从中返回模型名称。它只返回一个模型名称或关联数组的字符串,其中key是参数名称。 ```php use Phalcon\Mvc\Model\Binder\BindableInterface; use Models\Posts; class PostsController extends CrudController implements BindableInterface { public static function getModelName() { return Posts::class; } } ``` 通过声明与`PostsController`关联的模型,绑定器可以在将定义的模型传递到父显示操作之前检查控制器的`getModelName()`方法。 如果您的项目结构不使用任何父控制器,您当然仍然可以将模型直接绑定到控制器操作中: ```php 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; } } ``` >[warning] 目前,活页夹仅使用模型主键来执行`findFirst()`。以上的示例路线是`/posts/show/{1}` ## 处理 Not-Found 异常 使用EventsManager,可以在调度程序在未找到控制器/操作组合时抛出异常时插入挂钩点: ```php <?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 () { // 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; } ); ``` 当然,这个方法可以移动到独立的插件类上,允许多个类在调度循环中产生异常时采取操作: ```php <?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) { // Default error action $action = 'show503'; // Handle 404 exceptions if ($exception instanceof DispatchException) { $action = 'show404'; } $dispatcher->forward( [ 'controller' => 'index', 'action' => $action, ] ); return false; } } ``` >[danger] 只有调度程序生成的异常和执行的操作中产生的异常才会在`beforeException`事件中得到通知。在侦听器或控制器事件中生成的异常将重定向到最新的try/catch。 ## 实现自己的Dispatcher 必须实现 `Phalcon\Mvc\DispatcherInterface` 接口才能创建自己的调度程序,替换Phalcon提供的调度程序。