Edited at

Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る (5) テンプレート編

More than 1 year has passed since last update.


はじめに


このエントリーについて

この記事は「Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る」シリーズの一編です。

他の記事は目次からアクセスしてください。

今回はテンプレートの書き方に関するベストプラクティスです。

Laravel に標準で添付してくる Blade テンプレートエンジンは、Twig テンプレートエンジンに比べるとやや機能的に見劣りするかんじではありますが、テンプレートでは極力処理を行わない、というポリシーなのかな、と前向きに捉えて使っております (それでもときどきTwig のフィルター使いたくなるときがあります)。

とは言え、あまりテンプレートに関してはバリエーションもないのかな、という印象で、極力生の PHP コードを書かないようにしましょう、ということくらいでしょうか。

ついつい、テンプレート内で if/else でデータを整形したり CSS のクラスを作ったりしてしまいがちですが、できるだけモデルクラスやヘルパー関数に任せたいところです。


環境


  • PHP 5.6

  • Laravel 5.3


公式リファレンス

Blade Templates - Laravel - The PHP Framework For Web Artisans

Laravel Collective


詳細


よくあるシナリオ


  1. データベースに数値で保存されている値をテキストに変換して表示したい

  2. 一般的なデータ型に対して汎用的な整形処理をしたい


ガイドライン


  • laravelcollective/html パッケージを導入しましょう

  • 極力生の PHP コードを書かないようにしましょう

laravelcollective/html パッケージは、特に Form ファサードが有用で、フォーム要素があるならぜひとも入れておきたいところです。


サンプルコード

生のPHPコードがテンプレートに侵食してこないようにするためには、いくつかアプローチがあって、


  1. モデルクラスでデータを整形する

  2. ヘルパー関数を使う

  3. 事前にデータを整形しておく

モデルのプロパティ固有の整形処理であれば、モデルクラスで担当するのがいいと思いますが、そうでない場合は 2 か 3 のアプローチを取るのがいいケースが多いという実感があります。


1. モデルクラスでデータを整形する

データベースには数値で保存されているものをテンプレートでは文字列で表示したい、みたいなときに、


blade.php

@if ($user->gender === 0)

男性
@else
女性
@endif

ではなく、


blade.php

{{ $user->genderLabel }}


のように書き、モデル側で変換するようにします。


User.php

    public function getGenderLabelAttribute()

{
return ($this->attributes['gender'] === 0) ? '男性' : '女性';
}

CSS のクラスを male/female で分けてスタイルを変えたいという要件が出て、メソッドを増やす必要が出てきたら、Gender クラスをつくって、


User.php

    public function getGenderObjectAttribute()

{
return new Gender($this->attributes['gender']);
}

みたいにして、実体は、


Gender.php

<?php

namespace App;

use InvalidArgumentException;

class Gender
{
const MALE = 0;
const FEMALE = 1;

private $list = [
self::MALE => ['label' => '男性', 'class' => 'male'],
self::FEMALE => ['label' => '女性', 'class' => 'female']
];

public function __construct($value)
{
if (!in_array($value, array_keys($this->list))) {
throw new InvalidArgumentException('value must be in ' . implode(',', array_keys($this->list)));
}
$this->value = $value;
}

public function getLabel()
{
return $this->list[$this->value]['label'];
}

public function getClass()
{
return $this->list[$this->value]['class'];
}
}


と分離して、


blade.php

<p class="{{ $user->genderObject->getClass() }}">{{ $user->genderObject->getLabel() }}</p>


みたいにしてもいいかもしれません (上の例ではちょっとやり過ぎな感もありますが、もっと複雑なクラスなら分離するのは有用と思います)。


2. ヘルパー関数を使う

汎用的な整形処理をするときは、ヘルパー関数が便利です。

たとえば、テキスト中に含まれる URL をハイパーリンクにして表示したいときに、


helpers.php

function to_link($text)

{
$regexp = '|(https?://[\w/:%#\$&\?\(\)~\.=\+\-]+)|';
if (preg_match($regexp, $text, $matches)) {
$text = preg_replace($regexp, '<a href="${1}">${1}</a>', $text);
}
return $text;
}

みたいなヘルパー関数をつくっておいて、


blade.php

{!! to_link($comment) !!}


のように呼び出すことができます。


3. 事前にデータを整形しておく

モデルによる整形も、ヘルパー関数による整形も、どちらも相応しくないようなパターンはそうそうないとは思いますが、いちおう書いておきます。

たとえば複数あるリンクのうち、現在の URL と一致するときに active というクラスを付与したい、というケースを考えてみます。


blade.php

<ul>

<li><a href="{{ route('hoge') }}" class="@if (Route::currentRouteName() === 'hoge') active @endif">Hoge</a></li>
<li><a href="{{ route('fuga') }}" class="@if (Route::currentRouteName() === 'fuga') active @endif">Fuga</a></li>
</ul>

頻繁にこのリストが入れ替わらないのであればこの書き方でもいいとは思いますが、そうでない場合はもう少しコンフィギャラブルにしたいところです。

そこで、ビュー編 で使ったパーシャルビューと View Composer を組み合せて、事前に class をセットするようにしてみます。


LinksPartialViewComposer.php

<?php

namespace App\Http\ViewComposers;

use Illuminate\View\View;
use Route;

class LinksPartialViewComposer
{
public function compose(View $view)
{
$routes = [
['name' => 'hoge', 'label' => 'Hoge', 'class' => ''],
['name' => 'fuga', 'label' => 'Fuga', 'class' => ''],
];

foreach ($routes as &$route) {
if (Route::currentRouteName() === $route['name']) {
$route['class'] = 'active';
break;
}
}

$view->with('linksPartial', compact('routes'));
}
}


テンプレートはパーシャルにします。


partials/links.blade.php

<ul>

@foreach ($linksPartial['routes'] as $route)
<li><a href="{{ route($route['name']) }}" class="{{ $route['class'] }}">{{ $route['label'] }}</a></li>
@endforeach
</ul>

だいぶすっきりしました。

2016-10-30 11:30 追記


Blade カスタムディレクティブと Html マクロ

よくあるフォームのエラー表示で、以下のようなコードを見かけます。


blade.php

@if ($errors->has('name'))

<p class="alert alert-danger">{{ $errors->first('name') }}</p>
@endif

毎回 if/endif を書かなくちゃいけなくて、ウザいなぁと思っていたので、1行で済むような方法を調べてみました。


Blade カスタムディレクティブ

専用の ServiceProvider をつくって、カスタムディレクティブを登録します。


app/Providers/HtmlServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Blade;

class HtmlServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/

public function boot()
{
Blade::directive('error', function ($expression) {
list($errors, $field) = array_map(function ($exp) {
return trim($exp);
}, explode(',', $expression));
return "<?php if ({$errors}->has({$field})) { echo '<p class=\"alert alert-danger\">' . {$errors}->first({$field}) . '</p>'; } ?>";
});
}

/**
* Register the application services.
*
* @return void
*/

public function register()
{
//
}
}


こうすると、先程のエラー表示の処理は以下のように書けます。


blade.php

@error($errors, 'name')


1行になりました。


Html カスタムマクロ

laravelcollective/html パッケージを入れていれば、Html/Form Facade にカスタムマクロを登録できます。Blade カスタムディレクティブと同様、ServiceProvider で登録します。


app/Providers/HtmlServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Html;

class HtmlServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/

public function boot()
{
Html::macro('error', function ($errors, $field) {
return ($errors->has($field)) ? '<p class="alert alert-danger">' . $errors->first($field) . '</p>' : '';
});
}
/**
* Register the application services.
*
* @return void
*/

public function register()
{
//
}
}


テンプレートはこうなります。


blade.php

{!! Html::error($errors, 'name') !!}


Blade ディレクティブが式を文字列として受け取るため自力でパースしなくてはいけないので、どちらかというとこちらの方法の方がすっきり書けますね。

追記ここまで


まとめ


  • laravelcollective/html パッケージを導入しましょう

  • 極力生の PHP コードを書かないようにしましょう

生の PHP コードを書かないようにするには、


  • モデルのデータは、モデルクラスで整形する

  • 汎用的なデータは、ヘルパー関数で整形する

  • それ以外のケースでは View Composer で整形する

といった方法を使い分けていきましょう。

他にもこんなベストプラクティスがあるよ、などコメントや編集リクエストいただけると助かります :bow: