Table of Contents
This article is continued from CakePHP 3 the Way to Reach Controller Invoking part 2 of 3. webroot/index.php remains next 4 lines.
|
1 2 3 4 |
$dispatcher->dispatch( Request::createFromGlobals(), new Response() ); |
It is obvious that Request::createFromGlobals() creates request object from global variables like $_GET.
dispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/** * Dispatches and invokes given Request, handing over control to the involved controller. If the controller is set * to autoRender, via Controller::$autoRender, then Dispatcher will render the view. * * Actions in CakePHP can be any public method on a controller, that is not declared in Controller. If you * want controller methods to be public and in-accessible by URL, then prefix them with a `_`. * For example `public function _loadPosts() { }` would not be accessible via URL. Private and protected methods * are also not accessible via URL. * * If no controller of given name can be found, invoke() will throw an exception. * If the controller is found, and the action is not found an exception will be thrown. * * @param CakeNetworkRequest $request Request object to dispatch. * @param CakeNetworkResponse $response Response object to put the results of the dispatch into. * @return string|void if `$request['return']` is set then it returns response body, null otherwise * @throws CakeRoutingExceptionMissingControllerException When the controller is missing. */ public function dispatch(Request $request, Response $response) { $beforeEvent = $this->dispatchEvent('Dispatcher.beforeDispatch', compact('request', 'response')); $request = $beforeEvent->data['request']; if ($beforeEvent->result instanceof Response) { if (isset($request->params['return'])) { return $beforeEvent->result->body(); } $beforeEvent->result->send(); return; } $controller = false; if (isset($beforeEvent->data['controller'])) { $controller = $beforeEvent->data['controller']; } if (!($controller instanceof Controller)) { throw new MissingControllerException([ 'class' => $request->params['controller'], 'plugin' => empty($request->params['plugin']) ? null : $request->params['plugin'], 'prefix' => empty($request->params['prefix']) ? null : $request->params['prefix'], '_ext' => empty($request->params['_ext']) ? null : $request->params['_ext'] ]); } $response = $this->_invoke($controller); if (isset($request->params['return'])) { return $response->body(); } $afterEvent = $this->dispatchEvent('Dispatcher.afterDispatch', compact('request', 'response')); $afterEvent->data['response']->send(); } |
dispatchEvent method raises event. And when the result of process after event occurred is response object, it ends process.
dispatchEvent
dispatchEvent method is not defined in Dispatcher, is defined in EventManagerTrait used by Dispathcer class.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * Default class name for new event objects. * * @var string */ protected $_eventClass = 'CakeEventEvent'; /** * Wrapper for creating and dispatching events. * * Returns a dispatched event. * * @param string $name Name of the event. * @param array|null $data Any value you wish to be transported with this event to * it can be read by listeners. * @param object|null $subject The object that this event applies to * ($this by default). * * @return CakeEventEvent */ public function dispatchEvent($name, $data = null, $subject = null) { if ($subject === null) { $subject = $this; } $event = new $this->_eventClass($name, $subject, $data); $this->eventManager()->dispatch($event); return $event; } |
this method calls dispatch method of EventManager. $event which is passed as argument is the instance of CakeEventEvent.
Let’s look into the constructor of CakeEventEvent
CakeEventEvent __construct
The argument of the constructor is 'Dispatcher.beforeDispatch' in this case.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * Constructor * * ### Examples of usage: * * ``` * $event = new Event('Order.afterBuy', $this, ['buyer' => $userData]); * $event = new Event('User.afterRegister', $UserModel); * ``` * * @param string $name Name of the event * @param object|null $subject the object that this event applies to (usually the object that is generating the event) * @param array|null $data any value you wish to be transported with this event to it can be read by listeners */ public function __construct($name, $subject = null, $data = null) { $this->_name = $name; $this->data = $data; $this->_subject = $subject; } |
It only assigns argument values to instance variables. In this case, $data is an associative array whose keys are 'request' and 'response'.
dispatch method of EventManager is executed with $event as argument.
EventManager dispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * Dispatches a new event to all configured listeners * * @param string|CakeEventEvent $event the event key name or instance of Event * @return CakeEventEvent * @triggers $event */ public function dispatch($event) { if (is_string($event)) { $event = new Event($event); } $listeners = $this->listeners($event->name()); if (empty($listeners)) { return $event; } foreach ($listeners as $listener) { if ($event->isStopped()) { break; } $result = $this->_callListener($listener['callable'], $event); if ($result === false) { $event->stopPropagation(); } if ($result !== null) { $event->result = $result; } } return $event; } |
In dispatch method, listeners method is executed. The argument is $event->name(), it means 'Dispatcher.beforeDispatch'..
listeners method returns array of listeners which should be executed, in calling order.
listenres
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/** * Internal flag to distinguish a common manager from the singleton * * @var bool */ protected $_isGlobal = false; /** * Returns a list of all listeners for an eventKey in the order they should be called * * @param string $eventKey Event key. * @return array */ public function listeners($eventKey) { $localListeners = []; if (!$this->_isGlobal) { $localListeners = $this->prioritisedListeners($eventKey); $localListeners = empty($localListeners) ? [] : $localListeners; } $globalListeners = static::instance()->prioritisedListeners($eventKey); $globalListeners = empty($globalListeners) ? [] : $globalListeners; $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners)); $priorities = array_unique($priorities); asort($priorities); $result = []; foreach ($priorities as $priority) { if (isset($globalListeners[$priority])) { $result = array_merge($result, $globalListeners[$priority]); } if (isset($localListeners[$priority])) { $result = array_merge($result, $localListeners[$priority]); } } return $result; } |
Let’s look into prioritisedListeners method which is called in above code.
prioritisedListeners
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Returns the listeners for the specified event key indexed by priority * * @param string $eventKey Event key. * @return array */ public function prioritisedListeners($eventKey) { if (empty($this->_listeners[$eventKey])) { return []; } return $this->_listeners[$eventKey]; } |
There’s nothing difficult. It returns listener related to event key like 'Dispatcher.beforeDispatch' or 'Dispatcher.afterDispatch'.
And listeners method merges listeners in $globaListeners and $localListeners. EventManager is written in sort of singleton pattern, and we can separately use the instance for global use and the one for specific use. Actually in this case, $globalListeners is empty, then, the key in $localListeners added to $priorities and $priorities has no other keys. The key of listener is priority and $priorities is the array of int value, From low priority to high, and firstly global listener and secondly local listener, it generates listener array.
Back to dispatch method of EventManager, it call listener with _callListener method when isStopped is false, namely when the event is not ended.
_callListener
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** * Calls a listener. * * Direct callback invocation is up to 30% faster than using call_user_func_array. * Optimize the common cases to provide improved performance. * * @param callable $listener The listener to trigger. * @param CakeEventEvent $event Event instance. * @return mixed The result of the $listener function. */ protected function _callListener(callable $listener, Event $event) { $data = $event->data(); $length = count($data); if ($length) { $data = array_values($data); } switch ($length) { case 0: return $listener($event); case 1: return $listener($event, $data[0]); case 2: return $listener($event, $data[0], $data[1]); case 3: return $listener($event, $data[0], $data[1], $data[2]); default: array_unshift($data, $event); return call_user_func_array($listener, $data); } } |
The value passed to $listener is handled as method name and executed. In this case, the method name is 'handle'. $event is the event created with new Event('Dispatcher.beforeDispatch'). $data[0] is request and $data[1] is response.
So, the return value of _callListener is handle($event, request, response) of filter object. Actually, $listener is executed for all the registered 4 filters.
handle method is not defined in each filter, is defined in the parent class, DispathcerFilter. Every filter we duscuss about inherits DispatcherFilter.
handle
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Handler method that applies conditions and resolves the correct method to call. * * @param CakeEventEvent $event The event instance. * @return mixed */ public function handle(Event $event) { $name = $event->name(); list(, $method) = explode('.', $name); if (empty($this->_config['for']) && empty($this->_config['when'])) { return $this->{$method}($event); } if ($this->matches($event)) { return $this->{$method}($event); } } |
Now, $event->name() returns 'Dispatcher.beforeDispatch', so $name become 'beforeDispatch'. That is, beforeDispatch defined in each class which executes handle method is executed.
Now, let’s look into beforeDispatch method in each class. In order of execution, the classes are AssetFilter, RoutingFilter, ControllerFactoryFilter. (I omit DebugBarFilter.)
AssetFilter beforeDispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/** * Checks if a requested asset exists and sends it to the browser * * @param CakeEventEvent $event containing the request and response object * @return CakeNetworkResponse if the client is requesting a recognized asset, null otherwise * @throws CakeNetworkExceptionNotFoundException When asset not found */ public function beforeDispatch(Event $event) { $request = $event->data['request']; $url = urldecode($request->url); if (strpos($url, '..') !== false || strpos($url, '.') === false) { return null; } $assetFile = $this->_getAssetFile($url); if ($assetFile === null || !file_exists($assetFile)) { return null; } $response = $event->data['response']; $event->stopPropagation(); $response->modified(filemtime($assetFile)); if ($response->checkNotModified($request)) { return $response; } $pathSegments = explode('.', $url); $ext = array_pop($pathSegments); $this->_deliverAsset($request, $response, $assetFile, $ext); return $response; } |
It returns response when the request is for asset file. If request is for asset file, CakePHP 3 handle appropriate process like calling header method.
RoutingFilter beforeDispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Applies Routing and additionalParameters to the request to be dispatched. * If Routes have not been loaded they will be loaded, and config/routes.php will be run. * * @param CakeEventEvent $event containing the request, response and additional params * @return void */ public function beforeDispatch(Event $event) { $request = $event->data['request']; Router::setRequestInfo($request); if (empty($request->params['controller'])) { $params = Router::parse($request->url); $request->addParams($params); } } |
It configures which controller should be used.
ControllerFactoryFilter beforeDispatch
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Resolve the request parameters into a controller and attach the controller * to the event object. * * @param CakeEventEvent $event The event instance. * @return void */ public function beforeDispatch(Event $event) { $request = $event->data['request']; $response = $event->data['response']; $event->data['controller'] = $this->_getController($request, $response); } |
It create controller instance and assigns it to $event->data['controller'].
Configured controller in above process will be invoked by invoke method in Dispathcer. On invoke method, the controller execute its process and render html.
At last, afterDispatch method is executed in dispatch method and process ends.
