Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RelayerというPHPのフレームワークを作った

Avatar for polidog polidog
August 21, 2026

 RelayerというPHPのフレームワークを作った

Avatar for polidog

polidog

August 21, 2026

More Decks by polidog

Other Decks in Technology

Transcript

  1. Next.js は好き。でも、しんどくなってきた パッケージが重い。node_modulesと依存更新に疲れた RSC はサーバーで動く。ブラウザ前提のパッケージは で壊れるし、常駐プロセスなのでモジュール内にstateを持つライブラリは メモリリークの温床になる PHP なら FrankenPHP

    でシングルバイナリ化までできる。デプロイはファイ ル1個 そして今のPHPは普通にモダン。型も静的解析も常駐ランタイムもある npm window is not defined だったら、あの開発体験をPHPで作ればいいのでは? 5
  2. React ライクにかけるPHP、usePHP 独自拡張子 .psx // src/Pages/page.psx return fn () =>

    ( <section className="card"> <h1>It works</h1> <p> {date('Y')} </section> ); これは 年のページです。</p> でPHP式を埋め込み、PascalCaseタグはコンポーネント はReact流( に自動変換) コンパイルすると のPHPコードに変換される { ... } className class H::section(...) 9
  3. コンパイルするとこうなる コンパイル前( ) // .psx return fn () => (

    <section className="card"> <h1>It works</h1> <p> {date('Y')} </section> ); これは 年のページです。</p> コンパイル後 // return fn () => H::section(className: 'card', children: [ H::h1(children: 'It works'), H::p(children: [' ', date('Y'), ' ']), ]); これは 魔法じゃない。属性は名前付き引数、子要素は 年のページです。 children のただの関数呼び出し 10
  4. コンポーネントは関数スタイルとクラススタイル 関数スタイル // — src/Components/Card.psx return fn (array $props) =>

    ( <div className="card"> <h3>{$props['title']}</h3> <div>{$props['children'] ?? null}</div> </div> ); クラススタイル コンストラクタで依存が自動注入される // — final class UserDetailPage extends PageComponent { public function __construct(private readonly UserRepository $users) {} public function render(): Element { return <h1>{$this->users->find($this->getParam('id'))->name}</h1>; } } 11
  5. 条件分岐もループも、ただのPHP式 Twig/Blade のような専用構文( {% if %} / @foreach 条件分岐 →

    値を返す三項演算子で書く )はない {$loggedIn ? <a href="/logout">Logout</a> : <a href="/login">Login</a>} ループ → 要素の配列を返す array_map で書く <ul>{array_map(fn ($x) => <li>{$x['name']}</li>, $items)}</ul> JSX と同じ流儀。 { } に書けるのは式だけ。新しいテンプレート構文を覚えなくていい 12
  6. useState もある return fc(function () { [$count, $setCount] = useState(0);

    return ( <div> <span>Count: {$count}</span> <button onClick={fn () => $setCount($count + 1)}>+</button> </div> ); }); でラップした関数コンポーネントの中でだけHooksが使える セッターは に紐づく。実体はサーバーへのリクエスト fc() onClick 見た目はReactのカウンター。でも状態はサーバー側にある 13
  7. useEffect と状態の保存先 useEffect(function () use ($tab) { error_log("tab: {$tab}"); return

    fn () => error_log('cleanup'); }, [$tab]); // $tab が変わったときだけ実行 deps の意味はReactと同じ: は毎回、 は初回だけ、値は変更時 で現在URLや の動的セグメントも取れる useRouter() null [] [id] 状態の保存先 (既定) サーバーセッション。ページ遷移をまたいで残る リクエストごとにリセット 署名付きで HTML に埋め込む(サーバーはステートレス) StorageType Session Memory Snapshot 14
  8. 複雑なUIは本物のReactで書ける(React Islands) use Polidog\Relayer\React\Island; return fn () => ( <section>

    {Island::mount('Chart', ['points' => $data])} </section> ); クライアント側でコンポーネントを登録 // window.relayerIslands.register('Chart', (el, props) => { createRoot(el).render(<Chart {...props} />); }); チャートやエディタなど、複雑なUIが必要な場所だけReactをマウントでき る。propsは にJSONで渡る data-react-props 15
  9. Relayer Next.js App Router ク 風の規約で書けるPHPフルスタックフレームワー ルーティング・API・認証・キャッシュ・DBをひとつのbootエントリにまと める規約重視の設計 のディレクトリ構成がそのままURLになる( は動的セグメン

    ト) ビュー層にusePHPを使うので、すべてコンポーネントで書ける Server Actions みたいな機構がある src/Pages/ composer require polidog/relayer vendor/bin/relayer init php -S 127.0.0.1:8000 -t public [id] 17
  10. Relayer の技術構成 ブラウザ — HTML + usephp.js(プログレッシブエンハンスメント)/ React Islands ▲▼

    HTTP CDN — Defer + Cache-Control ▲▼ Relayer でキャッシュを最大化 本体(PHP) ルーティング Server Actions Validation src/Pages/ がそのまま URL に フォーム処理 + CSRF自動 Zodライクなスキーマ検証 usePHP — .psx を PHP コードにコンパイルするビュー層(← さっき説明したやつ) ▲▼ SQL Tehilim — スキーマファーストなDBツールキット(後半で解説) 18
  11. ディレクトリがそのままURLになる src/Pages/ ├── layout.psx ├── page.psx ├── users/ │ ├──

    page.psx │ └── [id]/page.psx ├── (admin)/ └── _private/ 全ページ共通レイアウト # # / # /users # /users/42 # URL # に出ないグループ化 ルーティング対象外 は階層ごとに重なる。Next.jsのlayoutと同じ感覚 URL の衝突は初回リクエストではなく 時にエラーになる layout.psx routes:compile 19
  12. API は route.php を置くだけ // src/Pages/api/users/route.php return [ 'GET' =>

    fn (UserRepository $users): Response => Response::json(['users' => $users->all()]), 'POST' => function (Request $req, UserRepository $users): Response { $users->create($req->allPost()); return Response::json(['ok' => true], 201); }, ]; メソッドをキーにしたハンドラマップを返すだけ 引数の型を見て Request・サービス・PageContext が自動で注入される HTTP 20
  13. Server Actions みたいな機構もある return function (PageContext $ctx, UserRepository $users): Closure

    { $save = $ctx->action('save', function (array $form) use ($users, $ctx): void { $users->create($form['name']); $ctx->redirect('/users'); // 303 See Other }); return fn () => ( <form action={$save}> <input name="name" /> <button>save</button> </form> ); }; フォーム処理をクロージャで直接書く。APIエンドポイントは不要、CSRFも自動 21
  14. Zod ライクなバリデーションが標準装備 use Polidog\Relayer\Validation\Validator; $schema = Validator::object([ 'email' => Validator::string()->trim()->email(),

    'name' => Validator::string()->trim()->min(1, ' 'age' => Validator::int()->min(0)->optional(), ]); 名前は必須です。'), $result = $schema->safeParse($form); if (!$result->success) { $errors = $result->errors; // return; // } 同じページを再描画 フィールド単位のエラー 失敗したらそのまま再描画、成功したらPRG。Server Actionsと相性がいい 22
  15. 認証もビルトイン クラススタイル 属性でページ保護。 でロール必須も // — #[Auth(roles: ['admin'])] #[Auth] final

    class DashboardPage extends PageComponent { /* ... */ } 関数スタイル 。未認証ならリダイレクト // — requireAuth() return function (PageContext $ctx): Closure { $user = $ctx->requireAuth(); // requireAuth(['admin']) return fn () => <h1> {$user->displayName}</h1>; }; ようこそ でロール必須 を実装してDIに登録すると でロ グインできる セッションログインもトークン認証(Firebase / Cognito)も、同じ仕組み で書ける 23 UserProvider $auth->attempt($email, $password)
  16. Defer コンポーネント 「ログイン名だけ動的」なページ、CDNに乗せられますか? セッションを使うと Set-Cookie が付き、ページ丸ごとキャッシュ不可に return fc( fn (array

    $props) => <UserHeader />, defer: new Defer(name: 'user-header', cacheControl: 'private, no-store'), ); ページには SSR 時は fallback だけ描画 → あとから ページ本体はCDNキャッシュ、この部分だけ <UserHeaderDeferred fallback={<HeaderSkeleton />} /> GET /_defer/user-header と置く で本体を取得 private, no-store 動的な部分だけを切り出せば、ページは丸ごとCDNに乗る 24
  17. Defer はクライアント側でもキャッシュできる localStorage にキャッシュ(opt-in) #[Defer(name: 'feed', localCache: true, localCacheTtl: 60)]

    リロード・タブをまたいで保持される (秒)で有効期限を指定 — HTTPの キャッシュ破棄は のバンプ or localCacheTtl DEFER_CACHE_VERSION CDN Cache-Control とは独立 clearDeferCache() とブラウザの2層キャッシュで、動的部分の再取得も最小限にできる 25
  18. 細かいところもひととおり揃えてある キャッシュ — ページ単位で / ETag を宣言。 が一致すれば描画もDBアクセスもせず304 セッションは遅延起動 —

    状態を触らないページは を吐かないの でCDNに乗る i18n — 依存ゼロの多言語化。ロケール解決は URL プレフィックス → セッシ ョン → Cookie → Accept-Language ミドルウェア / CORS / HTTPクライアント / ロガー ページ単位のJS読み込み — サービスはSymfony DIコンテナでオートワイヤ プロファイラ(dev環境限定)、CLIコマンド、.envカスケード HTTP Cache-Control If-None-Match Set-Cookie $ctx->js('/assets/chart.js', defer: true) 全部ドキュメントに書いてあります → https://relayer.polidog.jp/ 26
  19. schema.tehilim にスキーマを書く model User { id Int @id @default(autoincrement()) email

    String @unique name String? posts Post[] } model Post { id Int @id @default(autoincrement()) title String published Boolean @default(false) authorId Int author User @relation(fields: [authorId], references: [id]) } ほぼPrismaのschema。 でnull許可、 ? @relation でリレーション 29
  20. クエリもPrisma風 $found = $db->user->findUnique(['where' => ['email' => '[email protected]']]); $posts =

    $db->post->findMany([ 'where' => [ 'OR' => [ ['title' => ['contains' => 'PHP']], ['body' => ['contains' => 'PHP']], ], 'published' => true, ], 'orderBy' => ['createdAt' => 'desc'], 'take' => 20, ]); findUnique / findMany / insert Prisma 流 など、 where 演算子も select / include も 30
  21. 連想配列なのにPHPStanで型が見える $row = $db->user->findUnique([ 'where' => ['id' => 1], 'select'

    => ['email', 'name'], ]); // PHPStan : // array{email: string, name: string|null, id: int}|null にはこう見えている echo $row['email']; // OK echo $row['age']; // PHPStan がエラーにする 行データはプレーンな連想配列で、型情報は で供給する PHPStan 拡張が の内容から戻り値のarray shapeを絞り込む @phpstan-type select 31
  22. Relayer とTehilimの連携 final class UsersPage extends PageComponent { public function

    __construct(private readonly TehilimClient $db) {} public function render(): Element { $users = $this->db->user->findMany([ 'include' => ['posts' => ['where' => ['published' => true]]], ]); return <ul>{array_map(fn($u) => <li>{$u['name']}</li>, $users)}</ul>; } } 型付きクライアントをDIで注入して使う。 でリレーションも取れる プロファイラはdev環境だけ注入するので、本番コードに計測機構が載らな32 い include
  23. これからはAIと一緒に書く時代 Relayer はAI協働を前提に設計してある 規約は の1ファイルに集約。ファイル規約も各種契約も「やらな いこと」も全部ここ / は2行のポインタファイルで、どのAIツールから入って も同じ規約に辿り着く で

    にrelayer-routingスキルとrelayer-reviewerサブエ ージェントを生成する 生成コードはrelayer-reviewerが規約に沿っているかチェックする RELAYER.md AGENTS.md relayer init CLAUDE.md .claude/ 規約重視のフレームワークは、AIが迷わない 33
  24. まとめ でも みたいなコンポーネントベースの開発体験が欲しくて、 を作った PHP RSC Relayer すべてコンポーネントで書けて、Server Actionsみたいな機構もある。Defer +

    CDN キャッシュ はTehilim。スキーマファースト + PHPStanで型安全 規約重視だからAIが迷わない。AI時代を前提にしたフレームワーク DB 気になったら、一度触ってみてください 34