デザインワン・ジャパン Tech Blog

DesignOne Japan | Activate the World.

【Laravel】Blade Componentsとサブビューの対比

概要説明

こんにちは!株式会社デザインワン・ジャパンで口コミサービス エキテンのリニューアルを担当しているサービス開発部の鈴木セシル(@suzuki_cecil_)です。

リニューアルプロジェクトではLaravelを採用しており、ビューはBlade Componentsを活用して実装しています。今回の記事ではリニューアルプロジェクトを通して体感したBlade ComponentsとBlade Templatesのサブビューと対比をまとめて記事にしたいと思います。

注)今回はサブビューとの対比を目的としているため、slotとしてのBlade Componentsの利用に関する説明は省略させていただいております。

Blade Componentsとは

Blade ComponentsはLaravel 7から登場したビューの構成要素で、その名の通りコンポーネントの実装に特化したBladeファイルです。従来のBlade Templatesをサブビューとして扱うのとBlade Componentsの違いは以下の通りです。

  • サブビュー:他のBlade TemplatesをBlade Templatesの一部として取り込みます。取り込まれる側のBladeもBlade Templatesであるため、ControllerからそのBladeでレンダリングすることも可能です。

  • Blade Components:Blade ComponentsをBlade Templatesの一部として取り込みます。Bladeファイルは resources/views/components ディレクトリに集約され、またControllerからBlade Componentsでレンダリングすることは想定されないためコンポーネントとして明確に責務を持たせることが可能になります。

Blade ComponentsはClass Based ComponentsとAnonymous Componentsの2種類の実装方法があります。

Class Based Components

Class Based ComponentsはBladeファイルおよびクラスファイル(以下コンポーネントクラス)から構成されます。Bladeファイルからはコンポーネントクラスのプロパティやメソッドを参照することが可能なため、Bladeファイルで特定のデータを扱いたい場合に利用することを想定しています。

Anonymous Components

Anonymous ComponentsはClass Based Componentsとは異なりBladeファイルのみから構成されます。Bladeファイルはpropsにより呼び出し元のBlade TemplatesやBlade Componentsから受け取るデータを定義します。Bladeファイルで汎用的なデータを扱いたい場合に利用することを想定しています。

ソースコード

エキテンを模倣した以下のページをサブビューによる実装、Blade Componentsによる実装ごとにソースコードを見ていきたいと思います。

f:id:doj_suzuki_cecil:20211118192434p:plain

共通ファイル

以下に列挙するソースコードはサブビューによる実装、Blade Componentsによる実装それぞれで使用される共通のファイルとなります。

app/Http/Controllers/SamplePageController.php

<?php

namespace App\Http\Controllers;

use App\View\Model\Shop;

class SamplePageController extends Controller
{
    public function index()
    {
        $shops = $this->initShops();
        $nearlyShops = $this->initNearlyShops();
        return view('sample_page', ["shops" => $shops, "nearlyShops" => $nearlyShops]);
    }

    public function initShops(): array
    {
        return [
            new Shop(1, "エキテン整骨院", "1番目の店舗です", ["接骨・整骨", "整体"]),
            new Shop(2, "エキテン接骨院", "2番目の店舗です", ["接骨・整骨", "整体"]),
            new Shop(3, "エキテン治療院", "3番目の店舗です", ["マッサージ", "整体"]),
        ];
    }

    public function initNearlyShops(): array
    {
        return [
            new Shop(4, "エキテン整体院", "1番目の条件に近い店舗です", ["整体"]),
            new Shop(5, "エキテン鍼灸院", "2番目の条件に近い店舗です", ["鍼灸", "マッサージ", "整体"]),
        ];
    }
}

app/View/Model/Shop.php

<?php

namespace App\View\Model;

class Shop
{
    private int $id;
    private string $name;
    private string $introduction;
    private array $genreNames;

    public function __construct(int $id, string $name, string $introduction, array $genreNames)
    {
        $this->id = $id;
        $this->name = $name;
        $this->introduction = $introduction;
        $this->genreNames = $genreNames;
    }

    public function getUrl(): string
    {
        return "/shop_{$this->id}/";
    }

    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getIntroduction(): string
    {
        return $this->introduction;
    }

    /**
     * @return string
     */
    public function getGenreNames(): string
    {
        return implode("/", $this->genreNames);
    }
}

サブビューによる実装

以下に列挙するソースコードはサブビューによる実装をした場合となります。

/app
  ┣Http/
  ┃  ┗Controllers/
  ┃  ┃  ┗SimplePageController.php
  ┃  ┗View/
  ┃     ┗Model/
  ┃        ┗Shop.php
  ┗resources/
     ┗views/
        ┣sample_page.blade.php ・・・ControllerからレンダリングされるBlate Templates
        ┗shop_panel.blade.php ・・・sample_page.blade.phpから読み込まれるサブビュー

resources/views/sample_page.blade.php

<h1>店舗一覧</h1>
<h2>新宿区のおすすめ整体院</h2>
@foreach($shops as $shop)
    @include("shop_panel", ["shop" => $shop])
@endforeach
<h2>条件に近い整体院</h2>
@foreach($nearlyShops as $nearlyShop)
    @include("shop_panel", ["shop" => $nearlyShop])
@endforeach

resources/views/shop_panel.blade.php

<div>
    <p>{{ $shop->getIntroduction() }}</p>
    <h2><a href="{{ $shop->getUrl() }}">{{ $shop->getName() }}</a></h2>
    <p>{{ $shop->getGenreNames() }}</p>
</div>

Blade Componentsによる実装

以下に列挙するソースコードはBlade Componentsによる実装をした場合となります。

/app
  ┣Http/
  ┃  ┣Controllers/
  ┃  ┃  ┗SimplePageController.php ・・・ コントローラー
  ┃  ┗View/
  ┃     ┣Components/
  ┃        ┗ShopPanel.php ・・・ shop-panel.blade.phpのコンポーネントクラス
  ┃     ┗Model/
  ┃        ┗Shop.php
  ┗resources/
     ┗views/
        ┣components/ ・・・Blade Componentsが集約されるディレクトリ
        ┃  ┣anker_link.blade.php ・・・ shop-panel.blade.phpから読み込まれるAnonymous Components
        ┃  ┗shop-panel.blade.php ・・・ sample_page.blade.phpから読み込まれるClass Based Components
        ┗sample_page.blade.php ・・・ ControllerからレンダリングされるBlade Templates

resources/views/sample_page.blade.php

<h1>店舗一覧</h1>
<h2>新宿区のおすすめ整体院</h2>
@foreach($shops as $shop)
    <x-shop-panel :shop="$shop"/>
@endforeach
<h2>条件に近い整体院</h2>
@foreach($nearlyShops as $nearlyShop)
    <x-shop-panel :shop="$nearlyShop"/>
@endforeach

app/View/Components/ShopPanel.php

<?php

namespace App\View\Components;

use App\View\Model\Shop;
use Illuminate\View\Component;

class ShopPanel extends Component
{
    public string $name;
    public string $url;
    public string $introduction;
    public string $genreNames;

    public function __construct(Shop $shop)
    {
        $this->name = $shop->getName();
        $this->url = $shop->getUrl();
        $this->introduction = $shop->getIntroduction();
        $this->genreNames = $shop->getGenreNames();
    }

    public function render()
    {
        return view('components.shop-panel');
    }
}

resources/views/components/shop-panel.blade.php

<div>
    <p>{{ $introduction }}</p>
    <h2>
        <x-anker_link :url="$url" :text="$name"/>
    </h2>
    <p>{{ $genreNames }}</p>
</div>

resources/views/components/anker_link.blade.php

@props([
    'url',
    'text'
])

<a href="{{ $url }}">{{ $text }}</a>

1店舗分の情報を持つパネル(shop-panel )をClass Based Componentsで実装し、アンカーリンク(anker_link)をAnonymous Componentsで実装しています。

※リニューアルプロジェクトのコーディング規約ではClass Based ComponentsはBladeファイル名をケバブケース、Anonymous ComponentsはBladeファイル名をスネークケースで命名することで区別しております

呼び出し元からは <x-shop-panel :shop="$nearlyShop"/> のように x-{Bladeファイル名} タグで読み込みます。その際に変数の受け渡しが可能です。

Anonymous Componentsの場合は @props で受け取る値を定義します。Class Based Componentsの場合はBladeファイルに渡されるのではなく、コンポーネントクラスのコンストラクタに渡され、Bladeファイルからはコンストラクタ内で定義したコンポーネントクラスのプロパティを利用することが可能です。

今回の場合はコンポーネントクラスにはビューモデルを渡し、コンストラクタでビューモデルからプロパティの生成を行っています。

またBlade ComponentsのBladeファイルは resources/views/components ディレクトリ内に作られるため、他のBlade Templatesとは明示的に分けることが可能です。

Blade Componentsの所感

Blade Componentsを活用することでレイアウト(Blade Templates)とコンポーネント(Blade Components)を明示的に分けながらBladeの実装が行えています。当初はレイアウトとコンポーネントを従来通りBlade Templatesとして実装し、単にレイアウトとコンポーネントディレクトリで分割するという話もあがりましたが、新しくエンジニアがプロダクトにジョインした場合などを考えると可能な限りLaravelのルールに準じたディレクトリ構成にできている方がいいということでBlade Componentsを採用することになりました。

またClass Based Componentsを活用することでビューにアサインしたViewModelのメソッドをBladeファイルからではなくコンポーネントクラスから参照できるようになったため、PHPStormなどのIDEのナビゲーション機能が効かないメソッド呼び出しをする必要がなくなりました。副次的な効果にはなりますがこれもBlade Componentsを採用した恩恵です。

これらのことからリニューアルプロジェクトでBlade Componentsを採用したのは正解だったと思います。

仲間を募集しております

募集中の職種については以下を御覧ください。

www.wantedly.com

参考URL

readouble.com