実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その41)
0. 初めに
見習いエンジニアの僕がWebアプリケーションの作り方をゼロから解説しているシリーズです。
フロントエンド編も終わりが見えてきました...!!
今日は、ファビコンを作りたいと思います。
1. ファビコン作成
ファビコンは、ブラウザの画面上のタブの部分に表示されるアイコンことです。
1.1 ブランチ運用
いつも通り、developブランチを最新化させて新規ブランチを切って作業します。
ブランチ名は、feature/frontend/faviconとかにします。
1.2 ファビコン設定
以下のファイルをダウンロードしてpublic/に設置してください。
もともとあった、favicon.icoは削除してください。
そうしたら、resources/views/app.blade.phpに以下を追加してください。
クリックでコードを見る
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Fonts -->
<!-- 追加: favicon -->
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<!-- Scripts -->
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>
1.3 動作確認
問題なさそうなら、コミット・プッシュ、PR作成・マージをしておきましょう。
2. (おまけその11)不具合修正: ログアウト時に作成・編集・削除依頼用ポップオーバーが表示されてしまう
これで終わり!
...だったら嬉しいのですが、おまけコーナーにて現在起きている不具合を直していきますw
今の課題として、ご覧の通り、ログアウト状態でも大学・学部・研究室・レビューの作成(編集と削除依頼も)用のモーダルを開くことができてしまいます。


解決策はいくつかあるのですが、ログアウト状態の時は、そもそもポップオーバーメニューにそれらを表示させないようにすることにします。
2.1 ブランチ運用
ということで、developブランチを最新化させて、新規ブランチを切って作業します。
不具合の修正という意味合いで、fix/frontend/popoverとかでいきましょう。
2.2 メニューポップオーバー修正
resources/js/Components/Common/MenuPopover.jsx
クリックでコードを見る
import { usePage } from '@inertiajs/react'; // 追加
const MenuPopover = ({ addLabel, onAddClick, onEditClick, onViewHistoryClick, onDeletionRequestClick }) => {
const user = usePage().props.auth.user;
const menuItems = [
...(addLabel && user ? [{ label: addLabel, onClick: onAddClick }] : []),
...(user ? [{ label: '編集する', onClick: onEditClick }] : []),
{ label: '編集履歴を見る', onClick: onViewHistoryClick },
...(user ? [{ label: '削除依頼をする', onClick: onDeletionRequestClick }] : []),
];
2.3 動作確認
ログアウト時に「編集履歴を見る」の一つだけが表示されていればOK!

できたら、コミット・プッシュ、PR作成・マージを忘れずに行いましょう!
3. (おまけその12)バリューアップ: ヘッダー固定
おまけコーナーは続きます。
続いては、ヘッダーの固定です。
これまでは、スクロールをするとヘッダーが見えなくなってしまっていました。

バグというわけではないのですが、より使いやすいように修正することをバリューアップと呼んだりします。
ヘッダーが常に触れる状態にすれば、ホームページに戻れたり、サイドバーを開いたりするのが容易になるので、ユーザーにとってのUXの向上につながるでしょう!
3.1 ブランチ運用
developブランチを最新化させて、feature/frontend/fix-headerという新規ブランチを切って作業します。
3.2 ヘッダーコンポーネント修正
resources/js/Components/Header.jsx
クリックでコードを見る
<header
className="
fixed
top-0
left-0
w-full
z-50
3.3 共通レイアウトコンポーネント修正
resources/js/Layouts/AppLayout.jsx
クリックでコードを見る
{/* メインコンテンツ領域 */}
<main className={`flex-1 bg-transparent flex${isHome ? '' : ' pt-14'}`}>
ヘッダーと同じ高さのパディングを上に付けることで、常にヘッダーが表示されます(ホームページモードではヘッダーを表示しないのでそのままにします)。
3.4 動作確認
問題なければ、コミット・プッシュ、PR作成・マージをしておきましょう。
4. (おまけその13)バリューアップ: プログレスバー色変更
4.1 ブランチ運用
developブランチを最新化させて、feature/frontend/progress-bar-colorという新規ブランチを切って作業します。
4.2 プログレスバー色変更
画面のロード中に表示される画面上部のバーの色を、現在の黒から、青い色に変更します(その方がアプリのイメージに合うと思ったから)。
こちらもバリューアップ対応です。
resources/js/app.jsx
クリックでコードを見る
progress: {
color: '#297FF0',
},
できたら、コミット・プッシュ、PR作成・マージをお忘れなく!
5.(おまけその14)バリューアップ: レビュー未投稿時のボタン追加
現在、ログイン状態でレビューを投稿していないと「まだ、レビューを投稿していません。」と表示されます。
これをクリックすると作成モーダルが開くようにすると、初めての方にとって使いやすくなるでしょう。
また、ログアウト状態ではそもそもこの文言を表示させないようにします。

5.1 ブランチ運用
developブランチを最新化させて、新規ブランチfeature/frontend/no-review-postedを切って作業をします。
5.2 レビュー未投稿時モーダル開閉可能
resources/js/Pages/Lab/Show.jsx
クリックでコードを見る
{/* レビュー投稿状態 + ケバブメニュー 右寄せ */}
<div className="flex items-center gap-2">
{auth?.user && userReview ? (
<button
type="button"
onClick={() => setIsReviewEditModalOpen(true)}
className="text-[#747D8C] hover:underline cursor-pointer"
>
レビューを投稿済みです。
</button>
) : auth?.user ? (
<button
type="button"
onClick={() => setIsCreateModalOpen(true)}
className="text-[#747D8C] hover:underline cursor-pointer"
>
まだ、レビューを投稿していません。
</button>
) : (
<p className="text-[#747D8C]">まだ、レビューを投稿していません。</p>
)}

ログイン状態で、「まだ、レビューを投稿していません。」をクリックするとレビューが作成用のモーダルが開くことが確認できました!
問題なければ、コミットしましょう。
2.3 ログアウト時非表示
次に、ログアウト時には非表示にするようにしましょう。
resources/js/Pages/Lab/Show.jsx
クリックでコードを見る
{/* レビュー投稿状態 + ケバブメニュー 右寄せ */}
<div className="flex items-center gap-2">
{auth?.user && userReview ? (
<button
type="button"
onClick={() => setIsReviewEditModalOpen(true)}
className="text-[#747D8C] hover:underline cursor-pointer"
>
レビューを投稿済みです。
</button>
) : auth?.user ? (
<button
type="button"
onClick={() => setIsCreateModalOpen(true)}
className="text-[#747D8C] hover:underline cursor-pointer"
>
まだ、レビューを投稿していません。
</button>
) : null}
できたら、コミット・プッシュ、PR作成・マージをお忘れなく。
6.(おまけその15)修正: ヘッダーのオーバーレイ
気が付いたかもしれませんが、先ほどヘッダーを修正したせいでサイドバーやモーダルを開いたときに他の部分と同様な「暗くなる」「クリックできない」というしようが適用されなくなってしまいました。(笑)
直しましょう。
6.1 ブランチ運用
最新化されたdevelopからfix/frontend/header-overlayを切ります。
z-indexとは
原因は、z-indexがおかしいことです。
z-indexとは、いわば、x軸、y軸に次ぐ3つ目の軸です。
Webの画面は平面ですが、要素を重ねたときにどれを上に(もしくは下に)表示するかを決めるものです。
| コンポーネント | 現在のz-index | 問題 |
|---|---|---|
| Header | z-50 | ‐ |
| Sidebar オーバーレイ(背景暗転) | z-40 | ヘッダーより低いため暗転が届かない |
| Sidebar パネル | z-50 | ヘッダーと同じで重なる |
| Modal オーバーレイ(背景暗転) | なし | z-indexが未指定のためヘッダーの下になる |
| Modal コンテンツ | z-[101] | コンテンツ自体は問題なし |
こうすれば解決!(整頓)
| レイヤー | 修正後のz-index | 対象 |
|---|---|---|
| z-40 | Header / ホームのハンバーガー | 通常コンテンツの上 |
| z-50 | Sidebar・Modalのオーバーレイ(暗転背景) | ヘッダーを覆う |
| z-[60] | Sidebarパネル・Modalコンテンツ | オーバーレイの上 |
コンポーネント修正
resources/js/Layouts/AppLayout.jsx
クリックでコードを見る
{/* ホームモード: ハンバーガーアイコンのみを固定表示(サイドバー非表示時のみ) */}
{isHome ? (
!isSidebarOpen && (
<div className="fixed top-4 right-6 z-40">
<HamburgerMenu onOpenSidebar={() => setSidebarOpen(true)} />
</div>
resources/js/Components/Header.jsx
クリックでコードを見る
z-40
resources/js/Components/Sidebar.jsx
クリックでコードを見る
{/* オーバーレイ */}
<div
onClick={onClose}
className={`
fixed inset-0 bg-black/40 transition-opacity duration-300 z-50
${isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
`}
aria-hidden="true"
/>
{/* 本体 */}
<aside
role="dialog"
aria-modal="true"
className={`
fixed right-0 top-0 h-dvh w-[270px] max-w-[90vw] bg-[#EEF5F9] shadow-2xl z-[60]
transform transition-transform duration-300
${isOpen ? 'translate-x-0' : 'translate-x-full'}
flex flex-col
`}
>
resources/js/Components/Common/Modal.jsx
クリックでコードを見る
{/* 本体 */}
<div
role="dialog"
aria-modal="true"
aria-activedescendant={title ? 'modal-title' : undefined}
className={`
fixed inset-0 z-[60] flex items-center justify-center p-4
transition-all duration-300
${isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
`}
>
<div
className={`
w-full ${sizeClasses[size]}
bg-[#EEF7FB] rounded-lg shadow-xl
transform transition-all duration-300
${isOpen ? 'scale-100 translate-y-0' : 'scale-95 -translate-y-4'}
`}
>
動作確認
コミット・プッシュ、PR作成・マージをしましょう!
7.(おまけその16)リファクタリング: レビューの平均値計算
これも前々からやろうと思っていたことなのですが、ここでやってみたいと思います。というのも、今各評価指標ごとの平均値やレビューの総合値を計算するロジックが、LabController.phpのshow()メソッドやindex()メソッド、MyPageController.phpのshowUser()メソッドに散らばってしまっています。
共通しているこの処理を切り出してみたいと思います
(同じ処理が散らばっていると仕様変更などで修正する必要が出たときに、すべての箇所を修正する必要が出てくるため修正漏れが発生しやすい微妙な状態であるため、一般的に好まれません。また、長すぎるコントローラーはファットコントローラーなどと呼ばれ、一般的に好まれません)。
7.1 ブランチ運用
developブランチを最新化させて、refactor/review-averageブランチを切って作業します。
7.2 Labモデルメソッド追加
具体的には、Labモデルにメソッドを追加して、コントローラー側からはこれを呼び出すようにします。
Labモデルには、Reviewモデルとのリレーションが定義されているため、書きやすいと思います。
app/Models/Lab.php
クリックでコードを見る
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder; // 追加
use Illuminate\Support\Collection; // 追加
class Lab extends Model
{
use SoftDeletes;
// 追加
public const RATING_COLUMNS = [
'mentorship_style',
'lab_atmosphere',
'achievement_activity',
'constraint_level',
'facility_quality',
'work_style',
'student_balance',
];
protected $fillable = [
'name',
'faculty_id',
];
// リレーションの定義...
/**
* 各評価項目のユーザー間の平均値を取得する
* reviews リレーションがロード済みである前提
*/
public function getAveragePerItem(): Collection
{
return collect(self::RATING_COLUMNS)->mapWithKeys(function ($column) {
return [$column => $this->reviews->avg($column)];
});
}
/**
* 全評価項目の総合平均値を取得する
*/
public function getOverallAverage(): ?float
{
return $this->getAveragePerItem()->avg();
}
/**
* 特定レビューの全評価項目の平均値を取得する
*/
public static function getUserReviewAverage(Review $review): ?float
{
return collect(self::RATING_COLUMNS)
->map(fn($column) => $review->$column)
->filter(fn($value) => $value !== null)
->avg();
}
/**
* overall_avg, avg_{col}, reviews_count を動的属性としてセットする
* MyPageController 等でコレクション内の各 Lab に付与する用途
*/
public function appendRatingAverages(): self
{
$averagePerItem = $this->getAveragePerItem();
$this->overall_avg = $averagePerItem->avg();
foreach (self::RATING_COLUMNS as $column) {
$this->{"avg_{$column}"} = $averagePerItem[$column];
}
$this->reviews_count = $this->reviews->count();
return $this;
}
/**
* 一覧表示用: 各評価項目の平均・総合評価・レビュー数をクエリに付与するスコープ
*/
public function scopeWithRatingAverages(Builder $query): Builder
{
$query->select('labs.*')->withCount('reviews');
foreach (self::RATING_COLUMNS as $column) {
$query->withAvg("reviews as avg_{$column}", $column);
}
$avgSum = implode(' + ', array_map(fn($c) => "AVG($c)", self::RATING_COLUMNS));
$count = count(self::RATING_COLUMNS);
$query->addSelect([
'overall_avg' => Review::query()
->selectRaw("($avgSum) / $count")
->whereColumn('reviews.lab_id', 'labs.id'),
]);
return $query;
}
}
7.3 LabControllerで呼び出す
さて、今定義したメソッドたちをコントローラー側で呼び出しましょう。
app/Http/Controllers/LabController.php
クリックでコードを見る
// use App\Models\Review; 不要になったので削除
<?php
namespace App\Http\Controllers;
// use 宣言...
class LabController extends Controller
{
use AuthorizesRequests;
/**
* 研究室の詳細を表示する
*/
public function show(Lab $lab, Request $request): Response
{
// 大学・学部、レビューのデータも一緒に渡す
// universityはfacultyを経由して取得
$lab->load(['faculty.university', 'reviews']);
// コメントデータを取得(投稿者情報も含む)
$comments = $lab->comments()->with('user')->latest()->get();
// 各評価項目の平均値と総合評価を計算
$averagePerItem = $lab->getAveragePerItem();
$overallAverage = $lab->getOverallAverage();
// 現在のユーザーのレビューを取得
$userReview = null;
$userOverallAverage = null;
if (Auth::check()) {
$userReview = $lab->reviews->where('user_id', Auth::id())->first();
if ($userReview) {
$userOverallAverage = Lab::getUserReviewAverage($userReview);
}
}
// ユーザーのブックマーク状態を取得
$userBookmark = $lab->bookmarks()->where('user_id', Auth::id())->first();
$bookmarkCount = $lab->bookmarks()->count();
// 検索クエリを取得
$searchQuery = $request->input('query', '');
// 研究室のデータに加えて、求めたレビューの平均値とユーザーのレビュー、コメント、ブックマーク、認証情報も一緒に渡す
return Inertia::render('Lab/Show', [
'lab' => $lab,
'overallAverage' => $overallAverage,
'averagePerItem' => $averagePerItem,
'userReview' => $userReview,
'userOverallAverage' => $userOverallAverage,
'userBookmark' => $userBookmark,
'bookmarkCount' => $bookmarkCount,
'query' => $searchQuery,
'ratingData' => [
'columns' => Lab::RATING_COLUMNS,
],
'comments' => $comments,
'auth' => [
'user' => Auth::user(),
],
]);
}
store()...
/**
* 研究室の一覧を表示する
*/
public function index(Faculty $faculty, Request $request): Response
{
// UIからソート条件を取得
$sort = $request->query('sort', 'overall');
$query = $faculty->labs()->withRatingAverages();
// ソートマップを生成
$sortMap = ['overall' => 'overall_avg', 'reviews_count' => 'reviews_count'];
foreach (Lab::RATING_COLUMNS as $column) {
$sortMap[$column] = "avg_{$column}";
}
$sortColumn = $sortMap[$sort] ?? 'overall_avg';
// 検索クエリを取得
$searchQuery = $request->input('query', '');
$labs = $query
->orderByRaw("$sortColumn IS NULL")
->orderByDesc($sortColumn)
->paginate(10)
->withQueryString();
// 各ラボにランク(順位)を追加
$labs->getCollection()->transform(function ($lab, $index) use ($labs) {
$lab->rank = ($labs->currentPage() - 1) * $labs->perPage() + $index + 1;
return $lab;
});
return Inertia::render('Lab/Index', [
'labs' => $labs,
'faculty' => $faculty->load('university'),
'sort' => $sort,
'query' => $searchQuery,
]);
}
// update()...
/**
* 研究室の編集履歴を表示する
*/
public function history(Lab $lab): Response
{
$query = request('query', '');
$editHistory = $lab->users()
->withPivot('comment', 'created_at', 'updated_at')
->get()
->sortByDesc(fn($user) => $user->pivot->updated_at)
->values()
->map(function ($user) {
return [
'user' => $user->name,
'comment' => $user->pivot->comment,
'created_at' => $user->pivot->created_at,
'updated_at' => $user->pivot->updated_at,
];
});
return Inertia::render('Lab/History', [
'lab' => $lab,
'editHistory' => $editHistory,
'query' => $query,
]);
}
}
一つずつ見ていきましょう。
まず、show()メソッド内で各評価項目の平均値と総合評価の計算が長ったらしく書かれていた部分は以下のように2行であっさり書くことができるようになりました。
// 各評価項目の平均値と総合評価を計算
$averagePerItem = $lab->getAveragePerItem();
$overallAverage = $lab->getOverallAverage();
これは、これまで書いてきたものをモデルに切り出したからでした。
/**
* 各評価項目のユーザー間の平均値を取得する
* reviews リレーションがロード済みである前提
*/
public function getAveragePerItem(): Collection
{
return collect(self::RATING_COLUMNS)->mapWithKeys(function ($column) {
return [$column => $this->reviews->avg($column)];
});
}
/**
* 全評価項目の総合平均値を取得する
*/
public function getOverallAverage(): ?float
{
return $this->getAveragePerItem()->avg();
}
ちなみに、RATING_COLUMNSは、定数です。
定数は、静的メソッドと同じく::で呼び出すことができ、定義されているクラス内ではself::で呼び出すことができます。
このRATING_COLUMNSも毎回コントローラーで再定義していたものが、ここに集約されるようになりました。
public const RATING_COLUMNS = [
'mentorship_style',
'lab_atmosphere',
'achievement_activity',
'constraint_level',
'facility_quality',
'work_style',
'student_balance',
];
次に、ユーザーごとのレビューの平均値も以下のように1行に収まるようになりました。
if ($userReview) {
$userOverallAverage = Lab::getUserReviewAverage($userReview);
}
これもモデルに関数を切り出したからです。
レビューを引数にしています。
ちなみに、戻り値の型の?は、nullable な型と呼ばれていて、PHPの機能としては比較的新しいものです。
これは、型がnullでもよいことを許す機能です。
レビューが0件の時は、結果はnullになるためです。
public static function getUserReviewAverage(Review $review): ?float
{
return collect(self::RATING_COLUMNS)
->map(fn($column) => $review->$column)
->filter(fn($value) => $value !== null)
->avg();
}
続いて、index()メソッドを見てみましょう。
クエリを作っていた部分は以下のように1行にまとまりました。
$query = $faculty->labs()->withRatingAverages();
これは、モデルの方にスコープと呼ばれるものを定義したからです。
/**
* 一覧表示用: 各評価項目の平均・総合評価・レビュー数をクエリに付与するスコープ
*/
public function scopeWithRatingAverages(Builder $query): Builder
{
$query->select('labs.*')->withCount('reviews');
foreach (self::RATING_COLUMNS as $column) {
$query->withAvg("reviews as avg_{$column}", $column);
}
$avgSum = implode(' + ', array_map(fn($c) => "AVG($c)", self::RATING_COLUMNS));
$count = count(self::RATING_COLUMNS);
$query->addSelect([
'overall_avg' => Review::query()
->selectRaw("($avgSum) / $count")
->whereColumn('reviews.lab_id', 'labs.id'),
]);
return $query;
}
scopeという言葉を先頭に付けることでLaravelのスコープの機能を使うことができます。
スコープとは
今回の例はそこまでわかりやすいとは自分でも思っていないので、Claude AIに他の分かりやすい例を作ってもらいましたので、紹介します。
スコープの利点が分かる具体例:ECサイトの注文管理
ECサイトで「注文(Order)」を管理するシステムを例にします。
スコープなしの場合
コントローラーのあちこちに、似たような where が散らばります。
// 管理画面:未払いの注文一覧
public function unpaidOrders() {
return Order::where('status', 'unpaid')
->where('deleted_at', null)
->orderBy('created_at', 'desc')
->get();
}
// 管理画面:今月の未払い注文
public function unpaidThisMonth() {
return Order::where('status', 'unpaid')
->where('deleted_at', null)
->whereMonth('created_at', now()->month)
->orderBy('created_at', 'desc')
->get();
}
// ユーザー画面:自分の未払い注文
public function myUnpaidOrders($userId) {
return Order::where('status', 'unpaid')
->where('deleted_at', null)
->where('user_id', $userId)
->orderBy('created_at', 'desc')
->get();
}
問題点: 「未払い」の定義(status = 'unpaid')が3箇所に書かれています。もし仕様変更で「未払い」の条件が status IN ('unpaid', 'pending') になったら、全部の箇所を探して直す必要があります。
スコープありの場合
条件の「意味」をモデルに集約します。
// app/Models/Order.php
class Order extends Model
{
// 「未払い」の定義をここ一箇所にまとめる
public function scopeUnpaid($query)
{
return $query->whereIn('status', ['unpaid', 'pending']);
}
// 「新しい順」もスコープにできる
public function scopeLatest($query)
{
return $query->orderBy('created_at', 'desc');
}
// 特定ユーザーの注文
public function scopeForUser($query, $userId)
{
return $query->where('user_id', $userId);
}
}
コントローラーがこんなにスッキリします。
// 管理画面:未払いの注文一覧
public function unpaidOrders() {
return Order::unpaid()->latest()->get();
}
// 管理画面:今月の未払い注文
public function unpaidThisMonth() {
return Order::unpaid()->latest()->whereMonth('created_at', now()->month)->get();
}
// ユーザー画面:自分の未払い注文
public function myUnpaidOrders($userId) {
return Order::unpaid()->forUser($userId)->latest()->get();
}
仕様変更があっても scopeUnpaid を1箇所直すだけで全部に反映されます。
まとめ:スコープの3つの利点
| 利点 | 説明 |
|---|---|
| ① 再利用性 | 同じ条件を何度も書かなくていい |
| ② 保守性 | 条件変更が1箇所で済む(バグを防げる) |
| ③ 可読性 |
->where('status', 'unpaid') より ->unpaid() のほうが意味が伝わる |
特に②が一番重要で、「ビジネスルールをモデルに閉じ込める」という設計の考え方(ファットモデル・スリムコントローラー)につながります。コントローラーはDBの詳細を知らなくていい、という思想です。
...らしいです。(笑)
ということで、今回の場合ですと「各評価項目の平均・総合評価・レビュー数をクエリに付与する」という処理が1行にまとまっています。
あいにく、今のソースコードですと呼び出す箇所は1か所だけですが、他のページでもこれらを表示したい場合が出てきたときに、これを呼び出すだけでOKということになります!
続いて、MyPageControllerの方も見てみましょう。
7.4 MyPageControllerで呼び出す
app/Http/Controllers/MyPageController.php
クリックでコードを見る
/**
* ユーザー情報を表示する
*/
public function showUser(): Response
{
$user = Auth::user();
// 管理者・一般ユーザー問わず通知を取得
$notifications = $user->notifications()->latest()->get();
// ブックマーク済み研究室とそのレビュー情報・総合評価を取得
$bookmarks = $user->bookmarks()->with(['lab.reviews'])->get()->map(function($bookmark) {
$lab = $bookmark->lab;
if (!$lab) return null;
$lab->appendRatingAverages();
$lab->load(['faculty.university']);
return $lab;
})->filter();
// ユーザーが作成した大学・学部・研究室を取得
$universities = University::where('created_by', $user->id)->get();
$faculties = Faculty::where('created_by', $user->id)->with('university')->get();
$createdLabs = Lab::where('created_by', $user->id)->with(['faculty.university', 'reviews'])->get()->map(function ($lab) {
return $lab->appendRatingAverages();
});
return Inertia::render('MyPage/Index', [
'title' => "{$user->name}さんのマイページ",
'user' => $user,
'notifications' => $notifications,
'bookmarks' => $bookmarks,
'universities' => $universities,
'faculties' => $faculties,
'createdLabs' => $createdLabs,
]);
}
ここでは、「ブックマーク済み研究室」と「作成済み研究室」の二つに対してレビューの計算結果を付与する処理をモデルに追加したappendRatingAverages()メソッドを呼び出すことで、それぞれ1行で書くことに成功しています。
/**
* overall_avg, avg_{col}, reviews_count を動的属性としてセットする
* MyPageController 等でコレクション内の各 Lab に付与する用途
*/
public function appendRatingAverages(): self
{
$averagePerItem = $this->getAveragePerItem();
$this->overall_avg = $averagePerItem->avg();
foreach (self::RATING_COLUMNS as $column) {
$this->{"avg_{$column}"} = $averagePerItem[$column];
}
$this->reviews_count = $this->reviews->count();
return $this;
}
これまでに追加してきた他のメソッドと違う点は、Labモデルそのものを返すという点です。
7.5 動作確認
研究室一覧ページ、研究室詳細ページ、マイページのブックマーク済み一覧、作成済み一覧にレビューの平均値が表示されていればOK!!




コミット・プッシュ、PR作成・マージをお忘れなく!
8. まとめ・次回予告
今日は、ファビコンを作りました!
アプリのイメージを決める大切な物でした。
それ以外にもいろいろと修正しましたねw
次回は、いよいよフロントエンド編最終回、レスポンシブデザイン対応です...!!
最後までよろしくお願いします!
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
- その29: フロントエンド実装編⑧前編 ~研究室詳細・レビュー画面作成前編~
- その29.5: フロントエンド実装編⑧後編 ~研究室詳細・レビュー画面作成後編~
- その30: フロントエンド実装編⑨ ~マイページ作成~
- その31: フロントエンド実装編⑩ ~パンくずリスト作成~
- その32: フロントエンド実装編⑪ ~ログイン・新規登録モーダル作成~
- その33: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
- その34: フロントエンド実装編⑬ ~学部作成・編集モーダル作成~
- その35: フロントエンド実装編⑭ ~研究室作成・編集モーダル作成~
- その36: フロントエンド実装編⑮ ~レビュー作成・編集モーダル作成~
- その37: フロントエンド実装編⑯ ~コメント一覧表示モーダル作成~
- その38: フロントエンド実装編⑰ ~編集履歴ページ・削除依頼ページ・通知ドロップダウン作成~
- その39: フロントエンド実装編⑱ ~トースト作成~
- その40: フロントエンド実装編⑲ ~退会ページ作成~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"






