0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(バックエンド実装編②後編)~レビュー投稿機能作成~

Posted at

実務未経験エンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その8.5)

※この記事は前編の続きです。

3. 研究室一覧表示(続き)

モデルを作成する

では、前編の続きです!
モデルを作成していきます!

...と言ってもユーザー、大学・学部・研究室のモデルは作成済みなので、中身を編集していきたいと思います。

リレーションについて

その前に、これまで何度か登場しているリレーションについて一度見ていきたいと思います。

リレーションとは、その名の通り、関係性です。
データベースにおける、複数のテーブル同士の関係性を定義するものです。

例えば、Instagramのようなサービスを考えてみましょう。

Instagramでは、ユーザーが写真を投稿できる機能がありますよね。
おそらく、ユーザーテーブルとイメージテーブルのようなものがあるのだと思われます。
投稿される写真は、「誰が投稿したのか」という情報が必須なはずです。
もし、その情報がないと、投稿した後に他のユーザーが「この写真は誰が投稿したの??」となってしまいます。
それどころか、写真をたくさん投稿すると、投稿した本人も自分がどの写真を投稿したのかが分からなくなっていき、削除等の管理もできなくなってしまいます。

このようにデータベース設計において、リレーションという概念は重要です!
そもそもこれまで省略してきましたが、僕がこのシリーズを通して作成・管理している「データベース」は、正確には「リレーショナルデータベースシステム」(RDBS)のことで、長いので「単にデータベース」と呼んできました。
名前の通り、リレーションが重要です!

一対多のリレーションについて

今回は、その中でも、一対多のリレーションについてご紹介いたします。

先ほどのInstagramの例ですと、ユーザーは、写真をいくつも投稿することができます。
つまり、ユーザー一人に対して、写真は複数存在できるため、一対多の関係が成立しています。

そして、リレーションを定義するときは、自身のテーブルに外部キー(FK)を用意する必要があるのです。

Userモデルの編集

では、実際にデータベース操作をつかさどるモデルにリレーションを定義していきます。まずは、Userモデルにリレーションを定義しましょう!

\app\Models\User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    // 大学(多対多)
    public function universities()
    {
        return $this->belongsToMany(University::class)->withTimestamps();
    }

    // 学部(多対多)
    public function faculties()
    {
        return $this->belongsToMany(Faculty::class)->withTimestamps();
    }

    // 研究室(多対多)
    public function labs()
    {
        return $this->belongsToMany(Lab::class)->withTimestamps();
    }

    // ↓ここに注目!!
    // レビュー(一対多)
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }
}

公式ドキュメントを見てみるとわかりますが、一対多のリレーションでは、一の側にhasMany()メソッドを用い、引数に多の側である相手のモデルクラスを入れればOKです。

Review::class

という書き方は、Laravelに特有なものではなく、phpに標準搭載されている完全修飾クラス名を文字列で表示する方法です。
堅苦しい言葉ですが、前編にも出てきました通り、単にReviewとしか書かれなない場合、どのReviewクラスなのかがコンピュータ側で判断がつかないので、どの名前空間に所属しているのかまでを付けてあげる必要がありました。

前編では、ファイルの上の方でuse宣言をしましたが、今回はhasMany()メソッドが引数に完全修正形式のクラス名を要求するため、::classを使用します。

これらは、本質的には異なるのですが、しっかりと理解するためには名前空間について理解しておく必要があります。
公式ドキュメントは、表現が固いので、もしかしたら、こちらの【PHP超入門】名前空間(namespace・use)についてという記事を先に読んでみる方が良いかもしれません。

よくわからない人は、まあ、飛ばしちゃっても良いと思います(いいのかよ!)。
要するに「hasMany()メソッドの引数には、クラスのフルネームが求められているので、::classを付けてあげましょうよ!」ってことです。

※ちなみに現状だと、Reviewモデルは作成していないので、以下のようにエラーが起きてしまうと思いますが、この後すぐに作成するので今は気にしないでください。
image.png

その他のモデルの編集

同様にして、大学・学部・研究室のモデルを編集しましょう。
まずは、Universityモデルです。

\app\Models\University.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class University extends Model
{
    protected $fillable = ['name'];

    // リレーションの定義
    // ユーザー(多対多)
    public function users()
    {
        return $this->belongsToMany(User::class)->withTimestamps();
    }

    // 学部(一対多)
    public function faculties()
    {
        return $this->hasMany(Faculty::class);
    }
}

多対多に関しては難しいので一度置いておいて、学部について考えてみましょう。

一つの大学は複数の学部を持てるので、hasMany()を使います。
なお、facultiesのように関数名は複数形にします。

次に、Facultyモデルです。

\app\Models\Faculty.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Faculty extends Model
{
    protected $fillable = [
        'name',
        'university_id'
    ];

    // リレーションの定義
    // ユーザー(多対多)
    public function users()
    {
        return $this->belongsToMany(User::class)->withTimestamps();
    }

    // 大学(多対一)
    public function university()
    {
        return $this->belongsTo(University::class);
    }

    // 研究室(一対多)
    public function labs()
    {
        return $this->hasMany(Lab::class);
    }
}

複数の学部が一つの大学に所属する形になりますので、belongsToを使います。
なお、関数名はuniversityのように単数形にします。

また、一つの学部に複数の研究室が所属するようにしたいので、hasManyを使います。
なお、関数名はlabsのように複数形にします。

最後に、Labモデルです。

app\Models\Lab.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Lab extends Model
{
    protected $fillable = [
        'name',
        'faculty_id'
    ];

    // リレーションの定義
    // ユーザー(多対多)
    public function users()
    {
        return $this->belongsToMany(User::class)->withTimestamps();
    }

    // 学部(多対一)
    public function faculty()
    {
        return $this->belongsTo(Faculty::class);
    }

    // レビュー(一対多)
    public function reviews()
    {
        return $this->hasMany(Review::class);
    }
}

複数の研究室が一つの学部に所属するため、関数名はfacultyのように単数形にして、belongsTo()メソッドを使います。

また、一つの研究室に対して、レビューは複数持つことができるようにしたいので、reviews()関数を定義して、その中でhasMany()メソッドを使用します。

Userモデルの時と同様に以下のようにエラーが起きてしまいますが、このあとReviewモデルを作成すれば、消えますので大丈夫です。
image.png

ややこしくて混乱してしまったかもしれませんが、落ち着いて一つずつイメージしながらやっていけばきっと大丈夫なはずです...!!

シーダーを実行する(忘れないうちに)

モデルにリレーションを書けて満足しそうになりますが、そもそもシーダーでデータを入れるために、モデルを編集したのでした...!!(*^^)v

忘れないうちに、種まきを完了させましょう。

一つずつ順番にコマンドを実行していってもよいのですが、今回はDatabaseSeeder.phpを用いてまとめて一気にやる方法をご紹介いたします!

以下のファイルを開いて、編集します。

database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // 大学・学部・研究室のSeederクラスを呼び出す
        $this->call([
            UniversitySeeder::class,
            FacultySeeder::class,
            LabSeeder::class,
        ]);

        // ユーザー30人分を作成、さらにリレーションを付与
        User::factory()->count(30)->create()->each(function ($user) {
            // 大学・学部・研究室のリレーションを付与
            $user->universities()->attach(rand(1, 3)); // university_id 1〜3
            $user->faculties()->attach(rand(1, 4)); // faculty_id 1〜4
            $user->labs()->attach(rand(1, 5)); // lab_id 1〜5
        });
    }
}

できたら、以下のコマンドを実行しましょう!!

実行コマンド

/var/www
$ php artisan migrate:fresh --seed

実行出来たら、VS CodeのDatabase Clientで確認していきましょう!

大学
image.png

学部
image.png

研究室
image.png

ユーザー
image.png

よっしゃ~~~!!
ちゃんとデータが入っていますね!(≧◇≦)
ユーザーのデータはダミーデータが入っています。

中間テーブルも確認しましたが、そちらにもどうやらデータが入っているみたいです。

ちなみに、このコマンドは、テーブルをすべて消して、もう一回マイグレーションをやり直すので、本番環境では実行しないように注意しましょう。

これにて、モデル作成及びシーディングは完了です!

コントローラーを作成する

次に、コントローラーを作成します!
コントローラーの役割は、モデルからデータを取得して、ビューに渡すことでした。

以下のコマンドで作成します。
研究室用のコントローラーを作成します。

実行コマンド

/var/www
$ php artisan make:controller LabController

作成されたファイルを以下のように編集しましょう!

\app\Http\Controllers\LabController.php
<?php

namespace App\Http\Controllers;

use App\Models\Lab;
use Illuminate\Http\Request;

class LabController extends Controller
{
    public function index()
    {
        $labs = Lab::all();
        return view('labs.index', compact('labs'));
    }
}

Labクラスに対して、::all()というファサードを使うことで、すべてのLabを取得できます!
このLabクラスは、先ほど作ったLabのモデルクラスなので、

use App\Models\Lab;

のようにuse宣言をしましょう。

なお、VS Codeの機能で、Laくらいまで入力すると候補が出てくるので、その時点で「Tab」キーを押すと...
image.png
image.png
5行目のところに、use宣言が自動で追加されます(便利!)。

そして、returnの部分には、view()関数を使うことで、「labs.indexというビューにlabsという名前の変数として、$labsの中身が入った配列を渡しますよ」という意味になります。

ただし、今回の開発では、ただのビューではなく、Reactにデータを渡したいのです!
そのため、return文を書き換えます。

\app\Http\Controllers\LabController.php
<?php

namespace App\Http\Controllers;

use App\Models\Lab;
use Illuminate\Http\Request;
use Inertia\Inertia;

class LabController extends Controller
{
    public function index()
    {
        $labs = Lab::all();
        return Inertia::render('Lab/Index', [
            'labs' => $labs,
        ]);
    }
}

どうやって、Reactにデータを渡せばよいのか...!?
ここで満を持して登場するのがInertiaでございます!

Inertiaクラスをuse宣言して、::render()というファサードを利用することで、簡単にデータを渡すことができます!!!!

LabというディレクトリにあるIndex.jsxというReactのコンポーネントに対して、phpの変数$labsのデータが、labsという名前のpropsとして渡されるという意味ですね!

Reactに慣れていない方は、もしかしたら何を言っているのかよくわからないかもしれませんが、とりあえず、気にせずいったん先に進んでみましょう!

ビューを作成する

実は、まだ受取先のReactコンポーネントのファイルを作成していないので、作りましょう。
コマンドでもいいですが、今回はVS Codeのエクスプローラーでマウスでクリックして作ってみます。

「resources」というフォルダをクリックして開きます。
image.png

その中の「js」というフォルダを開くと、さらにその中に「Pages」というフォルダがあるので、そこで右クリックして新規フォルダを作成します。
フォルダ名は、Labとしてください。
image.png

作成出来たら、その中にIndex.jsxという名前のファイルを作成しましょう!
image.png

作成できましたか?
そうしたら、以下のように書きます!

import React from 'react';
import { Head } from '@inertiajs/react';

export default function Index({ labs }) {
  return (
    <div>
      <h1>研究室一覧</h1>

      <div>
        {labs.map((lab) => (
          <div
            key={lab.id}
          >
            <p>{lab.name}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

中身は、今のところは理解しなくてもOKです!
とりあえず、コピペしてみてください。(^_-)-☆

ルーティングを設定する

最後にルーティングというものを設定する必要があります。

\routes\web.php
<?php

use App\Http\Controllers\LabController; // 追加
use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::get('/labs', [LabController::class, 'index'])->name('labs.index'); // 追加

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__.'/auth.php';

ルーティングについては、次の「4. レビュー投稿機能作成」のところで合わせて解説しますので、少々お待ちください!

動作確認する

ファイルをそれぞれ保存したら、いよいよ動作確認です。

実行コマンド

/var/www
$ npm run dev

これで、開発用のサーバーが立ち上がります。

さて、http://localhost/labsにアクセスしてみましょう...!!!!
ワクワク♪o(・ω・o)(o・ω・)oワクワク♪

image.png
...できた...!!

いや~、どうにかできましたね。
良かったです。正直、ホッとしています。(>_<)

ちなみに、かなり今更ですが、Ubuntuでは複数のウインドウを開くことで同時に複数のコマンドを実行することができます。
例えば、一つのウインドウでnpm run devを実行しながら、別のウインドウでartisanコマンドを実行するなんてこともできます。

また、VS Code画面左上の「表示」から「ターミナル」をクリックすると、VS Code上でUbuntuを操作できるようになるので、コマンドを実行するたびにいちいちUbuntuのコマンドライン画面に切り替えなくて済みます。
良ければ、活用してみてください!
image.png
image.png

4. レビュー投稿機能作成

では、とうとうレビュー投稿機能を作成します!!!!
大変お待たせいたしました。
ここからが今日の目玉です!

レビュー作成画面を作成する

レビューを投稿するには、何が必要でしょうか?
投稿するレビューを作成する画面があってほしいですよね!

レビューコントローラーの作成

まずは、ReviewControllerを作成・編集して、レビューを作成する画面を呼び出す処理を書いてみましょう!

実行コマンド

/var/www
$ php artisan make:controller ReviewController
\project-root\src\app\Http\Controllers\ReviewController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;

class ReviewController extends Controller
{
    // 追加
    public function create() {
        return Inertia::render('Review/Create');
    }
}

namespace App\Http\Controllers;でこのクラスをApp\Http\Controllersという名前空間上に定義します。

useで、どの名前空間のクラスを使うかが宣言されています。

class ReviewController extends Controller;では、ReviewControllerがControllerクラスを継承していることを表しています。

このReviewControllerクラス内にメソッドを書いていきます!

そして、新規作成する場合は、文字通りcreateという言葉をよく使います。
したがって、create()という名前のメソッドを追加します!

このcreate()メソッドが呼び出されると、InertiaがReviewsというディレクトリ内にあるCreate.jsxというページをレンダリング(描画)するという感じです。

ちなみに、Reviews/Createというのは省略された書き方で、本来のファイルパスは、....\resources\js\Pages\Review\Create.jsxですが、Inertiaを使うと、最初の....\resources\js\Pages\までの部分と、最後の拡張子の.jsxの部分は省略できます。
Laravelが自動でフルパスを認識してくれます。
こういった部分もフレームワークを使う利点ですね。

Review/Createコンポーネントの作成

しかし、まだ、Review/Createは作成していません。
VS Codeで、先ほどLab/Indexを作ったときと同様にVS Code画面左のエクスプローラーからディレクトリ及びファイル作成しましょう!
拡張子は、.jsxです!

作成出来たら、分かりやすいように文字が表示されるようにします。

\resources\js\Pages\Reviews\Create.jsx
import React from 'react';
import { Head } from '@inertiajs/react';

export default function Create() {
  return (
    <div>
      <h1>レビュー作成画面</h1>
    </div>
  );
}

ルーティングの設定

出来ましたら、最後にルーティングを設定します。
公式ドキュメントの説明だと難しいと思いますので、私がGeminiに初心者向けの解説を依頼しましたのでどうぞ。

ルーティングとは、Webサイトへのアクセス(リクエスト)があった際に、「どのURLでアクセスされたか」に応じて、「どの処理を実行するか」を決定する仕組みのことです。
例えるなら、Webサイトの交通整理のようなものです。

例えば、さっき作った

\routes\web.php
Route::get('/labs', [LabController::class, 'index'])->name('labs.index');

これは、「URLでいうと最後の部分の/labsというところにアクセスが来たら、LabControllerのindexメソッドを呼び出すよ」という設定なわけです。

他にも、これからたくさんのページを作っていきますが、それらをURLで分けていくときに、「このURLにアクセスが来たら、このページを表示する」というように決めていなかないといけません。
そういったことを管理するのがこのweb.phpというわけです!

name()というのは、ルーティングに名前を付けることができるメソッドです。

細かい話なので、飛ばしてもらっても大丈夫ですが、::get()ファサードの第二引数に渡す配列の一つ目の要素には完全修飾形式のクラス名の文字列が必要です。
したがって、use宣言を使わずに、以下のように書いても問題ありませんが、長ったらしくて読みづらいですので、::classを使って完全修飾形式のクラス名の文字列を取得するやり方が推奨されているようです。
その場合は、use宣言をお忘れなくです。

Route::get('/labs', ['App\Http\Controllers\LabController', 'index'])->name('labs.index');

さらに細かい話ですが、Laravelのルーティングでは、この書き方だけで、自動的に内部でインスタンス化を実行するらしいので、LabControllerクラスをインスタンス化する処理を書かなくてもそのメソッドが使えるということらしいです。

では、LabControllerのcreateメソッドを呼び出すためのルーティングを設定しましょう!
先ほどのindexのルーティングの下にでも追加してみましょう。

\routes\web.php
Route::get('/labs', [LabController::class, 'index'])->name('labs.index');

// 追加
Route::get('/reviews/create', [ReviewController::class, 'create'])->name('review.create');

これにより、/reviews/createというURLにアクセスが来た時に、ReviewControllerクラスのcreateメソッドが発動します。
では、http://localhost/reviews/create
を開いて見ましょう。以下のコマンドを忘れずに!

$ npm run dev

image.png
表示できました!
おめでとうございます!(^_-)-☆

レビュー投稿処理を作成する

レビュー作成画面の編集

現状だと、「レビュー作成画面」という文字が表示されているだけで、レビューは投稿できません。
さっそく、レビューを投稿できるようにしていきます。

モデルを作成する

まずは、投稿されたレビューの内容をデータベースに保存するためにモデルを作成しましょう!
実行コマンド

/var/www
$ php artisan make:model Review
\project-root\src\app\Models\Review.php
<?php

namespace App\Models;

use Illuminate\Container\Attributes\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth as FacadesAuth;

class Review extends Model
{
    protected $fillable = [
        'user_id',
        'lab_id',
        'mentorship_style',
        'lab_atmosphere',
        'achievement_activity',
        'constraint_level',
        'facility_quality',
        'work_style',
        'student_balance',
    ];

    // リレーションシップの定義
    // ユーザーとのリレーション(多対一)
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // 研究室とのリレーション(多対一)
    public function lab()
    {
        return $this->belongsTo(Lab::class);
    }
}

リレーションの定義なども書いています。

コンポーネントを編集する

次に、今はReviews/Create.jsxは「レビュー作成画面」という文字が表示されているだけの状態ですので、レビューを実際に投稿できるように編集していきましょう!

\project-root\src\resources\js\Pages\Reviews\Create.jsx
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';

export default function Create({ lab }) {
    const [data, setData] = useState({
        lab_id: lab.id,
        mentorship_style: 3,
        lab_atmosphere: 3,
        achievement_activity: 3,
        constraint_level: 3,
        facility_quality: 3,
        work_style: 3,
        student_balance: 3,
    });

    const [errors, setErrors] = useState({});
    const [processing, setProcessing] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        setProcessing(true);

        router.post(`/labs/${data.lab_id}/reviews`, data, {
            onSuccess: () => {
                // 成功時の処理
            },
            onError: (errors) => {
                setErrors(errors);
                setProcessing(false);
            },
            onFinish: () => {
                setProcessing(false);
            }
        });
    };

    const handleChange = (field, value) => {
        setData(prev => ({
            ...prev,
            [field]: value
        }));

        if (errors[field]) {
            setErrors(prev => ({
                ...prev,
                [field]: undefined
            }));
        }
    };

    return (
        <div>
            <Head title={`${lab.name}のレビュー作成`} />
            <h1>{lab.name} のレビュー作成画面</h1>

            <form onSubmit={handleSubmit}>
                {/* 指導スタイル */}
                <div>
                    <label>指導スタイル: {data.mentorship_style}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.mentorship_style}
                        onChange={(e) => handleChange('mentorship_style', parseInt(e.target.value))}
                    />
                    {errors.mentorship_style && <div>{errors.mentorship_style}</div>}
                </div>

                {/* 雰囲気・文化 */}
                <div>
                    <label>雰囲気・文化: {data.lab_atmosphere}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.lab_atmosphere}
                        onChange={(e) => handleChange('lab_atmosphere', parseInt(e.target.value))}
                    />
                    {errors.lab_atmosphere && <div>{errors.lab_atmosphere}</div>}
                </div>

                {/* 成果・活動 */}
                <div>
                    <label>成果・活動: {data.achievement_activity}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.achievement_activity}
                        onChange={(e) => handleChange('achievement_activity', parseInt(e.target.value))}
                    />
                    {errors.achievement_activity && <div>{errors.achievement_activity}</div>}
                </div>

                {/* 拘束度 */}
                <div>
                    <label>拘束度: {data.constraint_level}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.constraint_level}
                        onChange={(e) => handleChange('constraint_level', parseInt(e.target.value))}
                    />
                    {errors.constraint_level && <div>{errors.constraint_level}</div>}
                </div>

                {/* 設備 */}
                <div>
                    <label>設備: {data.facility_quality}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.facility_quality}
                        onChange={(e) => handleChange('facility_quality', parseInt(e.target.value))}
                    />
                    {errors.facility_quality && <div>{errors.facility_quality}</div>}
                </div>

                {/* 働き方 */}
                <div>
                    <label>働き方: {data.work_style}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.work_style}
                        onChange={(e) => handleChange('work_style', parseInt(e.target.value))}
                    />
                    {errors.work_style && <div>{errors.work_style}</div>}
                </div>

                {/* 人数バランス */}
                <div>
                    <label>人数バランス: {data.student_balance}</label>
                    <input
                        type="range"
                        min="1"
                        max="5"
                        value={data.student_balance}
                        onChange={(e) => handleChange('student_balance', parseInt(e.target.value))}
                    />
                    {errors.student_balance && <div>{errors.student_balance}</div>}
                </div>

                {/* 送信ボタン */}
                <div>
                    <button type="submit" disabled={processing}>
                        {processing ? '投稿中...' : 'レビューを投稿'}
                    </button>
                </div>
            </form>
        </div>
    );
}

コントローラーを編集する

続いて、コントローラーを修正します。

今、レビュー作成ページを表示するためのcreate()メソッドが定義されています。

しかし、レビューの投稿は、本来研究室ごとに行いたいはずです。
よって、研究室ごとにレビュー作成ページを作るべきです。
したがって、Create.jsxコンポーネントにどの研究室のデータなのかと言う情報を渡してあげる必要がありますので、そのようにcreate()メソッドを修正します。

さらに、この下に、実際にユーザーが送信してきたデータを保存するための処理を呼び出すメソッドを追加で定義しましょう!

\project-root\src\app\Http\Controllers\ReviewController.php
    public function create(Lab $lab) {
        return Inertia::render('Review/Create', ['lab' => $lab]);
    }

    public function store(Request $request, Lab $lab) {
        // バリデーション
        $validated = $request->validate([
            'mentorship_style' => 'required|integer|min:1|max:5',
            'lab_atmosphere' => 'required|integer|min:1|max:5',
            'achievement_activity' => 'required|integer|min:1|max:5',
            'constraint_level' => 'required|integer|min:1|max:5',
            'facility_quality' => 'required|integer|min:1|max:5',
            'work_style' => 'required|integer|min:1|max:5',
            'student_balance' => 'required|integer|min:1|max:5',
        ]);

        // バリデーション済みのデータを保存
        $review = new Review();
        $review->user_id = Auth::id();
        $review->lab_id = $lab->id;
        $review->mentorship_style = $validated['mentorship_style'];
        $review->lab_atmosphere = $validated['lab_atmosphere'];
        $review->achievement_activity = $validated['achievement_activity'];
        $review->constraint_level = $validated['constraint_level'];
        $review->facility_quality = $validated['facility_quality'];
        $review->work_style = $validated['work_style'];
        $review->student_balance = $validated['student_balance'];
        $review->save();

        return redirect()->route('labs.index')->with('success', 'レビューが保存されました。');
    }    

create()メソッド内で、

return Inertia::render('Review/Create', ['lab' => $lab]);

のように書くことでどの研究室なのかという情報$labをCreate.jsxコンポーネント側でlabという変数名で使えるように配列の形で渡すことができます!

...ん?そもそも今コンポーネントに渡したい$labってどこから出てきた??

という疑問があると思います。
それついては、次の「ルーティングを作成する」のところで解説しますので、いったんおいておきましょう。

次にsotre()メソッドですが、これはユーザーが送信してきたレビューのデータを$requestとして、以下のように入力必須でかつ整数でかつ最小値が1でかつ最大値が5になるようにバリデーションして、その結果を$validatedという変数に配列として代入します。

$validated = $request->validate([
            'mentorship_style' => 'required|integer|min:1|max:5',
            'lab_atmosphere' => 'required|integer|min:1|max:5',
            'achievement_activity' => 'required|integer|min:1|max:5',
            'constraint_level' => 'required|integer|min:1|max:5',
            'facility_quality' => 'required|integer|min:1|max:5',
            'work_style' => 'required|integer|min:1|max:5',
            'student_balance' => 'required|integer|min:1|max:5',
        ]);

そして、その$validatedをレビューモデルのプロパティに一つずつ代入してsave()メソッドで保存します。
レビューモデルクラスをnewでインスタンス化することで、レビューモデルのメソッド(正確にはレビューモデルクラスの継承元のモデルクラスのメソッドなので、Review.phpを見ても、それらに関する記述はありません)を使えるようになります。

以下のように->を使うとそのクラスのプロパティを取ってこられるのでしたね!
一方で、連想配列においては、キーを[ ]内に書くことで値を取ってこられるので注意ですね!

        $review = new Review(); // レビューモデルクラスをインスタンス化
        $review->user_id = Auth::id(); // 現在ログインしているユーザーのIDはAuth::id()で取得
        $review->lab_id = $lab->id; // 研究室のIDは引数から取得(詳しくは次のセクションで解説)
        $review->mentorship_style = $validated['mentorship_style']; // 以下は、インスタンス化された$reviewの各プロパティに、$validatedという連想配列の値をそれぞれ取得して代入します
        $review->lab_atmosphere = $validated['lab_atmosphere'];
        $review->achievement_activity = $validated['achievement_activity'];
        $review->constraint_level = $validated['constraint_level'];
        $review->facility_quality = $validated['facility_quality'];
        $review->work_style = $validated['work_style'];
        $review->student_balance = $validated['student_balance'];
        $review->save(); // レビューをデータベースに保存

最後の部分は、リダイレクトと呼ばれる処理です。

return redirect()->route('labs.index')->with('success', 'レビューが保存されました。');

レビューの投稿が完了したら、そのままの画面ですと、イマイチ「投稿した感」が得られないですよね。
そこで、レビュー投稿後に、例えば研究室一覧ページに遷移することで、自分が投稿した結果が反映されていることをユーザーがすぐに確認できるというわけです。

ルーティングを作成する

最後は、web.phpに新しいルーティングを追加します。

その前に、先ほど作った以下の部分のルーティングについて修正を考えてみます。

Route::get('/reviews/create', [ReviewController::class, 'create'])->name('review.create');

今の状態ですと、ログインしていないユーザーでもこのページに到達できてしまいます。
本来、レビューを投稿するには、「どのユーザーが投稿するのか」という情報が必要であるため、ログインしていないユーザーやそもそも登録していないユーザーからは投稿できないようにしたいはずです。
なので、レビュー作成ページもログインしていないユーザーは見られないようにしたいです。

では、どうすればログインしているユーザーだけが作成ページにアクセスできるようにできるのか?
実はとても簡単です!

先ほどの一行をいったん削除してもらって、以下のようにmiddleware('auth')の中に一行追加してもらえればOKです!

\project-root\src\routes\web.php
Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
    Route::get(('/labs/{lab}/reviews/create'), [ReviewController::class, 'create'])->name('review.create'); // 追加
});

ミドルウェアというのは、簡単にいうと、よく使ういろいろな処理が事前にひとまとまりにされているもので、我々開発者はそれを使うことで一からそれらの処理を実装しなくても済むという感じです。

このauthというミドルウェアは、認証機能実装時にbreezeをインストールしたことで使えるようになりました!

よって、breezeさえインストールしていれば、ログイン中のユーザーだけに許したいルーティングは、このミドルウェア内に書けばOKっていうことです!(≧◇≦)

なので、先ほどの一行をauthの中に入れてあげればよいです。
ただし、URLの部分だけ少し修正しています。
先ほどコントローラー作成の時に「詳しくはルーティング作成の時に解説します」と言っていた話です。

レビューを投稿するには、「どの研究室に対して?」という情報が必要なので、それをコントローラーがReactコンポーネントに渡していましたが、そもそもコントローラーのcreate()メソッドの引数の$labはどうやって渡すのか?という疑問にここでお答えします。

\project-root\src\app\Http\Controllers\ReviewController.php
public function create(Lab $lab) { // ←この引数の$labってどうやって渡すの?
        return Inertia::render('Review/Create', ['lab' => $lab]);
    }

ズバリ、答えはルーティングのURL内にパラメータを持たせることで渡します!

/labs/{lab}/reviews/create

ここに渡されたID(例:1)をもとに、Laravel は自動的にLabモデルからそのIDのデータをデータベースから検索し、$labに代入してくれます。
この仕組みは「ルートモデルバインディング」と呼ばれます。

例えば、/labs/1/reviews/createというURLにアクセスすると、labモデルのidが1の情報が$labという変数でコントローラーのcreate()メソッドの仮引数に代入されます。

そしたら、ReviewControllerのstore()メソッドを呼び出すために、その下にさらに一行追加します。

\project-root\src\routes\web.php
Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
    Route::get(('/labs/{lab}/reviews/create'), [ReviewController::class, 'create'])->name('review.create');
    Route::post(('/labs/{lab}/reviews'), [ReviewController::class, 'store'])->name('review.store'); // 追加
});

getとかpostとかの説明をしてなかったので、分かりにくいかもしれませんが、とりあえずこれで、レビュー投稿時に/labs/{lab}/reviewsにアクセスが来るので、アクセスが来たらReviewControllerのstore()が呼ばれるように設定できました!
リクエストについての説明は、次回以降出来ればします。

動作確認をする

モデル、コントローラー、Reactコンポーネント、ルーティングと必要なものがそろったので動作確認をしてみましょう!

まず、研究室作成ページにアクセスしてみましょう。
今日は、研究室のIDが1の研究室に対してレビューを投稿したいので、以下のURLにアクセスしましょう!
http://localhost/labs/1/reviews/create'
image.png

あれれ...??
/loginにリダイレクトされてしまいました。
なぜでしょうか?

そうです!
ログインしていない状態で、auth内のルーティングに設定されたURLにアクセスしても、breezeをインストールしているので、見れらません。
しかも、「ログインしなさい!」と言わんばかりにログインページに自動的に遷移させられました。
ミドルウェアは便利ですね!

では、ログインしてみましょう!
VS CodeのDatabase Clientから登録されているアドレスとパスワードを入力します。
パスワードはpasswordでOKです。
スクリーンショット 2025-06-01 164644.png
スクリーンショット 2025-06-01 164712.png
image.png
今度は、見ることができました!!

では、投稿できるか確認します...!!
一番下の「レビューを投稿」をクリックしてみます...!!!!
ドキドキ......!!!!!!??????

image.png
期待通り、研究室一覧ページにリダイレクトされました!

でも、ここにはまだ投稿結果を表示するようにはしていないので、データベースの方を見て本当に投稿されているかを確認します!

Database Clientでdevelopmentをリフレッシュして、reviewsを開いて見ると...
image.png

データが入ってる...!!!!
よっしゃ~~~!!!!(≧◇≦)
大成功!◎
おめでとうございます!

5. まとめ・次回予告

ここまで読んでくださって本当にありがとうございます!!

とんでもなく長い記事になってしまいましたね。(笑)
前編と後編に分けてもこのボリュームですw

今回は、モデルやコントローラー、Reactコンポーネントやルーティング、さらにはマイグレーションやシーダー、ファクトリと新しいことをたくさん学び、しかもいろいろなファイルを行ったり来たりしてだいぶ頭が混乱してしまったかもしれません💦

僕もLaravelを勉強し始めたばかりのころは、ファイルが多すぎてどのファイルがどの役割をしているのかが分からなくなり、頭の中がぐちゃぐちゃになって大変でした。(泣)

そんな方にまず意識してほしいことは、MVCとは何なのかを常に意識することです!
忘れたら、すぐ戻って思い出しましょう。
それぞれを擬人化してイメージするのもよいでしょう!

最初からすべてを完璧に理解できる人なんていません!
仮にあなたの周りで理解力がある風の人がいたとしても、それは、その人も陰で分からないところを調べて文章の同じ個所を何度も読んだり、動画教材などの同じ部分を何度も見返したりして必死に理解しようとした結果の産物だと思います。
理解できないところがあっても全く焦る必要はないということです。

また、初心者のうちはすべてを完璧に理解して覚えようとすると逆に効率が悪いこともあります。
適度に手を抜いて、「ここは難しそうだから後で理解すればいいや」と割り切る心も重要です。
私も記事内で、「難しいので初心者の方は飛ばしてもらってもよい」という部分を極力お伝えしているつもりです。

とにかく、焦らず一つずつ着実に進めていきましょう!
仮に一見前に進んでいないように見える日が続いても諦めさえしなければ、きっと大丈夫です!

なんか最終回っぽい雰囲気になりましたが、全然まだまだ続きますのでよろしくお願いいたします!(笑)

あと、コミットやプッシュはとりあえずしなくて大丈夫です。

では、また次回お会いしましょう!!
ありがとうございました。

これまでの記事一覧

--- 要件定義・設計編 ---
その1: 要件定義・設計編

--- 環境構築編 ---
その2: 環境構築編① ~WSL, Ubuntuインストール~
その3: 環境構築編② ~Docker Desktopインストール~
その4: 環境構築編③ ~Dockerコンテナ立ち上げ~
その5: 環境構築編④ ~Laravelインストール~
その6: 環境構築編⑤ ~Gitリポジトリ接続~

--- バックエンド実装編 ---
その7: バックエンド実装編① ~認証機能作成~
その8: バックエンド実装編②前編 ~レビュー投稿機能作成~

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?