Laravel LivewireでCRUDを実装してみる

2020年9月8日にLaravel 8がリリースされました🎉

Laravel 8からはJetstreamというパッケージを使用してアプリの骨組みを簡単に構築することができるようになりました。

このJetstreamですが、インストール時にJetstreamの機能を実現するためのライブラリとして、LivewireもしくはInertiaのどちらかを選択する必要があります。

ただLivewireとInertiaがどういうものか分からないうちは、どちらを選択すれば良いのか迷ってしまいますよね。

この記事では以下の公式ドキュメントをもとに、Livewireの特徴や使い方をTodoリストのCRUDを実装しながら解説していきます。

ぜひLivewireとInertiaを選択する際の参考にしていただければと思います。

Livewireとは

LivewireとはPHPのみでVueやReactのようなリアクティブな動的コンポーネントを作成できるライブラリです。

Livewireのコンポーネント内ではBladeの構文を使用することができるので、VueやReactよりもLaravelとの相性が良くなっています。

もともとはVueやReactの複雑さを解消するために開発されたもので、VueやReactよりもシンプルに扱うことができ、Laravel向けのライブラリなので、Laravelの快適さを損なうことなく開発できます。

  • PHPのみで動的コンポーネントを作成できる
  • Bladeの構文をそのまま使える
  • VueやReactよりもシンプル
  • Laravelの快適さを損なうことなく開発

このようにLivewireの特徴だけ見ると「結構良さそう?」と思ってしまいますが、VueやReactのようなフロントエンドのフレームワークと違ってLivewireはバックエンドで様々な処理を実行します。

つまり、コンポーネントで何か処理が発生するたびにAjaxを使用してサーバと通信を行い、サーバからのレスポンスを画面に反映しています。

その分サーバにかかる負荷は大きくなるので、この辺りもLivewireかInteriaを選択する際の判断材料になりそうです。

Livewireのセットアップ

まずはプロジェクトを作成してLivewireを使えるところまで準備していきます。

Laravelプロジェクトを作成

まずはプロジェクトを作成します。

laravel new livewire-test

Laravelのプロジェクトが作成できたらcdしてプロジェクトルートに入ります。

cd livewire-test

Livewireをインストール

次にLivewireをインストールします。

Livewireは他のパッケージと同様にcomposerでインストールすることができます。

composer require livewire/livewire

次にLivewireのアセットを読み込む設定をしていきます。

ここでは後の作業を考えて、resources/views/layouts/app.blade.phpを作成して以下を記述します。

  • headタグ内に@livewireStyles
  • bodyの閉じタグ直前に@livewireScripts
mkdir resources/views/layouts
touch resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    @livewireStyles
</head>
<body>
    @yield("content")

    @livewireScripts
</body>
</html>

@yield(“content”)の部分は、後にこのレイアウトをextendsしてテンプレートを作成する際に使用します。

これでLivewireを使う準備が整いました。

LivewireでCRUDを実装

ここからは実際にLivewireでTodoリストのCRUDを実装していきます。

TodoリストCRUDの全体像

今回は最終的に以下のコンポーネントの完成を目指します。

  1. TodoCreate(作成)
  2. TodoList(一覧、削除)
  3. TodoShow(更新)

CREATE

いよいよ、ここからLivewireを使ってコンポーネントを作成していきます。

まずはコンポーネントを作成してみましょう。

Livewireではphp artisan make:livewireコマンドでコンポーネントを作成します。

php artisan make:livewire todo-create

このコマンドを実行すると以下のコンポーネントとテンプレートが作成されます。

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class TodoCreate extends Component
{
    public function render()
    {
        return view('livewire.todo-create');
    }
}
<div>
    {{-- Do your work, then step back. --}}
</div>

Livewireのコンポーネントではこのようにrender関数からviewを返します。

このままではテンプレートファイルでLivewireを使用することができないので、先ほど作成したレイアウトファイルをextendsするように設定します。

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class TodoCreate extends Component
{
    public function render()
    {
        return view('livewire.todo-create')
            // extendsでレイアウトファイルを継承
            ->extends("layouts.app");
    }
}

extends(“layouts.app”)の部分で先ほどLivewireのアセットを読み込んだレイアウトファイルを継承しています。

次にルーティングを設定してこのテンプレートがしっかりと表示されるか確認してみましょう。

以下のように、LivewireのコンポーネントはLaravelのコントローラと同じようにルーティングを設定することができます。

<?php

use App\Http\Livewire\TodoCreate;
use Illuminate\Support\Facades\Route;
// Todo作成画面へのルーティングを追加
Route::get("/todos/create", TodoCreate::class)->name("todos.create");

ルーティングを設定したら分かりやすいようにテンプレートファイルも書き換えます。

<div>
    <div>HELLO LIVEWIRE</div>
</div>

サーバを起動してlocalhost:8000/todos/createにアクセスしてみましょう。

php artisan serve

画面にHELLO LIVEWIREが表示されていれば成功です。

ルーティングがうまく設定できていることが確認できたので、次にTodo作成用のフォームを実装します。

テンプレートファイルを以下のように書き換えてください。

<div>
    <form wire:submit.prevent="save">
        <div>
            <input type="text" wire:model="title">
        </div>
        
        <div>
            <textarea wire:model="content"></textarea>
        </div>

        <button type="submit">保存</button>
    </form>
</div>

色々出てきたので1つ1つ見ていきましょう。

まずはイベントのハンドリングです。

Livewireではwire:イベント.修飾子という形式でイベントをハンドリングすることができます。

例えばイベントとしては以下が指定できます。

イベント ディレクティブ
click wire:click
keydown wire:keydown
submit wire:submit

修飾子としては以下が指定できます。

修飾子 説明
stop event.stopPropagation()と同じです。
prevent event.preventDefault()と同じです。
self 自分自身でイベントがトリガーされた場合だけアクションを実行します。
これによって外部のエレメントが子のエレメントからトリガーされたイベントをキャッチするのを防ぐことができます。
debounce.150ms アクションのハンドリングにXミリ秒の遅延を加えます。

今回はwire:submit.preventとしてformのsubmitイベントをハンドリングしてコンポーネントのsaveメソッドを実行しています。

このsaveメソッドは後ほど作成します。

次にデータバインディングです。

Livewireではwire:modelを使用してデータの双方向バインディングを行うことができます。

今回は以下の2つをバインディングしています。

テンプレート コンポーネント
title $title
content $content

次にコンポーネントにバリデーション、Todo作成処理を追加していきます。

コンポーネントを以下のように書き換えてください。

<?php

namespace App\Http\Livewire;

use App\Models\Todo;
use Livewire\Component;

class TodoCreate extends Component
{
    public string $title = "";
    public string $content = "";

    protected array $rules = [
        'title' => 'required|string|max:255',
        'content' => 'required|string|max:255',
    ];

    public function render()
    {
        return view('livewire.todo-create')
            ->extends("layouts.app");
    }

    public function save()
    {
        $this->validate();

        Todo::create([
            "title" => $this->title,
            "content" => $this->content
        ]);

        $this->reset();
    }
}

一気にいろいろ追加されているので、1つ1つ見ていきましょう。

まずはバリデーションです。

Livewireではprotectedプロパティに$rulesを定義することで、LaravelのFormRequestと同じような感覚でバリデーションを実装できます。

...
    protected array $rules = [
        'todo.title' => 'required|string|max:255',
        'todo.content' => 'required|string|max:255',
    ];
...

次にformのsubmitイベントがハンドリングされた際に呼び出されるsaveメソッドです。

...
public function save()
{
    $this->validate();

    Todo::create([
        "title" => $this->title,
        "content" => $this->content
    ]);

    $this->reset();
}
...

このsaveメソッドではバリデーションを実行して、もしバリデーションに成功すればTodoをDBに保存し、入力欄をクリアしています。

Livewireではこのようにresetメソッドを使用して各プロパティを初期値に戻すことができます。

それでは次にEloquentを使用してTodoを作成します。

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

php artisan make:model Todo -m

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

<?php

namespace App\Models;

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

class Todo extends Model
{
    use HasFactory;
}
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTodosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('todos');
    }
}

このTodoにtitleとcontentを追加します。

以下のようにコードを書き換えてください。

<?php

namespace App\Models;

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

class Todo extends Model
{
    use HasFactory;

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

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTodosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('todos');
    }
}

次にマイグレーションを実行します。

今回はサンプルということでSQLiteを使用します。

以下のコマンドを実行してSQLite用のDBファイルを作成します。

touch database/database.sqlite

次に.envのDB関連の部分をSQLite用に書き換えます。

...
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=livewire_test
# DB_USERNAME=root
# DB_PASSWORD=
...

次に以下のコマンドでマイグレーションを実行してください。

php artisan migrate

これでDBにtodosテーブルが作成されました。

DBの準備が整ったので、localhost:8000/todos/createにアクセスしてTodoを作成してみてください。

titleとcontentを入力して保存ボタンを押すとDBにTodoが保存されて入力欄がクリアされます。

READ

Todoの作成が実装できたら次に一覧表示を実装して作成したTodoを確認していきます。

以下のコマンドを実行して一覧表示表用のコンポーネントを作成してください。

php artisan make:livewire TodoList

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

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class TodoList extends Component
{
    public function render()
    {
        return view('livewire.todo-list');
    }
}
<div>
    {{-- The whole world belongs to you --}}
</div>

今回もテンプレートから作成していきます。

テンプレートを以下のように書き換えてください。

<div>
    <ul>
        @foreach($todos as $todo)
        <li wire:key="{{ $todo->id }}">
            <a>{{ $todo->title }}</a>
        </li>
        @endforeach
    </ul>
    <a href="{{ route('todos.create') }}">作成</a>
</div>

@foreachでコンポーネントの$todosを表示しています。

LivewireではVueと同じように繰り返しの中で要素を特定するためにwire:keyでキーを指定する必要があります。

今回は$todo->idをキーとして設定しています。

その他には先ほどのTodo作成画面へ遷移するためのリンクを設置しています。

それでは次にコンポーネントのコードを記述していきます。

コンポーネントを以下のように書き換えてください。

<?php

namespace App\Http\Livewire;

use App\Models\Todo;
use Illuminate\Support\Collection;
use Livewire\Component;

class TodoList extends Component
{

    public Collection $todos;

    public function mount()
    {
        $this->todos = Todo::all();
    }

    public function render()
    {
        return view('livewire.todo-list')
            ->extends("layouts.app");
    }
}

このコンポーネントでは@foreachで使用する$todosの取得とレンダリングするviewのレイアウトファイルをextendsで指定しています。

これで一覧表示が実装できたので、ルーティングを設定して確認してみましょう。

以下のルーティングを追加してください。

...
use App\Http\Livewire\TodoList;
...
Route::get("/todos", TodoList::class)->name("todos");
...

/todosへのルーティングを追加しています。

localhost:8000/todosへアクセスしてみてください。

先ほど作成したTodoのタイトルが表示されます。

UPDATE

Todoの作成と一覧表示が作成できたので、ここからはTodoの編集画面を作成していきます。

まずはコンポーネントとテンプレートを作成しましょう。

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

php artisan make:livewire todo-show

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

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class TodoShow extends Component
{
    public function render()
    {
        return view('livewire.todo-show');
    }
}
<div>
    {{-- In work, do what you enjoy. --}}
</div>

今回もテンプレートから作成していきます。

テンプレートを以下のように書き換えてください。

<div>
    <form wire:submit.prevent="update">
        <div>
            <input type="text" wire:model="todo.title">
        </div>
        
        <div>
            <textarea wire:model="todo.content"></textarea>
        </div>

        <button type="submit">更新</button>
    </form>
</div>

更新用のテンプレートは作成用のテンプレートとほとんど同じ内容になっています。

次にコンポーネントのコードを記述します。

コンポーネントを以下のように書き換えてください。

<?php

namespace App\Http\Livewire;

use App\Models\Todo;
use Livewire\Component;

class TodoShow extends Component
{
    public Todo $todo;

    protected  array $rules = [
        'todo.title' => 'required|string|max:255',
        'todo.content' => 'required|string|max:255',
    ];

    public function render()
    {
        return view('livewire.todo-show')
            ->extends("layouts.app");
    }

    public function update()
    {
        $this->validate();

        $this->todo->update();
    }
}

publicプロパティに$todoがありますが、初期化する処理を記述していません。

Livewireではこのようにpublicプロパティにタイプヒンティングを用いてEloquentを設定しておくと、ルートから自動的にモデルを判断してDBから取得してくれます。

例えばroutes/web.phpで以下のルーティング設定されている場合に

Route::get("/todos/{todo}", TodoShow::class);

以下のルートにアクセスされると自動的にidが1のTodoが$todoに割り当てられます。

/todos/1

今回もバリデーション用の$rulesを用意していますが、更新画面の場合はこの$rulesを全てのプロパティに対して設定していないと、formに初期値が反映されないので注意してください。

formからのsubmitイベントはupdateメソッドでハンドリングしています。

それでは更新処理の実装ができたので、ルーティングを設定します。

web/routes.phpに以下のルーティングを追加してください。

...
use App\Http\Livewire\TodoShow;
...
Route::get("/todos/{todo}", TodoShow::class)->name("todos.show");
...

ここまで実装できたら、先ほど作成した一覧画面にこの更新画面へのリンクを追加して動作確認を行います。

一覧画面のテンプレートを以下のように書き換えてください。

<div>
    <ul>
        @foreach($todos as $todo)
        <li wire:key="{{ $todo->id }}">
            <!-- 更新画面へのルーティングを追加 -->
            <a href="{{ route('todos.show', ['todo' => $todo->id]) }}">{{ $todo->title }}</a>
        </li>
        @endforeach
    </ul>
    <a href="{{ route('todos.create') }}">作成</a>
</div>

タイトルを表示しているaタグに更新画面へのリンクを追加しました。

それではlocalhost:8000/todosにアクセスしてどれか1つのTodoをクリックして更新画面を確認してみてください。

titleやcontentを書き換えた後に更新ボタンを押せば、内容がDBに反映されます。

DELETE

最後に一覧画面にTodoの削除処理を実装していきます。

一覧画面のテンプレートを以下のように書き換えてください。

<div>
    <ul>
        @foreach($todos as $todo)
        <li wire:key="{{ $todo->id }}">
            <a href="{{ route('todos.show', ['todo' => $todo->id]) }}">{{ $todo->title }}</a>
            <!-- 削除ボタンを追加 -->
            <button wire:click="delete({{ $todo->id }})">削除</button>
        </li>
        @endforeach
    </ul>
    <a href="{{ route('todos.create') }}">作成</a>
</div>

それぞれのタイトルの横に削除ボタンを追加しています。

次にこの削除ボタンのclickイベントをハンドリングするdeleteメソッドをコンポーネントに追加します。

一覧画面のコンポーネントに以下のメソッドを追加してください。

...
public function delete(int $id)
{
    $index = $this->todos->search(fn (Todo $todo) => $todo->id === $id);
    $this->todos[$index]->delete();

    $this->todos = $this->todos->filter(fn (Todo $todo) => $todo->id !== $id);
}
...

deleteメソッドを追加できたら一覧画面の削除ボタンをクリックしてみてください。

localhost:8000/todos

DBと一覧からTodoが削除されます。

まとめ

今回はLivewireで簡単なCRUDを実装してみました。

普段VueやReactを使っている方は比較的理解しやすかったのではないでしょうか。

動的コンポーネントの仕組みをサーバサイドで提供するという取り組み自体は非常に新しいものなので、これからどうなっていくのかが非常に楽しみです。

それでは、ここまでご覧いただきありがとうございました。