CakePHPのテンプレートエンジンでレイアウトファイルが読み出されるタイミング!


こんにちは、ユアマイスターで働くインターン京極です。

今回はCakePHPのテンプレートエンジンで、レイアウトファイルが読み出される順序についてまとめてみました。

確認環境

さくっと検証したいので、Docker環境で試しました。

docker run -ti --rm php:alpine sh # Docker内に入る
apk add icu-dev && docker-php-ext-install intl # CakePHPを動かすために必要なライブラリを追加する
cd /usr/src # 作業ディレクトリに移動

PHP7の環境上で、最新Composerをインストールし、CakePHPのプロジェクトを作成します。

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '93b54496392c062774670ac18b134c3b3a95e5a5e5c8f1a9f115f203b75bf9a129d5daa8ba6a13e2cc8a1da0806388a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

php composer.phar create-project --prefer-dist cakephp/app app

これで、PHP7.2とCakePHP3.6の環境が手に入りました。

/usr/src/appディレクトリに移動し、app.phpファイルを用意します。

cp config/app.default.php config/app.php 

データベースは使わないので、config/bootstrap.phpから以下の行を削除し、

# ConnectionManager::setConfig(Configure::consume('Datasources'));

サーバとして立ち上げておきます。(DebugKitをオフにするための環境変数付き。DebugKitのコードがレスポンスのHTMLへ含まれないようにするため。)

DEBUG=false ./bin/cake server &

レイアウトファイルの読み込み順序

CakePHPのプロジェクトを作ったとき、コントローラーには、PagesControllerクラスが用意されていて、トップページにアクセスすると、src/Template/Pages/home.ctpファイルがビューとして読まれるようになっています。

この、home.ctpファイルが読み込まれるときのレイアウトファイルが読み込まれる順序を確認したいと思います。

挙動をわかりやすくするために、src/Template/Layout/default.ctpファイルを以下のようにシンプルに修正します。

<!DOCTYPE html>                                                                            
<html>                                                                                     
<body>                                                                                     
  <?= $this->fetch('content') ?>                                                                  
</body>                                                                                           
</html>

src/Template/Pages/home.ctpファイルは、以下の内容に修正します。

Hogehoge

この状態のとき、トップページへアクセスしてみます。

# curl http://localhost:8765/
<!DOCTYPE html>
<html>
<body>
  Hogehoge
</body>
</html>

狙い通り、Layout/default.ctpファイルがレイアウトとして動いていて、コンテンツ部分のビューにはhome.ctpファイルの結果が含まれています。

この状態のとき、レイアウトとビューでどちらが先に評価されるのかを実験してみます。

仮説として、$this->fetch('content')という実装がレイアウトファイルの方にありますので、先にレイアウトファイルが評価され、内部のfetch関数が呼ばれた段階でコンテンツとしてのhome.ctpファイルが読まれると予想しました。
そのため以下のような修正を加えてみます。

src/Template/Layout/default.ctp

<?php $this->set('a', 1); ?>
<!DOCTYPE html>
<html>
<body>
  <?= $this->fetch('content') ?>
</body>
</html>

src/Template/Pages/home.ctp

<?php var_dump($a); ?>
Hogehoge

結果は以下のようになりました。

# curl http://localhost:8765/
<!DOCTYPE html>
<html>
<body>
  NULL
  Hogehoge
</body>
</html>

$aに値が入っていない状態となり、結果は想定していたものと異なりました。にゃ、ニャンだってー!

今度は逆にビューにて値を設定し、レイアウトファイルでは取得するように試してみます。

src/Template/Layout/default.ctp

<!DOCTYPE html>
<html>
<body>
  <?php var_dump($a); ?>
  <?= $this->fetch('content') ?>
</body>
</html

src/Template/Pages/home.ctp

<?php $this->set('a', 1); ?>
Hogehoge

この状態で実行してみるとうまく行きました。

# curl http://localhost:8765/
<!DOCTYPE html>
<html>
<body>
  int(1)
  Hogehoge
</body>
</html>

この挙動から、ビューファイルのほうが先に動き、レイアウトファイルはその後実行されていることが分かります。

ドキュメントを読んでみよう

こういった挙動はドキュメントを確認してどのように説明されているのか調べてみます。

はっきりと実行順序の記載について書かれているところは見つけれられませんでしたが、ビュー変数に関する説明で、

ビューファイルでは、次のように記述できます。

$this->set('activeMenuButton', 'posts');

そしてレイアウトでは、 $activeMenuButton 変数が使用でき、 ‘posts’ という値を持ちます。

と書かれている内容から、ビューファイルのあとにレイアウトが実行されると読み取れるという感じでしょうか。(記載を見逃してましたら教えてください!)

実装を読んでみよう

最後は、ソースコードを読んで、この挙動になっている部分を確認しようと思います。

取っ掛かりとしては、Controllerクラスのrenderメソッドからビューファイルを指定していますので、ここからコードを読んでみます。(該当ファイルは、vendor/cakephp/cakephp/src/Controller/Controller.phpファイルです。)

render関数内で、

        $this->View = $this->createView();
        $contents = $this->View->render($view, $layout);

との記述があり、この中でビューのデータを構築していそうです。

該当する、vendor/cakephp/cakephp/src/View/View.phpファイルを確認するとたしかにこの中にrender関数や、レイアウトファイルを処理するrenderLayout関数がありました。

render関数を確認したところ、

        $this->Blocks->set('content', $this->_render($viewFileName));
        ...中略...
        if ($this->layout && $this->autoLayout) {                                              
            $this->Blocks->set('content', $this->renderLayout('', $this->layout));             
        }  

というように、先にcontentとしてビューファイルの中身をセットしたあとに、レイアウトファイルをセットし直されていることが確認できます。この実装のため、すでにセットしているcontentが使われて、レイアウトファイル内のfetch('content')から、ビューファイルの中身が表示されるという挙動も納得できました。)

まとめ

vendor/cakephpの中を覗いてみることで、テンプレートエンジンとしての挙動がはっきりと分かり、すっきりして良い年越しが送れそうです。