Laravel + Livewire 3 で CRUD を実装してみる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、きっと皆さんもあると思うのですが「忙しくて出来ないけど、気にはなっているもの」って結構あったりしないでしょうか。

というのも、X(旧 Twitter)だけでなく登録しているRSSを見ていると「おおっ、これは時間ができたらやってみたい❗」と思うことが次々と出てくるからですね。

そして、今回やってみたいのが2023.7.20にリリースされた

Livewire 3

です❗

実際にお仕事の技術選定にも上がったことがあるのですが、その時は3がいつリリースされるか分からない状態だったので「ついに来た❗」といった印象です。

ちなみに、Livewireをご存じない方にざっくり言うとこうなります。

JavaScript 書きたくないよね?? じゃ、Livewireだ😆

そうです。

コンセプトは「PHP だけでも Vue や React のように動くようにするぞ❗」なので、特にバックエンドが得意な人にはもってこいじゃないでしょうか。

そこで❗

今回はこのLivewire 3CRUD(追加・表示・編集・削除)機能をつくってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😊✨

※ ちなみに前回のLivewire 2は以下の記事をご覧ください。

📝 参考記事: Laravel + Livewire で CRUD を実装してみる 〜 よりシンプルを求めて 〜

「大阪万博のマンホールが
設置されてるらしい!」

開発環境: Laravel 10.x、Livewire 3、PHP 8.2

Livewire 3 をインストールする

では、Laravel 10.xを新しくインストールしたら、早速Livewire 3をインストールしましょう。

以下のコマンドを実行してください。

composer require livewire/livewire

するとパッケージがインストールされますので、実際にプログラムしていきましょう❗

DB まわりをつくる

まずはじめにデータベースまわりを作っていきましょう。
以下のコマンドを実行してください。

php artisan make:model Article -ms

すると「モデル」「マイグレーション」「Seeder」の3ファイルが作成されるので、中身をそれぞれ以下のように変更します。

app/Models/Article.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
    ];
}

database/migrations/****_**_**_******_create_articles_table.php

// 省略

Schema::create('articles', function (Blueprint $table) {
    $table->id();
    $table->string('title')->comment('タイトル');
    $table->text('content')->comment('本文');
    $table->timestamps();
});

// 省略

database/seeders/ArticleSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Article;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class ArticleSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        for($i = 1; $i <= 50; $i++) {

            Article::create([
                'title' => 'タイトル(' . $i .')',
                'content' => $i ."番目\n記事の本文\n記事の本文\n記事の本文\n記事の本文\n記事の本文 ",
            ]);

        }
    }
}

Seederは作成するだけでは有効にはなってませんので、DatabaseSeeder.phpに登録しておきましょう。

database/seeders/DatabaseSeeder.php

<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            ArticleSeeder::class,
        ]);
    }
}

では、この状態でデータベースを初期化してみます。
以下のコマンドを実行してください。

php artisan migrate:fresh --seed

すると、実際のテーブルはこうなりました。

Livewire 3 の実装について

では、実際にLivewire 3の作業に行く前に実装方法についてまとめます。

Livewireのコンポーネントの呼び出し方は以下2つがあります。

  • フルページコンポーネント: ルートでコンポーネントを直接指定して呼び出す
  • インクルード: Bladeファイルの中でコンポーネントを呼び出す基本的な方法

なお、今回は面白そうなので「フルページコンポーネント」で実装することにします。(前回のLivewire 2の記事ではインクルード方式でした。比較してみると面白いかもしれません)

📝 参考記事: Laravel + Livewire で CRUD を実装してみる 〜 よりシンプルを求めて 〜

Livewire 3 のコンポーネントをつくる

では、ここからがLivewire 3の作業になります。
まずは大きく以下3つの機能をカバーするコンポーネント「ArticleComponent」を作成します。

  • 一覧表示(削除ボタンあり)
  • フォーム(追加&変更)
  • ページ送りのリンク

では、以下のコマンドを実行してください。

php artisan make:livewire ArticleComponent

すると、コンポーネント&ビューの2ファイルが自動的に作成されます。

コンポーネント本体をつくる

では、先ほど作ったコンポーネントの中身を以下のように変更してください。

app/Livewire/ArticleComponent.php

<?php

namespace App\Livewire;

use App\Models\Article;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;

class ArticleComponent extends Component
{
    use WithPagination;

    public string $title = '';
    public string $content = '';
    public ?Article $current_article = null;

    #[Url(as: 'q')] // URL に ?q= で検索キーワードを渡せるようにする
    public string $keyword = '';
    public function updatingKeyword() // 検索キーワードが変更されたらページを 1 に戻す
    {
        $this->resetPage();
    }

    #[Computed] // $this->inputMode で呼び出せるようになる
    public function inputMode() // `create` or `edit`
    {
        return is_null($this->current_article) ? 'create' : 'edit';
    }

    #[Title('記事の管理')] // レイアウトの $title に入る
    public function render()
    {
        $articles = Article::query()
            ->when($this->keyword, function ($query, $keyword) { // 検索キーワードがある場合は絞り込み

                $query->where('title', 'LIKE', "%{$keyword}%");

            })
            ->orderBy('id', 'desc')
            ->paginate(10);

        return view('livewire.article-component', [
            'articles' => $articles,
        ]);
    }

    public function create()
    {
        $this->current_article = null;
        $this->title = '';
        $this->content = '';
    }

    public function edit(Article $article) // 実際には ID を受け取るが、自動的にモデルに変換される
    {
        $this->current_article = $article;
        $this->title = $article->title;
        $this->content = $article->content;
    }

    public function save()
    {
        $validated = $this->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        $message = '';

        if(! is_null($this->current_article)) { // 編集する場合

            $this->current_article->update($validated);
            $this->current_article = null;
            $message = '更新しました。';

        } else { // 新規作成する場合

            Article::create($validated);
            $message = '作成しました。';

        }

        $this->title = '';
        $this->content = '';

        return back()->with('message', $message);
    }

    public function delete(Article $article)
    {
        $article->delete();

        return back();
    }
}

なお、これでもlivewire 3が持っている機能のすべてが入っているわけではないですがひとつずつ見ていきましょう。

Attributes

PHPを今まで書いたことがある人でも「おやっ??」と思うかもしれないのが以下のようなAttributesです。

#[Url]

これはPHP 8から使えるようになった書き方なんですが、これを使ってLivewire 3ではいろいろなことができるようになっています。

では実際のコードの中にあるものをご紹介しましょう。

#[Url(as: ‘q’)]

Livewire 3ではこのAttributesをつけることによってURLのパラメータにその値を追加することができます。

例えば、今回$keywordは検索キーワードが入ってくることになりますが、その値がURLに含まれるようになります。

「taro」で検索したとき

http://livewire3.test/article?q=taro

そして、as: 'q'をつけているのでq=になっていますが省略した場合はkeyword=になります。

では、なぜURLに値を含めるかというと、「後でその表示を再現できるから」です。

例えば、別の人にURLを送って「ここの3行目がなんかおかしいよね?」と言いたい場合、検索キーワードなどがURLに含まれていれば、すぐ再現できます。

もしくはブックマークしておいて後から見る場合も👍ですよね。

そんなカンジで便利につかえる訳です。

#[Computed]

これは、VueComputedのような機能で「他の変数などの状況によって中身が変わるもの」で、今回は以下の条件で「入力モード」が取得できるようにしています。

  • 編集中の記事データがあるとき: edit
  • ないとき:  create

つまり、$this->inputModeにアクセスすると必ずcreateeditという文字列が返ってくることになります。

※ なお、$inputModeではなく、$this->inputModeとなっている点に注意ですね👍

#[Title(‘記事の管理’)]

これは、render()で使うことによって、親テンプレートとなる「レイアウトファイル」の中で$titleとして使用することができます。

今回のケースでは、以下の中、つまり「ページタイトル」として使用されることになります。

resources/views/components/layouts/app.blade.php

<title>{{ $title ?? config('app.name') }}</title>

バインディング

後で紹介するビューと関連する話なのですが、例えばedit(1)が実行された場合でもタイプヒント(以下のケースで言うとArticle)をつけていればLaravelと同じく自動的にDBから該当するIDのデータを取ってきてくれることになります。

public function edit(Article $article)
{
    // 省略
}

ビューをつくる

続いて、作成したコンポーネント用のビューを変更します。

resources/views/livewire/article-component.blade.php

<div class="flex">
    <!-- 左側のデータ一覧 -->
    <div class="w-1/2 bg-gray-100 p-4 rounded shadow">
        <div class="flex justify-between items-center mb-4">
            <div class="flex justify-between items-center">
                <input wire:model.live="keyword" type="search" placeholder="検索..." class="p-2 rounded border">
            </div>
            <button wire:click="create" class="bg-green-600 text-white px-2 py-1 rounded text-sm">追加</button>
        </div>
        <ul>
            @foreach($articles as $article)
                <li wire:key="{{ $article->id }}" class="flex justify-between items-center border-b py-2">
                    <span>{{ $article->title }}</span>
                    <div>
                        <button wire:click="edit({{ $article->id }})" class="bg-blue-500 text-white px-1 py-0.5 rounded mr-1 text-sm">編集</button>
                        <button
                            wire:confirm="「{{ $article->title }}」を削除します。よろしいですか?"
                            wire:click="delete({{ $article->id }})"
                            class="bg-red-500 text-white px-1 py-0.5 rounded text-sm">
                            削除
                        </button>
                    </div>
                </li>
            @endforeach
            <!-- 他のデータも同様に -->
        </ul>
        <!-- Pagination -->
        <div class="mt-4 flex justify-between">
            {{ $articles->links() }}
        </div>
    </div>
    <!-- 右側の入力フォーム -->
    <div class="w-1/2 ml-4 bg-gray-100 p-4 rounded shadow">
        <form wire:submit="save">
            <h2 class="text-lg font-bold mb-4">
                {{ ($this->inputMode === 'create') ? '新規作成' : '編集' }}
            </h2>
            <div class="mb-4">
                <label for="title" class="block text-sm font-medium text-gray-600">タイトル</label>
                <input wire:model="title" type="text" id="title" name="title" class="mt-1 p-2 w-full rounded border">
                @error('title')
                    <p class="text-red-500 text-xs mt-1">{{ $message }}</p>
                @enderror
            </div>
            <div class="mb-4">
                <label for="content" class="block text-sm font-medium text-gray-600">内容</label>
                <textarea wire:model="content" id="content" name="content" class="mt-1 p-2 w-full h-32 rounded border"></textarea>
                @error('content')
                    <p class="text-red-500 text-xs mt-1">{{ $message }}</p>
                @enderror
            </div>
            <button type="submit" class="bg-green-600 text-white px-4 py-2 rounded">送信</button>
            @if (session('message'))
                <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mt-3" role="alert">
                    {{ session('message') }}
                </div>
            @endif
        </form>
    </div>
</div>

※ ご注意: ちなみにコンポーネントのビューでは一番外側の要素はひとつだけしかセットできません。つまり以下のような<div>...</div>が2つあるものはアウトです!

<!-- ⚠ これは間違った使い方です❗ -->

<div>
    ここに1つ目
</div>
<div>
    ここに2つ目
</div>

なお、ビューの中で1点気をつけたいのがwire:model.liveです。

<input wire:model.live="keyword" type="search" placeholder="検索..." class="p-2 rounded border">

これは、「live がついているものが変更があったらすぐに更新する」というもので、通常はボタンなどを押したときだけ更新作業が発生します。(つまり、「何回も何回もAjax通信するなよ・・・」ということに対応しているようです。

レイアウトファイルをつくる

今回は「フルページ・コンポーネント」で実装をしていますが、そうなると1つ必要になってくるものが「レイアウト」ファイルです。

つまり、(フルページな)コンポーネントは一部分だけを担当しているわけですが、レイアウトはそれを囲んだ全体的なファイルのことで、例えば<head>...</head><body>...</body>が入っています。

イメージで言うと、日本(レイアウト)の中に東京都(コンポーネント)があるという感じでしょうか。

では、以下のファイルを作ってください。

resources/views/components/layouts/app.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- TailwindCSS -->
    <script src="https://cdn.tailwindcss.com/3.3.3"></script>

    <title>{{ $title ?? config('app.name') }}</title>
</head>
<body class="p-5">

<!-- ここにフルページ・コンポーネントが入ってきます! -->
{{ $slot }}

<script>

    document.addEventListener('livewire:init', () => { // Livewire が初期化されたとき実行

        // 独自ディレクティブ
        Livewire.directive('confirm', ({ el, directive, cleanup }) => {

            const message = directive.expression;
            const onClick = e => {

                if (! confirm(message)) {

                    e.preventDefault()
                    e.stopImmediatePropagation()

                }

            }

            el.addEventListener('click', onClick);

            cleanup(() => { // 削除されたときに実行される

                el.removeEventListener('click', onClick);

            });

        });

    });

</script>

</body>
</html>

※ ちなみにLivewire 3は自動的にこのファイルを探し出すようになっていますが、臨機応変にレイアウト・ファイルを変更することもできます。

なお、この中には独自ディレクティブが入っていますが、詳しくは次の項目をご覧ください。

独自ディレクティブについて

ディレクティブ」というのはLivewireで言うと、

  • wire:click
  • wire:model
  • wire:submit

のことです。

そして、先ほどの<script>...</script>の中に入っているコードが独自につくったディレクティブになります。

つまり、以下のようにタグへセットすると

<button type="submit" wire:confirm="送信します。よろしいですか?">送信</button>

クリックしたら確認ダイアログが表示され、OKなら実行し、キャンセルなら何もしないという流れを実現することができます。

これで作業は終了です!
お疲れ様でした。😊✨

テストしてみる

では、実際にテストしてみましょう❗

まずは「http://******/article」にブラウザでアクセスします。

はい❗
左側に一覧、右側に入力フォームが表示されています。

では、新規登録から見てみましょう。
フォームに以下のように入力して送信してみます。

すると・・・・・・

はい❗
1行目に新しいデータが表示されました。

そして、送信ボタンの下にはメッセージも表示されています。

では、次に「タイトル(50)」というデータの「編集」ボタンをクリックしてみましょう。

はい❗
新規作成」が「編集」というテキストになり、フォームには対象のデータが入っています。

では、これは以下のように変更して送信してみましょう。

どうなるでしょうか・・・・・・

はい❗
こちらも成功です😊

では、次に削除をやってみましょう。
ページを2に移動して一番上にある「タイトル(41)」の削除ボタンをクリックします。

※ ページ2に移動しているのは、削除したときにページが初期化されないことを確認するためです。

うまくいくでしょうか・・・・・・

はい❗
データが消えて「タイトル(40)」が先頭にきました。

成功です😊👍

では、最後にページ2の状態で「23」というキーワードで検索をしてみましょう。

※ ページ2が初期化されないとデータは表示できません。

はい❗
ページネーションも初期化され、件数が1件だけ表示されました。

すべて成功です✨😊👍

企業様へのご提案

今回のようにLivewire 3を利用するとシンプルなものでしたら、高速にシステムを作成することができます。

また、学習コストがVueReactなどと比べて比較的低いので、後から参加する開発者さんもすぐ慣れることができるでしょう。

※ ただし、複雑なことをするようでしたらLivewireではない方がベターというパターンもあります。技術選定は難しいですね…。

もしそういったご相談がありましたら、ぜひお問い合わせからご相談ください。

お待ちしております。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はリリースされたばかりのLivewire 3を使ってCRUDを実装してみました。

ちなみに使ってみた感想としては、「え❗もうできたの❗❓」です。

というのも、Livewire 2もそうでしたが、ほぼあるべきパターンが網羅されているので基本的なことをするにはとてもシンプルでOKでした。

また、Livewire 3になって今回紹介できなかったwire:navigateなど、「より便利に使える機能」が盛りだくさんなため、前バージョンよりも複雑なことがしやすくなっている印象でした。

ただ、やはり大規模システムや複雑なことをやるには結局VueReactに一日の長があるわけで、この辺の技術選定の難しさは感じています。

後は知名度がもうちょっと上がってくれたらもっと新規プロジェクトに採用しやすいかもですね。

ぜひ皆さんもいろいろ研究してみてくださいね。

ではでは〜❗

「令和の虎、
島やん社長の動画
いろいろ勉強になります❗」

このエントリーをはてなブックマークに追加       follow us in feedly