🤦

【反省】Livewire 3でTraitを作った【備忘録】

2024/04/29に公開

どうも、りんでござんす。

今回は仕様はちゃんと公式ドキュメントをあたろうねって話をしたいと思います。

TL;DR(未来の自分へ)

php のtraitに、プロパティを持たせちゃダメだよ。

PHPマニュアル トレイト
https://www.php.net/manual/ja/language.oop5.traits.php#language.oop5.traits.properties

トレイトでプロパティを定義したときは、 クラスではそれと互換性 (公開範囲と型とreadonlyの有無、そして初期値が同じ) がない同じ名前のプロパティを定義できません。 互換性がない名前を定義すると、致命的なエラーが発生します。

技術構成

  • Laravel 10
  • Livewire 3
  • php 8.2

何をしたかったのか

現在担当のプロジェクトではLivewire 3でコンポーネントを多数作成しています。(Livewireとは[1]

一方で、Livewire の非同期通信だけではなく、伝統的な同期通信も使用しています。

そのため、一つの画面の中で以下を同居させたいことがママありました。

  • 一つのviewの中に複数のLivewire コンポーネントを配置したい
  • 配置したコンポーネントにフラッシュデータでメッセージを表示したい
  • 同期通信(リダイレクトなど)でもフラッシュメッセージを表示したい

問題

その結果、フラッシュデータが意図していた場所ではなく、各コンポーネントや、コンポーネントを保持するbladeファイル上に表示されてしまいました。

対応

そこで、以下のように対応しました。

  1. 同期通信によるフラッシュメッセージはコンポーネントを保持するbladeファイルで表示
  2. コンポーネント内で表示したいフラッシュメッセージはコンポーネントに保持させる(フラッシュデータを使わない)

具体的には、以下のようなコードとなります。

parent.blade.php
<div id="alert-area">
    @if(session('danger'))
        <div class="alert alert-danger alert-dismissible text-center" role="alert">
            {{ session('danger') }}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
    @endif
</div>

<livewire:any-component />
AnyComponent.php
<?php
 
namespace App\Livewire;
 
use Livewire\Component;
 
class AnyComponent extends Component
{
    // livewire コンポーネント用のalert実装
    public array $alert = [];

    private function setAlert(string $message, string $status) {
        $this->alert['message'] = $message;
        $this->alert['status'] = $status;
    }

    public function closeAlert () {
        $this->alert = [];
    }

    // コンポーネントの実装
}
any-component.blade.php
<div>
    <div class="livewire-alert-area">
        @if (!empty($this->alert))
            <div class="alert alert-{{ $this->alert['status'] ?? 'danger' }} text-center" role="alert">
                {{ $this->alert['message'] ?? 'エラーが発生しました。' }}
                <button wire:click="closeAlert" type="button" class="close" data-dismiss="alert" aria-label="閉じる">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        @endif
    </div>

    {{-- コンポーネントの実装 --}}
</div>

そして、この対応を複数のコンポーネントで使い回したいと考えました。

Livewire プロパティ

はじめは、以下のようなクラスを作り、Livewire コンポーネントプロパティに持たせようとしました。

Alert.php
<?php

namespace App\Utilities;

class Alert
{
    /**
     * boostrap alert-{{ $alertStatus }}
     */
    private string $alertStatus;

    private string $alertMessage;

    public function setAlert(string $alertStatus, string $alertMessage) {
        $this->alertStatus = $alertStatus;
        $this->alertMessage = $alertMessage;
    }

    public function hasAlert() : bool {
        return isset($this->alertStatus) && isset($this->alertMessage);
    }

    public function getStatus() : ?string {
        return $this->alertStatus;
    }

    public function getMessage() : ?string {
        return $this->alertMessage;
    }

    public function closeAlert() {
        unset($this->alertStatus, $this->alertMessage);
    }
}

しかし、Livewire プロパティに持たせられる型は限られています。

  1. プリミティブ型
  2. 以下の型
Type Full Class Name
BackedEnum BackedEnum
Collection Illuminate\Support\Collection
Eloquent Collection Illuminate\Database\Eloquent\Collection
Model Illuminate\Database\Model
DateTime DateTime
Carbon Carbon\Carbon
Stringable Illuminate\Support\Stringable

Livewire 3 Properties

https://livewire.laravel.com/docs/properties#supported-property-types

そのため、自作したクラスはプロパティに持たせることができませんでした。

Trait化

そこで、先程のクラスをTrait化しました。

AlertTrait.php
<?php

namespace App\Trait;

trait AlertTrait
{
    // 実装は同じ
}
AnyComponent.php
<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use App\Trait\AlertTrait;
 
class AnyComponent extends Component
{
    use AlertTrait;

    // コンポーネントの実装
}
any-component.blade.php
<div>
    @include('alert')
    {{-- コンポーネントの実装 --}}
</div>
alert.php
@if ($this->hasAlert())
    <div class="alert alert-{{ $this->getStatus() }} text-center" role="alert">
        {{ $this->getMessage() }}
        <button wire:click="closeAlert" type="button" class="close" data-dismiss="alert" aria-label="閉じる">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
@endif

そしてコンポーネントに実装したのでした。

しかし

しかし、Traitのプロパティは、それを実装したクラスと互換性のないプロパティを持つとFatal errorが発生してしまいます。

結果、他の開発者が使い回す可能性があるのなら、Traitとして利用するのは良くないと判断して、各コンポーネントに同じような実装を行うのでした。。。
(Livewireでもっと良い方法があれば知りたい。。。)

最後に

よく分かってない言語仕様はちゃんとドキュメント読もうね。

脚注
  1. Livewireとは、Laravelのpackageの一つです。非同期通信及び動的な画面描画を、javascriptなど別言語ではなく、phpコードにより実現可能にします。 ↩︎

Discussion