📘

なぜlaravelのbladeで@phpみたいなことができるのか

に公開

株式会社ランプでエンジニアをしている小野寺です。

私は元々機械学習/組み込みの出身でLaravel入門した時にいくつか混乱したことがありますので、過去の私に紹介するつもりでLaravelのbladeファイル関連の動作を紹介したいと思います。

初学者にはbladeファイルは超ごちゃ混ぜに見える

Laravel入門し、どういう原理でブラウザからページを見れるんだ?と考えて辿っていくとなんとかbladeファイルに行き着きました。
bladeファイルにはHTMLもJSもPHPも@foreachみたいな謎のシンタックスもごちゃ混ぜで書けるようになっており、当初はとても混乱しました。
ブラウザって@php ... @endphpとか解釈できるの?!
JSの中に{{}}とかが出てきてPHPぽいのが書かれてるなぁ。
でもbladeファイルの仕様を調べるとLaravelの公式ページが出てくるし・・・など色々思考が巡っていたのを思い出します。

ブラウザからレスポンスが届くまでの全体像

ブラウザから特定のページのパスを要求したときに、Laravelのフレームワークがどんな処理をしてブラウザへのレスポンスになっているのでしょうか。
例えばNginx + PHP-FPM + Laravelの構成を例に取ると、

  • Nginxがリクエストを取得し、FastCGIプロトコルでPHP-FPMにリクエストを送信
  • PHP-FPMがリクエストのあったPHPファイルを実行し、Nginxにレスポンス送信
  • Nginx経由でブラウザにレスポンス
  • ブラウザでJSが実行される
    の流れで基本的に実行されます。

今回はLaravelフレームワークがリクエストをレスポンスするまでの流れの紹介としたいので、PHP-FPMとLaravelのコミュニケーションの部分に焦点を当てて解説していきます。

PHP-FPMがLaravelを解釈する流れ

PHP-FPMは何かというと常にプロセスを起動し、コマンドを待つデーモンプロセスです。
実装はphp-src内に存在し、FastCGIのプロトコルで他のサーバーと通信する役割を持つPHPインタプリタ機能を持った1つのサーバーです。
PHP-FPMはLaravelのフレームワークが生成する

public/index.php

を実行し、HTMLやJSなどをNginxにレスポンスします。

Laravelのバージョンにもよりますが、9.52.xを例に取るとindex.phpは

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

となっており、

  1. Laravelの準備
  2. リクエストからレスポンスの作成
  3. レスポンスの送信
  4. 処理の終了
    とLaravel内部では処理が進んでいくようです。

bladeファイルはどのようにLaravel内部でレスポンスに変換されていくのか

今回は簡単な例として以下のようにコントローラとviewを用意しました。

class TestViewController extends Controller
{
    public function index(string $account, Request $request)
    {
        $data = [1, 2, 3];

        return view('test.home', [
            'data' => $data,
        ]);
    }
}
<html>
  <head>
    <title>Test Home</title>
  </head>
  <body>
    @php
      $memo = 'this is memo';
    @endphp
    <h1>Test Home</h1>
    <ul>
      @foreach ($data as $item)
        <li>{{ $item }}</li>
      @endforeach
    </ul>
    <div>{{ $memo }}</div>
  </body>
</html>

ざっくり説明するとbladeファイルは、主に以下の順に処理されていきます。

  1. Laravel独自シンタックスの解釈
  2. PHPの解釈
  3. ブラウザが解釈可能なボディをレスポンス

Laravel独自シンタックスがLaravel内部でPHPにコンパイルされる

Laravel独自シンタックスの解釈をするのは

vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php

です。

compile関数の中でPHPインタプリタが解釈可能なPHPのレベルまでコンパイルされ、基本的にはstorage/framework/views/にキャッシュされます。
今回のbladeファイルは

<html>
  <head>
    <title>Test Home</title>
  </head>
  <body>
    <?php
      $memo = 'this is memo';
    ?>
    <h1>Test Home</h1>
    <ul>
      <?php $__currentLoopData = $data; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $item): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
        <li><?php echo e($item); ?></li>
      <?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
    </ul>
    <div><?php echo e($memo); ?></div>
  </body>
</html>
<?php /**PATH /app/resources/views/test/home.blade.php ENDPATH**/ ?>

こんなファイルがキャッシュされることになります。
必要な変数を用意さえすれば、Laravel独自シンタックスがPHPインタプリタが解釈可能なPHPに変換されていることが確認できるかと思います。

PHPがブラウザが解釈可能なレベル(HTMLやJS)まで変換される

一つ前提としてPHPのincludeとrequireは少し特殊な意味があります。
PHPの公式にも例がありますが、基本的にはPHPファイルをincludeもしくはrequireすることでファイル内のPHPコードが評価されます。

Illuminate\View\Viewクラスはrenderという関数を持っていますが、その中で

vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php
$results = $this->evaluatePath($this->compiler->getCompiledPath($path), $data);

というコードがあり、Laravel独自シンタックスがすでにコンパイルされたPHPコードを評価します。
その中身は以下のようにコンパイルされたPHPコードをrequireするようになっており、ここでPHPコードはブラウザが解釈可能なレベルまで変換されます。

public function getRequire($path, array $data = [])
{
    if ($this->isFile($path)) {
        $__path = $path;
        $__data = $data;

        return (static function () use ($__path, $__data) {
            extract($__data, EXTR_SKIP);

            return require $__path;
        })();
    }

    throw new FileNotFoundException("File does not exist at path {$path}.");
}

このrequireによって以下のようにレスポンスが用意され、PHP-FPMからNginxへのレスポンスとして送られます。

<html>
    <head>
        <title>Test Home</title>
    </head>
    <body>
        <h1>Test Home</h1>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
        <div>this is memo</div>
    </body>
</html>

このレスポンスがさらにNginxからブラウザに届き、ユーザの画面に表示される、ということのようでした。

結論

なぜ@phpや@foreachが使え、なぜJavascriptの中にPHPのコードを{{}}という記法で追加できるかというと、Laravelがレスポンスを作る過程でコンパイルをしているから、という結論でした。
ウェブ開発をされている方には当たり前かもしれませんが、ブラウザにレスポンスが到達する頃には既にPHPは実行されHTMLやJavascriptになっていることに、私は衝撃を受けました。
私はブラウザはPHPを実行できると勘違いしていたことが判明しました。(アホ過ぎる😂)
いつかはPHPもそのまま実行してくれるんですかねぇー。

得られた学び

ウェブの初学者としてフレームワークや周辺の諸々を分からないまま開発していくと、他のメンバーによく分からない質問をしてしまうこともあります。
例えば「なぜクリックなどのユーザ操作起点のイベントリスナをbladeのPHPで書けないの?」と聞いたり、Javascriptで平気で

const data = $data;

と書いて、「コントローラーから渡してるんですけど、何かエラーになるんですよねぇ」とか言います。
これはレスポンスの流れを知った今だと当たり前に聞こえますが、正直ウェブ開発を始めた当初はなんとなく分かるっちゃ分かるがなんでだろうという状態でした。

私と同じ疑問を持った人(いるか?😵)がこの記事を読んで、ブラウザがPHPを直接解釈できないことに気づくことで、エンジニアメンバー間のコミュニケーションが円滑になると嬉しいです。

LAMP Inc. Tech Blog

Discussion