Laravelのコンポーネントクラス/Bladeコンポーネントタグを使ってビューを構築する

こちらのブログは個人ブログと同じ内容です

www.ritolab.com


Laravel ではビューの構築に Blade が利用できテンプレート等を定義できますが、Laravel 7 から、コンポーネントクラスや Blade コンポーネントタグが利用できるようになりました。

従来の実装

今回は、会員ページのトップページを想定して、ステージ制のポイントプログラムを表示する部分のビューを作っていきたいと思います。 まずは一連の処理と、従来とおりに簡単なビューを構築していきます。

app/Http/Controllers/TopController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Enums\Stage;
use App\Models\User;

class TopController extends Controller
{
    public function index()
    {
        // ユーザー情報を取得したつもり
        $user = new User([
            'name'  => 'Test User',
            'point' => 9613,
        ]);

        // ポイントステージのクラス
        $stage = Stage::getStage($user->point);

        return view('top', ['user' => $user, 'stage' => $stage]);
    }
}

メインはビューのため諸々端折っていますが、ユーザの情報を取得して(という想定)、さらにポイントプログラムのステージインスタンスを取得して、それらをビューに渡しています。

routes/web.php
Route::get('/top', 'TopController@index');

次に、ビューを作成します。まずはレイアウト部分、共通のテンプレートです。

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample App</title>
</head>
<body>
    <div class="container">
        @yield('content')
    </div>
</body>
</html>

そして、今回のメインとなるトップページの Blade テンプレートです。

resources/views/top.blade.php
@extends('layouts.app')

@section('content')
    <h2>こんにちは、{{$user->name}} さん</h2>

    <div class="point_stage">
        <h3>会員情報</h3>
        <table>
            <tbody>
            <tr>
                <td>ステージ</td>
                <td><span class="text_strong">{{$stage->value}}</span> 会員</td>
            </tr>
            <tr>
                <td>現在のポイント</td>
                <td><span class="text_strong">{{$user->point}}</span> pt</td>
            </tr>
            </tbody>
        </table>
        @if ($stage->getNextStage())
            <p>あと {{$stage->getRemainingPoints($user->point)}} ポイントで {{$stage->getNextStage()}} 会員です</p>
        @endif
    </div>
@endsection

渡ってきた User や Stage から、現在のポイントやステージを表示したり、次のステージやそのために必要なポイント数を取得して表示しています。

ちなみにブラウザからアクセスすると以下のように表示されます。

f:id:ro9rito:20200713184248p:plain

レイアウトの content の部分にこのメッセージと会員情報が挿入されるわけですが、ここの会員情報部分をコンポーネント化していきます。

コンポーネントクラスと Blade コンポーネントの作成

コンポーネントクラスを作成します。以下の artisan コマンドを実行します。

# コンポーネントクラス作成
php artisan make:component PointStage

artisan コマンドを実行すると、以下のファイルが作成されます。

  • app/View/Components/PointStage.php

  • resources/views/components/point-stage.blade.php

app
├─ View
    └─ Components
        └─ PointStage.php

resources
├─ views
    ├─ components
    │   └─ point-stage.blade.php
app/View/Components/PointStage.php
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class PointStage extends Component
{
    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\View\View|string
     */
    public function render()
    {
        return view('components.point-stage');
    }
}
resources/views/components/point-stage.blade.php
<div>
    <!-- Knowing is not enough; we must apply. Being willing is not enough; we must do. - Leonardo da Vinci -->
</div>

コンポーネントクラス

コンポーネントクラスを実装します。会員情報コンポーネントに必要なデータを受け取り、描画する為に必要なデータを作成するイメージです。

app/View/Components/PointStage.php
<?php

declare(strict_types=1);

namespace App\View\Components;

use App\Enums\Stage;
use App\Models\User;
use Illuminate\View\Component;

class PointStage extends Component
{
    /** @var int 現在のポイント */
    public $currentPoint;

    /** @var string 現在のステージ */
    public $currentStageName;

    /** @var string|null 次のステージ名 */
    public $nextStageName;

    /** @var int|null 次のステージアップに必要なポイント */
    public $pointsNeededForNextStage;

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct(User $user)
    {
        $stage = Stage::getStage($user->point);

        $this->currentPoint             = $user->point;
        $this->currentStageName         = $stage->value;
        $this->nextStageName            = $stage->getNextStage();
        $this->pointsNeededForNextStage = $stage->getPointsNeededForNextStage($user->point);
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\View\View|string
     */
    public function render()
    {
        return view('components.point-stage');
    }
}

User オブジェクトを受け取って、それぞれ描画に必要な値をセットしています。

これまでコントローラや blade テンプレート側で行っていた、値に対する処理や取得がここに集約されている事が確認できると思います。

また、コンポーネントクラスで Stage クラスを取得しているので、これまでコントローラから渡していた Stage オブジェクトが不要になったので削除しました。

app/Http/Controllers/TopController.php
public function index()
{
    // ユーザー情報を取得したつもり
    $user = new User([
        'name'  => 'Test User',
        'point' => 9613,
    ]);

    return view('top', ['user' => $user]);
}

Blade コンポーネント

Blade の方も実装していきます。基本的には top.blade.php に記述していたものをこちらに移植する形になります。

resources/views/components/point-stage.blade.php
<div class="point_stage">
    <h3>会員情報</h3>
    <table>
        <tbody>
        <tr>
            <td>ステージ</td>
            <td><span class="text_strong">{{$currentStageName}}</span> 会員</td>
        </tr>
        <tr>
            <td>現在のポイント</td>
            <td><span class="text_strong">{{$currentPoint}}</span> pt</td>
        </tr>
        </tbody>
    </table>
    @if ($nextStageName)
        <p>あと {{$pointsNeededForNextStage}} ポイントで {{$nextStageName}} 会員です</p>
    @endif
</div>

以前の blade テンプレートと違うのは、値が変数に置き換わった事です。

コンポーネントクラスでセットしたメンバ変数はそのままこちらで使用できるので、シンプルに変数を当ててやればよくなります。

Blade コンポーネントタグ

コンポーネントの実装ができたので、これを使用します。<x-component-class-name /> という記法(コンポーネントクラス名をケバブケースにして x- のプレフィックスをつけた名前のタグ)で Blade コンポーネントタグを使用します。

resources/views/top.blade.php
@extends('layouts.app')

@section('content')
    <h2>こんにちは、{{$user->name}} さん</h2>

    <x-point-stage :user="$user" />
@endsection

:user="$user" で、User オブジェクトをコンポーネントクラスへ渡していて、これがコンポーネントクラスのコンストラクタに入ってきます。

単純にコンポーネント化しただけですが、スッキリしました。

あとは再度ブラウザから画面を表示させてみれば、はじめと同じ表示がされ、コンポーネント化の完成です。

メールのビューにコンポーネントクラスを適用させる

画面表示についてはコンポーネントクラスと Blade コンポーネントタグを用いての実装が行えました。では、メールのビューではどうでしょうか。

app/Notifications/UserStageNotification.php
<?php

declare(strict_types=1);

namespace App\Notifications;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class UserStageNotification extends Notification
{
    use Queueable;

    /**
     * Create a new notification instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  User $user
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail(User $user)
    {
        return (new MailMessage)
            ->from(config('mail.from.address'))
            ->subject('現在のステージをお知らせします')
            ->markdown('mail.user-stage', [
                'user' => $user
            ]);
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

Notification クラスから email を送信します。メッセージは Markdown で記述します。

resources/views/mail/user-stage.blade.php
@component('mail::message')
# {{$user->name}} 様

いつもご利用いただき誠にありがとうございます。

現在のステージをお知らせいたします。

<x-point-stage :user="$user" />

詳細はメンバーページよりご確認いただけます。
@component('mail::button', ['url' => ''])
ログイン
@endcomponent

@endcomponent

Blade コンポーネントタグを設置しました。

メールを受信してみると、問題なくコンポーネントタグが展開されている事が確認できました。 f:id:ro9rito:20200713185113p:plain

まとめ

コンポーネントクラスを用いると、ビューへ表示する為の値の処理がまとめられるので全体的に整理されていい感じです。

また、ビューへ渡す前の処理で完全に値を整形し切ったりもできますが string とか int にならざるを得ないので、コンポーネントクラスへオブジェクトを渡すことで、コンポーネント単位で利用する情報の制約(型)がつけられるのは良いなと思います。

使い勝手もなかなか良いのでぜひ使ってみてください。

[Github] サンプルソース