Smarty を使っているとき、views/home.tpl
というテンプレートファイルに対して views/home.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
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
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
return function ($hoge) {
return [
'hoge' => $hoge * 2,
];
};
クロージャーの引数にはビューにアサインされている変数が渡されると同時に Container::call()
で実行してるので型宣言を元にコンテナで解決されたオブジェクトが DI されます。
ViewComposer を使う
Laravel Blade にはもともと ViewComposer という似たような機能が既にありますが・・テンプレートとセットで書きたいので、なるべくテンプレートと近い位置に配置できるように ViewComposer で処理します。
適当なサービスプロバイダで次のように全てのViewに適用される composer
を設定します(いわゆる composer コマンドとは関係ないものなので紛らわしい・・)。
<?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
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 テンプレートの一番上の方でそういうの書くだけで良いのでは?
と思ったのでこの案はお蔵入りかも。