Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

ViewBuilder

ビューを作る。

build()のところで作っている。

public function build($vars = [], ServerRequest $request = null, Response $response = null, EventManager $events = null)
    {
        $className = $this->_className;
        if ($className === null) {
            $className = App::className('App', 'View', 'View') ?: 'Cake\View\View';
        }
        if ($className === 'View') {
            $className = App::className($className, 'View');
        } else {
            $className = App::className($className, 'View', 'View');
        }
        if (!$className) {
            throw new MissingViewException(['class' => $this->_className]);
        }

        $data = [
            'name' => $this->_name,
            'templatePath' => $this->_templatePath,
            'template' => $this->_template,
            'plugin' => $this->_plugin,
            'theme' => $this->_theme,
            'layout' => $this->_layout,
            'autoLayout' => $this->_autoLayout,
            'layoutPath' => $this->_layoutPath,
            'helpers' => $this->_helpers,
            'viewVars' => $vars,
        ];
        $data += $this->_options;

        return new $className($request, $response, $events, $data);
    }

builderのbuild()はViewVarsTraitのcreateView($viewClass = null)で呼ばれている。

ViewVarsTraitはControllerで使われている。

Controllerのrender($view = null, $layout = null)関数。

    public function render($view = null, $layout = null)
    {
// ビュービルダーを取得
        $builder = $this->viewBuilder();
// ビルダーにテンプレートパスが設定されてないならコントローラの_viewPath()で設定する。
        if (!$builder->getTemplatePath()) {
            $builder->setTemplatePath($this->_viewPath());
        }
// リクエストにbareパラムがあるならAutoLayoutを切る
        if ($this->request->getParam('bare')) {
            $builder->enableAutoLayout(false);
        }
        $builder->getClassName($this->viewClass);
// コントローラのautoRenderを切る
        $this->autoRender = false;

// beforeRenderイベントを通知
        $event = $this->dispatchEvent('Controller.beforeRender');
        if ($event->getResult() instanceof Response) {
            return $event->getResult();
        }
        if ($event->isStopped()) {
            return $this->response;
        }

// ビルダーにテンプレートが設定されなく、リクエストにアクションが設定されているなら、そのアクションからテンプレートを引っ張ってきて設定する。
        if ($builder->getTemplate() === null && $this->request->getParam('action')) {
            $builder->setTemplate($this->request->getParam('action'));
        }
// ビューを生成する。
        $this->View = $this->createView();

// ビューをレイアウトを使ってレンダリングする。
        $this->response->body($this->View->render($view, $layout));

        return $this->response;
    }

renderはControllerのactionで明示的に呼ぶこともできるがautoRenderを設定しているときはactionが呼ばれたときに自動的に呼ばれている。その機構はActionDispatcherの_invoke(Controller $controller)にある。

protected function _invoke(Controller $controller)
    {
//  invokeControllerイベントを通知する。
        $this->dispatchEvent('Dispatcher.invokeController', ['controller' => $controller]);

        $result = $controller->startupProcess();
        if ($result instanceof Response) {
            return $result;
        }

// コントローラのinvokeActionを呼ぶ ここでコントローラのアクションが呼ばれる。
        $response = $controller->invokeAction();

// アクションからのレスポンスがなくてautoRenderがtrueならレンダリングしてそのresponseを返却する。
        if ($response !== null && !($response instanceof Response)) {
            throw new LogicException('Controller actions can only return Cake\Http\Response or null.');
        }

        if (!$response && $controller->autoRender) {
            $response = $controller->render();
        } elseif (!$response) {
            $response = $controller->response;
        }

        $result = $controller->shutdownProcess();
        if ($result instanceof Response) {
            return $result;
        }

        return $response;
    }

この_invoke関数はdispatch(ServerRequest $request, Response $response)関数から呼ばれる。

public function dispatch(ServerRequest $request, Response $response)
    {
        if (Router::getRequest(true) !== $request) {
            Router::pushRequest($request);
        }

//  beforeDispatchを通知
        $beforeEvent = $this->dispatchEvent('Dispatcher.beforeDispatch', compact('request', 'response'));

        $request = $beforeEvent->getData('request');
        if ($beforeEvent->getResult() instanceof Response) {
            return $beforeEvent->getResult();
        }

// コントローラを生成しているらしい。。

        // Use the controller built by an beforeDispatch
        // event handler if there is one.
        if ($beforeEvent->getData('controller') instanceof Controller) {
            $controller = $beforeEvent->getData('controller');
        } else {
            $controller = $this->factory->create($request, $response);
        }

// 生成したコントローラに対して_invokeを実行。
        $response = $this->_invoke($controller);

// requestにreturnが設定されていたらresponseを返却
        if (isset($request->params['return'])) {
            return $response;
        }

// afterDispatchを通知
        $afterEvent = $this->dispatchEvent('Dispatcher.afterDispatch', compact('request', 'response'));

// 
        return $afterEvent->getData('response');
    }

このdispatchはDispatcherのdispatchで呼ばれる。

    public function dispatch(ServerRequest $request, Response $response)
    {
        $actionDispatcher = new ActionDispatcher(null, $this->eventManager(), $this->_filters);
        $response = $actionDispatcher->dispatch($request, $response);
        if (isset($request->params['return'])) {
            return $response->body();
        }

        return $response->send();
    }

DispatcherはDispatcherFactoryのcreate関数で作られる。

    public static function create()
    {
        $dispatcher = new Dispatcher();
        foreach (static::$_stack as $middleware) {
            $dispatcher->addFilter($middleware);
        }

        return $dispatcher;
    }

アクションがどのように呼ばれるのか

このあたりで、CakePHPでどのようにアクションが呼ばれるのかについて調べていることを自覚した。

webrootのindex.phoを調べてみる。

// Bind your application to the server.
$server = new Server(new Application(dirname(__DIR__) . '/config'));

// Run the request/response through the application
// and emit the response.
$server->emit($server->run());

このあたりがポイントか。このApplicationはBaseApplicationを継承している。BaseApplicationではgetDispatcherでActionDispatcherを作り、__invokeでdispatchしている。

    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
    {
        return $this->getDispatcher()->dispatch($request, $response);
    }


というわけで

BaseApplicationの__invokeでActionDispatcherがdispatchし、その関数内でコントローラに_invokeする。
_invokeはautoRenderがtrueだとコントローラのrender()関数を呼び、render()関数はビュービルダーを使ってビューをビルドする。という流れのようだ。

Server

サーバは以下の方法でRunnerにApplicationとミドルウェアを追加してrunし、Runnerは__invokeして回るのでリクエストが処理されていくようだ。

public function run(ServerRequestInterface $request = null, ResponseInterface $response = null)
    {
        $this->app->bootstrap();
        $response = $response ?: new Response();

// リクエスト取得
        $request = $request ?: ServerRequestFactory::fromGlobals();

// ミドルウェア
        $middleware = $this->app->middleware(new MiddlewareQueue());
        if (!($middleware instanceof MiddlewareQueue)) {
            throw new RuntimeException('The application `middleware` method did not return a middleware queue.');
        }
// ミドルウェア生成イベント通知
        $this->dispatchEvent('Server.buildMiddleware', ['middleware' => $middleware]);

// まずミドルウェアとしてApplicationを登録
        $middleware->add($this->app);

// runnerにrunしレスポンスを取得
        $response = $this->runner->run($middleware, $request, $response);

        if (!($response instanceof ResponseInterface)) {
            throw new RuntimeException(sprintf(
                'Application did not create a response. Got "%s" instead.',
                is_object($response) ? get_class($response) : $response
            ));
        }

// レスポンスを返却
        return $response;
    }

このrunはMiddlewareDispatcherにてexecute($request)されるようだ。

public function execute($request)
    {
        try {
            $reflect = new ReflectionClass($this->_class);
            $app = $reflect->newInstanceArgs($this->_constructorArgs);
        } catch (ReflectionException $e) {
            throw new LogicException(sprintf(
                'Cannot load "%s" for use in integration testing.',
                $this->_class
            ));
        }

        // Spy on the controller using the initialize hook instead
        // of the dispatcher hooks as those will be going away one day.
        EventManager::instance()->on(
            'Controller.initialize',
            [$this->_test, 'controllerSpy']
        );

        $server = new Server($app);
        $psrRequest = $this->_createRequest($request);

        return $server->run($psrRequest);
    }