Laravel9で、Livewireの登録・表示・更新・削除のミニWEBシステムを作成しました。画像のアップロードやその前のプレビューも可能です。また、検索も可能ですし、登録・更新時のバリデーションチェックも備えています。
Live Wireは、Laravelのブレードを使って、ダイナミックでリアクティブ且つモダンなインターフェースを作りましょうといったスタンスで、VUEを覚えるにはハードルが高いけども、Livewireであれば お気軽に実現できますよといったスタンスのようです。
小規模なWEBシステムであれば、大体の機能が最初から有り、素のPHPで一から作ることを考えると遥かに少ない時間で作成が可能と思います。
こちらのyoutubeの解説記事でもあります。
Laravelプロジェクトの作成
composer create-project “laravel/laravel=9.*” livewire2022_1
上のコマンドをターミナルで実行し、プロジェクト livewire2022_1 を作成し、c:\xampp\laravel>cd livewire2022_1 プロジェクトフォルダに移動します。
phpMyAdminで、DB livewire_20221 を作成して、.envファイルに、
c:\xampp\laravel\livewire2022_1>php artisan -v
Laravel Framework 9.39.0
Laravelのバージョンは、9.39.0がインストールされました。
JetStreamとLivewireのインストール
Laravel Jetstreamのマニュアルに従い、Jet StreamとLivewireを以下のようにターミナルで実行。
マニュアルURL https://jetstream.laravel.com/2.x/installation.html
composer require laravel/jetstream
php artisan jetstream:install livewire
npm install
npm run build
php artisan migrate
php artisan serveで簡易サーバを起動。http://127.0.0.1:8000/で、Laravelの初期画面が表示。
Registerリンクをクリックして、ユーザー登録。
ログインできました!
これだけで、認証画面まで準備出来てしまっているので、昔のようにちまちま認証画面を作成していたことを考えれば、楽なものです(笑)。
マニュアルに従い、こちらもターミナルで実行し、導入。
php artisan vendor:publish –tag=jetstream-views
※tagの前は -ハイフンが二つ です。
Bookモデルの作成
ターミナルでBookモデルを作成します。
php artisan make:model Book -m
app\Models\Book.php
database\migrations\2022_11_13_012203_create_sessions_table.php
が作成されます。
booksテーブルにカラムをマイグレーションファイルで追加
2022_11_13_012203_create_sessions_table.phpのスキーマを拡張。デフォルトである $table->id() と timestampの間に、
タイトル、画像ファイル名、価格、詳細のためのカラムを追加し、上書き保存します。
$table->string(‘title’);
$table->string(‘image’);
$table->integer(‘price’);
$table->text(‘description’);
ターミナルでマイグレーションファイルを実行します。
php artisan migrate:fresh
カラムが追加されたことをphpmyadmin等で確認できました。
app\Models\Book.phpの、use HasFactory;の下に
protected $fillable = [‘title’,’image’,’price’,’description’];
を追記し、Create Update できるカラムを指定します。
LivewireコンポーネントBookIndexの作成
php artisan make:livewire BookIndex
ターミナルでBookIndexコンポーネントを作成します。途中でYesかNoを聞かれますが、Noと応えます。
CLASS: app/Http/Livewire//BookIndex.php ← バックエンド側
VIEW: resources\views/livewire/book-index.blade.php ← フロント側
このようにバックエンド側ファイル BookIndex.php と フロント側ファイル book-index.blade.php が生成されます。
以下 BookIndex.php をバックエンド側、book-index.blade.php を フロント側と呼びます。
BookIndex.phpの生成されたときの状態
1 2 3 4 5 6 7 8 9 10 |
<?php namespace App\Http\Livewire; use Livewire\Component; class BookIndex extends Component { public function render() { return view('livewire.book-index'); } } |
book-index.blade.phpの生成されたときの状態は、<div></div>があるだけです。
<div>は、体裁をよくするために、<div class=”max-w-6xl mx-auto”>としておきます。
web.phpに追記します。
上に use App\Http\Livewire\BookIndex;
Route::get(‘/books’, BookIndex::class)->name(‘books.index’);
http://127.0.0.1:8000/booksでヘッダーだけの表示を確認!
手動でイチイチ http://127.0.0.1:8000/books とかURL書くのも手間ですので、ヘッダーにbooksリンクを作ります。
resources\views\navigation-menu.blade.php内のNavigation Linksに以下追加します。
<div class=”hidden space-x-8 sm:-my-px sm:ml-10 sm:flex”>
<x-jet-nav-link href=”{{ route(‘books.index’) }}” :active=”request()->routeIs(‘dashboard’)”>
{{ __(‘Books’) }}
</x-jet-nav-link>
</div>
下のようにBooksリンクを確認しました。
Tailwind CSS
livewireのCSSは、Tailwindcss が基本的なCSSテンプレートとなっているようですの、VSの拡張機能で Tailwind CSS Intellisense を入れておくと便利です。Tailwindcss のマニュアルは、https://tailwindcss.com/docs/width#fixed-widths こちらにあります。
登録(Create,Store)
モーダルウインドウ
バックエンド側 app\Http\Livewire\BookIndex.php に追加していきます。
上部に
use App\Models\Book;//追加
public $liveModal = true;//モーダルウインドウ
public $title;//タイトル
public $newImage;//画像
public $price;//価格
public $description;//詳細
モーダルウインドウは、登録・編集のときのためですので、初期値はfalseにするべきですが、livewireのモーダルを理解するために敢えてtrueにしています。後ほど、falseにします。タイトルから詳細までは、変数を定義するのみです。
フロント側 resources\views\livewire\book-index.blade.php に以下追加します。
1 2 3 4 5 6 7 |
<x-jet-dialog-modal wire:model="liveModal"> <x-slot name="title"><h2 class="text-green-600">登録</h2></x-slot> <x-slot name="content">内容 </x-slot> <x-slot name="footer">フッター </x-slot> </x-jet-dialog-modal> |
フロント側に以下追加
<div class=”text-right m-2 p-2″>
<x-jet-button class=”bg-blue-600″ wire:click=”showBookModal”>登録</x-jet-button>
</div>
バックエンド側に以下追加
public function showBookModal(){
$this->reset();
$this->liveModal = true;
}
wire:clickで、showBookModalメソッドが実行され、モーダルが開きます。
「登録」ボタンでモーダルが開くことを確認しました!
登録フォーム フロント側
フロント側<x-slot name=”content”>内に以下追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<form enctype="multipart/form-data"> <x-jet-label for="title" value="Title" /> <input type="text" id="title" wire:model.lazy="title" class="block w-full bg-white border border-gray-400 rounded-md" /> @error('title') <span class="error text-red-400">{{ $message }}</span> @enderror <x-jet-label for="image" value="Book Image" class="mt-2" /> <input type="file" id="image" wire:model="newImage" class="block w-full bg-white border border-gray-400 rounded-md py-2 px-3" /> @error('newImage') <span class="error text-red-400">{{ $message }}</span> @enderror <x-jet-label for="price" value="Price" /> <input type="text" id="price" wire:model.lazy="price" class="block w-full bg-white border border-gray-400 rounded-md" /> @error('price') <span class="error text-red-400">{{ $message }}</span> @enderror <x-jet-label for="description" value="Description" class="mt-2" /> <textarea id="description" rows="3" wire:model.lazy="description" class="block w-full border-gray-400 rounded-md"></textarea> @error('description') <span class="error text-red-400">{{ $message }}</span> @enderror </form> |
<x-jet-button wire:click=”bookPost”>登録実行</x-jet-button>をフッター部分に追加。
wire:model.lazy=”***”の***箇所がフォームの部品名に該当します。@errorの箇所は、後ほど記述するバリデーション処理のエラーメッセージを表示する記述です。ブラウザでの表示は下のようになります。
登録 バックエンド側
バックエンド側に以下追加します。
use Livewire\WithFileUploads;//ファイルのアップロードに必要 上部に追加
use WithFileUploads;//ファイルのアップロードに必要 class BookIndex extends Component{の下に追加
bookPostメソッドを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public function bookPost(){ $this->validate([ 'title' => 'required', 'newImage' => 'image|max:2048', 'price' => 'integer|required', 'description' => 'required', ]); $image = $this->newImage->store('public/books'); Book::create([ 'title' => $this->title, 'image' => $image, 'price' => $this->price, 'description' => $this->description, ]); $this->reset(); } |
本の情報を登録する中で、誤った情報や未記入の項目は以下のようにエラーが表示されます。エラーの日本語化はしていませんが、たくさん情報があるはずですので簡単かと思います。
以下のように、正しく入力された情報が「登録実行」ボタンをクリックすると、テーブルbooksに登録されます。画像も、public\storage\booksに保存されたことを確認しました!
アップロード画像のプレビュー
画像のプレビューをするためには、フロント側に以下を追記します。
@if ($newImage)
Photo Preview:
<img src=”{{ $newImage->temporaryUrl() }}” class=”w-48″>
@endif
下のように画像のプレビューが確認できました!
一覧表示
登録したデータを一覧表示してみましょう。booksテーブルの全レコードを一覧表示します。
一覧表示 バックエンド側
use WithPagination;//別途追加 BookIndexクラスに追加
renderメソッド内を書き換えます。
return view(‘livewire.book-index’, [
‘books’ => Book::select(‘id’,’title’,’price’,’image’,’description’)
->orderBy(‘id’,’DESC’)->paginate(3),
]);
フロントが側にbooksテーブルの情報を並び順、ページングの結果を含めて渡していることになります。
一覧表示 フロント側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<div class="m-2 p-2"> <table class="w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th class="p-4 text-gray-500 text-left">Id</th> <th class="p-4 text-gray-500 text-left">Title</th> <th class="p-4 text-gray-500 text-left">Image</th> <th class="p-4 text-gray-500 text-left">price</th> <th class="p-4 text-gray-500 text-left">Description</th> <th class="p-4 text-gray-500 text-right">Edit</th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200"> @foreach($books as $book) <tr> <td class="p-4 whitespace-nowrap">{{ $book->id }}</td> <td class="p-4 whitespace-nowrap">{{ $book->title }}</td> <td class="p-4 whitespace-nowrap"> <img class="w-12 h-9 rounded" src="{{ Storage::url($book->image) }}" /> </td> <td class="p-4 whitespace-nowrap">{{ $book->price }}</td> <td class="p-4 whitespace-nowrap">{!! nl2br($book->description) !!}</td> <td class="p-4 text-right text-sm"> 編集 削除 </td> </tr> @endforeach </tbody> </table> <div class="m-2 p-2">{{ $books->links() }}</div> </div> |
ページングを含めたブラウザでの表示
登録した内容の一覧表示がブラウザで確認できました!Laravel9 Livewire凄く楽ですね!ページネーションまで実装できています!
編集(Edit、Update)
まずは、編集ボタンをクリックしたら、モーダルウインドウが開いて、該当する本の情報をDBから取り出し、ウインドウに表示するまでを作成します。
編集のためのモーダルウインドウの起動
フロント側に追加
<x-jet-button class=”bg-green-600″ wire:click=”showEditBookModal({{ $book->id }})”>編集</x-jet-button>
バックエンド側のメソッドshowEditBookModalに引数idを渡します。
バックエンド側
public $Id;
public $oldImage;
public$editWork = false;
メソッド showEditBookModal を追加
1 2 3 4 5 6 7 8 9 10 |
public function showEditBookModal($id){ $book = Book::findOrFail($id); $this->Id = $book->id; $this->title = $book->title; $this->oldImage = $book->image; $this->price = $book->price; $this->description = $book->description; $this->editWork = true; $this->liveModal = true; } |
登録されている画像の表示をするために、以下のようにフロント側を書き換えます。
1 2 3 4 5 6 7 8 9 |
<x-jet-label for="image" value="Book Image" class="mt-2" /> @if ($newImage) Photo Preview: <img src="{{ $newImage->temporaryUrl() }}" class="w-48"> @else @if ($oldImage) <img src="{{ Storage::url($oldImage) }}" class="w-48"> @endif @endif |
ブラウザで確認します。登録されている画像も表示されました!
タイトルの表示とフッターの表示も変更するために、
フロント側を書き換えます。
モーダルウインドウのタイトル部分
@if ($editWork)
<x-slot name=”title”><h2 class=”text-green-600″>編集</h2></x-slot>
@else
<x-slot name=”title”><h2 class=”text-blue-600″>登録</h2></x-slot>
@endif
モーダルウインドウのボタン部分
@if ($editWork)
<x-jet-button wire:click=”updateBook({{ $Id }})”>編集実行</x-jet-button>
@else
<x-jet-button wire:click=”bookPost”>登録実行</x-jet-button>
@endif
タイトルとフッター部分の表示が変わったことを確認しました!
編集の実行
フロント側に追加します。更新した結果のメッセージの表示のための記述です。
formの上あたりに追加@if (session()->has(‘message’))
<h3 class=”p-2 text-2xl text-green-600″>{{ session(‘message’) }}</h3>
@endif
バックエンド側にメソッドupdateBookを追加します。引数Idを受け取り、画像の入れ替えがあるかどうかで処理を分けています。updateが実行されたら、セッションmessageにメッセージを入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public function updateBook($Id){ $this->validate([ 'title' => 'required', 'price' => 'integer|required', 'description' => 'required', ]); if($this->newImage){ $image = $this->newImage->store('public/books'); Book::where('id',$Id)->update([ 'title' => $this->title, 'image' => $image, 'price' => $this->price, 'description' => $this->description, ]); }else{ Book::where('id',$Id)->update([ 'title' => $this->title, 'price' => $this->price, 'description' => $this->description, ]); } session()->flash('message','更新しました!'); } |
ブラウザで編集の実行を確認しました!
削除(Delete)
本の情報の削除です。
フロント側
<x-jet-button class=”bg-red-400″ wire:click=”deleteBook({{ $book->id }})”>削除</x-jet-button>
バックエンド側
メソッド deleteBook を追加
1 2 3 4 5 6 |
public function deleteBook($id){ $book = Book::findOrFail($id); Storage::delete($book->image); $book->delete(); $this->reset(); } |
検索
検索機能も設置します。Titleから検索できるようにします。
フロント側
登録ボタンの上に追記
<input type=“text” wire:model=“search” id=“search” class=“border-gray-300 rounded-md” placeholder=“キーワード” />
バックエンド側
public $search=”;
renderの箇所を以下に書き換えます。変数searchが空でない場合は、titleからあいまい検索するようにしています。
1 2 3 4 5 6 7 8 9 10 11 |
if( $this->search!="" ){ return view('livewire.book-index', [ 'books' => Book::Where('title','like','%'.$this->search.'%') ->orderBy('id','DESC')->paginate(3), ]); }else{ return view('livewire.book-index', [ 'books' => Book::select('id','title','price','image','description') ->orderBy('id','DESC')->paginate(3), ]); } |