Blade の画面を保ったまま動的な UI を足したい人向けに、Laravel 13 の fresh app へ Livewire を入れる最小手順をまとめます。空ディレクトリから始める構成なので、既存の CRUD 記事や認証記事を先に実装していなくても進められます。既存シリーズは前提外です。
Livewire は、Blade を保ったままサーバー側の状態変更で UI を更新できる Laravel 向けの仕組みです。この記事では JavaScript フレームワークを別に組まず、一覧と追加フォームが動く最小形だけを扱います。
補助導線として先に読んでおくと入りやすい記事:
この 2 本を未読でも、以下の手順だけで完走できます。
前提環境
- Windows 11
- WSL2(Ubuntu)
- VS Code(Remote - WSL)
- Docker Desktop
composer:2コンテナ- Laravel 13
- Livewire 4
- SQLite
以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。
1. ゴールと非対象
この記事で到達する状態は次のとおりです。
http://localhost:8000/booksに Livewire 付きの一覧画面を表示できるwire:model.liveで検索入力に合わせて一覧を絞り込めるwire:clickで検索条件をクリアできるwire:submitで本を追加し、そのまま一覧へ反映できる- Blade へ
<livewire:book-manager />を差し込む位置が分かる
今回は Livewire の入口に集中します。次の内容は扱いません。
- Breeze / Starter Kit による認証導入
- Volt や page component の使い分け
- ページネーション、query string、複数条件検索
- React / Vue / Inertia との比較
- テスト、CI、デプロイ
2. 先に全体像をつかむ
流れは 5 段です。
- fresh app を作って Laravel を起動する
- Livewire を追加する
Bookmodel と一覧用 component を作る- Blade に Livewire component を埋め込む
tinkerでデータを入れて/booksを確認する
作業ディレクトリは ~/projects/laravel-livewire-intro-demo にそろえます。
mkdir -p ~/projects/laravel-livewire-intro-demo
cd ~/projects/laravel-livewire-intro-demo
code .
Laravel プロジェクトは composer:2 コンテナから作成します。ホスト側に PHP や Composer を直接入れずに済み、既存シリーズの手順にも合わせやすいからです。
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .
Laravel 13 の fresh app では、この時点で .env 作成、APP_KEY 生成、database/database.sqlite の作成、初回 migration まで自動で進みます。
次に compose.yml を作成します。
services:
app:
image: composer:2
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /app
environment:
HOME: /tmp
XDG_CONFIG_HOME: /tmp/.config
volumes:
- ./:/app
ports:
- "8000:8000"
command: php artisan serve --host=0.0.0.0 --port=8000
.env の該当部分は次のとおりです。
APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=/app/database/database.sqlite
続いてコンテナを起動します。
export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
docker compose up -d
ブラウザで http://localhost:8000 を開き、Laravel の welcome 画面が見えれば準備完了です。
Bind for 0.0.0.0:8000 failed: port is already allocated が出た場合は、compose.yml の左側だけを 8001:8000 に変更し、.env の APP_URL も http://localhost:8001 にそろえてください。
3. Livewire と Book model を追加する
まず Livewire を追加します。
docker compose exec app composer require livewire/livewire
Laravel 13 に合う Livewire 4 系が入り、実検証では v4.2.1 でした。
次に、一覧表示と追加フォームで使う Book model と migration を作成します。
docker compose exec app php artisan make:model Book -m
生成された database/migrations/*_create_books_table.php は次の内容に更新してください。timestamp 部分は実行時刻によって変わります。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('author');
$table->unsignedInteger('price');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('books');
}
};
app/Models/Book.php を次の内容に更新します。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
protected $fillable = [
'title',
'author',
'price',
];
}
ここまでできたら migration を実行します。
docker compose exec app php artisan migrate
4. Livewire component を作る
ここで BookManager component を作ります。
docker compose exec app php artisan make:livewire BookManager --class
Livewire 4 の既定は single-file component です。php artisan make:livewire BookManager だけを実行すると resources/views/components/⚡book-manager.blade.php が生成されます。今回は PHP クラスと Blade の接続を追いやすくするため、--class を付けて class-based component にします。
app/Livewire/BookManager.php を次の内容に更新してください。
<?php
namespace App\Livewire;
use App\Models\Book;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Validate;
use Livewire\Component;
class BookManager extends Component
{
public string $search = '';
#[Validate('required|string|max:255')]
public string $title = '';
#[Validate('required|string|max:255')]
public string $author = '';
#[Validate('required|integer|min:1')]
public string $price = '';
public function addBook(): void
{
$validated = $this->validate();
Book::query()->create([
'title' => trim($validated['title']),
'author' => trim($validated['author']),
'price' => (int) $validated['price'],
]);
$this->reset('title', 'author', 'price');
session()->flash('status', '本を追加しました。');
}
public function clearSearch(): void
{
$this->reset('search');
}
public function render(): View
{
$books = Book::query()
->when($this->search !== '', function ($query) {
$query->where('title', 'like', '%' . $this->search . '%');
})
->orderBy('id')
->get();
return view('livewire.book-manager', [
'books' => $books,
]);
}
}
このクラスで押さえたい点は 3 つです。
searchは検索欄と結びつく public property ですclearSearch()はwire:clickから呼び出すメソッドですaddBook()はwire:submitで呼び出し、バリデーション後にBookを保存します
price を string にしているのは、input type="number" でもブラウザから Livewire へ届く値は最初は文字列だからです。保存時に (int) へ変換しておくと、入力時の扱いと DB へ入れる型の責務を分けやすくなります。
5. Blade に差し込む
次に component 用 Blade を作成します。resources/views/livewire/book-manager.blade.php は次の内容です。
<div class="book-manager">
<div class="panel">
<h2>Livewire で検索する</h2>
<p>wire:model.live で title を絞り込みます。</p>
<div class="inline-controls">
<input wire:model.live="search" type="text" placeholder="タイトルで検索">
<button wire:click="clearSearch" type="button">検索をクリア</button>
</div>
<p class="hint">現在の検索語: {{ $search === '' ? '未入力' : $search }}</p>
<ul class="book-list">
@forelse ($books as $book)
<li wire:key="book-{{ $book->id }}">
<strong>{{ $book->title }}</strong>
<span>{{ $book->author }}</span>
<span>{{ number_format($book->price) }}円</span>
</li>
@empty
<li>一致する本はありません。</li>
@endforelse
</ul>
</div>
<div class="panel">
<h2>wire:submit で本を追加する</h2>
@if (session('status'))
<p class="status">{{ session('status') }}</p>
@endif
<form wire:submit="addBook" class="book-form">
<label>
タイトル
<input wire:model="title" type="text">
</label>
@error('title') <p class="error">{{ $message }}</p> @enderror
<label>
著者
<input wire:model="author" type="text">
</label>
@error('author') <p class="error">{{ $message }}</p> @enderror
<label>
価格
<input wire:model="price" type="number" min="1">
</label>
@error('price') <p class="error">{{ $message }}</p> @enderror
<button type="submit">本を追加する</button>
</form>
</div>
</div>
wire:model.live を使っているのは、検索欄を入力するたびに一覧を更新したいからです。Livewire 4 では、修飾子なしの wire:model が毎キー入力で同期される前提ではありません。検索のような場面では .live を付けるほうが意図を伝えやすくなります。
Livewire component の public property は、対応する Blade から自動で参照できる仕組みです。そのため render() で渡しているのは $books だけですが、book-manager.blade.php 側では $search もそのまま使えます。
続いて、Livewire を埋め込むページ Blade を作成します。resources/views/books/index.blade.php は次の内容です。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel Livewire Intro</title>
<style>
body {
margin: 0;
font-family: sans-serif;
background: #f5f7fb;
color: #1f2937;
}
main {
max-width: 960px;
margin: 0 auto;
padding: 40px 16px 64px;
}
h1 {
margin-bottom: 8px;
}
.lead {
margin-top: 0;
color: #4b5563;
}
.book-manager {
display: grid;
gap: 24px;
}
.panel {
background: #ffffff;
border: 1px solid #dbe3f0;
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
}
.inline-controls,
.book-form {
display: grid;
gap: 12px;
}
input,
button {
font: inherit;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
}
button {
background: #2563eb;
border-color: #2563eb;
color: #ffffff;
cursor: pointer;
}
.book-list {
display: grid;
gap: 12px;
padding-left: 20px;
}
.book-list li {
display: grid;
gap: 4px;
}
.hint {
color: #475569;
font-size: 0.95rem;
}
.status {
color: #166534;
background: #dcfce7;
border-radius: 10px;
padding: 10px 12px;
}
.error {
color: #b91c1c;
margin: 0;
font-size: 0.95rem;
}
</style>
@livewireStyles
</head>
<body>
<main>
<h1>Laravel で Livewire を始めるデモ</h1>
<p class="lead">Blade に Livewire コンポーネントを差し込み、入力・クリック・送信の 3 つを確認します。</p>
<livewire:book-manager />
</main>
@livewireScripts
</body>
</html>
@livewireStyles と @livewireScripts は省略しないでください。ここが欠けると <livewire:book-manager /> 自体は HTML に出ても、ブラウザ上のやり取りは動きません。
最後にルートを作成します。routes/web.php を次の内容に更新してください。
<?php
use Illuminate\Support\Facades\Route;
Route::redirect('/', '/books');
Route::view('/books', 'books.index')->name('books.index');
ルート一覧を確認します。
docker compose exec app php artisan route:list --name=books
books.index が見えていれば、Blade と Livewire をつなぐルートは用意できます。
6. サンプルデータを tinker で投入する
一覧画面を確認しやすいよう、先に 5 件の本を入れます。
docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->delete(); collect([[\"title\" => \"Laravel 入門\", \"author\" => \"山田 太郎\", \"price\" => 2800], [\"title\" => \"Laravel 実践 Livewire\", \"author\" => \"佐藤 花子\", \"price\" => 3400], [\"title\" => \"PHP と Web 基礎\", \"author\" => \"鈴木 一郎\", \"price\" => 2200], [\"title\" => \"Laravel テスト入門\", \"author\" => \"田中 次郎\", \"price\" => 3200], [\"title\" => \"Livewire UI パターン\", \"author\" => \"高橋 美咲\", \"price\" => 3600]])->each(fn (array \$attributes) => App\\Models\\Book::query()->create(\$attributes));"
\$attributes とエスケープしているのは、WSL 上の Bash が $attributes を先に展開しないようにするためです。ここを素の $attributes にすると、シェル展開の影響で tinker 実行時に parse error になりやすくなります。
7. 画面で確認する
ブラウザで http://localhost:8000/books を開いて、次の 3 つを順に試してください。
- 検索欄へ
Laravelと入力し、Laravel 入門、Laravel 実践 Livewire、Laravel テスト入門に絞られることを確認する 検索をクリアを押し、一覧が 5 件に戻ることを確認する- フォームに
Livewire 実践メモ、検証 太郎、3900を入力して送信し、成功メッセージと新しい本がそのまま一覧へ出ることを確認する
wire:submit で保存したあとに一覧へ戻るルートを書いていないのは、同じ画面の中で状態が更新される感覚をつかみやすくするためです。POST 後に別ページへ戻さなくても画面が更新されるところが、Blade だけのフォーム送信との最初の違いになります。
8. 詰まりやすい点
画面は出るが入力しても反応しない
resources/views/books/index.blade.php に @livewireStyles または @livewireScripts が欠けていないかを確認してください。HTML だけ表示され、Livewire の更新リクエストが飛ばない状態になりやすい箇所です。
Component [book-manager] not found が出る
php artisan make:livewire BookManager --class を実行した場合、埋め込み側は <livewire:book-manager /> です。class 名の大文字小文字ではなく、Blade 側は kebab-case で書きます。
検索が Enter キーを押すまで反映されない
検索欄を wire:model="search" にしていると、期待したタイミングで一覧が変わらないことがあります。今回のように入力のたびに絞り込みたい場合は wire:model.live="search" を使います。
8000 番ポートが使えない
compose.yml の左側だけを 8001:8000 のように変更し、.env の APP_URL も同じポートへそろえます。右側のコンテナポート 8000 はそのままで構いません。
既定の single-file component で進めたい
php artisan make:livewire BookManager だけを実行すると single-file component が生成されます。Livewire 4 の既定に寄せたい場合はその形でも構いませんが、初回は PHP クラスと Blade を分けたほうが責務を追いやすいので、本記事では --class を採用しました。
9. まとめ
fresh app から Livewire を入れ、Blade に component を差し込んで wire:model.live、wire:click、wire:submit を一通り確認しました。Blade の延長で動的 UI を足す入口としては、この形が最も小さく始めやすい構成です。
次に進むなら、検索・並び替え・ページネーションまで Livewire へ寄せた一覧画面を作ると価値が見えやすくなります。補助導線として次の 2 本を置いておきます。