🌐

バックエンド基本(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 系サーバーエラー(サーバー側のバグ)
コード意味使う場面
200OK(成功)GET・PUT・PATCH 成功
201Created(作成成功)POST 成功
204No Content(返すデータなし)DELETE 成功
400Bad Request(不正なリクエスト)パラメータ不足・型違い
401Unauthorized(未認証)JWT なし・未ログイン
403Forbidden(権限なし)admin のみ許可など
404Not Found(存在しない)ID が見つからない
409Conflict(競合・重複)同じメールアドレスが既存
422Unprocessable Entityバリデーションエラー
500Internal Server ErrorDB エラー・バグ

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 実行 → レスポンス返す
サーバーとは: 通信を待ち続ける常駐プログラムのこと。Node.js・Apache・Nginx・MySQL も全部「ポートで待っているプログラム」です。listen() が呼ばれた時点で OS に登録され、TCP 待ち受けが開始されます。

Node.js のモジュールの種類

種類require の書き方
標準(built-in)http, fs, path, os, urlrequire("http")
外部(npm)express, axios, prismarequire("express")
自作app.js, utils.jsrequire("./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)IncomingMessageurl, method, headers, body, params, query
res(Response)ServerResponsejson(), 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/jsonJSONAPI 通信(現在の主流)
text/plainテキスト文字列
text/htmlHTMLページ返却
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.bodyundefined になります。

JSON の受け渡し(フロント ⇄ サーバー)

JSON 通信に必要な要素は 5 つです。

必要なもの理由
フロント(送信)methodPOST / 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
操作メソッドURLExpress
一覧取得GET/todosapp.get("/todos")
1件取得GET/todos/1app.get("/todos/:id")
作成POST/todosapp.post("/todos")
更新PUT / PATCH/todos/1app.patch("/todos/:id")
削除DELETE/todos/1app.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)静的ファイル配信
Routerexpress.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 はリクエストが来たとき、登録された処理を上から順番に実行します。

種類役割nextres を返す
ミドルウェア途中で処理する(ログ・認証・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/         // 型・スキーマ定義
役割
routesURL と controller をつなぐPOST /todos → todoCreateController
controllersreq を受けて res を返す入口req.body を受け取り service を呼ぶ
servicesビジネスロジックの処理バリデーション・加工・条件判定
repositoriesDB アクセスのみ担当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()

ポートとは / 非同期が重要な理由

ポート番号用途
3000Node.js / Express API サーバー
5173Vite(React 開発サーバー)
3306MySQL
5432PostgreSQL
6379Redis
80 / 443HTTP / 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);
});
💡 全体のまとめ: クライアント ⇄ HTTP(メソッド + ステータス + JSON) ⇄ Express(ミドルウェア + ルートハンドラー) ⇄ DB。この流れが理解できるとバックエンドの全体像が見えてきます。
🟢

Node.js

Node.js とは?

Node.js は JavaScript をサーバー側で実行できる環境(ランタイム)です。本来ブラウザで動く JavaScript を、PC やサーバー上でも実行できるようにします。Chrome の V8 JavaScript エンジン をベースに構築されています。

Node.js = V8エンジン + Node API + イベントループ

JavaScript との実行環境の違い

項目JavaScript(ブラウザ)Node.js
実行場所ブラウザPC / サーバー
目的UI 操作サーバー処理
主な APIDOM 操作・document・alertファイル・OS・DB・HTTP
グローバルwindowglobal
ボタン操作・画面制御API サーバー・ファイル操作

主な特徴

  • ① フロント・バックを同じ言語で書ける:JavaScript でフルスタック開発が可能
  • ② 非同期処理(ノンブロッキング I/O):処理を待たずに次へ進み、完了したら通知
  • ③ 高速(V8 エンジン):コンパイル型に近い速度で実行
  • ④ npm エコシステム:世界最大規模のパッケージリポジトリを利用可能

① イベントループ(Node 最大の特徴)

Node.js は イベント駆動型 で動作します。リクエストをキューに積み、イベントループが順に処理します。処理を待たずに次へ進むため、大量アクセスに強い構造です。

リクエスト
   ↓
イベントキュー
   ↓
イベントループ
   ↓
処理実行 → 完了したら通知
特徴: 処理を待たない → 次の処理に進む → 完了したら通知。これにより大量アクセスに強く、API サーバーに最適です。

② 非同期処理(ノンブロッキング)

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)
正式名称CommonJSES Modules
読み込みrequireimport
エクスポートmodule.exportsexport
読み込み方式同期非同期
標準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 }
ESM を使うには: package.json"type": "module" を追加します。

⑤ Express(Node の定番フレームワーク)

Node.js 単体でもサーバーを作れますが記述量が多いため、Express がよく使われます。

フレームワーク用途
ExpressWeb サーバー(定番)
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
ESM での注意: ESM(import/export)では __dirname / __filename は使えません。代わりに import.meta.url を使います。

ブラウザ vs Node.js のグローバルオブジェクト

JavaScript は実行環境によって使えるオブジェクトが異なります。

環境グローバルオブジェクトthis(トップレベル)
ブラウザwindowwindow
Node.js(CJS)global{}
Node.js(ESM)globalundefined
共通globalThis
ブラウザで使えるものNode.js で使えるもの
window / document / locationglobal / 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
💡 理解の全体像: Node.js = イベントループ × 非同期処理 × npm × モジュール(CJS/ESM)× Express。この 5 つが Node.js の核心です。
📦

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 devscripts の dev を実行
npm startscripts の start を実行
npm testscripts の test を実行

パッケージとは? 〜読み込みまでの流れ

パッケージ = 再利用できるプログラムのまとまり。npm レジストリに公開されており、npm install でダウンロードして使います。

パッケージ例用途
expressWeb サーバー作成
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.js の読み込み順: node_modules を探す → package.jsonmain / 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 が最初に読むファイル)
typemodule → ESM、commonjs → CJS
scriptsnpm run で実行できるコマンド
dependencies本番用パッケージ一覧
devDependencies開発用パッケージ一覧

バージョン記号の意味:

記号意味
^4.18.0同じメジャーバージョンで更新 OK(4.x.x)
~4.18.0同じマイナーバージョンで更新 OK(4.18.x)
4.18.0完全固定

dependencies vs devDependencies

項目dependenciesdevDependencies
用途本番で必要なパッケージ開発時だけ必要なパッケージ
本番で必要
インストールnpm install pkgnpm install -D pkg
本番 deploy 時入る入らないことが多い
dependencies の例devDependencies の例
express, react, prisma, axiostypescript, nodemon, jest, eslint, vite
分ける理由: 本番を軽くする / セキュリティ向上 / Docker 軽量化 / CI 高速化。実務では必須の使い分けです。
# 本番用だけインストール(devDependencies を除外)
npm install --production
NODE_ENV=production npm install

npm install の詳細

npm install はパッケージをインストールする基本コマンドです。内部では以下の順に動作します。

npm install
   ↓
package.json 読む
   ↓
package-lock.json 読む
   ↓
バージョン決定
   ↓
npm レジストリから取得
   ↓
node_modules に保存(依存関係も取得)
コマンド意味
npm installpackage.json から全パッケージをインストール
npm install pkgパッケージを追加(dependencies に入る)
npm install -D pkg開発用で追加(devDependencies に入る)
npm install -g pkgグローバルインストール(PC 全体で使える)
npm install --productiondependencies のみインストール
Git clone 後: node_modules.gitignore で除外されているため、clone 後は必ず npm install を実行します。

npm install vs npm ci

どちらもインストールコマンドですが、用途が異なります。

項目npm installnpm ci
用途通常の開発本番 / CI / Docker
lock ファイル必要✅(必須)
速度普通高速
バージョン変わる可能性ありなし(完全一致)
node_modules 削除しないする(クリーンインストール)
安定性普通高い
# 開発
npm install

# CI / GitHub Actions / Docker / 本番
npm ci
npm ci --production
なぜ本番では npm ci? バージョンずれ防止・再現性 100%・高速。実務の CI/Docker ではほぼ必須です。

--production と npm run build

本番デプロイ・Docker・CI で必ず使う重要コマンドです。

コマンド意味
npm install --productiondependencies のみインストール
npm ci --productionCI 用・dependencies のみ(推奨)
npm run buildscripts の build を実行(開発コード → 本番コードに変換)

npm run build が必要な理由: 本番環境では TypeScript・JSX・ESNext をそのまま実行できないため、変換(コンパイル)が必要です。

開発コードbuild 後
TypeScript(.ts)JavaScript(.js)
React / JSXJS bundle
Next.js.next/
Vitedist/
// Node.js + TypeScript の典型的な本番フロー
npm ci             // 全パッケージ入れる(devDeps も必要)
npm run build      // tsc → dist/ に JS を生成
npm ci --production // devDeps を除いて再インストール
npm start          // node dist/index.js
なぜ build 後に --production install? build には TypeScript など devDeps が必要。しかし本番実行には不要なので、build が終わったら devDeps を除いた状態にします。

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" }
devDependencies との関係: dev script で使うツール(nodemon, typescript, vite, jest)は devDependencies に入れます。本番では不要なためです。

npx とは? 〜 scripts との違い

npx = パッケージをインストールせず一時的に実行するコマンド(npm に付属)。

コマンド意味
npm installパッケージをインストール
npm run xxxscripts を実行
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.jsonCLI(その場で指定)
永続性登録して繰り返し使う一時実行
用途開発・ビルド・テスト初期化・CLI ツール
scripts 内で npx が不要な理由: scripts 内では node_modules/.bin が自動で PATH に追加されるため、nodemontscvite などをそのまま呼び出せます。

package-lock.json と node_modules

  • package-lock.jsonnpm install 時に作られる。インストールされたパッケージの正確なバージョンを記録し、チーム全員が同じ環境を再現できる。Git に含める
  • node_modules/:パッケージ本体が入るフォルダ。require / import はここから読み込む。.gitignore に追加して Git 管理外にする
💡 代替ツール: yarnpnpm も同様のパッケージ管理ツールです。pnpm はディスク効率が高く、モノレポでよく使われます。
🔴

Laravel

Laravel とは?

Laravel は PHP で書かれたフルスタック Web フレームワークです。「エレガントな文法」を理念に掲げ、MVC アーキテクチャを採用。ルーティング・ORM(Eloquent)・マイグレーション・認証・キューなど Web 開発に必要な機能がすべて揃っています。

Laravel = PHP のフレームワーク(Node.js における Express の上位互換に相当)
特徴内容
MVC アーキテクチャModel / View / Controller でコードを整理
Eloquent ORMSQL を書かずにオブジェクト指向で DB 操作
Blade テンプレートPHP と HTML を組み合わせたテンプレートエンジン
Artisan CLIコード生成・マイグレーション・キャッシュ操作を CLI で実行
Composer 管理パッケージは Composer(PHP の npm)で管理

Node.js / npm との対比

Node.js / npmLaravel / PHP役割
npmComposerパッケージ管理ツール
package.jsoncomposer.json依存関係の設定ファイル
package-lock.jsoncomposer.lockバージョン固定ファイル
node_modules/vendor/パッケージ本体の格納先
npm installcomposer installパッケージをインストール
npm run devphp artisan serve開発サーバー起動
ExpressLaravelWeb フレームワーク

プロジェクト作成〜起動の流れ

# ① 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.phpWeb ルーティング(画面返す)
routes/api.phpAPI ルーティング(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
config() で読み込む: .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 --resourceCRUD メソッド付きコントローラー作成
php artisan make:model Post -mモデル + マイグレーション作成
php artisan make:middleware CheckAgeミドルウェア作成
php artisan make:seeder UserSeederシーダー作成
php artisan migrateマイグレーション実行
php artisan migrate:rollback1つ前に戻す
php artisan migrate:fresh --seed全リセット+シーダー実行
php artisan db:seedシーダー(テストデータ)投入
php artisan route:list登録済みルート一覧表示
php artisan key:generateAPP_KEY 生成(初回セットアップ時)
php artisan cache:clearキャッシュクリア
php artisan config:clear設定キャッシュクリア
php artisan optimize本番用キャッシュ生成
php artisan tinker対話型 REPL(DB 操作の確認など)

認証・スターターキット

パッケージ用途
Laravel Breezeシンプルな認証スターターキット(セッション認証)
Laravel SanctumSPA・モバイル向け API 認証(トークン認証)
Laravel PassportOAuth2 サーバー実装(大規模 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
--no-dev: Composer の require-dev パッケージ(phpunit など)を除外して本番を軽くします。npm の --production に相当します。
💡 Laravel の強み: Eloquent ORM・Blade・Sanctum・Breeze など充実したエコシステム。「ゼロから書く量を最小に、読みやすいコードを最大に」という思想で設計されています。
🎼

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)役割
npmcomposerパッケージ管理ツール
npm レジストリPackagistパッケージ公開・配布場所
package.jsoncomposer.json設定・依存関係ファイル
package-lock.jsoncomposer.lockバージョン固定ファイル
node_modules/vendor/パッケージ本体の格納先
npm install pkgcomposer require pkgパッケージ追加
npm install -D pkgcomposer require --dev pkg開発用パッケージ追加
npm installcomposer install全パッケージをインストール
npm cicomposer install(lock 基準)lock ファイルどおりにインストール
npm install --productioncomposer install --no-dev本番用のみインストール
import / requireuse / 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');
Node.js との違い: npm では 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開発用クラスの自動読み込み設定
scriptsComposer コマンド実行時のフックscripts

require vs require-dev

項目requirerequire-dev
用途本番でも必要なパッケージ開発・テスト時のみ必要
本番に含まれる❌(--no-dev で除外)
追加コマンドcomposer require pkgcomposer require --dev pkg
require の例require-dev の例
laravel/framework, guzzlehttp/guzzlephpunit/phpunit, laravel/pint, fakerphp/faker
# 本番用のみインストール(require-dev を除外)
composer install --no-dev --optimize-autoloader

composer install vs composer update

項目composer installcomposer update
参照先composer.lock(固定バージョン)composer.json(範囲内で最新)
lock の更新しないする
用途既存環境の再現(CI・本番・チーム開発)パッケージを意図的に更新したいとき
npm の対応npm cinpm update
git clone 後: 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 initcomposer.json を対話形式で作成
composer installcomposer.lock に従って全パッケージをインストール
composer install --no-devrequire のみインストール(本番用)
composer updatecomposer.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 appLaravel プロジェクトを作成
💡 まとめ: composer.json が設定、vendor/ が本体、composer install で再現。Git には composer.lock を含め、vendor/ は除外するのが基本です。
🗺

ルーティング

ルーティングとは?

ルーティングとは、URL とプログラムを紐付ける仕組みのことです。ユーザーが特定の URL にアクセスしたときに、どのプログラム(処理)を実行するかを決定します。

例えば、https://example.com/about にアクセスしたら「会社概要ページ」を表示し、https://example.com/contact にアクセスしたら「お問い合わせページ」を表示する、といった振り分けをルーティングが担当します。

ルートファイルの場所

Laravel では、ルーティングの設定は以下のファイルで行います:

ファイル用途
routes/web.phpWeb ページ用のルート(セッション、CSRF 保護あり)
routes/api.phpAPI 用のルート(Laravel 11 以降はデフォルト非存在)
💡 api.phpphp 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>
💡 URL を変更したい場合、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');
});
役割ExpressLaravel
認証チェック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 のみ
⚠️ XSS 攻撃の例:
{!! $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
💡 @forelse を使おう: @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->index0 から始まるインデックス
$loop->iteration1 から始まる回数
$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>
💡 @csrf とは? CSRF 攻撃を防ぐトークンを自動生成します。Laravel では POST フォームに必ず付ける必要があります。
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
役割ExpressLaravel 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(コントローラー)ビジネスロジックリクエスト受け取り→データ取得→ビューへ渡す
💡 MVC は汎用的な知識です: MVCアーキテクチャはLaravel専用ではありません。Ruby on Rails、Django(Python)、ASP.NET MVC(C#)、Spring MVC(Java)、Express.js(Node.js)など、世界中の多くのWebフレームワークで採用されている普遍的な設計パターンです。Laravelで学んだMVCの考え方は、他の言語・フレームワークでも活用できます。

コントローラーを使わない場合の問題点

ルートファイルに直接処理を書くと、ファイルが肥大化して管理・テストがしにくくなります。

// 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 つのメソッドを一括生成できます。

操作意味SQLHTTPメソッド
Create作成INSERTPOST
Read読取SELECTGET
Update更新UPDATEPUT / PATCH
Delete削除DELETEDELETE
// --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/productsindex一覧表示
GET/products/createcreate新規作成フォーム
POST/productsstoreデータ保存
GET/products/{id}show個別表示
GET/products/{id}/editedit編集フォーム
PUT/PATCH/products/{id}updateデータ更新
DELETE/products/{id}destroyデータ削除
💡 HTML フォームで PUT / DELETE を使う方法: HTML の <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', '未分類');
}
💡 HTML の name 属性との対応: $request->input('name') で取得できる値は、HTML フォームの name 属性の値に対応しています。name 属性と input() の引数は完全一致が必要です。
💡 DI は現代フレームワークの標準機能: 依存性注入は Laravel だけでなく、Spring Framework(Java)・ASP.NET Core(C#)・NestJS(TypeScript)など多くのフレームワークで採用されている重要な設計パターンです。

バリデーション(入力チェック)

$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') }}">
💡 バリデーション失敗時の自動処理: ① エラーメッセージをセッションに保存 → ② 元のフォームページへリダイレクト → ③ ビューで $errorsold() が自動的に使えるようになります。開発者が手動でリダイレクトを書く必要はありません。

エラーメッセージの日本語化

# Laravel-lang パッケージをインストール(推奨)
composer require laravel-lang/common
php artisan lang:add ja

# config/app.php でロケールを変更
'locale' => 'ja',

リダイレクトとフラッシュメッセージ(PRGパターン)

フォーム送信後はビューを直接返さず、リダイレクトを使うのが正しいパターンです。

view を直接返す(NG)redirect する(OK)
URLPOST /products のままGET /products に変わる
F5 リロードPOSTが再送信される(二重登録)GET が再送信される(安全)
ブラウザ履歴POST リクエストが残るGET リクエストが残る
💡 PRGパターン(Post-Redirect-Get): POST でデータを送信 → リダイレクト命令(302)を返す → ブラウザが自動的に 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();
💡 Laravel がセッションを使っている場所:@csrf トークンの検証 ② バリデーション失敗時の old() による入力値の保持 ③ $errors のエラーメッセージ ④ フラッシュメッセージ。これらはすべて Laravel がセッションを自動的に使って処理しています。

セッションの保存先(SESSION_DRIVER)

ドライバー説明
databaseDB の sessions テーブル(Laravel 11 以降のデフォルト)
redisRedis サーバー(高速・本番推奨)
filestorage/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']);
機能ExpressLaravel
コントローラー通常の 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']);
💡 Route Model Binding: メソッドの引数にモデル型を指定すると、Laravel が URL の ID から自動的にモデルを取得してくれます。404 も自動処理されます。次の「モデル」の章で詳しく学びます。
🗄

モデル

モデル(Eloquent ORM)とは?

モデルとは、データベースのテーブルを操作するための PHP クラスです。SQL 文を書かずに、PHP のコードでデータベースを操作できます。モデルは MVC の「M」に相当します。

テーブル名モデル名(クラス)
productsProduct
usersUser
categoriesCategory
💡 ORM(Object-Relational Mapping)とは? データベースのテーブルをオブジェクトとして扱う仕組みです。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',
    ];
}
💡 なぜ $fillable が必要か(Mass Assignment 対策): 悪意のあるユーザーがフォームに 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 補完・型チェック弱い弱い強い
💡 結論: できるだけ Eloquent ORM を使いましょう。最大の理由はSQLインジェクション対策が自動で行われることです。生 SQL やクエリビルダーを使う場合は、複雑な集計や大量データの一括更新など Eloquent では書きづらい場面に限定しましょう。

リレーションシップ

テーブル間の関連(リレーション)をモデルに定義することで、関連データを直感的に操作できます。

メソッド関係性外部キーの場所
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 Loadinginclude: [{ model: Category }]with('category')
作成Product.create({...})Product::create([...])
更新product.update({...})$product->update([...])
削除product.destroy()$product->delete()
Mass Assignment 保護手動設定が必要$fillable で自動保護
多対多の操作中間テーブルを直接操作attach() sync()
💡 ORM の概念はフレームワーク間で共通: Eloquent ORM と Sequelize(Express)はメソッド名が異なりますが、概念(モデル・リレーション・Eager Loading・Mass Assignment 保護)は同じです。一方を学べばもう一方も理解しやすくなります。
💡 まとめ:$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 つで同じ構造を作れる
  • チーム開発 — 全員が同じ構造で開発できる
  • ロールバック — 変更を簡単に元に戻せる
  • 履歴管理 — いつ何が変更されたかわかる
💡 マイグレーションは「データベースの Git」: コードと同じように、データベースの変更履歴を管理し、いつでも特定の状態に戻せます。

データベース接続設定(.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');
    }
};
💡 up() と down() は対にする: up() でテーブル作成 → down() でテーブル削除、up() でカラム追加 → down() でカラム削除、というように必ず逆操作を書きましょう。ロールバックが機能するようになります。

よく使うカラム型

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 × 2created_at, updated_at(自動管理)
decimal('price', 8, 2)DECIMAL(8,2)小数点付き数値(金額)
date('birth_date')DATE日付のみ
json('options')JSONJSON データ
💡 boolean 型について: MySQL には真の BOOLEAN 型は存在しません。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)でも、SequelizeKnex.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:migratemigrate
ロールバックdb:migrate:undomigrate:rollback
全削除→再実行手動 or スクリプトmigrate:fresh
シーダー実行db:seed:alldb:seed
外部キー参照references: { model, key }->constrained()(自動推測)
タイムスタンプ自動管理手動で定義timestamps() 1 行
記述量多め(JavaScript オブジェクト形式)少ない(メソッドチェーン形式)
💡 概念は同じ: up()/down() の対構造・マイグレーション実行コマンド・シーダーによる初期データ投入・外部キー制約——これらの概念は Express(Sequelize)でも Laravel でも同じです。コマンド名とシンタックスが異なるだけです。
💡 まとめ:up()down() は必ず対にする ② 一度実行したファイルは編集せず新しいマイグレーションを作成する ③ 外部キーの参照先テーブルを先に作成する ④ 開発中は migrate:fresh --seed でクリーンな状態に戻す ⑤ 本番環境では migrate:fresh / migrate:refresh は絶対に使わない
🔐

認証

Laravel 認証ライブラリ一覧

Webアプリケーションには認証機能が必要不可欠です。Laravel には用途に応じた複数の認証ライブラリが用意されています。

ライブラリメカニズムUI主な用途
Breezeセッション✅ あり小〜中規模 Web
Jetstreamセッション + 高機能(2FA・チーム管理)✅ ありSaaS / エンタープライズ
Fortifyバックエンドのみ❌ なしSPA / カスタム UI
Sanctumトークン(API 認証特化)❌ なしAPI / モバイル
PassportOAuth2 サーバー❌ なしOAuth 提供側
SocialiteOAuth クライアント(ソーシャルログイン)❌ なし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
モバイルアプリ APISanctum
OAuth 提供サービスPassport

なぜスクラッチで学ぶべきか

ライブラリは便利ですが、学習段階では自分で認証機能を作ることが重要です。

ライブラリに頼るスクラッチで作る
仕組みの理解ブラックボックスになるセッション・Cookie・ハッシュ化を理解できる
カスタマイズ難しい自由に変更できる
エラー対応原因が分からない原因を特定できる
面接説明できない自信を持って説明できる
💡 学習の正しい順序: ① スクラッチで認証を作る(仕組みを理解)→ ② Breeze のコードを読む(自分の実装と比較)→ ③ 実務ではライブラリを使う(車輪の再発明をしない)

認証の仕組み — セッションとステートレス

HTTP はステートレス(状態を持たない)なプロトコルです。リクエストごとに独立しており、前回のリクエストの情報を覚えていません。そのためセッションでログイン状態を保持します。

// セッションの流れ

// 1. ログイン成功時
// → サーバーがセッション ID を発行(例: abc123)
// → セッション ID を Cookie に保存
// → サーバー側(DB の sessions テーブル)にユーザー ID を保存

// 2. 次回リクエスト時
// → ブラウザが Cookie を自動送信(abc123)
// → サーバーがセッション ID からユーザー情報を取得
// → 「あなたは田中さんですね!」と識別
💡 重要: Cookie にはセッション ID のみ保存されます。実際のユーザーデータ(名前・メール)はサーバー側の DB に保存されます。

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... として保存される
💡 Carbon とは: PHP の日時操作ライブラリです(Laravel 専用ではありません)。'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 トークンを新規生成古いフォームからの送信を無効化する
💡 3つセットで実行: 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']);
});
💡 auth ミドルウェアの動き: ① リクエストが来る → ② ログイン確認 → ③ ログイン済みならコントローラーへ → ④ 未ログインなら /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() が標準搭載
セットアップ量複数ライブラリのインストールと設定が必要フレームワーク標準機能で少量の設定
💡 概念は共通: セッション ID で状態を保持・パスワードはハッシュ化して保存・CSRF 対策・セッション固定攻撃対策——これらの概念は Express でも Laravel でも同じです。Laravel はこれらを標準機能として提供しているため、コード量が大幅に少なくなります。
💡 まとめ: ① パスワードは必ず 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 型のメリット: 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', ...);
});
機能ExpressLaravel
ミドルウェアの構造(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 行
💡 概念は同じ: Express の next() が Laravel の $next($request)、Express の router.use(fn) が Laravel の Route::middleware()->group() に対応します。構文が異なるだけで設計の考え方は共通です。
💡 まとめ: ① 認証(Authentication)= 「誰か」を確認 / 認可(Authorization)= 「権限があるか」を確認 ② ミドルウェアは return $next($request) で通過・redirect() / abort() で遮断 ③ enum 型でロールカラムを定義し、User モデルに判定メソッドを追加 ④ php artisan make:middleware IsAdminbootstrap/app.php で alias 登録 → ルートで適用 ⑤ ログイン後のリダイレクト先をロールで振り分ける(isAdmin() で判定)