Laravel9でCRUDを実装する

Laravel9でCRUDを実装する Laravel

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');
}

一覧画面で「削除」リンクをクリックすると、確認ダイアログを表示してデータを削除します。

一覧表示
削除確認ダイアログ

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

削除完了
タイトルとURLをコピーしました