Laravel9でCRUDを実装してみました。
環境
以下の環境で実装しています。
2023年1月に実装した時点の情報になります。
- PHP:8.1.14
- Laravel:9.46.0
コントローラーとモデルの作成
Laravelでは、コントローラーを作成する際に、「–resource」を付けることで、CRUDの一連のアクションを定義したコントローラーを作成できます。
php artisan make:controller BooksController --resource --model=Book
以下の形式でコントローラーが作成されます。
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Http\Request;
class BooksController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function show(Book $book)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function edit(Book $book)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Book $book)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function destroy(Book $book)
{
//
}
}
▼参考 リソースコントローラー
https://readouble.com/laravel/9.x/ja/controllers.html
モデルとマイグレーションファイルの内容は以下のようにしました。
<?php
namespace App\Models;
use App\Enums\BookCategoryType;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Book extends Model
{
use HasFactory;
protected $fillable = [
'title',
'category',
'author',
'purchase_date',
'evaluation',
'status',
'memo',
];
protected $casts = [
'purchase_date' => 'date',
];
protected $attributes = [
'status' => '1',
];
public function categoryText(): Attribute
{
return Attribute::make(
get: fn ($value) => BookCategoryType::getDescription($this->category)
);
}
public function displayPurchaseDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $this->purchase_date != null ? Carbon::parse($this->purchase_date)->format('Y年n月j日') : ''
);
}
public function formatYmdPurchaseDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $this->purchase_date != null ? Carbon::parse($this->purchase_date)->format('Y-m-d') : ''
);
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->char('category', 2);
$table->string('author')->nullable();
$table->timestamp('purchase_date')->nullable();
$table->integer('evaluation')->nullable();
$table->char('status', 2);
$table->text('memo')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('books');
}
};
マイグレーションを行います。
php artisan migrate
ルーティングの設定
ルーティングを追加します。
リソースコントローラーを使用する場合
Route::resource(‘books’, BooksController::class);
と書くと、それぞれのルーティングをまとめて定義できますが、実際の業務ではCRUDをカスタマイズしたり、そのまま使用しないことが多いので、今回は使用せず、各ルーティングをそれぞれ定義します。
Route::controller(BooksController::class)->name('books.')->group(function() {
Route::get('books', 'index')->name('index');
Route::get('books/create', 'create')->name('create');
Route::post('books', 'store')->name('store');
Route::get('books/{book}', 'show')->name('show');
Route::get('books/{book}/edit', 'edit')->name('edit');
Route::put('books/{book}', 'update')->name('update');
Route::delete('books/{book}', 'destroy')->name('destroy');
});
一覧画面
CRUDの実装に入る前に、一覧画面を用意しておきます。
<?php
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
class BooksController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$books = Book::orderBy('created_at', 'desc')->get();
return view('books.index', [
'books' => $books,
]);
}
}
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Books') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
@if (session('status') == 'books-stored')
<div class="mb-4 font-medium text-sm text-green-600">新規登録が完了しました。</div>
@elseif (session('status') == 'books-updated')
<div class="mb-4 font-medium text-sm text-green-600">更新が完了しました。</div>
@elseif (session('status') == 'books-deleted')
<div class="mb-4 font-medium text-sm text-green-600">削除が完了しました。</div>
@endif
<div class="pb-4">
<a href="{{ route('books.create') }}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white tracking-widest hover:bg-gray-700">新規登録</a>
</div>
<table class="w-full">
<tr>
<th>タイトル</th>
<th>カテゴリ</th>
<th>著者</th>
<th>購入日</th>
<th>評価</th>
<th>ステータス</th>
<th>メモ</th>
<th></th>
</tr>
@foreach ($books as $book)
<tr>
<td>{{ $book->title }}</td>
<td>{{ $book->category_text }}</td>
<td>{{ $book->author }}</td>
<td>{{ $book->display_purchase_date }}</td>
<td>{{ $book->evaluation }}</td>
<td>{{ $book->status }}</td>
<td>{{ $book->memo }}</td>
<td>
<form method="post" action="{{ route('books.destroy', $book->id) }}">
@csrf
@method('DELETE')
<a href="{{ route('books.show', $book->id) }}" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md">詳細</a>
<a href="{{ route('books.edit', $book->id) }}" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md">編集</a>
<button type="submit" onClick="return clickDelete()" class="delete-link underline text-sm text-gray-600 hover:text-gray-900 rounded-md">削除</button>
</form>
</td>
</tr>
@endforeach
</table>
</div>
</div>
<x-slot name="footer_script">
<script>
function clickDelete() {
if(!confirm('削除してもよろしいですか?')){
return false;
}
}
</script>
</x-slot>
</x-app-layout>
表示は以下のようになります。
(まだデータがないので、何も表示されません)

Create(生成)
まずはデータの登録です。
一覧画面で「新規登録」ボタンをクリックすると、画面が表示されます。
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view('books.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$request->validate([
'title' => 'required|string',
'category' => 'required|integer|digits_between:1,2',
'author' => 'nullable|string',
'purchase_date' => 'nullable|date|before_or_equal:today',
'evaluation' => 'nullable|integer|between:1,5',
'memo' => 'nullable|string',
]);
Book::create($request->all());
return Redirect::route('books.index')->with('status', 'books-stored');
}
<?php
use App\Enums\BookCategoryType;
?>
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Books') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
<form method="post" action="{{ route('books.store') }}" class="mt-6 space-y-6">
@csrf
<div>
<x-input-label for="title" value="タイトル" />
<x-text-input id="title" name="title" type="text" class="mt-1 block w-full" :value="old('title')" required autofocus />
<x-input-error class="mt-2" :messages="$errors->get('title')" />
</div>
<div>
<x-input-label for="category" value="カテゴリ" />
<select id="category" name="category" class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full" required>
<option value="" disabled selected style="display:none;"></option>
<option value="{{ BookCategoryType::NOVEL }}" @if(old('category')==BookCategoryType::NOVEL) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::NOVEL) }}</option>
<option value="{{ BookCategoryType::SPORTS }}" @if(old('category')==BookCategoryType::SPORTS) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::SPORTS) }}</option>
<option value="{{ BookCategoryType::PROGRAMMING }}" @if(old('category')==BookCategoryType::PROGRAMMING) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::PROGRAMMING) }}</option>
<option value="{{ BookCategoryType::BUSINESS }}" @if(old('category')==BookCategoryType::BUSINESS) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::BUSINESS) }}</option>
<option value="{{ BookCategoryType::OTHER }}" @if(old('category')==BookCategoryType::OTHER) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::OTHER) }}</option>
</select>
<x-input-error class="mt-2" :messages="$errors->get('category')" />
</div>
<div>
<x-input-label for="author" value="著者" />
<x-text-input id="author" name="author" type="text" class="mt-1 block w-full" :value="old('author')" />
<x-input-error class="mt-2" :messages="$errors->get('author')" />
</div>
<div>
<x-input-label for="purchase_date" value="購入日" />
<x-text-input id="purchase_date" name="purchase_date" type="date" class="mt-1 block w-full" :value="old('purchase_date')" />
<x-input-error class="mt-2" :messages="$errors->get('purchase_date')" />
</div>
<div>
<x-input-label for="evaluation" value="評価" />
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="1" @if(old('evaluation')=='1') checked @endif> 1</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="2" @if(old('evaluation')=='2') checked @endif> 2</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="3" @if(old('evaluation')=='3') checked @endif> 3</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="4" @if(old('evaluation')=='4') checked @endif> 4</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="5" @if(old('evaluation')=='5') checked @endif> 5</x-input-label>
<x-input-error class="mt-2" :messages="$errors->get('evaluation')" />
</div>
<div>
<x-input-label for="memo" value="メモ" />
<textarea id="memo" name="memo" class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full" rows="6">{{ old('memo') }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('memo')" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>登録する</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
表示は以下のようになります。

以下のように入力して、「登録する」ボタンをクリックすると登録されます。

登録完了後は一覧画面に戻り、登録した内容が表示されています。
また、画面上部に登録が完了した旨のメッセージが表示されます。

Read(読み取り)
続いて、データの確認です。
一覧画面で「詳細」リンクをクリックすると、画面が表示されます。

/**
* Display the specified resource.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function show(Book $book)
{
return view('books.show', ['book' => $book]);
}
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Books') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
<div class="border-t">
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">タイトル</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->title }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">カテゴリ</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->category_text }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">著者</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->author }}</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">購入日</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->display_purchase_date }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">評価</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->evaluation }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">ステータス</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->status }}</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium">メモ</dt>
<dd class="mt-1 text-sm sm:col-span-2 sm:mt-0">{{ $book->memo }}</dd>
</div>
</dl>
</div>
</div>
<div class="mt-4">
<a href="{{ route('books.index') }}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white tracking-widest hover:bg-gray-700">戻る</a>
</div>
</div>
</div>
</x-app-layout>
表示は以下のようになります。

Update(更新)
続いて、データの更新です。
一覧画面で「編集」リンクをクリックすると、画面が表示されます。
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function edit(Book $book)
{
return view('books.edit', ['book' => $book]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Book $book)
{
$request->validate([
'title' => 'required|string',
'category' => 'required|integer|digits_between:1,2',
'author' => 'nullable|string',
'purchase_date' => 'nullable|date|before_or_equal:today',
'evaluation' => 'nullable|integer|between:1,5',
'memo' => 'nullable|string',
]);
$book->update($request->all());
return Redirect::route('books.index')->with('status', 'books-updated');
}
<?php
use App\Enums\BookCategoryType;
?>
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Books') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
<form method="post" action="{{ route('books.update', $book->id) }}" class="mt-6 space-y-6">
@csrf
@method('PUT')
<div>
<x-input-label for="title" value="タイトル" />
<x-text-input id="title" name="title" type="text" class="mt-1 block w-full" :value="old('title', $book->title)" required autofocus />
<x-input-error class="mt-2" :messages="$errors->get('title')" />
</div>
<div>
<x-input-label for="category" value="カテゴリ" />
<select id="category" name="category" class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full" required>
<option value="" disabled selected style="display:none;"></option>
<option value="{{ BookCategoryType::NOVEL }}" @if(old('category', $book->category)==BookCategoryType::NOVEL) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::NOVEL) }}</option>
<option value="{{ BookCategoryType::SPORTS }}" @if(old('category', $book->category)==BookCategoryType::SPORTS) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::SPORTS) }}</option>
<option value="{{ BookCategoryType::PROGRAMMING }}" @if(old('category', $book->category)==BookCategoryType::PROGRAMMING) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::PROGRAMMING) }}</option>
<option value="{{ BookCategoryType::BUSINESS }}" @if(old('category', $book->category)==BookCategoryType::BUSINESS) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::BUSINESS) }}</option>
<option value="{{ BookCategoryType::OTHER }}" @if(old('category', $book->category)==BookCategoryType::OTHER) selected @endif>{{ BookCategoryType::getDescription(BookCategoryType::OTHER) }}</option>
</select>
<x-input-error class="mt-2" :messages="$errors->get('category')" />
</div>
<div>
<x-input-label for="author" value="著者" />
<x-text-input id="author" name="author" type="text" class="mt-1 block w-full" :value="old('author', $book->author)" />
<x-input-error class="mt-2" :messages="$errors->get('author')" />
</div>
<div>
<x-input-label for="purchase_date" value="購入日" />
<x-text-input id="purchase_date" name="purchase_date" type="date" class="mt-1 block w-full" :value="old('purchase_date', $book->format_ymd_purchase_date)" />
<x-input-error class="mt-2" :messages="$errors->get('purchase_date')" />
</div>
<div>
<x-input-label for="evaluation" value="評価" />
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="1" @if(old('evaluation', $book->evaluation)=='1') checked @endif> 1</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="2" @if(old('evaluation', $book->evaluation)=='2') checked @endif> 2</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="3" @if(old('evaluation', $book->evaluation)=='3') checked @endif> 3</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="4" @if(old('evaluation', $book->evaluation)=='4') checked @endif> 4</x-input-label>
<x-input-label><input class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" type="radio" name="evaluation" value="5" @if(old('evaluation', $book->evaluation)=='5') checked @endif> 5</x-input-label>
<x-input-error class="mt-2" :messages="$errors->get('evaluation')" />
</div>
<div>
<x-input-label for="memo" value="メモ" />
<textarea id="memo" name="memo" class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full" rows="6">{{ old('memo', $book->memo) }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('memo')" />
</div>
<div class="flex items-center gap-4">
<a href="{{ route('books.index') }}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white tracking-widest hover:bg-gray-700">戻る</a>
<x-primary-button>更新する</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>
表示は以下のようになります。

以下のようにタイトルを変更して、「更新する」ボタンをクリックすると、データが更新されます。

更新完了後は一覧画面に戻り、更新した内容が表示されています。
また、画面上部に更新が完了した旨のメッセージが表示されます。

Delete(削除)
最後に、データの削除です。
削除の場合は画面がありません。
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Book $book
* @return \Illuminate\Http\Response
*/
public function destroy(Book $book)
{
$book->delete();
return Redirect::route('books.index')->with('status', 'books-deleted');
}
一覧画面で「削除」リンクをクリックすると、確認ダイアログを表示してデータを削除します。


削除完了後は一覧画面に戻り、削除した内容が表示されなくなります。
また、画面上部に削除が完了した旨のメッセージが表示されます。
