名古屋出身ソフトウェアエンジニアのブログ

PHPer 向け Next.js 対照表的あんちょこ

公開:
更新:

Next.js は React ベースの Web アプリケーションフレームワークです。サーバーサイド関数とフロントエンドを組み合わせたハイブリッド構成が PHP に似ているという話を聞き、PHPer との親和性が高いのではないかと思い調査しました。

結果、PHPer 視点で気に入った部分が多かったので、今でもなんやかんや PHP に頼りがちな自分から見たメモを残しておきます。

本記事は App Router を前提としています。

TL;DR

PHP / Laravel でのアレコレNext.js での相当
ファイルベースルーティング (.php 拡張子) / ルーティング定義 (Laravel routes)ファイルベースルーティング (app/ 配下の page.tsx ファイル)
(MVC の場合は)コントローラで処理して view を返すpage.tsx (Server Component) でデータ取得 → JSX で描画
素の PHP / Blade テンプレートJSX/TSX (React)
クライアント側の UI 動作は PHP と直交し任意構成use client で作る
include, requireimport
$_GET, $_POSTsearchParams, request.json(), formData()

1. ルーティング

静的

  • PHP: about.php/about.php
  • Next.js: app/about/page.tsx/about

動的(パラメータあり)

  • PHP: user.php/user.php?id=123, /user/123(パラメータをパスに入れる場合は、Laravel routes / Virtual directory 等を使う)
  • Next.js: app/users/[id]/page.tsx/users/123
// app/users/[id]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return <div>User: {id}</div>;
}

ネスト(サブページ)

  • app/admin/page.tsx/admin
  • app/admin/users/page.tsx/admin/users

page などのファイル名は Next が予約しており、変更不可です。

Next のファイル階層ベースのルーティングは、old school PHPer には逆に馴染み深いと思いました。

2. MVC パターン相当

PHP(典型例)

// users.php
$users = find_users();
include "views/users.php";

Next.js

Server Component を基本的に使うことになります。

// app/users/page.tsx (デフォルトで Server Component になる)
export default async function Page() {
  const users = await fetch("https://example.com/api/users").then((r) =>
    r.json(),
  );

  return (
    <ul>
      {users.map((u: any) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
  • page.tsx は基本サーバー側で実行(DB アクセスや秘密鍵読み取りができる)
  • 画面の一部をブラウザ側で動かすときだけ use client する

App Router では Server Component と Client Component の区別が重要です。

3. use client: クライアント側の UI 動作

PHP で吐く HTML ドキュメントに jQuery を加えて DOM 操作する代わりに、Next では React コンポーネントでシームレスに書くことができます。use client ディレクティブを置いたファイルは、クライアント側で動作する React コンポーネントになります。

"use client";

import { useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>count: {n}</button>;
}

ページ固有のコンポーネントは page.tsx と同じ階層に置くと見通しが良いですが、共通部品は app/ 外の components/ 等、別フォルダに集約しても可です。

SSR が有効な場合、Client Component も初期レンダリングはサーバー側で実行され、hydration によりクライアント側でインタラクティブになるという手順を踏むことに注意してください。

サーバーで実行クライアントで実行
Server Componentするしない
Client Componentする(SSR 有効時)する

4. GET/POST エンドポイント

PHP

  • パラメータは $_GET, $_POST で受け取る
  • HTML フォームの POST 先は同一ファイルや別エンドポイント

Next.js

典型的なパターンとして 2 つ存在します。

A. Server Functions

フォーム内容を関数に POST するような感覚の機能です。use server ディレクティブは関数内の最上部に配置され、その関数を Server Function としてマークします。すると、その関数はサーバー側で実行されるようになります。

// app/todo/page.tsx
export default function Page() {
  async function addTodo(formData: FormData) {
    "use server";
    const title = String(formData.get("title") || "");
    // DB に保存など(サーバーで実行)
    // ...
  }

  return (
    <form action={addTodo}>
      <input name="title" />
      <button type="submit">Add</button>
    </form>
  );
}

B. API Route(REST エンドポイント)

// app/api/todos/route.ts
export async function POST(req: Request) {
  const body = await req.json();
  return Response.json({ ok: true, received: body });
}

5. クエリパラメータ

PHP

$page = (int)filter_var($_GET["page"] ?? 1, FILTER_VALIDATE_INT);

Next.js

export default async function Page({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const params = await searchParams;
  const page = Number(params.page ?? 1);
  return <div>page={page}</div>;
}

6. テンプレーティング

Blade (Laravel)JSX (Next.js)
{{ $x }}{x}
@if / @else{cond ? A : B} / {cond && A}
@foreach($xs as $x){xs.map(x => ...)}
@include('partial')<Partial />(コンポーネント化)
@extends/@sectionapp/layout.tsx(レイアウトファイル)

7. レイアウト(共通ヘッダー・フッター)

PHP

  • 相対パスを使った共通ファイルの取り込みによって行うことが多い
  • header.php, footer.phpinclude / require

Next.js

  • レイアウト定義用のファイルが存在する(よりトップダウン的)
  • app/layout.tsx が全体レイアウト定義を持つ
// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <header>Header</header>
        {children}
        <footer>Footer</footer>
      </body>
    </html>
  );
}

8. データベースアクセス

PHP

PHP では PDO / Laravel Eloquent などでデータベースアクセスを行います。

Next.js

Next.js には固有のデータベースアクセス機能は存在しないので、Prisma / Drizzle など自由に選択できます。

Server Component があるので、PHP と同様にサーバー側で問い合わせを行うことができます。

import { cookies } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies();
  const token = cookieStore.get("token")?.value;
  return <div>token={token}</div>;
}

10. Proxy(ミドルウェアレイヤー)の差し込み

proxy.ts をプロジェクト直下に置くことで、リクエストごとに任意の中間処理を行うことができます。 (どうも最近になって middleware.ts から改名されたようです。)

// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function proxy(req: NextRequest) {
  const token = req.cookies.get("token")?.value;
  if (!token && req.nextUrl.pathname.startsWith("/admin")) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
  return NextResponse.next();
}

これはちょっと Starlette (FastAPI) っぽいかもしれません。

11. エラーページ

ファイルベースのルーティングにより、エラーページもファイルベースで管理できます。

  • app/error.tsx: そのルート配下のエラー画面
  • app/not-found.tsx: 404 画面
  • app/loading.tsx: ロード中 UI (Suspense)

A. ファイル階層概観

app/
  layout.tsx        # 全体レイアウト
  page.tsx          # /
  users/
    page.tsx        # /users
    [id]/
      page.tsx      # /users/:id
  api/
    users/
      route.ts      # /api/users
proxy.ts            # 認証/リライト等
.env.local          # 環境変数

所感

バックエンド動作とフロントエンド view を近い場所に置いて開発することが多かった PHPer にとっては理解しやすいフレームワークかなと個人的には思います。やっててよかった PHP!

全体的に実用面を重視したフレームワークに感じられ、特に Server Function を使うことで画面のためだけの API を作成する手間を省いたりできるのは、面倒な問題を直接殴れるような感覚になれます。でも、脆弱性だけは勘弁な。