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.