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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for polidog polidog PRO
August 21, 2026

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

Avatar for polidog

polidog PRO

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