実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その34)
0. 初めに
こんにちは!
見習いエンジニアの僕が一からWebアプリケーションを開発する様子をお届けしているシリーズです。
正直、シリーズが長くなってきて飽き始めていますが、どうにか最後まで走りぬきたいと思います...w
今日は、前回に引き続き、学部の作成・編集用モーダルを作成したいと思います。
ぶっちゃけ、前回のやり方を踏襲すれば、ほとんど同じはずなので、すぐ終わると思います。(笑)
1. 作成モーダル作成
1.1 ブランチ運用
いつものごとく、developブランチを最新にして、新規ブランチを切って作業します。
ブランチ名は、feature/frontend/faculty-create-modalでいきます。
1.2 送信ボタン作成
resources\js\Components\Faculty\FacultySubmitButton.jsx
クリックでコードを表示
/**
* 学部作成・編集ボタンコンポーネント
* @param {Object} props
* @param {string} props.mode - 'create' | 'edit'
* @param {boolean} [props.disabled=false] - 無効状態
* @param {string} [props.type='submit'] - ボタンタイプ
* @returns {JSX.Element} コンポーネントのJSX
*/
const FacultySubmitButton = ({ mode, disabled = false, type = 'submit' }) => {
const label = mode === 'create' ? '作成する' : '編集する';
return (
<button
type={type}
disabled={disabled}
className={`
p-1 bg-[#33E1ED] shadow-md rounded-md
hover:shadow-lg transition-shadow
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<span className="px-14 py-1 block rounded text-white font-bold">
{label}
</span>
</button>
);
};
export default FacultySubmitButton;
1.3 作成モーダル作成
resources/js/Components/Faculty/CreateFacultyModal.jsx
クリックでコードを見る
import { useForm } from '@inertiajs/react';
import Modal from '../Common/Modal';
import FacultySubmitButton from './FacultySubmitButton';
import InputField from '../Common/InputField';
/**
* 学部作成モーダル
* @param {Object} props
* @param {boolean} props.isOpen - モーダルの開閉状態
* @param {Function} props.onClose - モーダルを閉じる
* @param {Object} props.university - 学部が所属する大学オブジェクト
* @returns {JSX.Element} コンポーネントのJSX
*/
const CreateFacultyModal = ({ isOpen, onClose, university }) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="学部を作成する"
size='sm'
>
<div className="h-[300px] flex flex-col">
{/* フォーム領域 */}
<div className="flex-1">
<CreateFacultyForm onClose={onClose} university={university} />
</div>
</div>
</Modal>
);
};
/**
* 学部作成フォーム
* @param {Object} props
* @param {Function} props.onClose - モーダルを閉じる
* @returns {JSX.Element} コンポーネントのJSX
*/
const CreateFacultyForm = ({ onClose, university }) => {
const { data, setData, post, processing, errors, reset } = useForm({
name: '',
});
const submit = e => {
e.preventDefault();
post(route('faculty.store', university.id), {
onSuccess: () => onClose(),
preserveScroll: true,
});
};
return (
<form onSubmit={submit} className="h-full flex flex-col">
<div className="flex-1">
{/* 入力欄 */}
<ErrorSlot message={errors.name} />
<InputField
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
placeholder="学部名(正式名称)"
size="sm"
className="mb-2 w-full"
/>
</div>
{/* 送信ボタン */}
<div className="mt-6 flex justify-center">
<FacultySubmitButton mode="create" disabled={processing} />
</div>
</form>
);
};
const ErrorSlot = ({ message }) => (
<p className="h-5 text-sm leading-5 text-red-500 overflow-hidden">
{message ?? '\u00A0'}
</p>
);
export default CreateFacultyModal;
1.4 メニューポップオーバー修正
resources/js/Components/University/MenuPopover.jsx
クリックでコードを表示
/**
* メニューポップオーバーコンポーネント
* @param {Object} props - コンポーネントのプロパティ
* @pram {function} props.onAddFacultyClick - 「学部を追加する」クリック時のハンドラ
* @param {Function} props.onEditClick - 「編集する」クリック時のハンドラ
* @returns {JSX.Element} コンポーネントのJSX
*/
const MenuPopover = ({ onAddFacultyClick, onEditClick }) => {
const menuItems = [
{ label: '学部を追加する', onClick: onAddFacultyClick }, // 修正
{ label: '編集する', onClick: onEditClick },
{ label: '編集履歴を見る', onClick: () => {} },
{ label: '削除依頼をする', onClick: () => {} },
];
return (
...
);
};
export default MenuPopover;
1.5 学部一覧画面修正
resources/js/Pages/Faculty/Index.jsx
クリックでコードを表示
// その他のインポート...
import CreateFacultyModal from "@/Components/Faculty/CreateFacultyModal"; // 追加
const Index = ({ faculties, university, query = '' }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); // 追加
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
// ...
// ↓追加
/**
* メニューポップオーバーの「学部を追加する」クリック時の処理
* @returns {void}
*/
const handleAddFacultyClick = () => {
setIsMenuOpen(false);
setIsCreateModalOpen(true);
}
/**
* メニューポップオーバーの「編集する」クリック時の処理
* @returns {void}
*/
const handleEditClick = () => {
setIsMenuOpen(false);
setIsEditModalOpen(true);
}
return (
<AppLayout title={`${university.name}の学部一覧`}>
<Head title={`${university.name}の学部一覧`} />
<div className="flex flex-col items-center min-h-full">
{/* ヘッダー部分(共通) */}
<div className="w-full flex flex-row items-center justify-between">
{/* パンくずリスト 左寄せ */}
<div>
<Breadcrumb university={university} query={query} />
</div>
{/* 学部件数 + ケバブメニュー 右寄せ */}
<div className="flex items-center gap-2">
<p className="text-[#747D8C]">{faculties.length}件の学部</p>
{/* ケバブメニュー */}
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="p-2 rounded-full"
>
<KebabIcon />
</button>
{/* ↓修正 */}
{isMenuOpen && <MenuPopover onAddFacultyClick={handleAddFacultyClick} onEditClick={handleEditClick} />}
</div>
</div>
</div>
{/* コンテンツ部分 */}
{hasResults ? (
<div className="w-full grid grid-cols-3 gap-6 mt-8 justify-items-center">
{faculties.map(faculty => (
<FacultyCard key={faculty.id} faculty={faculty} query={query} />
))}
</div>
) : (
<div className="flex-1 flex items-center justify-center">
<p className="text-[#747D8C]">まだ学部が登録されていません。</p>
</div>
)}
</div>
{/* 追加: 学部作成モーダル */}
<CreateFacultyModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
university={university}
/>
{/* 学部編集モーダル */}
<EditUniversityModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
university={university}
/>
</AppLayout>
);
};
export default Index;
1.6 動作確認
メニューポップアップの「学部を追加する」をクリックして、適当な名前で学部を追加してみましょう。

コミット・プッシュ、PR作成、developブランチへのマージを忘れずに!
2. 編集モーダル作成
2.1 ブランチ運用
先ほどの変更履歴をローカルのdevelopブランチでプルして、新しいブランチを切って作業します。
ブランチ名は、feature/frontend/faculty-edit-modalとかにします。
2.2 編集モーダル作成
resources/js/Components/Faculty/EditFacultyModal.jsx
クリックでコードを表示
import { useForm } from '@inertiajs/react';
import Modal from '../Common/Modal';
import FacultySubmitButton from './FacultySubmitButton';
import InputField from '../Common/InputField';
import TextareaField from '../Common/TextareaField';
/**
* 学部編集モーダル
* @param {Object} props
* @param {boolean} props.isOpen - モーダルの開閉状態
* @param {Function} props.onClose - モーダルを閉じる
* @param {Object} props.faculty - 編集対象の学部オブジェクト
* @returns {JSX.Element} コンポーネントのJSX
*/
const EditFacultyModal = ({ isOpen, onClose, faculty }) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title="学部を編集する"
size='sm'
>
<div className="h-[300px] flex flex-col">
{/* フォーム領域 */}
<div className="flex-1">
<EditFacultyForm onClose={onClose} faculty={faculty} />
</div>
</div>
</Modal>
);
};
/**
* 学部編集フォーム
* @param {Object} props
* @param {Function} props.onClose - モーダルを閉じる
* @returns {JSX.Element} コンポーネントのJSX
*/
const EditFacultyForm = ({ onClose, faculty }) => {
const { data, setData, put, processing, errors, reset } = useForm({
name: faculty?.name ?? '',
comment: '',
version: faculty?.version ?? 1,
});
const submit = e => {
e.preventDefault();
put(route('faculty.update', faculty.id), {
onSuccess: () => onClose(),
preserveScroll: true,
});
};
return (
<form onSubmit={submit} className="h-full flex flex-col">
<div className="flex-1">
{/* 入力欄 */}
<ErrorSlot message={errors.name} />
{/* 大学名 */}
<InputField
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
placeholder="大学名(正式名称)"
size="sm"
className="mb-2 w-full"
/>
{/* 編集理由 */}
<ErrorSlot message={errors.comment} />
<TextareaField
value={data.comment}
onChange={e => setData('comment', e.target.value)}
placeholder="編集理由を入力してください"
size="sm"
rows={4}
className="w-full"
/>
</div>
{/* 送信ボタン */}
<div className="mt-6 flex justify-center">
<FacultySubmitButton mode="edit" disabled={processing} />
</div>
</form>
);
};
const ErrorSlot = ({ message }) => (
<p className="h-5 text-sm leading-5 text-red-500 overflow-hidden">
{message ?? '\u00A0'}
</p>
);
export default EditFacultyModal;
2.3 メニューポップオーバー修正
これまでのメニューポップオーバーは、学部一覧ページで使用するのを前提に作られてきました。
しかし、学部を編集するメニューは研究室一覧ページに表示する必要があります。
そのため、両場面で使用できるように汎用性を高めます。
まず、ファイルの場所を移動させましょう。
- 移動前:
resources/js/Components/University/MenuPopover.jsx - 移動後:
resources/js/Components/Common/MenuPopover.jsx
「切り取り」→「貼り付け」をすると、VS Codeが勝手に「インポートを更新しますか?」と出してくれるので、「はい」を押せば大丈夫です。
もし、少し待っても表示がされない場合は、手動でインポート文を修正しましょう。
学部一覧画面コンポーネントでインポートしていたはずです。
import MenuPopover from "@/Components/Common/MenuPopover";
次に、中身を修正します。
標示するメニューのラベル用の文字列をpropsとして、親コンポーネントから受け取れるようにしました。
resources/js/Components/Common/MenuPopover.jsx
クリックでコードを表示
/**
* メニューポップオーバーコンポーネント
* @param {Object} props - コンポーネントのプロパティ
* @param {string} props.addLabel - 追加ボタンのラベルに表示する文字列
* @param {Function} props.onAddClick - 「〇〇を追加する」クリック時のハンドラ
* @param {Function} props.onEditClick - 「編集する」クリック時のハンドラ
* @returns {JSX.Element} コンポーネントのJSX
*/
const MenuPopover = ({ addLabel, onAddClick, onEditClick }) => {
const menuItems = [
{ label: addLabel, onClick: onAddClick },
{ label: '編集する', onClick: onEditClick },
{ label: '編集履歴を見る', onClick: () => {} },
{ label: '削除依頼をする', onClick: () => {} },
];
return (
<div
className="absolute top-full right-0 mt-2 w-48 rounded-lg shadow-xl overflow-hidden z-10"
style={{ backgroundColor: '#EEF7FB' }}
>
<ul className="py-1">
{menuItems.map((item, index) => (
<li
key={index}
style={
index !== menuItems.length - 1
? { borderBottom: '1px solid #747D8C' }
: {}
}
>
<button
className="w-full px-4 py-3 text-left text-sm"
style={{ color: '#747D8C', fontWeight: 'bold' }}
onClick={item.onClick}
>
{item.label}
</button>
</li>
))}
</ul>
</div>
);
};
export default MenuPopover;
2.4 研究室一覧画面修正
resources/js/Pages/Lab/Index.jsx
クリックでコードを表示
import { Head, router } from "@inertiajs/react";
import AppLayout from '@/Layouts/AppLayout';
import LabCard from '../../Components/Lab/LabCard';
import Pagination from "../../Components/Common/Pagination";
import Breadcrumb from "../../Components/Common/Breadcrumb";
import MenuPopover from "@/Components/Common/MenuPopover";
import KebabIcon from "@/Components/Common/KebabIcon";
import EditFacultyModal from "@/Components/Faculty/EditFacultyModal";
import { useState, useEffect, useRef } from "react";
/**
* ソートオプションの定義
*/
const sortOptions = [
{ value: 'overall', label: '総合評価の高い順' },
{ value: 'reviews_count', label: 'レビュー数の多い順' },
{ value: 'mentorship_style', label: '指導スタイルの高い順' },
{ value: 'lab_atmosphere', label: '雰囲気・文化の高い順' },
{ value: 'achievement_activity', label: '成果・活動の高い順' },
{ value: 'constraint_level', label: '拘束度の高い順' },
{ value: 'facility_quality', label: '設備の高い順' },
{ value: 'work_style', label: '働き方の高い順' },
{ value: 'student_balance', label: '人数バランスの高い順' },
];
/**
* 研究室一覧ページコンポーネント
* @param {Object} props - コンポーネントのprops
* @param {Object} props.labs - ページネーション付き研究室データ
* @param {Object} props.faculty - 学部オブジェクト
* @param {string} props.query - 検索クエリ文字列
* @param {string} props.sort - ソート条件
* @returns {JSX.Element} コンポーネントのJSX
*/
const Index = ({ labs, faculty, query, sort = 'overall' }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const menuRef = useRef(null);
const hasResults = labs.data.length > 0;
// 外側クリックでメニューポップオーバーを閉じる
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setIsMenuOpen(false);
}
};
if (isMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isMenuOpen]);
/**
* メニューポップオーバーの「編集する」クリック時の処理
* @returns {void}
*/
const handleEditClick = () => {
setIsMenuOpen(false);
setIsEditModalOpen(true);
}
/**
* ソート条件変更時のハンドラ
* @param {Event} e - イベントオブジェクト
*/
const handleSortChange = (e) => {
const newSort = e.target.value;
router.get(route('labs.index', { faculty: faculty.id }), {
query,
sort: newSort,
}, {
preserveState: true,
preserveScroll: true,
});
};
return (
<AppLayout title={`${faculty.university.name} ${faculty.name}`}>
<Head title={`${faculty.university.name} ${faculty.name}`} />
<div className="flex flex-col items-center min-h-full">
<div className="w-full flex flex-row items-center justify-between">
{/* パンくずリスト 左寄せ */}
<div>
<Breadcrumb university={faculty.university} faculty={faculty} query={query} />
</div>
{/* 研究室件数 + ケバブメニュー 右寄せ+ソート */}
<div className="flex items-center gap-2">
<p className="text-[#747D8C]">{labs.total}件の研究室</p>
<select
value={sort}
onChange={handleSortChange}
className="text-sm text-[#747D8C] bg-[#EEF5F9] border border-[#747D8C] rounded px-3 py-1 pr-8 outline-none focus:outline-none focus:ring-0 focus:border-[#747D8C]"
>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
{/* ケバブメニュー */}
<div className="relative" ref={menuRef}>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="p-2 rounded-full"
>
<KebabIcon />
</button>
{isMenuOpen && <MenuPopover addLabel="研究室を追加する" onEditClick={handleEditClick} />}
</div>
</div>
</div>
{hasResults ? (
<>
<div className="w-full max-w-xl space-y-6 mt-8">
{labs.data.map(lab => (
<LabCard key={lab.id} lab={lab} query={query} sort={sort} />
))}
</div>
{/* ページネーション */}
<Pagination paginator={labs} />
</>
) : (
<div className="flex-1 flex items-center justify-center">
<p className="text-[#747D8C]">まだ研究室は登録されていません。</p>
</div>
)}
</div>
{/* 学部編集モーダル */}
<EditFacultyModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
faculty={faculty}
/>
</AppLayout>
)
};
export default Index;
2.5 動作確認
研究室一覧ページで、ケバブアイコンをクリックしてポップオーバーが開くか確認してください。

少し、レイアウトを変更しています。
これにて、作業は完了なので、コミット・プッシュを忘れずにしておきましょう。
3. (おまけその8)バックエンドPHPDocその1
今回は、記事のボリュームに余裕がある(ないけど、いつもよりはマシというだけだが)ので、おまけコーナーを久々にやりたいと思います。
フロントエンド編なのに、バックエンドのリファクタリングを今日からやっていきたいと思いますw
やることとしては、
- PHPDoc形式のコメントアウト付与
- 型宣言の追加
- 不要になったメソッドの削除
を予定しています。
PHPDocについては、実はこのシリーズでも過去に登場しています。
怪しい方は調べて復習してみてください。
まあ、JSDocと似たような感じだと思ってもらえれば、、、いつも書いているアレです。
ただし、今回はタイプヒントを積極的にしていこうかなと思っています。
タイプヒントをすると、引数や戻り値の型は見ればすぐわかるので、PHPDocの方には特別な理由がない限り省略する方針にしたいと思います。
タイプヒントについても、過去にこのシリーズで取り扱ったことがあるので「何それ?」って方は復習しておきましょう。
※補足ですが、以前このシリーズで「PHPは、他のTypeScriptやJavaなどのように型宣言ができる」と説明したかと思います。
ただし、厳密にはこれらは異なっています。
PHPはソースコードが実行時に一行一行その都度機械語に翻訳される仕組みで動いています。
インタープリタ言語などと呼ばれています。
他にも有名なものとしては、PythonやRuby, JavaScriptが該当します。
一方で、Javaはソースコードをいっぺんにすべて機械語に変換します。
コンパイラ言語などと呼ばれることが多く、Javaの他にはGoやRustなんかが有名ですかね。
コンパイル言語は、構文にミスがあった場合に、実際に実行される前にエラーが発生するので、問題を早期(リリースする前の開発段階と言い換えてもOKです)に発見できます。
「じゃあ、PHPで型宣言をしても意味ないんじゃね?」
って思うかもしれません。
しかし、PHPの実行環境の上でVS Code等の現代的なエディタを使った開発をしていると、PHPの解析ツール(詳しくは割愛します)がコードを書いている段階で、構文エラーを検知して、警告を出してくれます。
例えば、InertiaのResponseを返す関数の戻り値にあえてstring型の宣言を追加してみます。
このように、「string型の戻り値を期待しているのに、違う戻り値が書かれてしまっていますよ!」と教えてくれます。
これで、型の不意一致によるバグを防ぐことができるというわけです。
伝わったかな?(´・ω・)
ちなみにTypeScriptは、一度すべてJavaScriptに変換して、その後実行します。
この辺はややこしいのでまた機会があれば、どこかで..
インタプリタ言語とコンパイラ言語のメリット・デメリットとかもあるので、興味がわいた方は調べてみてください。
とりあえず、これ以上補足で長々と語っても仕方ないので、実装に移りましょう。
まずは、コントローラーから直していくのが良いかなと思います!
3.1 ブランチ運用
developブランチを最新化させて、新規ブランチを切って作業しましょう。
ブランチ名は、refactor/php-doc/controllersにします。
3.2 大学コントローラー
記念すべき最初のコントローラーは、UniversityController.phpさんです。
レスポンスかリダイレクトありのレスポンスかの違いがあるくらいですね。
あと、createとeditメソッドは、前回モーダルを作成したことで、ページそのものが必要なくなったので削除しました。
クリックでコードを表示
<?php
namespace App\Http\Controllers;
use App\Models\University;
use App\Notifications\ModelChangedNotification;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class UniversityController extends Controller
{
use AuthorizesRequests;
/**
* 新規大学を保存する
*/
public function store(Request $request): RedirectResponse
{
$this->authorize('create', University::class);
$validated = $request->validate([
'name' => 'required|string|max:50|unique:universities,name',
'type' => 'required|string|in:national,public,private',
]);
$university = new University();
$university->name = $validated['name'];
$university->type = $validated['type'];
$university->created_by = $request->user()->id;
$university->save();
$userId = $request->user()->id;
$university->users()->attach($userId);
return redirect()->route('faculties.index', ['university' => $university])->with('success', '大学が作成されました。'); // 修正: リダイレクト先を変更
}
/**
* 大学一覧を表示する
*/
public function index(Request $request): Response
{
$query = $request->input('query', '');
$universities = University::query()
->when($query, function ($queryBuilder) use ($query) {
$queryBuilder->where('name', 'like', '%' . $query . '%');
})
->orderBy('name')
->paginate(10) // 10件ずつ表示に変更
->withQueryString();
return Inertia::render('University/Index', [
'universities' => $universities,
'query' => $query,
]);
}
/**
* 大学情報を更新する
*/
public function update(Request $request, University $university): RedirectResponse
{
$this->authorize('update', University::class);
$validated = $request->validate([
'name' => 'required|string|max:50|unique:universities,name,' . $university->id,
'type' => 'required|string|in:national,public,private',
'comment' => 'required|string|max:255',
'version' => 'required|integer',
]);
// トランザクション開始
DB::beginTransaction();
try {
// 他のユーザーが更新している可能性がある
// そのため、最初に最新の university を取得
$current = University::find($university->id);
if ($validated['version'] !== $current->version) {
throw ValidationException::withMessages([
'version' => '他のユーザーがこの大学情報を更新しました。最新の情報を確認してください。',
]);
}
// データ更新
$current->name = $validated['name'];
$current->type = $validated['type'];
$current->version += 1; // バージョンを1増やす
$current->save();
// 履歴保存
$userId = $request->user()->id;
$current->users()->attach($userId, [
'comment' => $validated['comment'],
'created_at' => now(),
'updated_at' => now(),
]);
DB::commit(); // トランザクション処理終了
// 作成者へ通知を送信
if ($userId !== $current->created_by && $current->creator) {
$changes = collect($current->getChanges())
->only(['name'])
->map(fn($new, $key) => [
'old' => $old[$key] ?? null,
'new' => $new,
])
->toArray();
$current->creator->notify(
new ModelChangedNotification('edited', '大学', $current->name, $changes)
);
}
// リダイレクト
return redirect()->route('faculties.index', ['university' => $current])->with('success', '大学情報が更新されました。');
} catch (\Exception $e) {
DB::rollBack(); // エラー時はロールバック
throw $e;
}
}
/**
* 大学編集履歴を表示する
*/
public function history(University $university): Response
{
$editHistory = $university->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('University/History', [
'university' => $university,
'editHistory' => $editHistory,
]);
}
}
また、ついでに、作成・編集用のページコンポーネント(\resources\js\Pages\University\Create.jsxと\src\resources\js\Pages\University\Edit.jsx)も必要なくなったので削除しておきましょうか。
LinuxコマンドでもVS Codeのエクスプローラー上でのマウス操作でもどちらでも大丈夫です。
3.3 学部コントローラー
続いて、学部コントローラーさん(なぜかさきほどから急にさん付け)です。
先ほどと同様に、createとeditメソッドは不要になったので削除です。
クリックでコードを表示
<?php
namespace App\Http\Controllers;
use App\Models\Faculty;
use App\Models\University;
use App\Notifications\ModelChangedNotification;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
class FacultyController extends Controller
{
use AuthorizesRequests;
public function store(Request $request, University $university): RedirectResponse
{
$this->authorize('create', Faculty::class);
$validated = $request->validate([
'name' => 'required|string|max:50|unique:faculties,name,NULL,id,university_id,' . $university->id,
]);
$faculty = new Faculty();
$faculty->name = $validated['name'];
$faculty->university_id = $university->id;
$faculty->created_by = $request->user()->id;
$faculty->save();
$userId = $request->user()->id;
$faculty->users()->attach($userId);
return redirect()->route('labs.index', ['faculty' => $faculty])->with('success', '学部が作成されました。');
}
public function index(Request $request, University $university): Response
{
$query = $request->input('query', '');
$faculties = $university->faculties()->get(); // 修正: 名前のソートを削除
return Inertia::render('Faculty/Index', [
'faculties' => $faculties,
'university' => $university,
'query' => $query,
]);
}
public function update(Request $request, Faculty $faculty): RedirectResponse
{
$this->authorize('update', Faculty::class);
$validated = $request->validate([
'name' => 'required|string|max:50|unique:universities,name,' . $faculty->id,
'comment' => 'required|string|max:255',
'version' => 'required|integer',
]);
// トランザクション
DB::beginTransaction();
try {
// 現在のバージョンを取得して比較
$current = Faculty::find($faculty->id);
if ($validated['version'] !== $current->version) {
throw ValidationException::withMessages([
'version' => '他のユーザーがこの学部情報を更新しました。最新の情報を確認してください。',
]);
}
// 更新
$current->name = $validated['name'];
$current->version += 1;
$current->save();
// 履歴保存
$userId = $request->user()->id;
$current->users()->attach($userId, [
'comment' => $validated['comment'],
'created_at' => now(),
'updated_at' => now(),
]);
DB::commit();
if ($userId !== $current->created_by && $current->creator) {
$changes = collect($current->getChanges())
->only(['name'])
->map(fn($new, $key) => [
'old' => $old[$key] ?? null,
'new' => $new,
])
->toArray();
$current->creator->notify(
new ModelChangedNotification('edited', '学部', $current->name, $changes)
);
}
return redirect()->route('labs.index', ['faculty' => $current])->with('success', '大学情報が更新されました。');
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
public function history(Faculty $faculty): Response
{
$editHistory = $faculty->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('Faculty/History', [
'faculty' => $faculty,
'editHistory' => $editHistory,
]);
}
}
ついでに、不要になったページコンポーネントも同時に削除しておきます。
本当は、Gitのコミット履歴の分かりやすさの観点から、モーダルを作っているときに削除したほうがよかったと思いますが...
(追記)肝心のPHPDocが書かれていません。型宣言をしているのでなくてもよいですが、気になる場合は適切なコメント追加しておいてください。
削除するコンポーネント:
\resources\js\Pages\Faculty\Create.jsx\resources\js\Pages\Faculty\Edit.jsx
今日のところは、ここまでにしておきます!
次回以降余裕があれば、続きをやっていきましょう。
コミット・プッシュをしておきましょう。
ただし、このブランチは引き続き次回以降も使おうと思うんで、あえてPR作成とdevelopブランチへのマージまではしないでおいてください。
4. まとめ・次回予告
今回は、学部作成・編集モーダルを作成しました!
「クリックでコードを表示」というワザを覚えたので、記事が読みやすくなったかと思います。(≧◇≦)
次回は、研究室の作成・編集用モーダルを作成したいと思います!
最後まで読んでくれありがとうございます!
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その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: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。
現在、まったく再生されておらず、落ち込みそうなので、見てくださる方はぜひ高評価を教えもらえると励みになると思います。"(-""-)"




