Laravel Blade で同名 php スクリプトを自動で ViewComposer みたいに適用

Smarty を使っているとき、`views/home.tpl` というテンプレートファイルに対して `views/home.php` のようなファイルを次のような内容で作成しておくと、自動的にテンプレートのレンダリング前にビューにアサインされた値をちょっと加工できる。

```php
<?php
// ビューにアサインされた変数＋DIコンテナに入ってるオブジェクトが引数に渡る
return function ($hoge, Fuga $fuga) {
    // 戻り値は元のビューにアサインされた変数とマージされる
    return [
        'hoge' => $hoge * 2,
        'fuga' => $fuga->func($hoge),
    ];
};
```

というのを `prefilter` や `postfilter` を使ってやっていました。

コントローラーに書くにはビューの都合すぎる、テンプレートに書くにはロジックすぎる、というときに、テンプレートに近い位置に PHP のコードを置くのに重宝していました（最近なら ViewModel とか Presenter とか呼ばれるそれ用のクラスを作ったりするのかもしれない）。

それと似たようなことを Laravel Blade でやる方法。

## CompilerEngine を差し替える

下記のような CompilerEngine をアプリで実装して、

```php
<?php
namespace App\Exceptions\View;

use Illuminate\Container\Container;
use Illuminate\View\Compilers\CompilerInterface;

class CompilerEngine extends \Illuminate\View\Engines\CompilerEngine
{
    /**
     * @var Container
     */
    private $container;

    public function __construct(Container $container, CompilerInterface $compiler)
    {
        parent::__construct($compiler);
        $this->container = $container;
    }

    protected function evaluatePath($__path, $__data)
    {
        $path = end($this->lastCompiled);
        if (substr($path, -strlen('.blade.php')) === '.blade.php') {
            $path = substr($path, 0, -strlen('.blade.php')) . '.php';
            if (file_exists($path)) {
                $func = require $path;
                $__data = $this->container->call($func, $__data) + $__data;
            }
        }
        return parent::evaluatePath($__path, $__data);
    }
}
```

これを適当なサービスプロバイダの `boot()` で元のものから差し替えます。

```php
<?php
namespace App\Providers;

use App\Exceptions\View\CompilerEngine;
use Illuminate\Container\Container;
use Illuminate\Support\ServiceProvider;

class ViewServiceProvider extends ServiceProvider
{
    public function boot(Container $container)
    {
        $container->get('view.engine.resolver')->register('blade', function () use ($container) {
            return new CompilerEngine($container, $this->app['blade.compiler']);
        });
    }
}
```

`views/home.blade.php` をレンダリングするときに下記のような `views/home.php` というファイルがあれば自動で実行されます。

```php
<?php
return function ($hoge) {
    return [
        'hoge' => $hoge * 2,
    ];
};
```

クロージャーの引数にはビューにアサインされている変数が渡されると同時に `Container::call()` で実行してるので型宣言を元にコンテナで解決されたオブジェクトが DI されます。

## ViewComposer を使う

Laravel Blade にはもともと ViewComposer という似たような機能が既にありますが・・テンプレートとセットで書きたいので、なるべくテンプレートと近い位置に配置できるように ViewComposer で処理します。

適当なサービスプロバイダで次のように全てのViewに適用される `composer` を設定します（いわゆる composer コマンドとは関係ないものなので紛らわしい・・）。

```php
<?php
namespace App\Providers;

use Illuminate\Container\Container;
use Illuminate\Support\Facades\View as ViewFacade;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\View;

class ViewServiceProvider extends ServiceProvider
{
    public function boot(Container $container)
    {
        ViewFacade::composer('*', function (View $view) use ($container) {
            $path = $view->getPath();
            if (substr($path, -strlen('.blade.php')) === '.blade.php') {
                $path = substr($path, 0, -strlen('.blade.php')) . '.php';
                if (file_exists($path)) {
                    $func = require $path;
                    $container->call($func, ['view' => $view]);
                }
            }
        });
    }
}
```

すると `views/home.blade.php` に対して下記のような `views/home.php` ファイルが実行されます。

```php
<?php
use Illuminate\View\View;

return function (View $view) {
    $view->with('hoge', $view['hoge'] * 2);
};
```

勿論このクロージャーも `Container::call()` で実行されるので型宣言を元にコンテナで解決されたオブジェクトが DI されます。

ただ `View $view` はコンテナから DI されているわけではないので型ではなく引数名で解決されます、なので `View $v` とかで引数を書くとエラーです。

## さいごに

よく考えたら blade 自体がほぼほぼ PHP みたいなもので `@php @endphp` とかで PHP のコードかけるし PhpStorm もその中身を PHP として解釈してくれるので blade テンプレートの一番上の方でそういうの書くだけで良いのでは？

と思ったのでこの案はお蔵入りかも。