バックエンド基本(Express で理解する)
クライアントとサーバーとは
クライアントはユーザー側(ブラウザ・アプリ)、サーバーはリクエストを受けて処理して返す側です。
| 種類 | 役割 | 例 |
|---|---|---|
| クライアント | リクエストを送る側 | Chrome・React アプリ・スマホアプリ |
| サーバー | リクエストを受けて処理して返す側 | Node.js・PHP・Java・Python |
Client → Request → Server
Client ← Response ← Server
HTTP 通信の流れ
クライアントとサーバーは HTTP というルールで通信します。
① クライアントがリクエスト送信
② サーバーが受け取る
③ サーバーが処理(DB操作など)
④ レスポンスを返す
例:
GET /todos
↓
Node.js
↓
DB
↓
JSON 返す
| HTTP メソッド | 意味 | URL 例 |
|---|---|---|
| GET | 取得 | GET /todos |
| POST | 作成 | POST /todos |
| PUT | 更新(全体) | PUT /todos/1 |
| PATCH | 部分更新 | PATCH /todos/1 |
| DELETE | 削除 | DELETE /todos/1 |
HTTP ステータスコード
サーバーの処理結果を表す番号です。レスポンスに含まれます。
| 範囲 | 意味 |
|---|---|
| 200 系 | 成功 |
| 300 系 | リダイレクト |
| 400 系 | クライアントエラー(リクエストが悪い) |
| 500 系 | サーバーエラー(サーバー側のバグ) |
| コード | 意味 | 使う場面 |
|---|---|---|
| 200 | OK(成功) | GET・PUT・PATCH 成功 |
| 201 | Created(作成成功) | POST 成功 |
| 204 | No Content(返すデータなし) | DELETE 成功 |
| 400 | Bad Request(不正なリクエスト) | パラメータ不足・型違い |
| 401 | Unauthorized(未認証) | JWT なし・未ログイン |
| 403 | Forbidden(権限なし) | admin のみ許可など |
| 404 | Not Found(存在しない) | ID が見つからない |
| 409 | Conflict(競合・重複) | 同じメールアドレスが既存 |
| 422 | Unprocessable Entity | バリデーションエラー |
| 500 | Internal Server Error | DB エラー・バグ |
Node.js の役割(バックエンド)
Node.js は JavaScript でサーバーを作る環境です。以下の処理をサーバー側で行います。
| できること | 内容 |
|---|---|
| API 作成 | フロントからのリクエストを受けて JSON を返す |
| DB 操作 | MySQL・PostgreSQL・MongoDB などへの読み書き |
| 認証 | JWT・セッション・Cookie の処理 |
| ファイル処理 | ファイルの読み書き・アップロード |
| バッチ処理 | 定期実行・非同期タスク処理 |
Node.js の標準 HTTP サーバー
require("http") は Node.js に最初から入っている標準モジュール(built-in module)を読み込みます。HTTP サーバーを作る機能がオブジェクトとして入っています。
const http = require("http");
// http オブジェクトの中身(イメージ)
// { createServer, request, get, Server, IncomingMessage, ServerResponse }
const server = http.createServer((req, res) => {
res.end("Hello");
});
server.listen(3000);
// 流れ:
// ① http 読み込み(工具箱を取り出す)
// ② createServer でサーバーオブジェクト生成(建物を建てる)
// ③ listen(3000) で OS にポート登録・通信待ち受け開始
// ④ リクエスト来る → callback 実行 → レスポンス返す
listen() が呼ばれた時点で OS に登録され、TCP 待ち受けが開始されます。Node.js のモジュールの種類
| 種類 | 例 | require の書き方 |
|---|---|---|
| 標準(built-in) | http, fs, path, os, url | require("http") |
| 外部(npm) | express, axios, prisma | require("express") |
| 自作 | app.js, utils.js | require("./app") |
Express を使う理由
Node.js 標準の http だけではルーティングや JSON 処理を全部手書きする必要があり大変です。Express は http をラップして書きやすくしたフレームワークです。
| 項目 | http(標準) | express |
|---|---|---|
| レベル | 低レベル | 高レベル |
| 記述量 | 多い | 少ない |
| ルーティング | 手書き | app.get() など |
| JSON 処理 | 手書き | express.json() |
| 取得方法 | 標準(最初から) | npm install express |
const express = require("express");
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
res.json({ message: "ok" });
});
app.listen(3000);
req と res の中身
コールバックの (req, res) はそれぞれクライアントから送られてきた情報と、クライアントへ返す操作を持つオブジェクトです。
| オブジェクト | 実体クラス | 主な中身 |
|---|---|---|
| req(Request) | IncomingMessage | url, method, headers, body, params, query |
| res(Response) | ServerResponse | json(), send(), end(), status(), setHeader(), redirect() |
app.post("/todos", (req, res) => {
console.log(req.method); // "POST"
console.log(req.url); // "/todos"
console.log(req.body); // { title: "task" }(express.json() が必要)
console.log(req.params.id);// URL の :id パラメータ
console.log(req.query.page);// ?page=1 のクエリ
res.status(201).json({ success: true });
});
// HTTP レスポンスの実体(res.json() が自動生成するもの)
HTTP/1.1 201 Created
Content-Type: application/json
{"success":true}
Content-Type とは
送るデータの種類をヘッダーで知らせる仕組みです。これがないとサーバーもブラウザも「何のデータか」が分かりません。
| Content-Type | 意味 | 用途 |
|---|---|---|
| application/json | JSON | API 通信(現在の主流) |
| text/plain | テキスト | 文字列 |
| text/html | HTML | ページ返却 |
| multipart/form-data | フォーム+ファイル | ファイルアップロード |
| application/x-www-form-urlencoded | フォーム | HTML フォーム送信 |
// Node.js(手動設定)
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: true }));
// Express(自動設定)
res.json({ ok: true }); // Content-Type: application/json を自動付与
Content-Type: application/json を付けずに送ると、サーバーで express.json() が動かず req.body が undefined になります。JSON の受け渡し(フロント ⇄ サーバー)
JSON 通信に必要な要素は 5 つです。
| 側 | 必要なもの | 理由 |
|---|---|---|
| フロント(送信) | method | POST / PUT など操作を指定 |
| Content-Type: application/json | サーバーに「JSON を送る」と伝える | |
| JSON.stringify(data) | HTTP は文字しか送れないため変換が必要 | |
| サーバー(受信) | app.use(express.json()) | 文字列の body を JSON にパースして req.body に入れる |
| res.json() | JSON に変換して Content-Type を自動付与して返す |
// フロント(送信)
fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "task" })
})
.then(res => res.json()) // 文字列 → JSON に変換
.then(data => console.log(data));
// サーバー(受信・返却)
app.use(express.json()); // これがないと req.body が undefined
app.post("/api/todos", (req, res) => {
console.log(req.body.title); // "task"
res.status(201).json({ success: true });
});
REST API とは
REST = Web の設計ルール。URL でリソース(データ)を表し、HTTP メソッドで操作します。
| REST のルール | 例 |
|---|---|
| URL は名詞(動詞は使わない) | ✅ /users ❌ /getUsers |
| 操作は HTTP メソッドで表す | GET/POST/PUT/PATCH/DELETE |
| データは JSON でやり取り | application/json |
| ステートレス(状態を持たない) | 毎回 token や cookie を送る |
| ステータスコードを使う | 200/201/204/400/404/500 |
| 操作 | メソッド | URL | Express |
|---|---|---|---|
| 一覧取得 | GET | /todos | app.get("/todos") |
| 1件取得 | GET | /todos/1 | app.get("/todos/:id") |
| 作成 | POST | /todos | app.post("/todos") |
| 更新 | PUT / PATCH | /todos/1 | app.patch("/todos/:id") |
| 削除 | DELETE | /todos/1 | app.delete("/todos/:id") |
Express の app メソッド一覧
const app = express() で作られる app オブジェクトに使えるメソッドの分類です。
| 分類 | メソッド | 意味 |
|---|---|---|
| 起動 | app.listen(port) | ポート待ち受け開始 |
| ルーティング | app.get(path, handler) | GET |
| app.post(path, handler) | POST | |
| app.put(path, handler) | PUT | |
| app.patch(path, handler) | PATCH | |
| app.delete(path, handler) | DELETE | |
| app.all(path, handler) | 全メソッド | |
| ミドルウェア | app.use(fn) | 全リクエストに処理を登録 |
| express.json() | body を JSON にパース | |
| express.urlencoded() | フォームデータをパース | |
| express.static(dir) | 静的ファイル配信 | |
| Router | express.Router() | ルートをファイルに分割 |
| 設定 | app.set(key, val) | アプリ設定(テンプレートエンジンなど) |
const express = require("express");
const app = express();
// ミドルウェア(全リクエストに適用)
app.use(express.json());
app.use(cors());
// ルーティング
app.get("/todos", (req, res) => { res.json([]); });
app.post("/todos", (req, res) => { res.status(201).json({}); });
// Router で分割
app.use("/api/todos", todoRouter);
// エラーハンドラー(引数 4 つが必須)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
app.listen(3000);
ミドルウェアとルートハンドラーの違い
Express はリクエストが来たとき、登録された処理を上から順番に実行します。
| 種類 | 役割 | next | res を返す |
|---|---|---|---|
| ミドルウェア | 途中で処理する(ログ・認証・JSON パースなど) | 必須 | 任意 |
| ルートハンドラー | URL に一致したとき最後に実行する | 任意 | 必須 |
| エラーミドルウェア | next(err) が呼ばれたとき実行 | — | 必須 |
// リクエストの流れ
Request
↓
app.use(cors()) // ミドルウェア①
↓
app.use(express.json()) // ミドルウェア②(body をパース)
↓
router.use(auth) // ミドルウェア③(認証チェック)
↓
router.get("/todos", handler) // ルートハンドラー(レスポンス返す)
↓
app.use(errorHandler) // エラーミドルウェア(エラー時のみ)
↓
Response
app.use とは
app.use(fn) はすべてのリクエストに対して fn を実行する処理を登録する関数です。Express 内部では配列(スタック)に登録し、リクエストが来るたびに順番に実行します。
// Express 内部のイメージ
stack = [mw1, mw2, mw3, routeHandler, errorHandler]
// next() = 配列の次の関数を呼ぶ
function run(index) {
const fn = stack[index];
fn(req, res, function next() { run(index + 1); });
}
run(0);
| next() の種類 | 意味 |
|---|---|
| next() | 次のミドルウェアへ進む |
| next(err) | エラーミドルウェアへジャンプ |
| next 呼ばない | そこで止まる(ルートハンドラーに届かない) |
// next() の基本
app.use((req, res, next) => {
console.log("通過");
next(); // これを呼ばないとハンドラーに届かない
});
// エラーを next に渡す(async では必須)
app.get("/todos", async (req, res, next) => {
try {
const data = await db.query();
res.json(data);
} catch (err) {
next(err); // エラーミドルウェアへ
}
});
// エラーミドルウェア(引数が 4 つ = 必須)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
バックエンドの基本構成(実務)
実務ではコードを役割ごとにファイル分割します。
src/
app.js // Express 設定・ミドルウェア登録
routes/ // URL とコントローラーの対応
controllers/ // リクエスト受け取り・レスポンス返す
services/ // ビジネスロジック
repositories/ // DB アクセス
models/ // 型・スキーマ定義
| 層 | 役割 | 例 |
|---|---|---|
| routes | URL と controller をつなぐ | POST /todos → todoCreateController |
| controllers | req を受けて res を返す入口 | req.body を受け取り service を呼ぶ |
| services | ビジネスロジックの処理 | バリデーション・加工・条件判定 |
| repositories | DB アクセスのみ担当 | INSERT / SELECT / UPDATE / DELETE |
| models | 型・スキーマ定義 | Todo の型、DB テーブル定義 |
// リクエストの流れ
POST /todos
↓ routes/todo.js
↓ controllers/todoCreateController.js
↓ services/todoService.js
↓ repositories/todoRepository.js
↓ DB
↓ res.status(201).json()
ポートとは / 非同期が重要な理由
| ポート番号 | 用途 |
|---|---|
| 3000 | Node.js / Express API サーバー |
| 5173 | Vite(React 開発サーバー) |
| 3306 | MySQL |
| 5432 | PostgreSQL |
| 6379 | Redis |
| 80 / 443 | HTTP / HTTPS(本番) |
Node.js では DB・外部 API・ファイルなど 「待ち時間が発生する処理」がすべて非同期です。await を付け忘れると undefined が返ります。
// ❌ await なし → undefined が返る
app.get("/todos", async (req, res) => {
const todos = db.findAll(); // Promise が返るが await していない
res.json(todos); // undefined
});
// ✅ await あり
app.get("/todos", async (req, res) => {
const todos = await db.findAll();
res.json(todos);
});
Node.js
Node.js とは?
Node.js は JavaScript をサーバー側で実行できる環境(ランタイム)です。本来ブラウザで動く JavaScript を、PC やサーバー上でも実行できるようにします。Chrome の V8 JavaScript エンジン をベースに構築されています。
Node.js = V8エンジン + Node API + イベントループ
JavaScript との実行環境の違い
| 項目 | JavaScript(ブラウザ) | Node.js |
|---|---|---|
| 実行場所 | ブラウザ | PC / サーバー |
| 目的 | UI 操作 | サーバー処理 |
| 主な API | DOM 操作・document・alert | ファイル・OS・DB・HTTP |
| グローバル | window | global |
| 例 | ボタン操作・画面制御 | API サーバー・ファイル操作 |
主な特徴
- ① フロント・バックを同じ言語で書ける:JavaScript でフルスタック開発が可能
- ② 非同期処理(ノンブロッキング I/O):処理を待たずに次へ進み、完了したら通知
- ③ 高速(V8 エンジン):コンパイル型に近い速度で実行
- ④ npm エコシステム:世界最大規模のパッケージリポジトリを利用可能
① イベントループ(Node 最大の特徴)
Node.js は イベント駆動型 で動作します。リクエストをキューに積み、イベントループが順に処理します。処理を待たずに次へ進むため、大量アクセスに強い構造です。
リクエスト
↓
イベントキュー
↓
イベントループ
↓
処理実行 → 完了したら通知
② 非同期処理(ノンブロッキング)
Node.js は Non Blocking、つまり「待たない処理」が基本です。
// 同期:順番待ち
①処理 → ②処理 → ③処理
// 非同期:同時に開始し、終わった順に処理
①開始
②開始
③開始
↓ 終わった順に処理
setTimeout(() => {
console.log("3秒後")
}, 3000)
console.log("先に表示")
// 結果:
// 先に表示
// 3秒後
| 方法 | 説明 |
|---|---|
| callback | 古い方式 |
| Promise | 標準的な方式 |
| async / await | 現在の主流 |
async function fetchUser() {
const data = await fetchData()
console.log(data)
}
③ npm(Node Package Manager)
npm は Node.js に同梱されている世界最大のパッケージ管理ツールです。
# インストール
npm install express
# 開発用
npm install -D typescript
# 全依存パッケージをインストール
npm install
package.json にプロジェクトの依存ライブラリ・スクリプト・バージョンが記録されます。
④ モジュール(CJS vs ESM)
Node.js はモジュールシステムでコードを分割・再利用します。方式は 2 種類あります。
| 項目 | CJS(CommonJS) | ESM(ES Modules) |
|---|---|---|
| 正式名称 | CommonJS | ES Modules |
| 読み込み | require | import |
| エクスポート | module.exports | export |
| 読み込み方式 | 同期 | 非同期 |
| 標準 | Node.js 旧来方式 | JavaScript 標準・現在主流 |
// CJS(CommonJS)
const add = require("./math")
module.exports = add
// ESM(ES Modules)
import { add } from "./math.js"
export function add(a, b) { return a + b }
package.json に "type": "module" を追加します。⑤ Express(Node の定番フレームワーク)
Node.js 単体でもサーバーを作れますが記述量が多いため、Express がよく使われます。
| フレームワーク | 用途 |
|---|---|
| Express | Web サーバー(定番) |
| NestJS | 大規模開発 |
| Fastify | 高速・軽量 |
const express = require("express")
const app = express()
app.get("/", (req, res) => {
res.send("Hello World")
})
app.listen(3000, () => {
console.log("サーバー起動: http://localhost:3000")
})
__dirname と __filename(CJS のみ)
実行中のファイルのパスを取得するための変数です(CommonJS 環境で使用可)。
| 変数 | 内容 | 出力例 |
|---|---|---|
__dirname | 現在のファイルが存在するディレクトリのパス | /project/src |
__filename | 現在実行しているファイルのフルパス | /project/src/app.js |
console.log(__dirname) // /project/src
console.log(__filename) // /project/src/app.js
__dirname / __filename は使えません。代わりに import.meta.url を使います。ブラウザ vs Node.js のグローバルオブジェクト
JavaScript は実行環境によって使えるオブジェクトが異なります。
| 環境 | グローバルオブジェクト | this(トップレベル) |
|---|---|---|
| ブラウザ | window | window |
| Node.js(CJS) | global | {} |
| Node.js(ESM) | global | undefined |
| 共通 | globalThis | — |
| ブラウザで使えるもの | Node.js で使えるもの |
|---|---|
| window / document / location | global / process / module |
| navigator / alert | __dirname / __filename / require |
| DOM 操作 API | ファイル・OS・DB・HTTP API |
// ブラウザのみ → Node では "window is not defined"
window.alert("hello")
// どの環境でも使える共通グローバル
console.log(globalThis)
// Node.js のみ
console.log(process.version) // Node.js のバージョン
console.log(__dirname) // ディレクトリパス
基本コマンド
# バージョン確認
node -v
# スクリプト実行
node app.js
# 対話型 REPL 起動
node
npm とは
npm(Node Package Manager)とは?
npm は Node.js に同梱されているパッケージ管理ツールです。JavaScript のライブラリやツールをインストール・管理・実行するために使います。世界中の開発者が公開したパッケージを npm レジストリ から取得できます。
npm = Node.js のライブラリを管理するツール
| コマンド | 意味 |
|---|---|
| npm init | プロジェクト初期化(package.json 生成) |
| npm install | 全パッケージをインストール |
| npm install express | パッケージを追加 |
| npm install -D nodemon | 開発用パッケージを追加 |
| npm install -g pkg | グローバルインストール(PC 全体で使える) |
| npm uninstall express | パッケージを削除 |
| npm list | インストール済み一覧確認 |
| npm run dev | scripts の dev を実行 |
| npm start | scripts の start を実行 |
| npm test | scripts の test を実行 |
パッケージとは? 〜読み込みまでの流れ
パッケージ = 再利用できるプログラムのまとまり。npm レジストリに公開されており、npm install でダウンロードして使います。
| パッケージ例 | 用途 |
|---|---|
| express | Web サーバー作成 |
| lodash | 便利なユーティリティ関数 |
| dotenv | 環境変数の管理 |
npm init // ① package.json 作成
npm install express // ② パッケージ取得
// ③ node_modules/express/ に保存
// ④ package.json の dependencies に記録
// ⑤ コードで読み込む
const express = require("express") // CJS
import express from "express" // ESM
node app.js // ⑥ 実行
node_modules を探す → package.json の main / exports を読む → ファイルを実行package.json とは?
package.json = プロジェクトの設定ファイル。npm の中心となるファイルで、使用するパッケージ・実行コマンド・バージョン・プロジェクト情報を管理します。
{
"name": "app",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "nodemon src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"nodemon": "^3.0.0",
"typescript": "^5.0.0"
}
}
| フィールド | 内容 |
|---|---|
| name / version | プロジェクト名・バージョン |
| main | エントリーポイント(Node が最初に読むファイル) |
| type | module → ESM、commonjs → CJS |
| scripts | npm run で実行できるコマンド |
| dependencies | 本番用パッケージ一覧 |
| devDependencies | 開発用パッケージ一覧 |
バージョン記号の意味:
| 記号 | 意味 |
|---|---|
| ^4.18.0 | 同じメジャーバージョンで更新 OK(4.x.x) |
| ~4.18.0 | 同じマイナーバージョンで更新 OK(4.18.x) |
| 4.18.0 | 完全固定 |
dependencies vs devDependencies
| 項目 | dependencies | devDependencies |
|---|---|---|
| 用途 | 本番で必要なパッケージ | 開発時だけ必要なパッケージ |
| 本番で必要 | ✅ | ❌ |
| インストール | npm install pkg | npm install -D pkg |
| 本番 deploy 時 | 入る | 入らないことが多い |
| dependencies の例 | devDependencies の例 |
|---|---|
| express, react, prisma, axios | typescript, nodemon, jest, eslint, vite |
# 本番用だけインストール(devDependencies を除外)
npm install --production
NODE_ENV=production npm install
npm install の詳細
npm install はパッケージをインストールする基本コマンドです。内部では以下の順に動作します。
npm install
↓
package.json 読む
↓
package-lock.json 読む
↓
バージョン決定
↓
npm レジストリから取得
↓
node_modules に保存(依存関係も取得)
| コマンド | 意味 |
|---|---|
| npm install | package.json から全パッケージをインストール |
| npm install pkg | パッケージを追加(dependencies に入る) |
| npm install -D pkg | 開発用で追加(devDependencies に入る) |
| npm install -g pkg | グローバルインストール(PC 全体で使える) |
| npm install --production | dependencies のみインストール |
node_modules は .gitignore で除外されているため、clone 後は必ず npm install を実行します。npm install vs npm ci
どちらもインストールコマンドですが、用途が異なります。
| 項目 | npm install | npm ci |
|---|---|---|
| 用途 | 通常の開発 | 本番 / CI / Docker |
| lock ファイル必要 | ❌ | ✅(必須) |
| 速度 | 普通 | 高速 |
| バージョン変わる可能性 | あり | なし(完全一致) |
| node_modules 削除 | しない | する(クリーンインストール) |
| 安定性 | 普通 | 高い |
# 開発
npm install
# CI / GitHub Actions / Docker / 本番
npm ci
npm ci --production
--production と npm run build
本番デプロイ・Docker・CI で必ず使う重要コマンドです。
| コマンド | 意味 |
|---|---|
| npm install --production | dependencies のみインストール |
| npm ci --production | CI 用・dependencies のみ(推奨) |
| npm run build | scripts の build を実行(開発コード → 本番コードに変換) |
npm run build が必要な理由: 本番環境では TypeScript・JSX・ESNext をそのまま実行できないため、変換(コンパイル)が必要です。
| 開発コード | build 後 |
|---|---|
| TypeScript(.ts) | JavaScript(.js) |
| React / JSX | JS bundle |
| Next.js | .next/ |
| Vite | dist/ |
// Node.js + TypeScript の典型的な本番フロー
npm ci // 全パッケージ入れる(devDeps も必要)
npm run build // tsc → dist/ に JS を生成
npm ci --production // devDeps を除いて再インストール
npm start // node dist/index.js
scripts と run が付く / 付かない違い
scripts に登録したコマンドは npm run 名前 で実行できます。ただし一部は run なしで実行できます。
| コマンド | run 必要? | 理由 |
|---|---|---|
| npm start | 不要 | npm が特別扱いするコマンド |
| npm test | 不要 | npm が特別扱いするコマンド |
| npm stop / restart | 不要 | npm が特別扱いするコマンド |
| npm run dev | 必要 | カスタムコマンドは run が必要 |
| npm run build | 必要 | カスタムコマンドは run が必要 |
| npm run lint | 必要 | カスタムコマンドは run が必要 |
{
"scripts": {
"dev": "nodemon src/index.ts", // npm run dev
"build": "tsc", // npm run build
"start": "node dist/index.js", // npm start(run 不要)
"test": "jest", // npm test(run 不要)
"lint": "eslint ." // npm run lint
}
}
start / test / stop / restart だけ run 不要。それ以外は npm run が必要。dev(開発用 script)について
npm run dev は特別なコマンドではなく、scripts に登録した "dev" という名前を実行するだけです。開発時は自動再起動・デバッグ・高速リロードが必要なため、本番の start とは設定を分けます。
| script | 用途 | よく使うツール |
|---|---|---|
| dev | 開発サーバー起動 | nodemon, ts-node, vite, next dev |
| build | 本番用コードを生成 | tsc, vite build, next build |
| start | 本番サーバー起動 | node dist/index.js |
| test | テスト実行 | jest, vitest |
// Node.js + TypeScript
{ "dev": "ts-node src/index.ts", "build": "tsc", "start": "node dist/index.js" }
// React / Vite
{ "dev": "vite", "build": "vite build", "preview": "vite preview" }
// Next.js
{ "dev": "next dev", "build": "next build", "start": "next start" }
npx とは? 〜 scripts との違い
npx = パッケージをインストールせず一時的に実行するコマンド(npm に付属)。
| コマンド | 意味 |
|---|---|
| npm install | パッケージをインストール |
| npm run xxx | scripts を実行 |
| npx xxx | パッケージを直接実行(一時インストール or ローカル実行) |
// 一回だけ使う CLI(インストール不要)
npx create-react-app my-app
npx create-next-app my-app
// ローカルパッケージを直接実行
npx eslint .
npx prisma migrate dev
npx vite
| 項目 | scripts(npm run) | npx |
|---|---|---|
| 定義場所 | package.json | CLI(その場で指定) |
| 永続性 | 登録して繰り返し使う | 一時実行 |
| 用途 | 開発・ビルド・テスト | 初期化・CLI ツール |
node_modules/.bin が自動で PATH に追加されるため、nodemon・tsc・vite などをそのまま呼び出せます。package-lock.json と node_modules
package-lock.json:npm install時に作られる。インストールされたパッケージの正確なバージョンを記録し、チーム全員が同じ環境を再現できる。Git に含めるnode_modules/:パッケージ本体が入るフォルダ。require / import はここから読み込む。.gitignore に追加して Git 管理外にする
yarn、pnpm も同様のパッケージ管理ツールです。pnpm はディスク効率が高く、モノレポでよく使われます。Laravel
Laravel とは?
Laravel は PHP で書かれたフルスタック Web フレームワークです。「エレガントな文法」を理念に掲げ、MVC アーキテクチャを採用。ルーティング・ORM(Eloquent)・マイグレーション・認証・キューなど Web 開発に必要な機能がすべて揃っています。
Laravel = PHP のフレームワーク(Node.js における Express の上位互換に相当)
| 特徴 | 内容 |
|---|---|
| MVC アーキテクチャ | Model / View / Controller でコードを整理 |
| Eloquent ORM | SQL を書かずにオブジェクト指向で DB 操作 |
| Blade テンプレート | PHP と HTML を組み合わせたテンプレートエンジン |
| Artisan CLI | コード生成・マイグレーション・キャッシュ操作を CLI で実行 |
| Composer 管理 | パッケージは Composer(PHP の npm)で管理 |
Node.js / npm との対比
| Node.js / npm | Laravel / PHP | 役割 |
|---|---|---|
| npm | Composer | パッケージ管理ツール |
| package.json | composer.json | 依存関係の設定ファイル |
| package-lock.json | composer.lock | バージョン固定ファイル |
| node_modules/ | vendor/ | パッケージ本体の格納先 |
| npm install | composer install | パッケージをインストール |
| npm run dev | php artisan serve | 開発サーバー起動 |
| Express | Laravel | Web フレームワーク |
プロジェクト作成〜起動の流れ
# ① Composer でプロジェクト作成
composer create-project laravel/laravel my-app
# または Laravel インストーラーを使う場合
composer global require laravel/installer
laravel new my-app
# ② プロジェクトに移動
cd my-app
# ③ .env を設定(DB 接続情報など)
cp .env.example .env
php artisan key:generate
# ④ DB マイグレーション実行
php artisan migrate
# ⑤ 開発サーバー起動
php artisan serve
# → http://localhost:8000
ディレクトリ構造
| パス | 役割 |
|---|---|
app/Http/Controllers/ | コントローラー(リクエスト処理) |
app/Http/Middleware/ | ミドルウェア(認証・ログなど) |
app/Models/ | Eloquent モデル(DB とのやり取り) |
routes/web.php | Web ルーティング(画面返す) |
routes/api.php | API ルーティング(JSON 返す) |
resources/views/ | Blade テンプレート(HTML) |
database/migrations/ | DB テーブル定義ファイル |
database/seeders/ | テストデータ投入ファイル |
config/ | 各種設定ファイル |
storage/ | ログ・ファイルアップロード・キャッシュ |
public/ | Web 公開ディレクトリ(index.php がここ) |
.env | 環境変数(DB 接続情報・APIキーなど) |
vendor/ | Composer でインストールしたパッケージ(.gitignore に追加) |
.env ファイルとは
プロジェクトの環境変数を管理するファイルです。DB 接続情報・API キーなどを記述します。Git には含めず(.gitignore に追加)、.env.example をテンプレートとしてチームで共有します。
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:...
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=my_database
DB_USERNAME=root
DB_PASSWORD=secret
.env の値は config/ 経由で参照します。直接 env('DB_HOST') と書くよりも config('database.connections.mysql.host') が推奨です(キャッシュの影響のため)。MVC の流れ(リクエストからレスポンスまで)
ブラウザ / クライアント
↓ HTTP リクエスト
public/index.php (エントリーポイント)
↓
ミドルウェア (認証チェック・ログ記録など)
↓
routes/web.php (URL と処理を対応付け)
↓
Controller (リクエスト処理・ビジネスロジック)
↓
Model (DB からデータ取得・保存)
↓
View(Blade) (HTML を生成)
↓ HTTP レスポンス
ブラウザ / クライアント
Artisan コマンド一覧(よく使うもの)
Artisan = Laravel に付属する CLI ツール。php artisan から実行します。
| コマンド | 意味 |
|---|---|
| php artisan serve | 開発サーバー起動(localhost:8000) |
| php artisan make:controller UserController | コントローラー作成 |
| php artisan make:controller UserController --resource | CRUD メソッド付きコントローラー作成 |
| php artisan make:model Post -m | モデル + マイグレーション作成 |
| php artisan make:middleware CheckAge | ミドルウェア作成 |
| php artisan make:seeder UserSeeder | シーダー作成 |
| php artisan migrate | マイグレーション実行 |
| php artisan migrate:rollback | 1つ前に戻す |
| php artisan migrate:fresh --seed | 全リセット+シーダー実行 |
| php artisan db:seed | シーダー(テストデータ)投入 |
| php artisan route:list | 登録済みルート一覧表示 |
| php artisan key:generate | APP_KEY 生成(初回セットアップ時) |
| php artisan cache:clear | キャッシュクリア |
| php artisan config:clear | 設定キャッシュクリア |
| php artisan optimize | 本番用キャッシュ生成 |
| php artisan tinker | 対話型 REPL(DB 操作の確認など) |
認証・スターターキット
| パッケージ | 用途 |
|---|---|
| Laravel Breeze | シンプルな認証スターターキット(セッション認証) |
| Laravel Sanctum | SPA・モバイル向け API 認証(トークン認証) |
| Laravel Passport | OAuth2 サーバー実装(大規模 API 向け) |
# Breeze インストール(シンプルな認証 UI)
composer require laravel/breeze --dev
php artisan breeze:install
npm install && npm run dev
php artisan migrate
# Sanctum インストール(API トークン認証)
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
本番デプロイ時の流れ
# ① パッケージインストール(本番用のみ)
composer install --no-dev --optimize-autoloader
# ② 環境設定
php artisan key:generate
php artisan config:cache
php artisan route:cache
php artisan view:cache
# ③ マイグレーション
php artisan migrate --force
# ④ フロントエンドビルド(必要な場合)
npm ci
npm run build
require-dev パッケージ(phpunit など)を除外して本番を軽くします。npm の --production に相当します。Composer とは
Composer とは?
Composer は PHP のパッケージ管理ツールです。npm の PHP 版と考えると分かりやすいです。composer.json に依存パッケージを記述し、composer install でライブラリを自動的にダウンロード・管理します。パッケージは Packagist(PHP のレジストリ)から取得します。
Composer = PHP の npm
Packagist = PHP の npm レジストリ
composer.json = package.json
vendor/ = node_modules/
npm との完全対比
| npm(Node.js) | Composer(PHP) | 役割 |
|---|---|---|
| npm | composer | パッケージ管理ツール |
| npm レジストリ | Packagist | パッケージ公開・配布場所 |
| package.json | composer.json | 設定・依存関係ファイル |
| package-lock.json | composer.lock | バージョン固定ファイル |
| node_modules/ | vendor/ | パッケージ本体の格納先 |
| npm install pkg | composer require pkg | パッケージ追加 |
| npm install -D pkg | composer require --dev pkg | 開発用パッケージ追加 |
| npm install | composer install | 全パッケージをインストール |
| npm ci | composer install(lock 基準) | lock ファイルどおりにインストール |
| npm install --production | composer install --no-dev | 本番用のみインストール |
| import / require | use / require autoload.php | パッケージを読み込む |
パッケージを使う流れ
# ① プロジェクト初期化
composer init // composer.json 作成
# ② パッケージインストール
composer require guzzlehttp/guzzle
// ③ vendor/guzzlehttp/guzzle/ に保存
// ④ composer.json の require に記録
// ⑤ composer.lock 更新
# ⑥ コードで読み込む
require __DIR__ . '/vendor/autoload.php'; // オートロード読み込み(1回だけ)
use GuzzleHttp\Client; // クラスを use で指定
$client = new Client();
$response = $client->get('https://api.example.com/users');
require("pkg") や import で即使えますが、PHP では vendor/autoload.php を1回読み込んでから use クラス名 で使います。composer.json の詳細
{
"name": "my/app",
"description": "My Laravel Application",
"require": {
"php": "^8.2",
"laravel/framework": "^11.0",
"guzzlehttp/guzzle": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"laravel/pint": "^1.0",
"fakerphp/faker": "^1.9"
},
"autoload": {
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
]
}
}
| フィールド | 内容 | npm 対応 |
|---|---|---|
| require | 本番用パッケージ | dependencies |
| require-dev | 開発用パッケージ | devDependencies |
| autoload | 本番クラスの自動読み込み設定 | — |
| autoload-dev | 開発用クラスの自動読み込み設定 | — |
| scripts | Composer コマンド実行時のフック | scripts |
require vs require-dev
| 項目 | require | require-dev |
|---|---|---|
| 用途 | 本番でも必要なパッケージ | 開発・テスト時のみ必要 |
| 本番に含まれる | ✅ | ❌(--no-dev で除外) |
| 追加コマンド | composer require pkg | composer require --dev pkg |
| require の例 | require-dev の例 |
|---|---|
| laravel/framework, guzzlehttp/guzzle | phpunit/phpunit, laravel/pint, fakerphp/faker |
# 本番用のみインストール(require-dev を除外)
composer install --no-dev --optimize-autoloader
composer install vs composer update
| 項目 | composer install | composer update |
|---|---|---|
| 参照先 | composer.lock(固定バージョン) | composer.json(範囲内で最新) |
| lock の更新 | しない | する |
| 用途 | 既存環境の再現(CI・本番・チーム開発) | パッケージを意図的に更新したいとき |
| npm の対応 | npm ci | npm update |
composer install を実行します(update は意図しないバージョン変更が起きるため通常は使いません)。オートロード(PSR-4)とは
PHP でクラスを require なしに自動で読み込む仕組みです。Composer が vendor/autoload.php を生成し、名前空間とディレクトリを対応付けます。
// autoload 設定(composer.json)
"autoload": {
"psr-4": {
"App\\": "app/" // App\ 名前空間 → app/ フォルダに対応
}
}
// 使い方:vendor/autoload.php を1回読み込むだけで OK
require __DIR__ . '/vendor/autoload.php';
use App\Models\User; // app/Models/User.php が自動で読まれる
use GuzzleHttp\Client; // vendor/guzzlehttp/... が自動で読まれる
$user = new User();
$client = new Client();
# クラスの追加・変更後はオートロードを再生成
composer dump-autoload
composer dump-autoload -o # 本番向け最適化版
composer.lock と vendor/ の扱い
composer.lock:インストールされたパッケージの正確なバージョンを記録。Git に含める(チーム全員が同じバージョンを使えるようにする)vendor/:パッケージ本体が入るフォルダ。容量が大きいため .gitignore に追加して Git 管理外にする。clone 後にcomposer installで再生成する
Composer コマンド一覧
| コマンド | 意味 |
|---|---|
| composer init | composer.json を対話形式で作成 |
| composer install | composer.lock に従って全パッケージをインストール |
| composer install --no-dev | require のみインストール(本番用) |
| composer update | composer.json の範囲で最新に更新 |
| composer require pkg | パッケージを追加(require に記録) |
| composer require --dev pkg | 開発用パッケージを追加(require-dev に記録) |
| composer remove pkg | パッケージを削除 |
| composer show | インストール済みパッケージ一覧 |
| composer dump-autoload | オートロードを再生成 |
| composer dump-autoload -o | 本番向け最適化でオートロード再生成 |
| composer create-project laravel/laravel app | Laravel プロジェクトを作成 |
composer.json が設定、vendor/ が本体、composer install で再現。Git には composer.lock を含め、vendor/ は除外するのが基本です。ルーティング
ルーティングとは?
ルーティングとは、URL とプログラムを紐付ける仕組みのことです。ユーザーが特定の URL にアクセスしたときに、どのプログラム(処理)を実行するかを決定します。
例えば、https://example.com/about にアクセスしたら「会社概要ページ」を表示し、https://example.com/contact にアクセスしたら「お問い合わせページ」を表示する、といった振り分けをルーティングが担当します。
ルートファイルの場所
Laravel では、ルーティングの設定は以下のファイルで行います:
| ファイル | 用途 |
|---|---|
routes/web.php | Web ページ用のルート(セッション、CSRF 保護あり) |
routes/api.php | API 用のルート(Laravel 11 以降はデフォルト非存在) |
api.php は php artisan install:api コマンドで作成できます。最もシンプルなルーティング
// routes/web.php
Route::get('/hello', function () {
return 'Hello, World!';
});
| コード | 説明 |
|---|---|
Route::get() | GET リクエストを処理するルートを定義 |
'/hello' | アクセスする URL のパス |
function () { ... } | 実行される処理(クロージャ) |
return 'Hello, World!' | ブラウザに表示される内容 |
ビュー(View)を返す
HTML が複雑になると、ルートファイルに直接書くのは大変です。そこで ビュー(View) という仕組みを使います。
// routes/web.php
Route::get('/company', function () {
return view('company');
});
view('company') は resources/views/company.blade.php というファイルを読み込んで表示します。
動的なルート(パラメータ)
// 基本的なパラメータ
Route::get('/user/{id}', function ($id) {
return 'ユーザーID: ' . $id;
});
// 複数のパラメータ
Route::get('/post/{category}/{id}', function ($category, $id) {
return "カテゴリ: {$category}, 記事ID: {$id}";
});
// オプションパラメータ(? を付けると省略可能)
Route::get('/greeting/{name?}', function ($name = 'ゲスト') {
return "こんにちは、{$name}さん";
});
| URL | 結果 |
|---|---|
| /user/1 | ユーザーID: 1 |
| /post/tech/123 | カテゴリ: tech, 記事ID: 123 |
| /greeting/太郎 | こんにちは、太郎さん |
| /greeting | こんにちは、ゲストさん |
HTTP メソッドの種類
Route::get('/users', function () { // 一覧を表示
return 'ユーザー一覧';
});
Route::post('/users', function () { // 新規作成
return 'ユーザーを作成しました';
});
Route::put('/users/{id}', function ($id) { // 更新
return "ユーザー{$id}を更新しました";
});
Route::delete('/users/{id}', function ($id) { // 削除
return "ユーザー{$id}を削除しました";
});
| メソッド | 用途 |
|---|---|
Route::get() | データを取得する(ページ表示など) |
Route::post() | データを送信する(フォーム送信など) |
Route::put() / patch() | データを更新する |
Route::delete() | データを削除する |
ルート名(Named Routes)
ルートに名前を付けることで、後から URL を変更しても影響を受けにくくなります。
// ルート名の定義
Route::get('/profile', function () {
return view('profile');
})->name('profile');
// ビューでの使用
// <a href="{{ route('profile') }}">プロフィール</a>
// コントローラーでのリダイレクト
// return redirect()->route('profile');
// パラメータ付きルート名
Route::get('/user/{id}', function ($id) {
return view('user', ['id' => $id]);
})->name('user.show');
// <a href="{{ route('user.show', ['id' => 1]) }}">ユーザー1</a>
routes/web.php の1箇所だけ修正すればOKです。実務ではほとんどのルートに名前を付けるのが一般的です。ルートグループ(Route Group)
複数のルートに共通の設定を適用したい場合、ルートグループを使うと便利です。
// プレフィックス + ルート名 + ミドルウェアをまとめて指定
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
Route::get('/dashboard', function () {
return view('admin.dashboard');
})->name('dashboard'); // ルート名: admin.dashboard
Route::get('/users', function () {
return view('admin.users');
})->name('users'); // ルート名: admin.users
});
// アクセスする URL
// /admin/dashboard → ルート名: admin.dashboard
// /admin/users → ルート名: admin.users
| よくある使い方 | 例 |
|---|---|
| 管理画面のまとめ | prefix('admin') |
| API バージョン管理 | prefix('api/v1') |
| 多言語対応 | prefix('ja') / prefix('en') |
php artisan route:list で登録済みの全ルートを一覧表示できます。HTTP メソッド・URI・ルート名・アクションが確認できます。Express との比較(Todo API)
同じ Todo の CRUD ルートを Express と Laravel で書き比べてみましょう。
Express(Node.js)
// routes/todo.js
todoRouter
.route("/")
.post(authHandler, validator(createTodoSchema), (req, res, next) => {
todoCreateController.create(req, res, next);
})
.get(authHandler, validator(getTodosSchema), (req, res, next) => {
todosGetController.list(req, res, next);
});
todoRouter
.route("/:id")
.get(authHandler, validator(requestIdSchema), (req, res, next) => {
todoGetController.find(req, res, next);
})
.put(authHandler, validator(updateTodoSchema), (req, res, next) => {
todoUpdateController.update(req, res, next);
})
.delete(authHandler, validator(requestIdSchema), (req, res, next) => {
todoDeleteController.delete(req, res, next);
});
Laravel(PHP)
// routes/web.php
Route::prefix('todos')->name('todos.')->middleware(['auth'])->group(function () {
// POST /todos - Todo 作成
Route::post('/', function (Request $request) {
return view('todos.create');
})->name('create');
// GET /todos - Todo 一覧
Route::get('/', function (Request $request) {
return view('todos.list');
})->name('list');
// GET /todos/{id} - Todo 単体
Route::get('/{id}', function (string $id) {
return view('todos.find', ['id' => $id]);
})->name('find');
// PUT /todos/{id} - Todo 更新
Route::put('/{id}', function (Request $request, string $id) {
return view('todos.update', ['id' => $id]);
})->name('update');
// DELETE /todos/{id} - Todo 削除
Route::delete('/{id}', function (string $id) {
return view('todos.delete', ['id' => $id]);
})->name('delete');
});
| 役割 | Express | Laravel |
|---|---|---|
| 認証チェック | authHandler ミドルウェア | middleware(['auth']) |
| バリデーション | validator(schema) ミドルウェア | $request->validate() |
| URL のまとめ | router.route("/") チェーン | Route::prefix()->group() |
| 処理の定義 | 各コントローラーのインスタンス | クロージャ または コントローラーメソッド |
| 画面を返す | res.render('view') | return view('todos.list') |
| URL パラメータ | req.params.id | ルート引数 $id |
ビュー(View)
ビューとは?
ビューとは、HTML を記述するための専用ファイルのことです。プログラムのロジック(処理)と表示(HTML)を分離することで、コードの保守性が向上します。
前章のルーティングでは、HTML をルートファイルに直接書いていましたが、実際の開発では複雑な HTML になるため、ビューファイルに分けて管理します。
ビューファイルの場所
| 項目 | 内容 |
|---|---|
| 配置場所 | resources/views/ |
| 拡張子 | .blade.php(Blade テンプレートエンジン) |
| 呼び出し | view('ファイル名')(.blade.php は省略) |
基本的なビューの使い方
① ビューファイルを作成
<!-- resources/views/hello.blade.php -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Hello Page</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>これはビューファイルから表示されています。</p>
</body>
</html>
② ルートからビューを返す
// routes/web.php
Route::get('/hello', function () {
return view('hello');
});
ビューにデータを渡す(3つの方法)
// 方法1: 第2引数に配列で渡す
Route::get('/todos', function () {
$todos = ['買い物', '洗濯', '掃除'];
return view('todos.index', ['todos' => $todos]);
});
// 方法2: with() メソッドを使う
Route::get('/todos', function () {
$todos = ['買い物', '洗濯', '掃除'];
return view('todos.index')->with('todos', $todos);
});
// 方法3: compact() を使う(複数変数に便利)
Route::get('/todos/{id}', function ($id) {
$todo = '買い物';
$done = false;
return view('todos.show', compact('id', 'todo', 'done'));
});
compact() が便利です。compact() とは?
compact() は、変数名の文字列からキーと値のペアを自動で作る PHP の組み込み関数です。
通常の配列渡しと比べてみましょう:
// ❌ 配列で書くと、変数名を2回書く必要がある
$title = 'Todo 一覧';
$todos = ['買い物', '洗濯', '掃除'];
$user = 'Taro';
return view('todos.index', [
'title' => $title, // 'title' と $title で同じ名前を2回書いている
'todos' => $todos, // 'todos' と $todos で同じ名前を2回書いている
'user' => $user, // 'user' と $user で同じ名前を2回書いている
]);
// ✅ compact() を使うと、変数名を1回書くだけでOK
return view('todos.index', compact('title', 'todos', 'user'));
compact('title', 'todos', 'user') は内部的に以下と同じです:
['title' => $title, 'todos' => $todos, 'user' => $user]
compact('変数名') に渡す文字列は、必ず同名の変数が存在する必要があります。$title があれば compact('title') ✅ / $title がないのに compact('title') はエラー ❌
Blade の変数表示と XSS 対策
{{-- 変数の出力(XSS エスケープあり・通常はこちらを使う) --}}
{{ $todo->title }}
{{-- エスケープなし(信頼できるHTMLのみ) --}}
{!! $todo->description !!}
| 構文 | エスケープ | 用途 |
|---|---|---|
{{ }} | あり(安全) | ユーザー入力・通常の変数表示 |
{!! !!} | なし(危険) | 管理者が作成した HTML のみ |
{!! $comment !!} に <script>alert('攻撃')</script> が入ると JavaScript が実行されます。{{ $comment }} なら文字列としてそのまま表示され安全です。99% のケースで
{{ }} を使えば問題ありません。Blade の制御構文
{{-- if 文 --}}
@if ($todo->done)
<span>完了</span>
@elseif ($todo->inProgress())
<span>進行中</span>
@else
<span>未着手</span>
@endif
{{-- foreach:Todo 一覧 --}}
@foreach ($todos as $todo)
<li>{{ $todo->title }}</li>
@endforeach
{{-- forelse:空のときのメッセージ付き --}}
@forelse ($todos as $todo)
<li>{{ $todo->title }}</li>
@empty
<p>Todo がありません</p>
@endforelse
@foreach に「データが空の場合の処理」を追加したものです。一覧表示では
@forelse を使うのが実務の定番です。ループ変数 $loop
@foreach ($todos as $todo)
<p>{{ $loop->iteration }}件目: {{ $todo->title }}</p>
@if ($loop->first)
<span>最初の Todo</span>
@endif
@if ($loop->last)
<span>最後の Todo</span>
@endif
@endforeach
| 変数 | 内容 |
|---|---|
$loop->index | 0 から始まるインデックス |
$loop->iteration | 1 から始まる回数 |
$loop->first | 最初の要素か |
$loop->last | 最後の要素か |
$loop->count | 要素の総数 |
サブディレクトリによる整理(Todo の場合)
resources/views/
├── todos/
│ ├── index.blade.php ← Todo 一覧
│ ├── show.blade.php ← Todo 詳細
│ ├── create.blade.php ← Todo 作成フォーム
│ └── edit.blade.php ← Todo 編集フォーム
└── layouts/
└── app.blade.php ← 共通レイアウト
// ドット記法でアクセス
Route::get('/todos', function () {
return view('todos.index'); // todos/index.blade.php
});
Route::get('/todos/{id}', function ($id) {
return view('todos.show', compact('id')); // todos/show.blade.php
});
レイアウトの共通化(@extends / @yield / @section)
{{-- 仕組みのイメージ --}}
親ファイル (layouts/app.blade.php)
├── <header>ヘッダー</header>
├── @yield('content') ← ここに子ページの内容が入る
└── <footer>フッター</footer>
↑ @extends で継承
子ファイル (todos/index.blade.php)
└── @section('content') ... @endsection
① 親ファイル(共通レイアウト)
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Todo App')</title>
</head>
<body>
<header><h1>Todo App</h1></header>
<main>
@yield('content')
</main>
<footer><p>© 2025 Todo App</p></footer>
</body>
</html>
② 子ファイル(各ページ)
<!-- resources/views/todos/index.blade.php -->
@extends('layouts.app')
@section('title', 'Todo 一覧')
@section('content')
<h2>Todo 一覧</h2>
@forelse ($todos as $todo)
<li>{{ $todo->title }}</li>
@empty
<p>Todo がありません</p>
@endforelse
@endsection
| ディレクティブ | 意味 |
|---|---|
@extends | 親レイアウトを継承(必ず1行目) |
@yield('name') | 親ファイルに「穴」を開ける |
@section('name') | 穴に埋める内容を指定 |
@endsection | @section の終わり |
コンポーネント(再利用可能な UI 部品)
コンポーネントは resources/views/components/ に配置します。<x-コンポーネント名> で呼び出せます。
<!-- resources/views/components/button.blade.php -->
@props(['type' => 'primary'])
@php
$styles = [
'primary' => 'background:#3b82f6; color:white;',
'danger' => 'background:#ef4444; color:white;',
];
@endphp
<button style="padding:8px 16px; border-radius:4px; border:none; cursor:pointer; {{ $styles[$type] ?? $styles['primary'] }}">
{{ $slot }}
</button>
<!-- 使用例(Todo の操作ボタン) -->
<x-button type="primary">保存</x-button>
<x-button type="danger">削除</x-button>
| 記法 | 説明 |
|---|---|
@props() | 受け取る属性とデフォルト値を定義 |
{{ $slot }} | タグの間の内容(例: 「保存」) |
<x-slot:footer> | 名前付きスロット(複数領域を渡す) |
フォーム関連のディレクティブ
<!-- Todo 作成フォーム -->
<form method="POST" action="/todos">
@csrf
<input type="text" name="title" placeholder="Todo を入力">
<button type="submit">追加</button>
</form>
<!-- Todo 更新フォーム(PUT の擬似指定) -->
<form method="POST" action="/todos/{{ $todo->id }}">
@csrf
@method('PUT')
<input type="text" name="title" value="{{ $todo->title }}">
<button type="submit">更新</button>
</form>
<!-- Todo 削除フォーム(DELETE の擬似指定) -->
<form method="POST" action="/todos/{{ $todo->id }}">
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
HTML フォームは GET/POST しか使えないため、@method('PUT') などで PUT/DELETE を擬似的に指定します。
Todo CRUD のビュー全体像
Express の todoRouter と対応する Laravel のビュー構成を比べてみましょう。
Express(Node.js)
// routes/todo.js
todoRouter
.route("/")
.post(authHandler, validator(createTodoSchema), (req, res, next) => {
todoCreateController.create(req, res, next); // 作成
})
.get(authHandler, validator(getTodosSchema), (req, res, next) => {
todosGetController.list(req, res, next); // 一覧
});
todoRouter
.route("/:id")
.get(authHandler, validator(requestIdSchema), (req, res, next) => {
todoGetController.find(req, res, next); // 詳細
})
.put(authHandler, validator(updateTodoSchema), (req, res, next) => {
todoUpdateController.update(req, res, next); // 更新
})
.delete(authHandler, validator(requestIdSchema), (req, res, next) => {
todoDeleteController.delete(req, res, next); // 削除
});
Laravel のルート+ビューの対応
// routes/web.php
Route::prefix('todos')->name('todos.')->middleware(['auth'])->group(function () {
Route::get('/', fn() => view('todos.index')) ->name('index'); // 一覧
Route::get('/create', fn() => view('todos.create')) ->name('create'); // 作成フォーム
Route::post('/', fn(Request $r) => view('todos.index')) ->name('store'); // 作成処理
Route::get('/{id}', fn($id) => view('todos.show', compact('id'))) ->name('show'); // 詳細
Route::get('/{id}/edit', fn($id) => view('todos.edit', compact('id'))) ->name('edit'); // 編集フォーム
Route::put('/{id}', fn(Request $r, $id) => view('todos.show', compact('id'))) ->name('update'); // 更新
Route::delete('/{id}',fn($id) => view('todos.index')) ->name('destroy'); // 削除
});
ビューファイル構成
resources/views/todos/
├── index.blade.php ← Todo 一覧(GET /todos)
├── show.blade.php ← Todo 詳細(GET /todos/{id})
├── create.blade.php ← 作成フォーム(GET /todos/create)
└── edit.blade.php ← 編集フォーム(GET /todos/{id}/edit)
index.blade.php(一覧)の例
@extends('layouts.app')
@section('title', 'Todo 一覧')
@section('content')
<h2>Todo 一覧</h2>
<a href="{{ route('todos.create') }}">新規作成</a>
@forelse ($todos as $todo)
<div>
<span>{{ $todo->title }}</span>
<a href="{{ route('todos.show', $todo->id) }}">詳細</a>
<a href="{{ route('todos.edit', $todo->id) }}">編集</a>
<form method="POST" action="{{ route('todos.destroy', $todo->id) }}">
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
</div>
@empty
<p>Todo がありません</p>
@endforelse
@endsection
| 役割 | Express | Laravel View |
|---|---|---|
| 一覧表示 | todosGetController.list() | view('todos.index') + @forelse |
| 詳細表示 | todoGetController.find() | view('todos.show', compact('id')) |
| 作成フォーム | (別途 HTML 返す処理) | view('todos.create') |
| 更新フォーム | (別途 HTML 返す処理) | view('todos.edit', compact('id')) |
| PUT/DELETE 送信 | fetch('/todos/:id', {method:'PUT'}) | @method('PUT') / @method('DELETE') |
| 空データ表示 | 条件分岐を自前で書く | @forelse ... @empty |
resources/views/ に .blade.php で作成。{{ }} で安全に変数表示。@forelse で一覧+空メッセージ。@extends でレイアウト共通化。@csrf / @method でフォーム送信。コントローラー
コントローラーとは?
コントローラーとは、アプリケーションのロジック(処理)をまとめて管理するクラスのことです。ルートファイル(routes/web.php)に直接処理を書くこともできますが、アプリケーションが大きくなると管理が大変になります。コントローラーを使うことで、ルートファイルをシンプルに保ち、処理を整理して管理できます。
コントローラーは MVC の「C」に相当します。ルーターから受け取ったリクエストを処理し、モデルからデータを取得してビューに渡す役割を担います。
| 役割 | 担当 | 例 |
|---|---|---|
| Model(モデル) | データベースとのやり取り | 商品データの取得・保存 |
| View(ビュー) | 画面表示(HTML) | 商品一覧ページの表示 |
| Controller(コントローラー) | ビジネスロジック | リクエスト受け取り→データ取得→ビューへ渡す |
コントローラーを使わない場合の問題点
ルートファイルに直接処理を書くと、ファイルが肥大化して管理・テストがしにくくなります。
// routes/web.php(NG例:処理が増えるほど読みにくくなる)
Route::get('/products', function () {
$products = ['ノートPC', 'マウス', 'キーボード'];
return view('products.index', compact('products'));
});
Route::post('/products', function () {
// バリデーション処理
// データベース保存処理
// リダイレクト処理
return redirect('/products');
});
コントローラーに処理を移すことで、ルートファイルは「どのURLにどの処理を割り当てるか」という 交通整理だけを担当 するシンプルな形になります。
コントローラーの作成(Artisan コマンド)
# シンプルなコントローラー
php artisan make:controller ProductController
# リソースコントローラー(CRUD 7メソッド付き)
php artisan make:controller ProductController --resource
# API コントローラー(create/edit フォーム表示なし)
php artisan make:controller ProductController --api
作成されたファイルは app/Http/Controllers/ProductController.php に配置されます。
基本的な使い方
// app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProductController extends Controller
{
// 商品一覧を表示
public function index()
{
$products = [
['id' => 1, 'name' => 'ノートPC', 'price' => 120000],
['id' => 2, 'name' => 'マウス', 'price' => 3000],
['id' => 3, 'name' => 'キーボード', 'price' => 8000],
];
return view('products.index', compact('products'));
}
// 商品詳細を表示
public function show($id)
{
$products = [
1 => ['id' => 1, 'name' => 'ノートPC', 'price' => 120000, 'description' => '高性能なノートパソコンです'],
2 => ['id' => 2, 'name' => 'マウス', 'price' => 3000, 'description' => 'ワイヤレスマウスです'],
];
$product = $products[$id] ?? null;
if (!$product) {
abort(404, '商品が見つかりません');
}
return view('products.show', compact('product'));
}
}
ルートからコントローラーを呼び出す
// routes/web.php
use App\Http\Controllers\ProductController;
Route::get('/products', [ProductController::class, 'index']);
Route::get('/products/{id}', [ProductController::class, 'show']);
::class とは? クラスの完全修飾名を文字列で取得する PHP の記法です。UserController::class は 'App\Http\Controllers\UserController' と同じ意味です。タイプミスを防ぎ、IDE の補完も効くため Laravel 8 以降では推奨されています。リソースコントローラー(CRUD)
CRUD(Create / Read / Update / Delete)操作を行う 7 つのメソッドを一括生成できます。
| 操作 | 意味 | SQL | HTTPメソッド |
|---|---|---|---|
| Create | 作成 | INSERT | POST |
| Read | 読取 | SELECT | GET |
| Update | 更新 | UPDATE | PUT / PATCH |
| Delete | 削除 | DELETE | DELETE |
// --resource で7メソッドが自動生成される
php artisan make:controller ProductController --resource
class ProductController extends Controller
{
public function index() { /* 一覧表示 GET /products */ }
public function create() { /* 作成フォーム表示 GET /products/create */ }
public function store() { /* データ保存 POST /products */ }
public function show() { /* 詳細表示 GET /products/{id} */ }
public function edit() { /* 編集フォーム表示 GET /products/{id}/edit */ }
public function update() { /* データ更新 PUT /products/{id} */ }
public function destroy() { /* データ削除 DELETE /products/{id} */ }
}
リソースルートの定義(1行で7ルート)
// routes/web.php
Route::resource('products', ProductController::class);
| HTTPメソッド | URI | アクション | 用途 |
|---|---|---|---|
| GET | /products | index | 一覧表示 |
| GET | /products/create | create | 新規作成フォーム |
| POST | /products | store | データ保存 |
| GET | /products/{id} | show | 個別表示 |
| GET | /products/{id}/edit | edit | 編集フォーム |
| PUT/PATCH | /products/{id} | update | データ更新 |
| DELETE | /products/{id} | destroy | データ削除 |
<form> は GET と POST しか対応していません。Laravel では @method('PUT') / @method('DELETE') ディレクティブを使います。Blade が内部的に <input type="hidden" name="_method" value="PUT"> を生成してくれます。<!-- 更新フォーム -->
<form action="/products/{{ $id }}" method="POST">
@csrf
@method('PUT')
<input type="text" name="name">
<button type="submit">更新</button>
</form>
<!-- 削除フォーム -->
<form action="/products/{{ $id }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">削除</button>
</form>
フォームデータの受け取り(Request オブジェクト)
コントローラーのメソッドに Request $request と書くと、Laravel が自動的に Request オブジェクトを生成して渡してくれます(依存性注入 / Dependency Injection)。
public function store(Request $request)
{
// フォームから送信されたデータを取得
$name = $request->input('name');
$price = $request->input('price');
$description = $request->input('description');
// 全データを取得
$all = $request->all();
// 特定のキーだけ取得
$data = $request->only(['name', 'price']);
// デフォルト値を指定
$category = $request->input('category', '未分類');
}
name 属性との対応: $request->input('name') で取得できる値は、HTML フォームの name 属性の値に対応しています。name 属性と input() の引数は完全一致が必要です。バリデーション(入力チェック)
$request->validate() でフォームの入力値を検証できます。バリデーション失敗時は、Laravel が 自動的に元のページへリダイレクトし、エラーメッセージと入力値をセッションに保存します。
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|max:100',
'price' => 'required|integer|min:0|max:10000000',
'description' => 'required|max:500',
]);
// バリデーション成功時のみここに到達
return redirect('/products')->with('success', "商品「{$validated['name']}」を登録しました!");
}
| ルール | 意味 |
|---|---|
required | 必須項目 |
max:n | 最大文字数(文字列)/ 最大値(数値) |
min:n | 最小値 |
integer | 整数のみ |
email | メールアドレス形式 |
unique:テーブル名 | 一意性チェック(DB使用時) |
ビューでエラーを表示する
<!-- エラー一覧 -->
@if($errors->any())
<ul>
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
<!-- フィールド個別のエラー -->
@error('name')
<span style="color:red">{{ $message }}</span>
@enderror
<!-- old() で前回の入力値を復元 -->
<input type="text" name="name" value="{{ old('name') }}">
$errors と old() が自動的に使えるようになります。開発者が手動でリダイレクトを書く必要はありません。エラーメッセージの日本語化
# Laravel-lang パッケージをインストール(推奨)
composer require laravel-lang/common
php artisan lang:add ja
# config/app.php でロケールを変更
'locale' => 'ja',
リダイレクトとフラッシュメッセージ(PRGパターン)
フォーム送信後はビューを直接返さず、リダイレクトを使うのが正しいパターンです。
| view を直接返す(NG) | redirect する(OK) | |
|---|---|---|
| URL | POST /products のまま | GET /products に変わる |
| F5 リロード | POSTが再送信される(二重登録) | GET が再送信される(安全) |
| ブラウザ履歴 | POST リクエストが残る | GET リクエストが残る |
よく使うリダイレクト
// URL 直接指定
return redirect('/products');
// ルート名で指定(推奨)
return redirect()->route('products.index');
// 一つ前のページに戻る
return back();
// フラッシュメッセージを添付(1回だけ表示)
return redirect('/products')->with('success', '登録しました!');
return redirect('/products')->with('error', '処理に失敗しました');
ビューでフラッシュメッセージを表示
@if(session('success'))
<div style="background:#d4edda; border:1px solid #28a745; padding:15px; border-radius:4px;">
✅ {{ session('success') }}
</div>
@endif
セッションの仕組み
セッションとは、ユーザーごとの一時的なデータ保存場所です。フラッシュメッセージ・ログイン状態・ショッピングカートなどに使われます。
| 保存場所 | 内容 |
|---|---|
| サーバー側(DB / ファイル) | 実際のセッションデータ(ユーザー名・カート内容など) |
| ブラウザ(Cookie) | セッション ID のみ(暗号化済み) |
// データを保存
session(['user_name' => '太郎']);
session()->put('key', 'value');
// データを取得
$name = session('user_name');
$name = session('user_name', 'ゲスト'); // デフォルト値付き
// フラッシュデータ(次のリクエストで1回だけ取得可能)
session()->flash('message', '成功しました');
// 全削除
session()->flush();
@csrf トークンの検証 ② バリデーション失敗時の old() による入力値の保持 ③ $errors のエラーメッセージ ④ フラッシュメッセージ。これらはすべて Laravel がセッションを自動的に使って処理しています。セッションの保存先(SESSION_DRIVER)
| ドライバー | 説明 |
|---|---|
database | DB の sessions テーブル(Laravel 11 以降のデフォルト) |
redis | Redis サーバー(高速・本番推奨) |
file | storage/framework/sessions/(旧デフォルト) |
Express との比較
同じ商品登録の処理を Express と Laravel で書き比べてみましょう。
Express(Node.js)
// controllers/productController.js
const create = (req, res) => {
const { name, price, description } = req.body;
// 手動バリデーション(または express-validator などを使う)
if (!name || !price) {
return res.status(422).render('products/create', {
errors: { name: '商品名と価格は必須です' },
old: req.body,
});
}
// セッションにフラッシュメッセージを保存(connect-flash などを使う)
req.flash('success', `商品「${name}」を登録しました!`);
res.redirect('/products');
};
// routes/products.js
router.get('/products/create', (req, res) => res.render('products/create'));
router.post('/products', productController.create);
Laravel(PHP)
// app/Http/Controllers/ProductController.php
public function create()
{
return view('products.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|max:100',
'price' => 'required|integer|min:0',
]);
// バリデーション失敗時はLaravelが自動でリダイレクト+エラー保存
return redirect('/products')
->with('success', "商品「{$validated['name']}」を登録しました!");
}
// routes/web.php
Route::get('/products/create', [ProductController::class, 'create']);
Route::post('/products', [ProductController::class, 'store']);
| 機能 | Express | Laravel |
|---|---|---|
| コントローラー | 通常の JS 関数をエクスポート | Controller クラスを継承したメソッド |
| リクエストデータ | req.body.name | $request->input('name') |
| バリデーション | 手動 or express-validator | $request->validate() で自動化 |
| バリデーション失敗 | 手動でエラー処理・リダイレクト | Laravel が自動でリダイレクト+エラー保存 |
| フラッシュメッセージ | connect-flash などのライブラリが必要 | ->with('key', '値') で組み込み対応 |
| セッション | express-session ライブラリが必要 | フレームワーク標準機能 |
| CRUD ルート生成 | 手動で定義 | Route::resource() で1行 |
| ルートとコントローラーの紐付け | 関数を直接渡す | [Controller::class, 'method'] |
コントローラーの整理(サブディレクトリ)
コントローラーが増えてきたらサブディレクトリで整理できます。
app/Http/Controllers/
├── Admin/
│ ├── UserController.php
│ └── ProductController.php
├── Api/
│ └── ProductController.php
└── ProductController.php
# サブディレクトリ付きで作成
php artisan make:controller Admin/ProductController
// routes/web.php
use App\Http\Controllers\Admin\ProductController;
Route::get('/admin/products', [ProductController::class, 'index']);
モデル
モデル(Eloquent ORM)とは?
モデルとは、データベースのテーブルを操作するための PHP クラスです。SQL 文を書かずに、PHP のコードでデータベースを操作できます。モデルは MVC の「M」に相当します。
| テーブル名 | モデル名(クラス) |
|---|---|
products | Product |
users | User |
categories | Category |
products テーブルの 1 行が $product オブジェクトに、name カラムが $product->name に対応します。Laravel では Eloquent ORM という名前で実装されています。Ruby on Rails の Active Record、Django の Django ORM、ASP.NET の Entity Framework など、他のフレームワークにも同様の仕組みがあります。モデルの作成
# モデルのみ
php artisan make:model Product
# モデル + マイグレーション
php artisan make:model Product -m
# モデル + マイグレーション + コントローラー + リソース
php artisan make:model Product -mcr
作成されたファイルは app/Models/Product.php に配置されます。
$fillable を設定する(必須)
Product::create([...]) で一括代入できるカラムを明示的に指定します。これを設定しないと MassAssignmentException エラーになります。
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
// ⭐ 一括代入を許可するカラムを指定
protected $fillable = [
'name',
'description',
'price',
'stock',
'is_published',
'category_id',
];
}
is_admin=true のようなパラメーターを送り込み、許可していないカラムを書き換えようとする攻撃(Mass Assignment 攻撃)を防ぐためです。$fillable で許可したカラムだけ受け付けることでこの攻撃を防げます。CRUD 操作(基本メソッド)
Eloquent ORM を使った基本的なデータ操作です。php artisan tinker で実際に試せます。
Read(取得)
// 全件取得
$products = Product::all();
// IDで1件取得
$product = Product::find(1);
// 見つからない場合は 404 エラー
$product = Product::findOrFail($id);
// 条件付き取得
$products = Product::where('price', '>', 10000)->get();
// 最初の1件
$product = Product::where('price', '<', 5000)->first();
// 件数だけ取得
$count = Product::where('price', '>', 10000)->count();
// 並び替え・件数制限
$products = Product::orderBy('price', 'desc')->limit(10)->get();
Create(作成)
// 一括代入で作成($fillable が必要)
$product = Product::create([
'name' => 'Bluetoothキーボード',
'description' => '打ちやすいキーボード',
'price' => 8000,
'stock' => 15,
'is_published' => true,
'category_id' => 1,
]);
Update(更新)
$product = Product::find(1);
// プロパティを変更して保存
$product->price = 7500;
$product->save();
// update() でまとめて更新
$product->update(['price' => 7500, 'stock' => 20]);
Delete(削除)
$product = Product::find(1);
$product->delete();
| 重要度 | メソッド | 説明 |
|---|---|---|
| ★★★ | all() | 全件取得 |
| ★★★ | find($id) | ID で 1 件取得 |
| ★★★ | where()->get() | 条件付き取得 |
| ★★★ | first() | 最初の 1 件 |
| ★★★ | create([...]) | 新規作成 |
| ★★★ | update([...]) | 更新 |
| ★★★ | delete() | 削除 |
| ★★☆ | orderBy() | 並び替え |
| ★★☆ | count() | 件数取得 |
| ★★☆ | with() | リレーション一括取得(Eager Loading) |
| ★☆☆ | pluck('name') | 特定カラムの配列を取得 |
:: と -> の使い分け
初心者がよくつまずくポイントです。シンプルに覚えましょう。
| 記号 | 使う場面 | 例 |
|---|---|---|
::(ダブルコロン) | データを検索・作成する(クラスに対して操作) | Product::where(...) Product::find(1) Product::create([...]) |
->(アロー) | すでにあるオブジェクトを操作する | $product->name $product->save() $product->delete() |
// :: = クラスに対して操作(DBを検索・新規作成)
$product = Product::find(1); // IDで検索
$products = Product::where(...)->get(); // 条件で検索
Product::create([...]); // 新規作成
// -> = 取得済みオブジェクトを操作
echo $product->name; // 値を読む
$product->price = 7500; // 値を変更
$product->save(); // DBに保存
$product->delete(); // 削除
終端メソッド — いつ SQL が実行される?
where() などのメソッドをチェーンしている間は SQL は発行されません。終端メソッドを呼んだ瞬間に SQL が実行されます。
// ここまでは SQL 未実行(クエリを組み立てているだけ)
$query = Product::where('price', '>', 10000)
->where('is_published', true)
->orderBy('created_at', 'desc');
// 終端メソッドを呼んで初めて SQL 実行
$products = $query->get(); // → SELECT * FROM products WHERE ...
$product = $query->first(); // → ... LIMIT 1
$count = $query->count(); // → SELECT COUNT(*)
| 種類 | メソッド例 | SQL 実行 |
|---|---|---|
| 終端メソッド | get() first() find() count() pluck() exists() | ✅ 実行される |
| チェーン可能 | where() orderBy() limit() select() with() | ❌ 実行されない |
toSql() で SQL 文を確認できる)・同じクエリを複数の終端メソッドで再利用できる。// 条件を動的に組み立てる実例
$query = Product::query();
if ($request->has('min_price')) {
$query->where('price', '>=', $request->min_price);
}
if ($request->has('category_id')) {
$query->where('category_id', $request->category_id);
}
$products = $query->get(); // 最後に1回だけ SQL 実行
なぜ Eloquent ORM を使うのか — 生SQL・クエリビルダーとの比較
Laravel でデータベース操作をする方法は 3 つあります。それぞれの問題点と Eloquent ORM が推奨される理由を理解しましょう。
❌ 方法①:生 SQL(最も危険)
use Illuminate\Support\Facades\DB;
// ✅ プレースホルダーを使えば安全
$products = DB::select('SELECT * FROM products WHERE price > ?', [10000]);
// ❌ 危険!SQLインジェクションの脆弱性
$products = DB::select("SELECT * FROM products WHERE name = '$name'");
⚠️ 方法②:クエリビルダー(まだリスクが残る)
// 基本的には安全
$products = DB::table('products')->where('price', '>', 10000)->get();
// ❌ whereRaw() を使うとSQLインジェクションのリスクが残る
$products = DB::table('products')
->whereRaw("name = '$name'") // 危険!
->get();
// リレーション取得には JOIN を手動で書く必要がある
$products = DB::table('products')
->join('categories', 'products.category_id', '=', 'categories.id')
->select('products.*', 'categories.name as category_name')
->get();
// 結果は配列。$product->category->id には アクセスできない
✅ 方法③:Eloquent ORM(推奨)
// ユーザー入力を使っても自動でエスケープされる(SQLインジェクション対策済み)
$products = Product::where('name', $request->name)->get();
// リレーションはメソッドで簡単に取得
$products = Product::with('category')->get();
foreach ($products as $product) {
echo $product->category->name; // オブジェクトとしてアクセス可能
}
| 比較項目 | 生 SQL | クエリビルダー | Eloquent ORM |
|---|---|---|---|
| SQLインジェクション対策 | 手動 | whereRaw で危険 | 自動(安全) |
| リレーション取得 | JOIN 手書き | join() を手書き | with() で簡単 |
| タイムスタンプ自動管理 | 手動 | 手動 | 自動 |
| モデルのカスタムメソッド | 使えない | 使えない | 使える |
| IDE 補完・型チェック | 弱い | 弱い | 強い |
リレーションシップ
テーブル間の関連(リレーション)をモデルに定義することで、関連データを直感的に操作できます。
| メソッド | 関係性 | 外部キーの場所 | 例 |
|---|---|---|---|
hasMany() | 1 対多(1つが複数を持つ) | 子テーブル側 | 1つのカテゴリーは複数の商品を持つ |
belongsTo() | 多 対 1(多くが1つに属する) | 自テーブル側 | 複数の商品は1つのカテゴリーに属する |
hasOne() | 1 対 1(1つが1つを持つ) | 子テーブル側 | 1つの商品は1つの詳細情報を持つ |
belongsToMany() | 多 対 多 | 中間テーブル | 複数の商品に複数のタグが付く |
外部キー(xxx_id)を自分のテーブルに持っている側」が belongsTo() を使います。外部キーを持たない側は hasOne() / hasMany() を使います。1 対多(Category → Product)
// app/Models/Category.php(外部キーを持たない側 → has系)
public function products()
{
return $this->hasMany(Product::class);
}
// app/Models/Product.php(category_id を持つ側 → belongsTo)
public function category()
{
return $this->belongsTo(Category::class);
}
// 使い方
$product->category->name; // 商品のカテゴリー名
$category->products; // カテゴリーに属する全商品
1 対 1(Product → ProductDetail)
// app/Models/Product.php(外部キーを持たない側 → hasOne)
public function detail()
{
return $this->hasOne(ProductDetail::class);
}
// app/Models/ProductDetail.php(product_id を持つ側 → belongsTo)
public function product()
{
return $this->belongsTo(Product::class);
}
// 使い方
$product->detail->manufacturer; // 詳細情報にアクセス
if ($product->detail) { /* null チェック必須 */ }
多 対 多(Product ↔ Tag)
中間テーブル(product_tag)が必要です。両側が belongsToMany() を使います。
// app/Models/Product.php
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// app/Models/Tag.php
public function products()
{
return $this->belongsToMany(Product::class);
}
// 使い方
$product->tags->pluck('name'); // タグ名の一覧
$product->tags()->attach($tagId); // タグを追加
$product->tags()->detach($tagId); // タグを削除
$product->tags()->sync([1, 2, 3]); // 指定IDのタグだけにする(それ以外は削除)
| メソッド | 動作 |
|---|---|
attach($id) | 中間テーブルに関連を追加(既存は保持) |
detach($id) | 中間テーブルから関連を削除 |
sync([1,2,3]) | 指定した ID だけにする(それ以外は削除) |
Eager Loading と N+1 問題
with()(Eager Loading)を使わないと、ループのたびに SQL が実行されます(N+1 問題)。
❌ N+1 問題が発生するコード
$products = Product::all(); // SQL 1回
foreach ($products as $product) {
echo $product->category->name; // ← 商品の数だけ SQL が実行される!
}
// 商品が 100 件 → 合計 101 回の SQL(1 + 100)
✅ Eager Loading で解決
$products = Product::with('category')->get(); // SQL 2回で完了
foreach ($products as $product) {
echo $product->category->name; // 追加の SQL なし
}
// 商品が 100 件でも 1000 件でも、常に SQL は 2 回だけ
| 商品数 | Eager Loading なし | Eager Loading あり |
|---|---|---|
| 5 件 | 6 回(5+1) | 2 回 |
| 100 件 | 101 回 | 2 回 |
| 1000 件 | 1001 回 | 2 回 |
with() を使う習慣をつけましょう。 複数のリレーションも with(['category', 'tags']) のようにまとめて指定できます。モデルのカスタムメソッド
ビジネスロジックをモデルにまとめることで、コードの重複を防ぎ可読性が上がります。
class Product extends Model
{
const TAX_RATE_STANDARD = 0.1; // 標準税率 10%
const TAX_RATE_REDUCED = 0.08; // 軽減税率 8%
// 高額商品かどうか判定
public function isExpensive(): bool
{
return $this->price >= 100000;
}
// 在庫があるか確認
public function isInStock(): bool
{
return $this->stock > 0;
}
// フォーマットされた価格を返す
public function getFormattedPrice(): string
{
return '¥' . number_format($this->price);
}
// 税込価格を返す
public function getPriceWithTax(?float $taxRate = null): string
{
$rate = $taxRate ?? self::TAX_RATE_STANDARD;
return '¥' . number_format($this->price * (1 + $rate));
}
}
// 使い方
$product->isExpensive(); // true / false
$product->isInStock(); // true / false
$product->getFormattedPrice(); // "¥120,000"
$product->getPriceWithTax(); // "¥132,000"
$product->getPriceWithTax(Product::TAX_RATE_REDUCED); // "¥129,600"
型キャスト(Casts)
データベースの値を自動的に型変換します。API 開発で特に役立ちます。
protected $casts = [
'price' => 'integer', // 文字列 → 数値
'is_published' => 'boolean', // "1" → true / "0" → false
'options' => 'array', // JSON 文字列 → 配列
'published_at' => 'datetime', // 文字列 → Carbon オブジェクト
];
Express との比較
同じ商品データの操作を Express と Laravel で書き比べてみましょう。
Express(Node.js)— Sequelize ORM を使う場合
// models/Product.js(Sequelize)
const { DataTypes } = require('sequelize');
const sequelize = require('../db');
const Product = sequelize.define('Product', {
name: { type: DataTypes.STRING, allowNull: false },
price: { type: DataTypes.INTEGER, allowNull: false },
description: { type: DataTypes.TEXT },
stock: { type: DataTypes.INTEGER, defaultValue: 0 },
});
// Association(リレーション定義)
Product.belongsTo(Category, { foreignKey: 'category_id' });
Category.hasMany(Product, { foreignKey: 'category_id' });
module.exports = Product;
// コントローラーでの使い方
const products = await Product.findAll({
where: { price: { [Op.gt]: 10000 } },
include: [{ model: Category }], // Eager Loading
order: [['price', 'DESC']],
});
const product = await Product.findByPk(1);
await product.update({ price: 7500 });
await product.destroy();
Laravel(PHP)— Eloquent ORM
// app/Models/Product.php
class Product extends Model
{
protected $fillable = ['name', 'price', 'description', 'stock', 'category_id'];
public function category()
{
return $this->belongsTo(Category::class);
}
}
// コントローラーでの使い方
$products = Product::where('price', '>', 10000)
->with('category') // Eager Loading
->orderBy('price', 'desc')
->get();
$product = Product::find(1);
$product->update(['price' => 7500]);
$product->delete();
| 機能 | Express(Sequelize) | Laravel(Eloquent) |
|---|---|---|
| モデル定義 | sequelize.define() | class extends Model |
| 全件取得 | Product.findAll() | Product::all() |
| ID で取得 | Product.findByPk(1) | Product::find(1) |
| 条件付き取得 | findAll({ where: {...} }) | where()->get() |
| Eager Loading | include: [{ model: Category }] | with('category') |
| 作成 | Product.create({...}) | Product::create([...]) |
| 更新 | product.update({...}) | $product->update([...]) |
| 削除 | product.destroy() | $product->delete() |
| Mass Assignment 保護 | 手動設定が必要 | $fillable で自動保護 |
| 多対多の操作 | 中間テーブルを直接操作 | attach() sync() |
$fillable を必ず設定(Mass Assignment 対策) ② Eloquent ORM を基本とする(SQLインジェクション対策が自動) ③ リレーション使用時は必ず with()(N+1 問題対策) ④ ビジネスロジックはカスタムメソッドとしてモデルにまとめるミドルウェア
ミドルウェアとは?
ミドルウェアは HTTP リクエストとレスポンスの間に処理を挟む仕組みです。認証チェック・ログ記録・CORS 設定・レート制限など、コントローラーに到達する前後に共通処理を行いたい場合に使います。
ミドルウェア作成
php artisan make:middleware CheckAge
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckAge
{
public function handle(Request $request, Closure $next)
{
// リクエスト前の処理
if ($request->age < 18) {
return redirect('/home')->with('error', '18歳以上限定です');
}
// 次の処理へ(コントローラーや次のミドルウェア)
$response = $next($request);
// レスポンス後の処理(ここに書くこともできる)
return $response;
}
}
ミドルウェアの登録・適用
// bootstrap/app.php(Laravel 11)でグローバル登録
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'check.age' => \App\Http\Middleware\CheckAge::class,
]);
})
// ルートへの適用
Route::get('/adults', [AdultController::class, 'index'])
->middleware('check.age');
// グループへの適用
Route::middleware(['auth', 'check.age'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
Laravel 組み込みミドルウェア
| エイリアス | 役割 |
|---|---|
| auth | 認証チェック(未ログインはリダイレクト) |
| guest | 未ログインのみ通過(ログイン画面など) |
| verified | メール認証済みチェック |
| throttle | レート制限(例:throttle:60,1) |
| can | 認可ポリシーチェック |
マイグレーション
マイグレーションとは?
マイグレーションとは、データベースのテーブル構造をコードで管理する仕組みです。SQL を手動で実行せずに、PHP のファイルとしてテーブル定義を記述・管理できます。
❌ マイグレーションなしの問題点
- 手動で SQL を実行してテーブルを作成しなければならない
- チームメンバー間でテーブル構造が異なる
- 本番環境と開発環境で構造が違う
- 変更履歴が追えない
- ロールバック(元に戻す)ができない
✅ マイグレーションのメリット
- バージョン管理 — Git でテーブル構造を管理できる
- 再現性 — コマンド 1 つで同じ構造を作れる
- チーム開発 — 全員が同じ構造で開発できる
- ロールバック — 変更を簡単に元に戻せる
- 履歴管理 — いつ何が変更されたかわかる
データベース接続設定(.env)
マイグレーションを実行する前に .env ファイルでデータベース接続を設定します。
# .env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=your_password
# 接続できるか確認(エラーが出なければ OK)
php artisan migrate:status
# .env を変更した場合はキャッシュをクリア
php artisan config:clear
マイグレーションファイルの作成
# テーブル作成用
php artisan make:migration create_products_table
# カラム追加用
php artisan make:migration add_price_to_products_table
# カラム変更用
php artisan make:migration modify_description_in_products_table
作成されるファイル名例:2025_01_22_123456_create_products_table.php(タイムスタンプ順に実行される)
命名規則
| 目的 | 命名例 |
|---|---|
| テーブル作成 | create_products_table |
| カラム追加 | add_price_to_products_table |
| カラム変更 | modify_description_in_products_table |
| テーブル削除 | drop_old_products_table |
create_xxx_table という名前にすると Schema::create() を使ったテンプレートが、add_xxx_to_yyy_table にすると Schema::table() を使ったテンプレートが自動生成されます。マイグレーションファイルの構造
// database/migrations/2025_01_22_000001_create_products_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
// migrate 実行時に呼ばれる(テーブル作成・カラム追加)
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('商品名');
$table->text('description')->nullable()->comment('商品説明');
$table->integer('price')->comment('価格');
$table->integer('stock')->default(0)->comment('在庫数');
$table->boolean('is_published')->default(false)->comment('公開状態');
$table->timestamps(); // created_at, updated_at を自動作成
});
}
// migrate:rollback 実行時に呼ばれる(up() の逆操作)
public function down(): void
{
Schema::dropIfExists('products');
}
};
よく使うカラム型
| Laravelメソッド | MySQL 型 | 用途 |
|---|---|---|
id() | BIGINT UNSIGNED | 主キー(自動採番) |
string('name') | VARCHAR(255) | 短いテキスト(名前・タイトル・メール) |
text('description') | TEXT | 長いテキスト(説明・本文・コメント) |
integer('price') | INT | 整数(価格・数量・年齢) |
boolean('is_active') | TINYINT(1) | 真偽値(有効/無効・公開/非公開) |
foreignId('user_id') | BIGINT | 外部キー(他テーブルとの関連) |
timestamps() | TIMESTAMP × 2 | created_at, updated_at(自動管理) |
decimal('price', 8, 2) | DECIMAL(8,2) | 小数点付き数値(金額) |
date('birth_date') | DATE | 日付のみ |
json('options') | JSON | JSON データ |
boolean() は実際には TINYINT(1) として作成されます(0 = false / 1 = true)。Laravel が自動的に true/false として扱ってくれるので通常は意識不要です。よく使うカラム修飾子
| 修飾子 | 説明 | 例 |
|---|---|---|
nullable() | NULL 許可(空でも OK) | ->nullable() |
default($value) | デフォルト値を設定 | ->default(0) |
unique() | 一意制約(重複不可) | ->unique() |
comment('説明') | カラムの説明を追加(GUI ツールで確認可能) | ->comment('商品名') |
after('column') | 指定カラムの後に挿入 | ->after('name') |
マイグレーションコマンド一覧
# 未実行のマイグレーションをすべて実行
php artisan migrate
# 実行状態を確認
php artisan migrate:status
# 最後のバッチをロールバック(down() を実行)
php artisan migrate:rollback
# 最後の N バッチをロールバック
php artisan migrate:rollback --step=3
# 全ロールバック → 再実行(down() 経由・遅い)
php artisan migrate:refresh
# 全テーブル DROP → 再実行(高速・推奨)
php artisan migrate:fresh
# テーブル再作成 + シーダーも実行(開発中に最もよく使う)
php artisan migrate:fresh --seed
# 実行せずに SQL だけ表示(確認用)
php artisan migrate --pretend
| コマンド | 動作 | データ消失 |
|---|---|---|
migrate | 未実行分だけ実行 | なし |
migrate:rollback | 最後のバッチを down() で元に戻す | ロールバック分のみ |
migrate:refresh | 全 down() → 再実行(遅い) | 全データ消失 |
migrate:fresh | 全 DROP → 再実行(高速・推奨) | 全データ消失 |
migrate:fresh / migrate:refresh は絶対に使わない: 全データが消えます。本番環境では migrate のみ使用してください。Laravel は APP_ENV=production の場合、migrate 実行前に確認メッセージを表示します(--force でスキップ可)。カラムの追加・変更
既存テーブルへのカラム追加・変更は、既存ファイルを編集せず新しいマイグレーションを作成します。
カラム追加
// Schema::create() ではなく Schema::table() を使う
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->string('category')->nullable()->after('name');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('category');
});
}
カラム変更・リネーム
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
// カラムの型・属性を変更(.change() を付ける)
$table->text('description')->nullable()->change();
$table->string('name', 100)->change();
// カラム名を変更
$table->renameColumn('old_name', 'new_name');
});
}
// down() には逆操作を書く
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->text('description')->change();
$table->string('name', 255)->change();
$table->renameColumn('new_name', 'old_name');
});
}
外部キー制約
外部キーは、あるテーブルのカラムが別テーブルの主キーを参照する仕組みです。データベースレベルでデータの整合性を保証します。
❌ 外部キーなしの問題点
- 存在しないカテゴリー ID を持つ商品が登録できてしまう
- カテゴリーを削除しても、そのカテゴリー ID を持つ商品が残る(孤立データ)
- 整合性チェックをアプリ側で毎回書く必要がある
✅ 外部キーありのメリット
- 不正なデータはデータベースが自動で弾く
- カテゴリー名の変更が 1 箇所で済む(データの正規化)
- 削除時の動作(CASCADE / SET NULL / RESTRICT)を制御できる
- PHP 側での整合性チェックコードが不要になる
書き方(推奨:シンプルな書き方)
// Laravel 7 以降の推奨書き方
$table->foreignId('category_id')
->constrained() // カラム名から categories テーブルを自動推測
->onDelete('restrict'); // 商品がある場合はカテゴリーを削除不可
// テーブル名を明示する場合
$table->foreignId('category_id')
->constrained('categories')
->onDelete('cascade');
onDelete() — 削除時の動作
| 指定値 | 動作 | 実務での用途 |
|---|---|---|
'restrict'(デフォルト) | 子が存在すると親を削除できない | 誤削除防止(カテゴリー・部署など) |
'cascade' | 親を削除すると子も削除 | 注文と注文明細など親なしで意味がないデータ |
'set null' | 親を削除すると子は NULL になる | ユーザーが退会してもコメントは残す場合 |
実践例:カテゴリーと商品のリレーション
// 1. categories テーブルを先に作成(参照元が先に必要)
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('カテゴリー名');
$table->timestamps();
});
// 2. products テーブルに外部キーを設定
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('商品名');
$table->integer('price')->comment('価格');
$table->integer('stock')->default(0);
$table->boolean('is_published')->default(false);
// 外部キー:存在するカテゴリー ID しか登録できない
$table->foreignId('category_id')
->constrained()
->onDelete('restrict');
$table->timestamps();
});
categories を先に、products を後に作成してください。ファイル名のタイムスタンプで実行順序が決まります。シーダー(Seeder)— 初期データの投入
シーダーとは、データベースに初期データを投入する仕組みです。
| 用途 | 例 |
|---|---|
| マスターデータの投入(本番でも使用) | カテゴリー・都道府県・権限・ステータス |
| 開発用テストデータの投入 | サンプル商品・サンプルユーザー |
シーダーの作成と実装
# シーダーを作成
php artisan make:seeder CategorySeeder
php artisan make:seeder ProductSeeder
// database/seeders/CategorySeeder.php
class CategorySeeder extends Seeder
{
public function run(): void
{
DB::table('categories')->insert([
['name' => '家電', 'created_at' => now(), 'updated_at' => now()],
['name' => '食品', 'created_at' => now(), 'updated_at' => now()],
['name' => '衣類', 'created_at' => now(), 'updated_at' => now()],
['name' => '書籍', 'created_at' => now(), 'updated_at' => now()],
]);
}
}
// database/seeders/ProductSeeder.php
class ProductSeeder extends Seeder
{
public function run(): void
{
DB::table('products')->insert([
[
'name' => 'ノートPC',
'description' => '高性能なノートパソコン',
'price' => 120000,
'stock' => 10,
'is_published' => true,
'category_id' => 1, // 家電(CategorySeeder が先に実行されている必要がある)
'created_at' => now(),
'updated_at' => now(),
],
// ... 他の商品
]);
}
}
DatabaseSeeder で実行順序を管理
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
CategorySeeder::class, // 外部キー参照元を先に実行
ProductSeeder::class, // 後に実行
]);
}
}
# 全シーダーを実行(DatabaseSeeder の call() 順に実行)
php artisan db:seed
# 特定のシーダーだけ実行
php artisan db:seed --class=CategorySeeder
# テーブル再作成 + シーダー実行(開発中に最もよく使う)
php artisan migrate:fresh --seed
php artisan migrate:fresh --seed → テーブル再作成 + マスターデータ投入 完了!このワンコマンドでいつでもクリーンな状態に戻せます。Express との比較
Express(Node.js)でも、Sequelize や Knex.js などのライブラリでマイグレーションを管理できます。概念は Laravel と同じです。
Express — Sequelize を使った場合
// migrations/20250122-create-products.js(Sequelize)
'use strict';
module.exports = {
// migrate 相当
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: {
type: Sequelize.BIGINT,
primaryKey: true,
autoIncrement: true,
},
name: {
type: Sequelize.STRING(255),
allowNull: false,
},
price: {
type: Sequelize.INTEGER,
allowNull: false,
},
stock: {
type: Sequelize.INTEGER,
defaultValue: 0,
},
is_published: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
category_id: {
type: Sequelize.BIGINT,
references: { model: 'categories', key: 'id' },
onDelete: 'RESTRICT',
},
created_at: { type: Sequelize.DATE },
updated_at: { type: Sequelize.DATE },
});
},
// rollback 相当
async down(queryInterface) {
await queryInterface.dropTable('products');
},
};
# Sequelize のコマンド
npx sequelize-cli db:migrate # migrate 実行
npx sequelize-cli db:migrate:undo # rollback
npx sequelize-cli db:seed:all # シーダー実行
Laravel(PHP)
// database/migrations/2025_01_22_create_products_table.php
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('price');
$table->integer('stock')->default(0);
$table->boolean('is_published')->default(false);
$table->foreignId('category_id')->constrained()->onDelete('restrict');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
# Laravel のコマンド
php artisan migrate # migrate 実行
php artisan migrate:rollback # rollback
php artisan db:seed # シーダー実行
| 機能 | Express(Sequelize) | Laravel |
|---|---|---|
| マイグレーション実行 | db:migrate | migrate |
| ロールバック | db:migrate:undo | migrate:rollback |
| 全削除→再実行 | 手動 or スクリプト | migrate:fresh |
| シーダー実行 | db:seed:all | db:seed |
| 外部キー参照 | references: { model, key } | ->constrained()(自動推測) |
| タイムスタンプ自動管理 | 手動で定義 | timestamps() 1 行 |
| 記述量 | 多め(JavaScript オブジェクト形式) | 少ない(メソッドチェーン形式) |
up() と down() は必ず対にする
② 一度実行したファイルは編集せず新しいマイグレーションを作成する
③ 外部キーの参照先テーブルを先に作成する
④ 開発中は migrate:fresh --seed でクリーンな状態に戻す
⑤ 本番環境では migrate:fresh / migrate:refresh は絶対に使わない
認証
Laravel 認証ライブラリ一覧
Webアプリケーションには認証機能が必要不可欠です。Laravel には用途に応じた複数の認証ライブラリが用意されています。
| ライブラリ | メカニズム | UI | 主な用途 |
|---|---|---|---|
| Breeze | セッション | ✅ あり | 小〜中規模 Web |
| Jetstream | セッション + 高機能(2FA・チーム管理) | ✅ あり | SaaS / エンタープライズ |
| Fortify | バックエンドのみ | ❌ なし | SPA / カスタム UI |
| Sanctum | トークン(API 認証特化) | ❌ なし | API / モバイル |
| Passport | OAuth2 サーバー | ❌ なし | OAuth 提供側 |
| Socialite | OAuth クライアント(ソーシャルログイン) | ❌ なし | Google / GitHub ログイン |
Breeze(最もシンプル・学習向け)
composer require laravel/breeze
php artisan breeze:install blade # ログイン/登録 UI を生成
npm install && npm run dev # Tailwind CSS のビルド
php artisan migrate # users / sessions / password_reset_tokens テーブルを作成
生成されるもの:認証コントローラー・Blade ビュー・ルート定義・マイグレーション
Sanctum(API 認証)
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
SPA(Vue / React)やモバイルアプリのバックエンド API 認証に使います。Bearer Token と Cookie 認証の両方をサポートします。
プロジェクト規模別の推奨
| プロジェクト | 推奨ライブラリ |
|---|---|
| 小規模 Web アプリ | Breeze + Socialite |
| SaaS アプリ | Jetstream + Socialite |
| API + SPA アプリ | Sanctum + Fortify |
| モバイルアプリ API | Sanctum |
| OAuth 提供サービス | Passport |
なぜスクラッチで学ぶべきか
ライブラリは便利ですが、学習段階では自分で認証機能を作ることが重要です。
| ライブラリに頼る | スクラッチで作る | |
|---|---|---|
| 仕組みの理解 | ブラックボックスになる | セッション・Cookie・ハッシュ化を理解できる |
| カスタマイズ | 難しい | 自由に変更できる |
| エラー対応 | 原因が分からない | 原因を特定できる |
| 面接 | 説明できない | 自信を持って説明できる |
認証の仕組み — セッションとステートレス
HTTP はステートレス(状態を持たない)なプロトコルです。リクエストごとに独立しており、前回のリクエストの情報を覚えていません。そのためセッションでログイン状態を保持します。
// セッションの流れ
// 1. ログイン成功時
// → サーバーがセッション ID を発行(例: abc123)
// → セッション ID を Cookie に保存
// → サーバー側(DB の sessions テーブル)にユーザー ID を保存
// 2. 次回リクエスト時
// → ブラウザが Cookie を自動送信(abc123)
// → サーバーがセッション ID からユーザー情報を取得
// → 「あなたは田中さんですね!」と識別
User モデル — $fillable / $hidden / $casts
Laravel の新規プロジェクトには users テーブルのマイグレーションと User モデルがあらかじめ用意されています。
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
// 一括代入を許可するカラム(セキュリティ対策)
protected $fillable = ['name', 'email', 'password'];
// JSON / 配列変換時に隠すカラム(APIレスポンスでパスワードを隠す)
protected $hidden = ['password', 'remember_token'];
protected $casts = [
'email_verified_at' => 'datetime', // 文字列 → Carbon オブジェクトに自動変換
'password' => 'hashed', // 保存時に自動ハッシュ化(Laravel 10以降)
];
}
$hidden — API レスポンスでパスワードを隠す
// $hidden なしの場合(❌ パスワードが漏れる)
return response()->json($user);
// {"id":1,"name":"田中","email":"...","password":"$2y$10$abc..."}
// $hidden ありの場合(✅ 自動的に除外される)
return response()->json($user);
// {"id":1,"name":"田中","email":"..."}
$casts — 型の自動変換
// cast なし: DB は全て文字列で返す
$user->email_verified_at // "2025-01-01 12:00:00"(文字列)
// 'datetime' cast あり: 自動で Carbon オブジェクトに変換
$user->email_verified_at->format('Y年m月d日'); // "2025年01月01日"
$user->email_verified_at->addDays(7); // 7日後の日時を計算
// 'hashed' cast あり(Laravel 10以降): 保存時に自動ハッシュ化
User::create(['password' => 'password123']); // 平文で渡してOK
// DB には $2y$10$abc... として保存される
'datetime' キャストを使うと文字列が自動で Carbon オブジェクトに変換され、format()・addDays()・diffForHumans()(「3日前」のような相対表現)などが使えます。ユーザー登録機能の実装
php artisan make:controller Auth/RegisterController
// app/Http/Controllers/Auth/RegisterController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RegisterController extends Controller
{
// 登録フォーム表示
public function showRegistrationForm()
{
return view('auth.register');
}
// ユーザー登録処理
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
// 'confirmed' → password_confirmation フィールドと一致するかチェック
// 'unique:users' → DB の重複チェック(バリデーションで先にチェックして分かりやすいエラーを返す)
]);
// $casts の 'hashed' が自動的にパスワードをハッシュ化する
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => $validated['password'],
]);
Auth::login($user); // 登録後に自動ログイン
return redirect()->route('dashboard');
}
}
// routes/web.php
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/register', [RegisterController::class, 'register']);
Facade(ファサード)とヘルパー関数
Facade は複雑なクラスへのシンプルな静的インターフェースです。use でインポートして Auth:: のように使います。
// Facade を使う
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
Auth::login($user); // ログイン
Auth::logout(); // ログアウト
Auth::check(); // ログイン確認(true/false)
Auth::user(); // ログイン中のユーザーオブジェクト
Hash::make('password'); // パスワードをハッシュ化
Hash::check('password', $h); // パスワードを照合
// ヘルパー関数(use 不要・どこでも使える)
auth()->login($user); // Auth::login() と同じ
auth()->check(); // Auth::check() と同じ
auth()->user(); // Auth::user() と同じ
auth()->id(); // ログイン中のユーザー ID
auth() ヘルパーが短くてシンプル。Blade では auth()->user()->name のように直接使えます(use 不要)。よく使う Facade 一覧
| Facade | 主なメソッド | 用途 |
|---|---|---|
Auth:: | login() logout() check() user() | 認証・ログイン状態管理 |
Hash:: | make() check() | パスワードハッシュ化 |
DB:: | table()->get() select() | 生 SQL / クエリビルダー |
Storage:: | exists() delete() download() | ファイル操作 |
Log:: | info() warning() error() | ログ出力 |
Mail:: | to()->send() | メール送信 |
Session:: | put() get() flash() | セッション操作 |
Config:: | get() set() | 設定値取得・変更 |
よく使うヘルパー関数一覧
| 関数 | 用途 |
|---|---|
auth() | 認証(auth()->user() でログインユーザー取得) |
view('name') | Blade ビューを返す |
redirect('/url') | リダイレクト |
route('name') | 名前付きルートの URL 生成 |
old('field') | バリデーションエラー時に前回の入力値を復元 |
session('key') | セッションの読み書き |
now() | 現在日時(Carbon オブジェクト) |
config('key') | 設定値取得(config('app.name')) |
env('KEY') | .env から環境変数取得 |
dd($var) | デバッグ用:変数を表示して処理を止める |
ログイン機能の実装
php artisan make:controller Auth/LoginController
// app/Http/Controllers/Auth/LoginController.php
class LoginController extends Controller
{
public function showLoginForm()
{
return view('auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
// ✅ セッション固定攻撃を防ぐためにセッション ID を再生成
$request->session()->regenerate();
// ログイン前にアクセスしようとしていたページへ(なければダッシュボード)
return redirect()->intended(route('dashboard'));
}
// ❌ 認証失敗(パスワードは復元しない)
return back()->withErrors([
'email' => 'メールアドレスまたはパスワードが正しくありません。',
])->onlyInput('email');
}
}
Auth::attempt() の仕組み: ① email で DB からユーザーを検索 → ② Hash::check() でパスワードを照合 → ③ 成功したらセッションにユーザー ID を保存 → ④ true / false を返すredirect()->intended(): 未ログインで /dashboard にアクセス → auth ミドルウェアが /login にリダイレクト(元の URL をセッションに保存)→ ログイン成功 → intended() が元の URL に戻す。元の URL がなければ引数のデフォルト URL へ。// routes/web.php
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login']);
ログアウト機能の実装
// routes/web.php
use Illuminate\Support\Facades\Auth;
Route::post('/logout', function (Request $request) {
Auth::logout(); // ① セッションから認証情報を削除
$request->session()->invalidate(); // ② セッション ID を無効化(新 ID を発行)
$request->session()->regenerateToken(); // ③ CSRF トークンを再生成
return redirect('/');
})->name('logout');
// Blade での使い方
// <form action="{{ route('logout') }}" method="POST">
// @csrf
// <button type="submit">ログアウト</button>
// </form>
| 処理 | 何をするか | なぜ必要か |
|---|---|---|
Auth::logout() | セッションの payload から認証情報(ユーザー ID)を削除 | ログイン状態を解除する |
invalidate() | セッション ID 自体を無効化し、新しい ID を発行 | セッション固定攻撃を防ぐ |
regenerateToken() | CSRF トークンを新規生成 | 古いフォームからの送信を無効化する |
Auth::logout() だけでもログアウトは機能しますが、セキュリティを重視するなら 3 つセットが推奨です(Laravel 公式も推奨)。ログイン状態の確認
コントローラー・ルートで確認
auth()->check(); // ログインしているか(true / false)
auth()->user(); // ログイン中の User オブジェクト(未ログインなら null)
auth()->id(); // ログイン中のユーザー ID(未ログインなら null)
Blade テンプレートで確認
@auth
<p>ようこそ、{{ auth()->user()->name }}さん!</p>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit">ログアウト</button>
</form>
@endauth
@guest
<a href="{{ route('login') }}">ログイン</a>
<a href="{{ route('register') }}">新規登録</a>
@endguest
ミドルウェアでルートを保護
// 単一ルートを保護
Route::get('/dashboard', fn() => view('dashboard'))
->middleware('auth')
->name('dashboard');
// 複数ルートをまとめて保護
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/profile', [ProfileController::class, 'show']);
Route::get('/settings', [SettingsController::class, 'index']);
});
/login にリダイレクト(元の URL をセッションに保存)セキュリティ考慮事項
① パスワードのハッシュ化
// ❌ 絶対ダメ:平文で保存
User::create(['password' => $request->password]);
// ✅ ハッシュ化して保存(Laravel 10以降は $casts で自動化可能)
User::create(['password' => Hash::make($request->password)]);
② CSRF 保護(フォームには必ず @csrf)
<form method="POST" action="/login">
@csrf <!-- Laravelが自動的にトークンを検証。ないと 419 エラー -->
...
</form>
③ セッション固定攻撃の防止
// ログイン成功時にセッション ID を再生成(必須)
if (Auth::attempt($credentials)) {
$request->session()->regenerate(); // ← これがないと攻撃者が古い ID でアクセス可能
return redirect()->intended(route('dashboard'));
}
④ XSS(クロスサイトスクリプティング)対策
{{-- ✅ 自動的に HTML エスケープされる --}}
<p>{{ $user->name }}</p>
{{-- ❌ エスケープされない(悪意あるスクリプトが実行される可能性)--}}
<p>{!! $user->name !!}</p>
⑤ ブルートフォース攻撃対策(レート制限)
use Illuminate\Support\Facades\RateLimiter;
public function login(Request $request)
{
$key = 'login:' . $request->email . ':' . $request->ip();
// 5回失敗したら 60 秒間ロック
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return back()->withErrors([
'email' => "{$seconds}秒後に再試行してください。",
]);
}
if (Auth::attempt($credentials)) {
RateLimiter::clear($key); // 成功したらリセット
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
RateLimiter::hit($key, 60); // 失敗でカウントアップ
return back()->withErrors(['email' => '認証に失敗しました。'])->onlyInput('email');
}
| 攻撃種別 | 対策 |
|---|---|
| パスワード漏洩 | Hash::make() または 'hashed' キャストで必ずハッシュ化 |
| CSRF 攻撃 | 全フォームに @csrf |
| セッション固定攻撃 | ログイン時に regenerate()・ログアウト時に 3 メソッドセット |
| XSS 攻撃 | Blade で {{ }}(自動エスケープ)を使う |
| SQL インジェクション | Eloquent ORM / クエリビルダーを使う(生 SQL に変数を直接埋め込まない) |
| ブルートフォース攻撃 | RateLimiter でログイン試行回数を制限 |
Express との比較
同じ「ログイン機能」を Express と Laravel で書き比べてみましょう。
Express(Node.js)— passport.js / bcrypt を使った場合
// npm install express-session passport passport-local bcryptjs
const bcrypt = require('bcryptjs');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
// パスワード照合戦略を定義
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
const user = await db.query('SELECT * FROM users WHERE email = ?', [email]);
if (!user) return done(null, false, { message: 'ユーザーが見つかりません' });
const match = await bcrypt.compare(password, user.password);
if (!match) return done(null, false, { message: 'パスワードが違います' });
return done(null, user);
}
));
// ユーザー登録
app.post('/register', async (req, res) => {
const { name, email, password } = req.body;
const hashed = await bcrypt.hash(password, 10); // ← 手動でハッシュ化が必要
await db.query('INSERT INTO users ...', [name, email, hashed]);
res.redirect('/dashboard');
});
// ログイン
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
}));
Laravel(PHP)
// ユーザー登録
public function register(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
]);
$user = User::create($validated); // ← $casts が自動でハッシュ化
Auth::login($user);
return redirect()->route('dashboard');
}
// ログイン
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) { // ← DB 検索 + Hash::check() を自動実行
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
return back()->withErrors(['email' => '認証に失敗しました。'])->onlyInput('email');
}
| 機能 | Express(passport.js + bcrypt) | Laravel |
|---|---|---|
| パスワードハッシュ化 | bcrypt.hash() を手動で呼ぶ | 'hashed' キャストで自動化 |
| パスワード照合 | bcrypt.compare() を手動で呼ぶ | Auth::attempt() が自動実行 |
| セッション管理 | express-session ライブラリが必要 | フレームワーク標準機能 |
| CSRF 保護 | csurf ライブラリが必要 | @csrf 1 行で完結 |
| 認証ミドルウェア | passport.authenticate() を手動適用 | ->middleware('auth') で適用 |
| バリデーション | express-validator ライブラリが必要 | $request->validate() が標準搭載 |
| セットアップ量 | 複数ライブラリのインストールと設定が必要 | フレームワーク標準機能で少量の設定 |
Hash::make() でハッシュ化('hashed' キャストで自動化可能)
② 全フォームに @csrf(CSRF 保護)
③ ログイン成功時に session()->regenerate()(セッション固定攻撃対策)
④ ログアウトは logout() + invalidate() + regenerateToken() の 3 セット
⑤ Blade で {{ }} を使う(XSS 対策)
ミドルウェア・権限管理
認証(Authentication)と認可(Authorization)の違い
前章で学んだのは認証、この章で学ぶのは認可です。
| 認証(Authentication) | 認可(Authorization) | |
|---|---|---|
| 問い | 「あなたは誰ですか?」 | 「あなたには権限がありますか?」 |
| 確認内容 | ログインしているか | 何をしてよいか |
| 実装 | ログイン機能 | ミドルウェア・ロール管理 |
// 認証:ログインしているか確認
if (auth()->check()) {
echo auth()->user()->name; // 「田中太郎」
}
// 認可:権限を持っているか確認
if (auth()->user()->isAdmin()) {
echo '管理者です';
}
| ページ | 認証 | 認可 |
|---|---|---|
| トップページ | 不要 | 不要 |
| ダッシュボード | 必要(ログイン必須) | 不要 |
| 記事編集 | 必要 | 必要(編集者または管理者) |
| ユーザー管理 | 必要 | 必要(管理者のみ) |
ミドルウェアの仕組み
ミドルウェアは、コントローラーが実行される前にチェックを行う「番人」のような仕組みです。
// app/Http/Middleware/Authenticate.php(Laravel 標準ミドルウェアの構造)
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Authenticate
{
public function handle(Request $request, Closure $next)
{
// ログインしていないなら → ここで処理を止める
if (!auth()->check()) {
return redirect('/login');
}
// ログインしているなら → 次の処理(コントローラー)へ進む
return $next($request);
}
}
| コード | 意味 |
|---|---|
return $next($request); | 次の処理(コントローラーまたは次のミドルウェア)へ進む |
return redirect('/login'); | 処理を止めてリダイレクト |
abort(403); | 処理を止めて 403 エラー(Forbidden)を表示 |
$next を使うのか: ミドルウェアは「次に何が実行されるか(別のミドルウェアかコントローラーか)」を知りません。$next($request) で次の処理に委ねるのが標準的な設計です。Laravel 標準ミドルウェア
| 名前 | 役割 |
|---|---|
auth | ログインしているかチェック(未ログインなら /login へ) |
guest | ログインしていないかチェック(ログイン済みなら弾く) |
verified | メール認証済みかチェック |
throttle | レート制限(アクセス回数制限) |
auth ミドルウェアの適用方法
// 方法① 個別のルートに適用
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth');
// 方法② 複数のルートにまとめて適用(推奨)
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
Route::get('/profile', [ProfileController::class, 'show']);
Route::get('/settings', [SettingsController::class, 'index']);
});
ロール(権限)管理の設計
users テーブルに role カラムを追加するシンプルな方法です。
マイグレーションで role カラムを追加
php artisan make:migration add_role_to_users_table
// database/migrations/xxxx_add_role_to_users_table.php
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// enum 型:入力できる値を限定('admin' か 'user' のみ)
$table->enum('role', ['admin', 'user'])->default('user')->after('name');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('role');
});
}
php artisan migrate
User モデルにロール判定メソッドを追加
// app/Models/User.php
class User extends Authenticatable
{
protected $fillable = ['name', 'email', 'password', 'role']; // role を追加
// 管理者かどうか
public function isAdmin(): bool
{
return $this->role === 'admin';
}
// 特定のロールを持っているか
public function hasRole(string $role): bool
{
return $this->role === $role;
}
// いずれかのロールを持っているか
public function hasAnyRole(array $roles): bool
{
return in_array($this->role, $roles);
}
}
enum('role', ['admin', 'user']) とすると、DB レベルで 'admin' か 'user' 以外の値を入れられなくなります(型安全)。文字列カラムより安全です。カスタムミドルウェアの作成
① ミドルウェアファイルを作成
php artisan make:middleware IsAdmin
// app/Http/Middleware/IsAdmin.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
public function handle(Request $request, Closure $next): Response
{
// ログインしていない → ログインページへ
if (!auth()->check()) {
return redirect('/login');
}
// 管理者でない → 403 エラー
if (!auth()->user()->isAdmin()) {
abort(403, 'このページにアクセスする権限がありません。');
}
// 管理者 → 次の処理へ
return $next($request);
}
}
② ミドルウェアを登録(Laravel 11 以降)
// bootstrap/app.php(Laravel 11 / 12 の書き方)
use App\Http\Middleware\IsAdmin;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin' => IsAdmin::class, // 'admin' という短縮名で使えるようにする
]);
})
->create();
③ ルートに適用
// routes/web.php
// 管理者専用ルートグループ
Route::middleware(['admin'])->group(function () {
Route::get('/dashboard', fn() => view('dashboard'))->name('dashboard');
});
// 一般ユーザー用ルートグループ(ログインのみ必要)
Route::middleware(['auth'])->group(function () {
Route::get('/home', fn() => view('home'))->name('home');
});
admin ミドルウェアは内部でログインチェックも行うので auth との併用は不要です。 また ->middleware(['auth', 'admin']) のように複数指定も可能で、配列の順番通りに実行されます。ログイン後のリダイレクト振り分け
ロールに応じて適切な画面へリダイレクトするよう、LoginController を修正します。
// app/Http/Controllers/Auth/LoginController.php
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
// 管理者 → /dashboard、一般ユーザー → /home に振り分け
if (auth()->user()->isAdmin()) {
return redirect()->route('dashboard');
}
return redirect()->route('home');
}
return back()->withErrors([
'email' => 'メールアドレスまたはパスワードが正しくありません。',
])->onlyInput('email');
}
| ユーザー種別 | ログイン後の遷移先 | 管理者ページへの直接アクセス |
|---|---|---|
| 管理者(admin) | /dashboard | ✅ アクセス可能 |
| 一般ユーザー(user) | /home | ❌ 403 エラー |
| 未ログイン | /login へリダイレクト | ❌ ログインページへ |
Express との比較
Express でのミドルウェアと権限管理を Laravel と比較してみましょう。
Express(Node.js)
// middleware/isAdmin.js
const isAdmin = (req, res, next) => {
// ログインチェック(passport.js の場合)
if (!req.isAuthenticated()) {
return res.redirect('/login');
}
// 権限チェック
if (req.user.role !== 'admin') {
return res.status(403).render('errors/403');
}
// OK → 次の処理へ
next(); // Laravel の $next($request) に相当
};
// ルートに適用
app.get('/dashboard', isAdmin, (req, res) => {
res.render('dashboard');
});
// 複数ルートにまとめて適用
const adminRouter = express.Router();
adminRouter.use(isAdmin); // このルーター全体に適用
adminRouter.get('/dashboard', ...);
adminRouter.get('/users', ...);
app.use('/admin', adminRouter);
Laravel(PHP)
// app/Http/Middleware/IsAdmin.php
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check()) {
return redirect('/login');
}
if (!auth()->user()->isAdmin()) {
abort(403);
}
return $next($request); // Express の next() に相当
}
// ルートに適用
Route::middleware(['admin'])->group(function () {
Route::get('/dashboard', ...);
Route::get('/users', ...);
});
| 機能 | Express | Laravel |
|---|---|---|
| ミドルウェアの構造 | (req, res, next) => { ... } | handle(Request $request, Closure $next) |
| 次の処理へ進む | next() | return $next($request); |
| 処理を止める | return res.redirect() | return redirect() / abort(403) |
| ミドルウェアの登録 | 関数を直接渡す | bootstrap/app.php で alias 登録 |
| ルートへの適用 | 引数として渡す | ->middleware('name') |
| グループへの適用 | router.use(fn) | Route::middleware()->group() |
| 403 エラー | res.status(403).render(...) | abort(403) 1 行 |
next() が Laravel の $next($request)、Express の router.use(fn) が Laravel の Route::middleware()->group() に対応します。構文が異なるだけで設計の考え方は共通です。return $next($request) で通過・redirect() / abort() で遮断
③ enum 型でロールカラムを定義し、User モデルに判定メソッドを追加
④ php artisan make:middleware IsAdmin → bootstrap/app.php で alias 登録 → ルートで適用
⑤ ログイン後のリダイレクト先をロールで振り分ける(isAdmin() で判定)