【Laravel+Breeze】はじめから学ぶアプリの作りかた-英会話教室管理アプリを作ろう

この記事はLaravelをインストールして,初めてアプリを作ろうとしている初学者を対象としています。この記事を通じて以下の項目を学ぶことができます。

  • Breezeを用いたユーザー認証
  • CRUD(クラッド)処理
  • 一体多のリレーションシップ
  • 多対多のリレーションシップ

LaravelとBreezeの導入については以下を参考にして下さい。

WSL+Ubuntu22.04+Larabel+Breezeのインストールからウェブサイトを作るまでの流れ(初学者向け備忘録)

windowsにWSL2とUbuntu22.04を導入し,Visual Studio Codeを使用していることを前提とします。またLaravel9.xのインストールが終わっている状態から話を始めます。

これから作るもの

これから「まりも英会話教室・管理アプリ」を作成します。

アプリはあらかじめ登録された管理者だけがアクセスでき,講師と生徒を登録できます。また時間割表を作成し,それぞれのレッスンを受講している生徒を一覧表として表示します。

見た目はあまり気にせず,なるべくシンプルなコードを紹介しています。アプリ作成の基本を学んでいきましょう。

プロジェクトの準備

まずはプロジェクトを作成してBreezeをインストールしましょう。

プロジェクトの作成

まずはプロジェクトを作成しましょう。プロジェクト名はmarimoです。

$ curl -s https://laravel.build/marimo | bash

プロジェクトのフォルダに移動してsailを起動します。

$ sail up -d

Breezeのインストール

ユーザー認証機能が使えるBreezeをインストールします。Ubuntuをインストールしたばかりの状態ではComposerやnpmのインストールが必要になります。VS codeのターミナルから以下のコマンドを順次実行して下さい。

$ sail up -d
$ sail artisan migrate
$ sudo apt install composer

$ sudo apt install openssl php-common php-curl php-json php-mbstring php-mysql php-xml php-zip

$ composer require laravel/breeze --dev
$ sail artisan breeze:install
$ sudo apt install npm
$ npm install
$ npm run dev

Breezeの日本語化

Breezeをインストールした状態ではさまざまな表示が英語になっているので,これらを日本語で表示できるようにします。

config/app.phpを開いて以下の項目を編集します。

'timezone' => 'Asia/Tokyo',   -->タイムゾーンを日本に変更
'locale' => 'ja',  --> 言語を日本語に変更

次に日本語化のためのファイルをgithubからダウンロードしましょう。

こちらのページに移動してをクリックし,Download ZIPを選択します。

ダウンロードしたファイルを解凍して,lang-main > locales から フォルダ ja をプロジェクトの lang にコピーします。そして ja.json を一つ上のフォルダに移動します。VS Codeのエクスプローラーで確認して次のようになっていれば大丈夫です。

ブラウザのアドレスバーに localhost と入力しましょう。

右上の Log in をクリックします。

表示が日本語になりました。

管理者の登録

今回のアプリは特定の英会話教室専用であるため,あらかじめ管理者を登録してログインするようにします。そのためユーザー登録の機能は必要ありません。

Seederを用いて管理者を登録しましょう。

sail artisan make:seeder UserSeeder

作成された database/seeders/UserSeeder.php を開いて編集します。

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\User;                <-- 入れ忘れに注意
use Illuminate\Database\Seeder;

class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = new User();
        $user->name = "admin";
        $user->email = "admin@gmail.com";
        //パスワードを保存する場合はbcrypt()で暗号化する。
        $user->password = bcrypt("12345678");
        $user->save();
    }
}

Seederを実行します。

sail artisan db:seed --class=UserSeeder

レコードがちゃんと保存されているか確認しましょう。mysqlに接続して確認します。

$ sail mysql
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| marimo             |
| testing            |
+--------------------+

データベースからmarimoを選んで,テーブルを確認します。

mysql> show tables from marimo;
+------------------------+
| Tables_in_marimo       |
+------------------------+
| failed_jobs            |
| migrations             |
| password_resets        |
| personal_access_tokens |
| users                  |
+------------------------+

管理者はテーブルusersに登録されています。テーブルの中身を確認します。

mysql> use marimo;
mysql> select * from users;

次のような表示が出れば,レコードが保存されています。

| 1 | admin | admin@gmail.com | NULL | $2y$10$l1oKvquR44C2kXU・・・ |

exitで終了しましょう。

mysql> exit

ログイン

ブラウザのアドレスバーに localhost/login を入力してログイン画面からログインしてみましょう。

Seederで作成したメールアドレス admin@gmail.com,パスワード 12345678 を入力してログインします。

ログインに成功するとダッシュボードが表示されます。これで管理者がログインできるようになりました。

ユーザー登録機能の削除

今の状態では localhost/register から新しいユーザーを登録できます。さまざまなユーザーに登録してもらうサービスを作る場合にはこれでよいのですが,今回のアプリは一つの英会話教室専用として作成するので,あらかじめ登録した管理者のみがアクセスできるようにします。

routes/auth.php を開いて,次の行を削除します。

Route::get('register', [RegisteredUserController::class, 'create'])
            ->name('register');    -->削除
Route::post('register', [RegisteredUserController::class, 'store']);  -->削除

再び localhost/register を入力すると次のように表示されます。

これでユーザー登録機能が削除されました。

トップページとレイアウトの作成

Breezeをインストールしたときにあらかじめ用意されているテンプレートを書き換えていきましょう。

トップページの作成

トップページを作成しましょう。ここからは話を簡単にするためホームページのデザインをなるべくシンプルなものにしていきます。基本的なアプリの作り方を習得したら,デザインにも挑戦してみましょう。

ビューを作成します。resources/views にフォルダ home を作成し,ファイル index.blade.php を作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>まりも英会話教室・管理アプリ</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <h1>まりも英会話教室・管理アプリ</h1>
    <a href="login">ログイン</a>
</body>
</html>

次に routes/web.php を開いてルーティングを設定します。

Route::get('/', function () {
    return view('welcome');
});

上の部分を次に変更します。

Route::get('/', function () {
    return view('home.index');
})->name('home.index');

localhost を入力して,トップページが変更されていることを確認しましょう。

管理画面のレイアウト

ログイン後の管理画面を作っていきます。まずはレイアウトのひな形を作成します。Breezeにはあらかじめデザインの整ったレイアウトやナビゲーションメニューが用意されているのですが,これらのデザインをいったん放棄してシンプルなコードに書き換えていきます。

resources/views/layouts/app.blade.php を開いてコードをすべて削除し,次のコードを張り付けて下さい。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>まりも英会話教室・管理アプリ</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    <div>
        @include('layouts.navigation')
        <main>
            {{ $slot }}
        </main>
    </div>
</body>
</html>

ヘッダー部にある,viewportはスケールを指定するものです。スマートフォンなどの端末では文字が小さく表示されることがあるのでスケールを指定して文字が通常の大きさで表示されるようにします。

また,Breezeをインストールするとtailwindcssも使えるようになります。具体的な書き方は以下を参照してください。

tailwindチートシート

ただし,tailwindcssはコードを書き換えただけでは画面に反映されず,毎回 npm run dev を実行する必要があります。これを回避するために開発環境では <script src="https://cdn.tailwindcss.com"></script> を挿入しておきます。本番環境にデプロイするときにこの行は削除しましょう。

@include('layouts.navigation')は部分テンプレートを読み込みます。同じフォルダにある navigation.blade.phpの内容がこの部分に組み込まれます。

navigation.blade.phpは画面上部にあるナビゲーションメニューのテンプレートです。中身を作り変えていきましょう。

<nav class="flex">
    <div>
        <a class="mr-2" href="{{ route('dashboard') }}">ダッシュボード</a>
    </div>
    <div>
        <form method="POST" action="{{ route('logout') }}">
            @csrf
            <input type="submit" value="{{ __('Log Out') }}">
        </form>
    </div>
</nav>

Laravelではフォームから悪意のあるコードが送りこまれるのを防ぐために@csrfを記述しなければなりません。

講師のデータベース

講師のデータベースを作成し,登録や削除などができるようにします。

データベースの準備

マイグレーションファイルを作成し,編集します。

$ sail artisan make:migration create_lecturers_table

Laravelのようなフレームワークではファイルやモデルの命名にルールがあるので,サンプルコードを参考にルールに従っていきましょう。例えば,テーブル名は lecturers のように名詞の複数形を用います。

public function up()
{
    Schema::create('lecturers', function (Blueprint $table) {
        $table->id();
        $table->string('name');   <--追加
        $table->timestamps();
    });
}

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

$ sail artisan migrate

コントローラーからテーブルを操作するためにモデルを作成します。

sail artisan make:model Lecturer

作成された app/Models/Lecturer.php を開きます。

class Lecturer extends Model
{
    use HasFactory;
    protected $fillable = ['name'];   <--追加
}

このように書くことで,カラム name にデータを追加することを許可します。

コントローラーの作成

コントローラーを作成します。

sail artisan make:controller LecturerController --resource

7つのメソッドを持つ,app/Http/Controllers/LecturerController.php が作成されました。それぞれのメソッドの役割は,index(一覧),create(新規登録),store(保存),show(詳細),edit(編集),update(更新),destroy(削除)です。

ファイルを開いて,次の行を追加しておきます。

use Illuminate\Support\Facades\DB;
use App\Models\Lecturer;

ルーティングの設定

それぞれのコントローラーにアクセスするためにルーティングを設定しましょう。routes/web.php を開いて以下のコードを追加します。

Route::resource('/lecturer', 'App\Http\Controllers\LecturerController')
    ->middleware(['auth']);

ルーティングの設定を確認しましょう。これらの情報はあとでリンク先を指定するときに必要になります。

$ sail artisan route:list

GET|HEAD /                 home.index
GET|HEAD dashboard         dashboard
GET|HEAD lecturer          lecturer.index › LecturerController@index
POST     lecturer          lecturer.store › LecturerController@store
GET|HEAD lecturer/create   lecturer.create › LecturerController@create
GET|HEAD lecturer/{lecturer}   lecturer.show › LecturerController@show
PUT|PATCH lecturer/{lecturer}  lecturer.update › LecturerController@update
DELETE    lecturer/{lecturer}  lecturer.destroy › LecturerController@destroy
GET|HEAD  lecturer/{lecturer}/edit   lecturer.edit › LecturerController@edit

ルーティングに->middleware(['auth']);を加えると,ログインしたユーザーのみがページにアクセスできるようになります。

createメソッド:新規登録画面

講師を新規登録するためのビューを作成します。resources/viewslecturerフォルダを作り,その中にファイル create.blade.php を作成して編集します。

<x-app-layout>
    <h1>講師の登録</h1>
    <form action="{{ route('lecturer.store') }}" method="POST">
        @csrf
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') }}">
        </div>
        <input type="submit" value="登録する">
    </form>
</x-app-layout>

<x-app-layout></x-app-layout> で囲むと,さきほど作成したレイアウト app.blade.php の一部として表示されるようになります。具体的には {{ $slot }} の部分が置き換えられます。

コントローラーのcreateメソッドを次のように編集します。

public function create()
{
    return view('student.create');
}

ブラウザから localhost/lecturer/create にアクセスすると,レイアウトに含まれるナビゲーションメニューと create.blade.php の内容がともに表示されていることが確認できます。

このようにレイアウトを利用すると,ヘッダーやフッターなどそれぞれのページで共通する部分を再利用できるようになります。

storeメソッドとバリデーション

route('lecturer.store') と記述することによって,「登録する」をクリックすると,LecturerControllerstore メソッドが呼び出されます。LecturerController.php を開いてメソッドにコードを書いていきましょう。

public function store(Request $request)
{
    //バリデーション
    $request->validate([
        'name' => 'required|max:20',
    ]);

    //新規レコードの生成
    $lecturer = Lecturer::create([
        'name' => $request->name,
    ]);

    //indexページにリダイレクト
    return redirect()
        ->route('lecturer.index')
        //フラッシュメッセージを表示する
        ->with('status', '講師が登録されました。');
}

意図しない情報がデータベースに書き込まれるのを防ぐためにvalidate()を用いてバリデーションを行います。ここでは,'required|max:20' として名前を入力必須とし,最大20文字の制限をかけています。

バリデーションに失敗すると,直前のページにリダイレクトされます。create.blade.php に戻ってエラーメッセージを表示するように修正しましょう。

<x-app-layout>
    <h1>講師の登録</h1>
    <form action="{{ route('lecturer.store') }}" method="POST">
        @csrf
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') }}">
        </div>

        <!-- エラーメッセージの表示 -->
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <input type="submit" value="登録する">
    </form>
</x-app-layout>

また,lang/ja/validation.php の最後に以下のコードを追加して項目を日本語にします。

return [

    ・・・

    'attributes' => [
        'name' => '名前',
    ],
];

ためしにブラウザから localhost/lecturer/create にアクセスして,入力フォームを空欄にして「登録する」をクリックしてみます。

次に入力フォームに20文字以上の文字を入力して「登録する」をクリックしてみます。

バリデーションに失敗するとエラーメッセージが表示されるようになりました。

このようにバリデーションに失敗すると直前のページに戻ってエラーメッセージが表示されますが,このとき入力した文字が入力フォームに再び表示されています。これを実現する部分が value="{{ old('name') }}" です。old()で前の画面で入力された情報を取り出すことができます。

バリデーションに成功すると,create()によってテーブルに新しいレコードが保存され,redirect()でindexページに移動します。同時に->with()でセッションにフラッシュデータを保存します。これによってリダイレクト先のページにメッセージを表示できるようになります。

view()とredirect()の違いについて混乱するかもしれません。私自身,この違いを明確に理解しているかと言われると答えに窮するのですが,view()は単にビューを表示するだけであるのに対し,redirect()はルーターを呼び出すのでルーティングに付随する様々な処理を伴うことになります。実際のところredirect()でなければならない理由はケースバイケースのようです。一般的にはGETメソッドで呼び出したコントローラーメソッドではview()を用い,それ以外ではredirect()を用いると理解しておけば大丈夫です。

indexメソッド:講師一覧の表示

講師一覧のページを作ります。まずはコントローラーのindexメソッドにコードを追加しましょう。

public function index()
{
    $lecturers = Lecturer::all();
    return view('lecturer.index', [
        'lecturers' => $lecturers
    ]);
}

Lecturers::all() でレコードを全件取得し,ビューに渡します。

ビューを作成します。resources/views/lecturer/index.blade.php を新規作成してコードを追加します。

<x-app-layout>
    <H1>講師一覧</H1>
    <a href="{{ route('lecturer.create') }}">講師の新規登録</a>

    <!-- フラッシュデータが存在する場合にデータの内容を表示する -->
    @if (session('status'))
        <p class="text-green-400">{{ session('status')  }}</p>
    @endif

    <!-- $lcturersが空でない場合,以下を実行する -->
    @if($lecturers->isNotEmpty())
        <!-- 繰り返し処理でそれぞれの講師を表示する -->
        @foreach($lecturers as $lecturer)
            <div class="flex">
                <p class="mr-2">{{ $lecturer->id }}</p>
                <!-- 名前をクリックすると詳細ページのリンクへ移動する -->
                <a href="{{ route('lecturer.show', ['lecturer' => $lecturer->id]) }}">{{ $lecturer->name }}</a>
            </div>
        @endforeach
    @else
        <!-- $lecturersが空の場合にメッセージを表示する -->
        <p class="text-red-400">講師は登録されていません。</p>
    @endif
</x-app-layout>

講師の名前を登録して,localhost/lecturer にアクセスすると一覧が表示されます。

ナビゲーションメニューに講師一覧のリンクを追加します。

<nav class="flex">
    <div>
        <a class="mr-2" href="{{ route('dashboard') }}">ダッシュボード</a>
    </div>

    <!-- 講師一覧のリンクを追加  -->
    <div>
        <a class="mr-2" href="{{ route('lecturer.index') }}">講師</a>
    </div>

    <div>
        <form method="POST" action="{{ route('logout') }}">
            @csrf
            <input type="submit" value="{{ __('Log Out') }}">
        </form>
    </div>
</nav>

テーブルから取得したデータはphpの一般的な配列操作ではうまく扱うことができません。そこでCollectionクラスを使用します。isNotEmpty()もその一つです。こちらから他にどのようなメソッドがあるか調べてみると良いでしょう。

showメソッド:詳細の表示

今回は講師テーブルのカラムが名前だけなので詳細ページを作成するメリットはあまり無いのですが,学習のために方針を示しておきます。

コントローラーにコードを追加します。

public function show($id)
{
    //講師idから講師のレコードを取得
    $lecturer = Lecturer::find($id);

    return view('lecturer.show', [
        'lecturer' => $lecturer
    ]);
}

詳細のビューを作成します。

<x-app-layout>
    <H1>講師の詳細</H1>
    <div class="flex">
        <p>ID:</p>
        <p>{{ $lecturer->id }}</p>
    </div>
    <div class="flex">
        <p>名前:</p>
        <p class="mr-4">{{ $lecturer->name }}</p>
        
        <!-- 編集ボタンの設置 -->
        <a class="mr-2" href="{{ route('lecturer.edit', ['lecturer' => $lecturer->id]) }}">編集</a>

        <!-- 削除ボタンの設置 -->
        <form action="{{ route('lecturer.destroy', ['lecturer' => $lecturer->id]) }}" method="POST">
            @csrf
            @method('DELETE')
            <input type="submit" value="削除">
        </form>

    </div>
    <a href="{{ route('lecturer.index') }}">講師の一覧に戻る</a>
</x-app-layout>

一覧で講師の名前をクリックすると詳細ページが表示されます。

editとupdateメソッド:レコードの編集

editメソッドでレコードを編集できるようにしましょう。まずコントローラーにメソッドを追加します。

public function edit($id)
{
    $student = Student::find($id);
    return view('student.edit', [
        'student' => $student
    ]);
}

edit.blade.php を作成し,コードを追加します。

<x-app-layout>
    <h1>講師の編集</h1>
    <form action="{{ route('lecturer.update', ['lecturer' => $lecturer->id]) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') ?? $lecturer->name }}">
        </div>
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="更新する">
    </form>
</x-app-layout>

このコードは create.php とほとんど同じものですが,異なる部分については説明が必要です。

テーブルの更新を行う場合,PUT または PATCH メソッドでリクエストを送信します。Laravel では,method="POST" として @method('PUT') を書き加えることでPUTメソッドを可能にしています。

また,少し見慣れない書き方ですが old('name') ?? $lecturer->name は,old('name')null であれば $lecturer->namevalue に設定するという意味です。

名前を変更して「更新する」をクリックするとupdateメソッドが実行されます。メソッドにコードを追加します。

public function update(Request $request, $id)
{
    $request->validate([
        'name' => 'required|max:20',
    ]);

    Lecturer::find($id)
        ->update(['name' => $request->name]);
    
    return redirect()
        ->route('lecturer.index')
        ->with('status', '講師の情報が更新されました。');
}

storeメソッドと同じバリデーションが登場します。今回はメソッドの中に直接バリデーションを書いていますが,別ファイルに分離して再利用する方法もあります。

deleteメソッド:レコードの削除

詳細ページからレコードを削除できるようにします。レコードを削除する場合はDELETEメソッドで送信します。PUTのときと同様の書き方を用います。

コントローラーのdestoyメソッドにコードを追加しましょう。

public function destroy($id)
{
    Lecturer::find($id)->delete();

    return redirect()
        ->route('lecturer.index')
        ->with('status', '講師が削除されました。');
}

「削除」ボタンをクリックすると,レコードが削除されます。

これでCRUD処理が一通り行えるようになりました。

生徒のデータベース

講師のデータベースで作成したコードを流用して生徒のデータベースを作りましょう。

$ sail artisan make:migration create_students_table
public function up()
{
    Schema::create('students', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
}
$ sail artisan migrate

モデルを作成します。

$ sail artisan make:model Student
class Student extends Model
{
    use HasFactory;
    protected $fillable = ['name'];
}

ルーティングを設定します。

Route::resource('/student', 'App\Http\Controllers\StudentController')
    ->middleware(['auth']);

コントローラーを作成します。

$ sail artisan make:controller StudentController --resource
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Student;

class StudentController extends Controller
{
    public function index()
    {
        $students = Student::all();
        return view('student.index',[
            'students' => $students
        ]);
    }

    public function create()
    {
        return view('student.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|max:20',
        ]);

        $student = Student::create([
            'name' => $request->name,
        ]);

    return redirect()
        ->route('student.index')
        ->with('status', '生徒が登録されました。');
    }

    public function show($id)
    {
        $student = Student::find($id);
        return view('student.show', [
            'student' => $student
        ]);
    }

    public function edit($id)
    {
        $student = Student::find($id);
        return view('student.edit', [
            'student' => $student
        ]);
    }

    public function update(Request $request, $id)
    {
        $request->validate([
            'name' => 'required|max:20',
        ]);

        Student::find($id)
            ->update(['name' => $request->name]);
        
        return redirect()
            ->route('student.index')
            ->with('status', '生徒の情報が更新されました。');
    }

    public function destroy($id)
    {
        Student::find($id)->delete();

        return redirect()
            ->route('student.index')
            ->with('status', '生徒が削除されました。');
    }
}

ナビゲーションメニューに生徒一覧ページのリンクを加えます。

<nav class="flex">
    <div>
        <a class="mr-2" href="{{ route('dashboard') }}">ダッシュボード</a>
    </div>
    <div>
        <a class="mr-2" href="{{ route('lecturer.index') }}">講師</a>
    </div>

    <!-- 生徒一覧ページのリンクを追加 -->
    <div>
        <a class="mr-2" href="{{ route('student.index') }}">生徒</a>
    </div>

    <div>
        <form method="POST" action="{{ route('logout') }}">
            @csrf
            <input type="submit" value="{{ __('Log Out') }}">
        </form>
    </div>
</nav>

フォルダstudentを作成して,4つのビューを作成します。

<x-app-layout>
    <H1>生徒一覧</H1>
    <a href="{{ route('student.create') }}">生徒の新規登録</a>
    @if ($students->isEmpty())
        <p class="text-red-400">生徒は登録されていません。</p>                
    @endif
    @if (session('status'))
        <p class="text-green-400">{{ session('status')  }}</p>
    @endif
    @foreach($students as $student)
        <div class="flex">
            <p class="mr-2">{{ $student->id }}</p>
            <p class="mr-2"><a href="{{ route('student.show', ['student' => $student->id]) }}">{{ $student->name }}</a></p>
        </div>
    @endforeach
</x-app-layout>
<x-app-layout>
    <h1>生徒の登録</h1>
    <form action="{{ route('student.store') }}" method="POST">
        @csrf
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') }}">
        </div>
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="登録する">
    </form>
</x-app-layout>
<x-app-layout>
    <H1>生徒の詳細</H1>
    <div class="flex">
        <p>ID:</p>
        <p>{{ $student->id }}</p>
    </div>
    <div class="flex">
        <p>名前:</p>
        <p class="mr-4">{{ $student->name }}</p>
        <a class="mr-2" href="{{ route('student.edit', ['student' => $student->id]) }}">編集</a>
        <form action="{{ route('student.destroy', ['student' => $student->id]) }}" method="POST">
            @csrf
            @method('DELETE')
            <input type="submit" value="削除">
        </form>
    </div>
    <a href="{{ route('student.index') }}">生徒の一覧に戻る</a>
</x-app-layout>
<x-app-layout>
    <h1>生徒の編集</h1>
    <form action="{{ route('student.update', ['student' => $student->id]) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') ?? $student->name }}">
        </div>
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="更新する">
    </form>
</x-app-layout>

一対多のリレーションシップ

ここから講師と生徒のモデルを結び付けていきます。まずは,一人の講師が複数のレッスンを持つという関係を一体多のリレーションシップで表します。

レッスンのテーブルを作成します。

$ sail artisan make:migration create_lessons_table
public function up()
{
    Schema::create('lessons', function (Blueprint $table) {
        $table->id();
        $table->bigInteger('lecturer_id');   <--講師id
        $table->integer('day');              <--曜日を0~6の数値で表すことにする
        $table->time('begin');               <--開始時間
        $table->time('end');                 <--終了時間
        $table->timestamps();
    });
}
$ sail artisan migrate
$ sail artisan make:model Lesson

モデルに一体多のリレーションシップを設定します。

class Lecturer extends Model
{
    use HasFactory;
    protected $fillable = ['name'];

    public function lessons()
    {
        //Lecturerは多くのLessonを持つ
        return $this->hasMany(Lesson::class);
    }
}
class Lesson extends Model
{
    use HasFactory;
    protected $fillable = [
        'lecturer_id',
        'day',
        'begin',
        'end',
    ];

    public function lecturer()
    {
        //LessonはLecturerに所属する
        return $this->belongsTo(Lecturer::class);
    }
}

このときLeturerモデルを参照元と言い,Lessonモデルを参照先と呼びます。

講師が持つレッスンを抽出するには次のように書きます。

//id=1の講師を抽出
$lecturer = Lecturer::find(1);
//講師が持つレッスンを抽出
$lessons = $lecturer->lessons;

反対に,あるレッスンがどの講師に属しているかを抽出するには次のように書きます。

//id=1のレッスンを抽出
$lesson = Lesson::find(1);
//レッスンが所属する講師を抽出
$lecturer = $lesson->lecturer;

多対多のリレーションシップ

今度はレッスンと生徒のモデルを結び付けてみましょう。一つのレッスンは複数の生徒を持ち,一人の生徒は複数のレッスンを持ちます。このような関係は多対多のリレーションシップとなります。

多対多の場合,2つのテーブルをつなぐ中間テーブルが必要です。

$ sail artisan make:migration create_lesson_student_table
public function up()
{
    Schema::create('lesson_student', function (Blueprint $table) {
        $table->id();
        $table->bigInteger('lesson_id');
        $table->bigInteger('student_id');
        $table->timestamps();
    });
}

中間テーブルの名前は上のように関連付けしたいテーブルどうしを単数形_単数形の形で結びます。またテーブル名はアルファベット順に並べます。

$ sail artisan migrate
//以下を追加
public function students()
{
    return $this->belongsToMany(Student::class);
}
public function lessons()
{
    return $this->belongsToMany(Lesson::class);
}

中間テーブルのモデルを作成する必要はありません。

一体多の抽出方法が理解できれば,多対多の書き方も想像がつくでしょう。

$lesson = Lesson:find(1);
$students = $lesson->students;

$student = Student:find(1);
$lessons = $student->lessons;

レッスンのデータベース

Lessonモデルのルーティングとコントローラーを作成します。

$ sail artisan make:controller LessonController --resource
Route::resource('/lesson', 'App\Http\Controllers\LessonController');  <--追加
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Lecturer;
use App\Models\Lesson;

class LessonController extends Controller
{
    //それぞれのメソッドで共通する変数や配列はpublicで定義する
    public $days = ['月', '火', '水', '木', '金', '土', '日'];

    public function index()
    {
        $lecturers = Lecturer::all();
        $lessons = Lesson::all();

        return view('lesson.index', [
            'lecturers' => $lecturers,
            'lessons' => $lessons,
            //メソッドの外側で定義した変数や配列は$this->を加えることで利用できる
            'days' => $this->days,
        ]);
    }

    public function create()
    {
        $lecturers = Lecturer::all();

        return view('lesson.create', [
            'lecturers' => $lecturers,
            'days' => $this->days
        ]);
    }

    public function store(Request $request)
    {
        $request->validate([
            //バリデーションで形式が時刻になっているかどうかを
            //判定するにはdate_format:H:iとする
            'begin' => 'required|date_format:H:i',

            //afterを用いることで時刻がbeginよりも後の時刻で
            //あるかどうかを判定できる
            'end' => 'required|date_format:H:i|after:begin',
        ]);

        $lesson = Lesson::create([
            'lecturer_id' => $request->lecturer_id,
            'day' => $request->day,
            'begin' => $request->begin,
            'end' => $request->end,
        ]);

        return redirect()
            ->route('lesson.index')
            ->with('status', 'レッスンが登録されました。');
    }

    public function show($id)
    {
    }

    public function edit($id)
    {
        $lecturers = Lecturer::all();
        $lesson = Lesson::find($id);

        return view('lesson.edit', [
            'lecturers' => $lecturers,
            'lesson' => $lesson,
            'days' => $this->days,
        ]);
    }

    public function update(Request $request, $id)
    {
        $request->validate([
            'begin' => 'required|date_format:H:i',
            'end' => 'required|date_format:H:i|after:begin',
        ]);

        Lesson::find($id)
            ->update([
                'lecturer_id' => $request->lecturer_id,
                'day' => $request->day,
                'begin' => $request->begin,
                'end' => $request->end,
            ]);
        
        return redirect()
            ->route('lesson.index')
            ->with('status', 'レッスンの情報が更新されました。');
    }

    public function destroy($id)
    {
        Lesson::find($id)->delete();

        return redirect()
            ->route('lesson.index')
            ->with('status', 'レッスンが削除されました。');
    }
}

バリデーションのメッセージを編集します。

return [
    'after'                => ':attributeには、:dateより後の時間を指定してください。',  <--修正
    'attributes' => [
        'name' => '名前',
        'begin' => '開始時間',   <--追加
        'end' => '終了時間'      <--追加
    ],
];

ナビゲーションメニューにレッスン一覧のリンクを追加します。

<!-- レッスン一覧のリンクを追加 -->
<div>
    <a class="mr-2" href="{{ route('lesson.index') }}">レッスン</a>
</div>

ビューを作成します。

<x-app-layout>
    <H1>レッスン一覧</H1>
    <!-- 講師が登録されていない場合メッセージを表示する -->
    <!-- 講師が登録されている場合,新規登録のリンクを表示する -->
    @if ($lecturers->isEmpty())
        <p class="text-red-400">講師が登録されていません。初めに講師を登録して下さい。</p>
    @else
        <a href="{{ route('lesson.create') }}">レッスンの新規登録</a>
    @endif
    <!-- レッスンが登録されていない場合メッセージを表示する -->
    @if ($lessons->isEmpty())
        <p class="text-red-400">レッスンは登録されていません。</p>                
    @endif
    <!-- フラッシュメッセージ -->
    @if (session('status'))
        <p class="text-green-400">{{ session('status')  }}</p>
    @endif

    @foreach($lessons as $lesson)
        <div class="flex">
            <p class="mr-2">{{ $lesson->id }}</p>
            <p class="mr-2">{{ $lesson->lecturer->name}}</p>
            <p class="mr-2">({{ $days[$lesson->day] }})</p>
            <!-- 時間の表示形式の指定は
                    dateとstrtotimeの組み合わせで -->
            <p class="mr-2">{{ date('H:i', strtotime($lesson->begin)) }} ~ {{ date('H:i', strtotime($lesson->end)) }}</p>
            <a class="mr-2" href="{{ route('lesson.edit', ['lesson' => $lesson->id]) }}">編集</a>
            <form action="{{ route('lesson.destroy', ['lesson' => $lesson->id]) }}" method="POST">
                @csrf
                @method('DELETE')
                <input type="submit" value="削除">
            </form>
        </div>
    @endforeach
</x-app-layout>
<x-app-layout>
    <h1>レッスンの登録</h1>
    <form action="{{ route('lesson.store') }}" method="POST">
        @csrf
        <div class="flex">
            <!-- 講師セレクトボックス  -->
            <label for="lecturer">講師:</label>
            <select class="border-2" name="lecturer_id">
                <option value="">選択してください</option>
                <!-- それぞれの講師を選択肢に追加 -->
                @foreach ($lecturers as $lecturer)
                    <option value="{{ $lecturer->id }}">{{ $lecturer->name }}</option>
                @endforeach
            </select>
        </div>
        <div class="flex">
            <label for="day">曜日:</label>
            <select class="border-2" name="day">
                <option value="">選択してください</option>
                <!-- それぞれの曜日を選択肢に追加 -->
                @foreach ($days as $day)
                    <option value="{{ $loop->index }}">{{ $day }}</option>
                @endforeach
            </select>
        </div>
        <div class="flex">
            <label for="begin">開始:</label>
            <input class="border-2" type="time" name="begin">
        </div>
        <div class="flex">
            <label for="end">終了:</label>
            <input class="border-2" type="time" name="end">
        </div>
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="登録する">
    </form>
</x-app-layout>

$loop->index は @foreach の中で用います。ループを繰り返すときに 0,1,2,・・・と増加していきます。ここでは曜日に対するvlueを設定するために利用し,月のvalueを0,火のvalueを1,・・・としています。

<x-app-layout>
    <h1>レッスンの編集</h1>
    <form action="{{ route('lesson.update', ['lesson' => $lesson->id]) }}" method="POST">
        @csrf
        @method('PUT')
        @csrf
        <div class="flex">
            <!-- 講師セレクトボックス  -->
            <label for="lecturer">講師:</label>
            <select class="border-2" name="lecturer_id">
                <option value="">選択してください</option>
                <!-- それぞれの講師を選択肢に追加 -->
                @foreach ($lecturers as $lecturer)
                    @if($lecturer->id == (old('lecturer_id') ?? $lesson->lecturer->id))
                        <option value="{{ $lecturer->id }}" selected="selected">{{ $lecturer->name }}</option>
                    @else
                        <option value="{{ $lecturer->id }}">{{ $lecturer->name }}</option>
                    @endif
                @endforeach
            </select>
        </div>
        <div class="flex">
            <label for="day">曜日:</label>
            <select class="border-2" name="day">
                <option value="">選択してください</option>
                <!-- それぞれの曜日を選択肢に追加 -->
                @foreach ($days as $day)
                    @if($loop->index == (old('day') ?? $lesson->day))
                        <option value="{{ $loop->index }}" selected="selected">{{ $day }}</option>
                    @else
                        <option value="{{ $loop->index }}">{{ $day }}</option>
                    @endif
                @endforeach
            </select>
        </div>
        <div class="flex">
            <label for="begin">開始:</label>
            <input class="border-2" type="time" name="begin"
                value="{{ old('begin') ?? date('H:i', strtotime($lesson->begin)) }}">
        </div>
        <div class="flex">
            <label for="end">終了:</label>
            <input class="border-2" type="time" name="end"
                value="{{ old('end') ?? date('H:i', strtotime($lesson->end)) }}">
        </div>
        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="更新する">
    </form>
</x-app-layout>

編集画面のビューはコードがやや複雑になります。以下の部分について説明をしておきます。

@foreach ($lecturers as $lecturer)
    @if($lecturer->id == (old('lecturer_id') ?? $lesson->lecturer->id))
        <option value="{{ $lecturer->id }}" selected="selected">{{ $lecturer->name }}</option>
    @else
        <option value="{{ $lecturer->id }}">{{ $lecturer->name }}</option>
    @endif
@endforeach

まず @foreach でそれぞれの講師を選択肢に加えていきます。初めて編集画面を開いた状態では old('lecturer_id') は値が存在しないので,@if$lecturer->id$lesson->lecturer->id がイコールかどうかを判定します。

例えば講師に田中(id=1),佐藤(id=2)が登録され,レッスン(id=1)に佐藤(id=2)が登録されているとします。@foreachによるループは田中(id=1),佐藤(id=2)の順に進み,名前が選択肢に加えられます。1回目のループでは $lecturer->id = 1$lesson->lecturer->id = 2 となるので,@else 以下が実行されます。2回目のループでは $lecturer->id = 2 となるので $lesson->lecturer->id = 2 と一致します。このとき <option> selected="selected" を加えることで,この選択肢があらかじめ選択された状態にします。こうして,レコードに保存されたレッスンの講師があらかじめ選択された状態で表示されることになるのです。

またバリデーションに失敗した場合,old('lecturer_id') には入力された講師の id が入っているので,入力時に選択した講師が再び表示されることになります。

レッスンが登録できるようになりました。

レッスンと生徒を関連付ける

完成まであと一歩です。レッスンと生徒を関連付けていきましょう。

今回は生徒の編集画面から受講するレッスンの登録を行うことにします。まずは編集画面にレッスンを登録するセレクトボックスを設置します。

<x-app-layout>
    <h1>生徒の編集</h1>
    <form action="{{ route('student.update', ['student' => $student->id]) }}" method="POST">
        @csrf
        @method('PUT')
        <div class="flex">
            <label for="name">名前:</label>
            <input type="text" name="name" class="border-2" size="20" value="{{ old('name') ?? $student->name }}">
        </div>

        <!--  以下を追加  -->
        <div class="flex">
            <label for="lesson">レッスンの登録:</label>
            <!-- レッスンセレクトボックス  -->
            <select class="border-2" name="lesson">
                <option value="">選択してください</option>
                <!-- それぞれのレッスンを選択肢に追加 -->
                @foreach ($lessons as $lesson)
                    <option value="{{ $lesson->id }}">
                        {{ $lesson->lecturer->name }}(
                        {{ $days[$lesson->day] }})
                        {{ date('H:i', strtotime($lesson->begin)) }}~
                        {{ date('H:i', strtotime($lesson->end)) }}
                    </option>
                @endforeach
            </select>
        </div>

        @if ($errors->any())
            <div class="text-red-400">
                <ul>
                    @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                    @endforeach
                </ul>
            </div>
        @endif
        <input type="submit" value="更新する">
    </form>
</x-app-layout>

studentコントローラーに関連付けを削除するためのメソッドdetach()を追加します。ルーティングにも情報を追加しましょう。

ビューではなくコントローラーを呼び出すときは書き方が異なります。次のように,配列にコントローラーのクラス名とメソッド名を指定します。

Route::post('/student/{student}/{lesson}', [
    \App\Http\Controllers\StudentController::class,
    'detach'
])->middleware(['auth'])->name('student.detach');

コントローラーを編集していきます。

use App\Models\Lesson;    <--追加

class StudentController extends Controller
{
    public $days = ['月', '火', '水', '木', '金', '土', '日'];  <--追加

    public function index()
    {
        $students = Student::all();
        return view('student.index',[
            'students' => $students,
            'days' => $this->days,
        ]);
    }

    public function edit($id)
    {
        $student = Student::find($id);
        $lessons = Lesson::all();
        return view('student.edit', [
            'student' => $student,
            'lessons' => $lessons,
            'days' => $this->days,
        ]);
    }

    public function update(Request $request, $id)
    {
        $request->validate([
            'name' => 'required|max:20',
        ]);

        $student = Student::find($id);
        $student->update(['name' => $request->name]);

        //レッスンと生徒の関連付け
        if ($request->lesson) {
            //同じ関連付けがすでに登録されているかどうかをチェック
            //して二重に書き込まれるのを防ぐ
            $lesson_student = DB::table('lesson_student')
                                ->where([
                                    ['lesson_id', '=', $request->lesson],
                                    ['student_id', '=', $student->id]
                                ])->get();

            if ($lesson_student->isEmpty()) {
                //同じ関連付けが存在しない場合
                $student->lessons()->attach($request->lesson);
                $student->save();
            }
        }
        
        return redirect()
            ->route('student.index')
            ->with('status', '生徒の情報が更新されました。');
    }

    //関連付けを削除するメソッド
    public function detach($student_id, $lesson_id)
    {
        $student = Student::find($student_id);
        $student->lessons()->detach($lesson_id);

        return redirect()
            ->route('student.index')
            ->with('status', 'レッスンが削除されました。');
    }
}

生徒とレッスンを関連付けるコードは以下の部分です。lessons()app/Models/Student.php で作成したメソッドです。これに attach(レッスンのid) をつなげて関連付けます。そして save() で保存すると中間テーブルにレコードをが書き込まれます。

$student = Student::find($student_id);           -->生徒のレコードを抽出
$student->lessons()->attach($request->lesson);   -->レッスンのidを関連付け
$student->save();                                -->保存

関連付けを削除する場合は detach() を用い,save() は必要ありません。

$student = Student::find($student_id);
$student->lessons()->detach($lesson_id);

レッスンと生徒の関連付けができるようになりました。

ダッシュボードに時間割を表示する

最後にダッシュボードに時間割を表示できるようにしましょう。まずはコントローラーを作成します。

$ sail artisan make:controller DashboardController

ダッシュボードのルーティングを次のように書き換えます。

Route::get('/dashboard', [
    \App\Http\Controllers\DashboardController::class,
    'dashboard'
])->middleware(['auth'])->name('dashboard');

ビューを書いていきます。

<x-app-layout>
    <h1>ダッシュボード</h1>

    <!-- フラッシュメッセージ -->
    @if (session('status'))
        <p class="text-green-400">{{ session('status')  }}</p>
    @endif

    <h2>レッスン</h2>

    <!-- レッスンが登録されていない場合メッセージを表示する -->
    @if ($lessons->isEmpty())
        <p class="text-red-400">レッスンは登録されていません。</p>
    @endif

    <!-- それぞれのレッスンと登録されている生徒を表示 -->
    @foreach ($lessons as $lesson)
        <div class="flex">
            <p class="mr-2">{{ $lesson->id }}</p>
            <p class="mr-2">{{ $lesson->lecturer->name }}</p>
            <p class="mr-2">({{ $days[$lesson->day] }})</p>
            <p class="mr-2">{{ date('H:i', strtotime($lesson->begin)) }}~</p>
            <p class="mr-2">{{ date('H:i', strtotime($lesson->end)) }}</p>
            @foreach ($lesson->students as $student)
                <p class="mr-2">{{ $student->name }}</p>
            @endforeach
        </div>
    @endforeach
</x-app-layout>

これで完成です。

まとめ

この記事ではBreezeを導入してユーザー認証を行う方法を学びました。大抵のwebサービスではユーザー認証が必要になるのでプロジェクトを作成した段階ではじめにBreezeをインストールしておきます。

また,テーブルのCRUD処理とリレーションシップはwebサービスを構築するための基本となります。ここで示したサンプルコードをコピーして,さまざまな実験を試みてみると良いでしょう。