Table of Contents
中編に続いて処理内容を見ていきます。 webroot/index.php に残されたのは次の4行でした。
|
1 2 3 4 |
$dispatcher->dispatch( Request::createFromGlobals(), new Response() ); |
Request::createFromGlobals() はメソッド名から明らかですが、 グローバル変数 $_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 でイベントを発生させています。 そしてイベントが発生した後の処理結果がレスポンスオブジェクトになっていたら終了します。
dispatchEvent
dispatchEvent は、 Dispatcher では定義されておらず、 Dispathcer で使用されている EventManagerTrait で定義されています。
|
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; } |
このメソッドが更に EventManager の dispatch を呼んでいます。 引数として渡される $event は CakeEventEvent のインスタンスです。
CakeEventEvent のコンストラクタを確認します。
CakeEventEvent __construct
コンストラクタの引数は、 'Dispatcher.beforeDispatch' になります。
|
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; } |
インスタンス変数に引数の値を格納しているだけでした。 今回 $data は 'request' と 'response' をキーにもつ連想配列になります。
作成された $event を引数にして EventManager の dispatch メソッド が実行されます。
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; } |
dispatch の中で listeners メソッドが実行されます。 引数は $event->name() ですので 'Dispatcher.beforeDispatch' です。
listeners は 呼び出されるべきリスナを、呼び出される順序で配列にして返します。
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; } |
呼び出されているメソッド prioritisedListeners の中身を見てみます。
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]; } |
とくに難しいことは行っていません。 イベントキー すなわち 'Dispatcher.beforeDispatch' や 'Dispatcher.afterDispatch' に関係するリスナを返すメソッドです。
そして listeners では $globaListeners と $localListeners のリスナーをマージしています。 Eventmanager はシングルトンっぽいつくりになっていて、 グローバルに扱えるインスタンスと、 個別の用途に使うインスタンスを分けることができるようになっています。 実際のところ今回の処理では $globalListeners が空なので、 $localListeners のキーがそのまま $priorities に入ります。 リスナのキーは優先度(priority) ですので、 $priorities には int 型 の 優先度の配列になります。
優先度の数値が低いものから順に、そしてグローバルリスナを優先して、リスナの配列を作成しています。
EventManager の dispatch に戻り、 isStopped が false の場合、すなわちイベントが終了していない場合に _callListener でリスナを呼び出します。
_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); } } |
$listener に渡された値がメソッドの名前になり、実行されます。 実際に渡されるメソッド名は 'handle' です。 $event は new Event('Dispatcher.beforeDispatch') で作成されたイベントです。 $data[0] が request で $data[1] が response です。
すると _callListener の 返り値 は フィルタオブジェクト の handle($event, request, response) になります。 実際のところ、 登録された 4つのフィルタについて $listener が実行されます。
handle メソッドは それぞれのフィルタではなく、 その親クラスの DispathcerFilter で定義されています。 今まで出てきたすべてのフィルタは 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); } } |
今、 $event->name() は 'Dispatcher.beforeDispatch' になりますから、 $name には 'beforeDispatch' が入ります。 つまり、 handle を実行するそれぞれのクラスで定義された beforeDispatch メソッド が実行されることになります。
それぞれのクラスの beforeDispatch を見てみましょう。
実行される順で見ると、 AssetFilter, RoutingFilter, ControllerFactoryFilter です。 (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; } |
Asset ファイル のリクエストだった場合にレスポンスを返します。 Asset ファイル だった場合、 header メソッド を呼び出したりして適宜処理をします。
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); } } |
どのコントローラを使うべきなのかを指定しています。
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); } |
コントローラをインスタンス化して $event->data['controller'] に代入しています。
このようにして設定されたコントローラが、 Dispatcher の invoke メソッド で実行されます。 invoke が実行されると、 Controller が render まで実行します。 そして html の生成まで行われます。
あとは、 dispatch メソッド のところで afterDispatch を実行して終わります。
