Technology

静的Webの自然な仕組みを、動的アプリケーションへ。

BEAR.Sundayのキャッシュは、レスポンスを一時保存する仕組みではありません。本質的に静的なリソース表現をRead Modelとして生成し、サーバー、CDN、クライアントの各層で同一性と依存関係を維持するアーキテクチャです。

Central idea

キャッシュではなく、Read Modelの生成。

ブログ記事、商品情報、ニュース本文、プロフィールのようなコンテンツは、リクエストのたびに PHPとDBを通るため「動的」に見えます。しかし、同じリソース状態から同じ表現が返るなら、 それは本質的には静的です。変わるのは時刻ではなくイベントです。

BEAR.Sundayはこの性質をアーキテクチャとして扱います。ResourceObjectがHTTP表現を生成し、 依存関係をURIタグとして保持し、ETagで同一性を表します。生成された表現はサーバーキャッシュや CDNに配置され、変更イベントが起きるまで静的コンテンツとして配信されます。

Write model

Database + Commands

Projection

ResourceObject creates representation

Server-side resource cache

Cache body, dependency tags, and ETag identity.

CDN shared cache

Cache body, dependency tags, and ETag identity.

Client HTTP cache

Cache body, dependency tags, and ETag identity.

Event-driven content

タイミングが読めない静的コンテンツ。

キャッシュのTTLを短くして、まだ古いかもしれないと祈る。多くのアプリで、これが「キャッシュ戦略」と呼ばれています。

Event-driven contentの要点は、変化の時刻が予測できないことと、内容が常に揺らぐことを 混同しないところにあります。記事はいつ編集されるか分かりません。コメントはいつ追加されるか 分かりません。それでも、イベントが起きていない間、その表現は静的です。

Fastlyはこの種のコンテンツを、未知の期間だけ静的で、変わるかもしれないコンテンツとして整理しました。 必要なのは短いTTLではなく、アプリケーションが知っている変更イベントから、CDNへ即時かつプログラム可能に purgeを届けることです。

だからTTLを短く刻んで不安を薄めるのではなく、変更を知っているアプリケーションが 依存する表現を破棄します。変更がなければCDNは同じ表現を配信し続け、クライアントは ETagで同一性を確認し、同じなら304 Not Modifiedで済ませます。

Classification

本質的に静的

状態が変わらない限り、同じURIは同じ表現になる。イベントでのみ変化する。

本質的に動的

リクエストごとに意味が変わる。個人化、乱数、現在時刻、計算過程そのものが表現になる。

An actual exchange

1  GET /article/42                          200  ETag:"a1"  Surrogate-Key: article-42 profile-7
   → CDNが保存

2  GET /article/42                          CDN HIT          ← PHPもDBも動かない

3  著者が profile-7 を編集
   → PURGE  Surrogate-Key: profile-7        ← 依存する article-42 も連鎖で破棄

4  GET /article/42                          200  ETag:"b9"   ← この時だけ再生成

5  GET /article/42  If-None-Match:"b9"       304 Not Modified ← 本文を送らない

Dependency resolution

連鎖破壊は、コンテンツとETagの両方に届く。

コンテンツAがBに依存し、BがCに依存するなら、Cの変更はCのキャッシュだけで終わりません。 BとAの表現も、BとAのETagも、古い同一性を示してしまうため破棄される必要があります。

BEAR.Sundayでは、`#[Embed]`されたリソースや明示された依存URIがタグになります。 AOPが変更を検知すると、サーバーサイドのキャッシュとETagが連鎖的に無効化され、可能な場合は CDNのSurrogate-Keyにも同じ依存関係が伝播します。依存解決はサーバーの中だけで閉じません。

Dependency graph

Article

depends on: Profile, Comments, Weather

Comments

depends on: Comment items

Weather

depends on: Forecast source

Forecast sourceが変わると、WeatherとArticleのキャッシュ、そしてそれぞれのETagがサーバー層とCDN層で無効化されます。

Partial read models

部分コンテンツも、Read Modelの一部になる。

ページ全体をキャッシュできるかどうか、という二択ではありません。BEAR.Sundayはドーナツキャッシュと ドーナツの穴キャッシュを持ち、キャッシュできる部分、できない部分、別の周期で変化する部分を分けて扱います。

重要なのは、部分表現の依存も全体の同一性に反映されることです。穴の中身が変われば、必要な範囲だけを 再生成し、全体のETagも更新されます。静的なWebのキャッシュモデルを、部分表現の合成にまで広げています。

Donut cache

全体の中にキャッシュできない穴がある場合、周辺の変化しない部分を再利用します。

Donut hole cache

穴そのものもキャッシュ可能な場合、部分リソースの変更が全体のキャッシュとETagへ伝播します。

Recursive composition

AがBを含み、BがCを含む場合でも、変更されたC以外を再利用して最小コストで再生成します。

Performance and delivery quality

速さは、最適化ではなく設計の帰結。

BEAR.Sundayは、極端なほどパフォーマンスを設計の中に置きます。速くするための後付け最適化ではなく、 SQL、リソースグラフ、DI graph、root objectが明示的な構造として存在すること自体が性能につながります。 だから出荷前に検査でき、実行時にはバッチ化、DI compile、ルートオブジェクトキャッシュ、並列化へ切り替えられます。

質の悪いDBアクセスを出荷前に止める

SQL fileとparameterが独立しているため、実行計画、フルテーブルスキャン、非効率なJOINをCIで解析できます。本番で遅さとして発見する前に、DBアクセスを品質ゲートにかけられます。

DataLoaderがN+1をバッチ化する

linkCrawlでリソースグラフを構成するとき、子リソースごとのDBアクセスはDataLoaderでまとめられます。複数のリソースリクエストを、1つの効率的なクエリへ変換できます。

DIコンパイラで起動コストを抑える

依存グラフは実行時に毎回探索する対象ではありません。ScriptInjectorでPHP factory codeへ生成し、本番ではDI containerを解釈せず、生成済みのobject graphで起動できます。

ルートオブジェクトキャッシュ

contextに応じて組み立てたアプリケーションのルートオブジェクトをシリアライズし、リクエスト間で再利用できます。DIコンテナとAOP構成を毎回生成せず、通常のリクエスト処理から外せます。

Embed表現を並列実行へ切り替えられる

BEAR.Asyncを使うと、逐次取得されていた#[Embed]リソースを、リソースコードを変えずに並列取得へ切り替えられます。HTML表現でもJSON表現でも、埋め込まれたリソースは並列に取得され、Moduleの差し替えだけで実行戦略を変えられます。

SQL as a first-class citizen

SQLを、隠さず第一級市民にする。

BEAR.Sundayは標準技術を好み、SQLをORMの裏に隠しません。Ray.MediaQueryでは、SQLは独立した ファイル、入口は型付きインターフェースです。SQLが第一級市民であることは、性能の検査 (出荷前のEXPLAIN)だけでなく、開発のしかたそのものを変えます。契約で分業でき、SQL特化のIDEも AIも、同じSQL資産を直接扱えます。

SQLを隠さず、第一級市民にする

Ray.MediaQueryでは、SQLは var/sql の独立したファイル、入口は #[DbQuery] を付けた型付きインターフェースです。ORMの裏に隠さないので、JOIN、CTE、ウィンドウ関数、ベンダー固有のSQLをそのまま書けます。

契約があるから、分業・並行開発できる

インターフェース(署名・戻り値型・SQLファイル名)が契約になります。SQL担当とアプリ担当は互いの完成を待たず並行で進み、アプリ側はfakeでDBがなくてもユースケースを先に組めます。

SQL特化のツールがそのまま効く

独立した .sql ファイルなので、DataGripのようなSQL特化IDEで、スキーマ補完、実行、EXPLAIN、整形、リファクタが直接使えます。実行計画やインデックスの検討を、PHPランタイムと切り離して回せます。

AIも、隠れた生成なしに読み書きできる

動的なクエリ生成で隠れないため、AIはインターフェースの契約と実際のSQLを直接読み、書けます。人間の専用ツールとAIが、同じSQL資産に同じようにアクセスできます。

Transparent parallel execution

並列化のために、コードは書き換えない。

`#[Embed]` は、リソースの「結果」を埋め込むのではありません。リソースへの「リクエスト」、 つまりリソース間の関係そのものを埋め込みます。だから、逐次に取るか、ext-parallelのスレッドで 並列に取るか、Swooleのコルーチンで取るかを決めるのはLinkerの仕事です。 リソースクラスは、自分が並列に呼ばれたことを知りません。

URIが意図(What)を表し、実行方法(How)をModuleへ隠すからこそ、実行戦略は後から差し替えられます。 10年前に書いたリソースが、Moduleを足すだけで並列実行の恩恵を受けます。標準のPHPで開発・デバッグし、 本番では設定の変更だけで並列へ切り替えられます。

非同期プログラミングでよく語られる「関数の色」問題 ―― 非同期関数を呼ぶ関数自身も非同期になり、 コードベース全体へ伝播する ―― は、リソース境界で断ち切られます。逐次でも並列でもコードは同じ。 変わるのは実行戦略だけです。

sequential vs parallel

Sequential               Parallel
Request                  Request
 ├ Embed 1 ── 50ms        ├ Embed 1 ─┐
 ├ Embed 2 ── 50ms        ├ Embed 2 ─┤
 ├ Embed 3 ── 50ms        ├ Embed 3 ─┤
 └ Embed 4 ── 50ms        └ Embed 4 ─┘
Response 200ms           Response 50ms

runtimes — application code unchanged

ext-parallel

スレッドプール。PHP-FPM / Apache 向け。bin/async.php を足すだけ。

Swoole

コルーチン。常駐サーバーで高い並行性。AsyncSwooleModuleをinstall。

mysqli

DBクエリのみ並列。最小構成。

class Dashboard extends ResourceObject
{
    #[Embed(rel: 'user', src: '/user{?id}')]
    #[Embed(rel: 'notifications', src: '/notifications{?user_id}')]
    #[Embed(rel: 'stats', src: '/stats{?user_id}')]
    public function onGet(string $id): static
    {
        $this->body['id'] = $id;

        return $this;
    }
}

この埋め込み宣言は、逐次でも並列でも一文字も変わりません。MVCが「どう実行するか」を 手続きで書くのに対し、BEAR.Sundayは「リソース間の関係」を宣言します。宣言が実行戦略から 独立しているため、戦略を入れ替えてもコードに影響しません。

※ BEAR.Async は現在 Alpha。ext-parallel は ZTS版PHPとext-parallel拡張、Swooleはext-swooleが必要です。

Tests that follow links

一つのストーリーが、リソースからHTTPまで、同じコードで通る。

これは便利機能ではありません。クライアントは次の操作を推測せず、リソースが差し出すリンクを辿る—— そのWebの原則(HATEOAS)を、そのまま実行できるテストにしたものです。ユーザーストーリーを リンクを辿るフローとして書き、トランスポートをDIで差し替えれば、同じ筋書きが実HTTP/JSONを通り、 さらにHTMLのlink/formへ続きます。

in-process — follows the affordances

class PurchaseFlowTest extends AbstractWorkflowTest
{
    #[Alps('goProduct')]
    public function testProduct(): ResourceObject
    {
        return $this->resource->get('page://self/product', ['id' => 1]);
    }

    #[Alps('doAddCartItem')]
    #[Depends('testProduct')]
    public function testAddToCart(ResourceObject $product): ResourceObject
    {
        // ベタ書きURIではなく、差し出されたリンクを辿る
        $cart = $this->resource->post(
            $this->linkHref($product, 'doAddCartItem'),
            ['qty' => 2],
        );
        $this->assertSame(Code::CREATED, $cart->code);

        return $cart;
    }

    #[Alps('goCheckout')]
    #[Depends('testAddToCart')]
    public function testCheckout(ResourceObject $cart): ResourceObject
    {
        return $this->follow($cart, 'goCheckout');
    }
}

real HTTP/JSON — swap newResource() only

// 全工程を、実HTTP/JSONで再実行。
// 変えるのは newResource() だけ。筋書きは継承する。
final class HttpPurchaseFlowTest extends PurchaseFlowTest
{
    protected function newResource(): ResourceInterface
    {
        return new HttpResource(
            '127.0.0.1:8080',
            __DIR__ . '/index.php',
        );
    }
}

同じテストが、in-processのリソースグラフ(ミリ秒・ブラウザ無し)でも、実HTTP境界(cookie・リダイレクト込み)でも走ります。

テストが、リンクを辿る

URIをベタ書きせず、レスポンスが差し出す _links や Location(=affordance)を辿ります。クライアントと同じ歩き方。HATEOASを、実行できるテストにしたものです。

spec(ALPS)に整列する

各ステップを #[Alps('goCheckout')] のようにALPSの遷移へ束縛します。テスト手順が、そのまま意味の状態遷移の走査になります。

トランスポートはDIで差し替える

in-processのリソースでも実HTTPでも、リソースは同じものです。newResource()をHttpResourceに変えるだけで、同じ筋書きが実HTTP/JSONを通ります。

E2Eは、得意分野に絞れる

「APIが正しく振る舞うか」はリソース層へ降ろせます。ブラウザのE2Eは、視覚回帰・実ブラウザのJS・認証フローという本来の領分に縮められます。

※ 同じ仕組みで、Fake実装とSQL実装を同じアサーションの“双子”にすれば、移行を「祈り」ではなく等価性の確認にできます——これは「すべてが注入される」ことの一例です。

Context-agnostic DI

生成されたオブジェクトは、モードを知らない。

BEAR.SundayはDIをアプリケーションの便利機能として使うだけではありません。 Google Guiceのコンセプトを継ぐRay.Diを基盤に、フレームワークそのものがDIPとADPに従います。 実行時にグローバルなモードや設定を参照して振る舞いを変えることを避けます。

contextは `prod-hal-api-app` のように、環境、表現、入出力面、アプリケーション種別を 組み合わせるマトリクスです。ただし、それはオブジェクトグラフを組み立てるためだけに使われます。 生成後のオブジェクトは、自分がproductionなのか、HTMLなのか、APIなのかを参照する必要も、 参照する手段も持ちません。

context string

prod-hal-api-app

prod: production constraints

hal: representation

api: application surface

app: resource namespace

Key = type + qualifier

同じinterfaceでもQualifier属性で別の依存として識別します。依存の意味は文字列の実行時分岐ではなく、型付きのKeyになります。

Module as binding map

ModuleはGuiceと同じく束縛の集合です。installとoverrideで機能単位の構成を組み替え、contextごとのgraphを作ります。

Provider and scope

複雑な生成、遅延生成、singletonやprototypeの寿命はProviderとScopeに閉じ込めます。利用側のオブジェクトは生成事情を知りません。

Compiled factories

ScriptInjectorは依存グラフからPHP factory codeを生成します。本番ではコンテナを毎回解釈せず、生成済みのgraphで起動できます。

実行モードを読まない

APP_DEBUGやAPP_MODEのようなグローバル値で分岐しません。振る舞いの差はinterfaceへの束縛として注入されます。

contextは組み立て時だけ存在する

prod-hal-api-appのようなcontextはオブジェクトグラフ生成に使われ、生成後のオブジェクトは自分がどのcontextで作られたかを知りません。

DIPとADPをフレームワークまで徹底

アプリケーションだけでなくフレームワークのpackage構造も依存方向を保ちます。外側の都合を内側が参照しない設計です。

表現ではなくリソースをテストする

HTMLアプリケーションもAPIアプリケーションも同じResourceObjectを通るため、テストはapp:// URIでリソースを呼び、body、headers、linksを確認できます。レンダリング後のHTMLを解析して業務結果を推測する必要はありません。

Application composition

HTTPの壁を立てずに、複数アプリケーションを統合する。

同じリソース群をHTMLアプリケーションとしてもAPIアプリケーションとしても実行できるのは、 振る舞いがcontext moduleとDI bindingで組み替えられるからです。リソースのコード自身は、 どの表現で呼ばれているかを知る必要がありません。

つまり、HTMLサイトとAPIサイトを別々の実装として作る必要はありません。HTML表現もAPI表現も 同じリソースから生まれるため、画面の機能テストでもHTML文字列を解析せず、APIと同じように リソースの状態、リンク、ヘッダーを確認できます。

さらに、別のアプリケーションをvendor packageとして取り込み、名前空間と依存関係で独立性を保ったまま 統合できます。HTTPでサービス境界を作って分割しなくても、DIPとADPに沿って独立したアプリケーションを 1つのオブジェクトグラフに組み込めます。

この性質は外部からの呼び出しにも効きます。BEAR.Sundayで作ったリソースをpackageとして取り込み、 Resource clientを既存アプリケーションへ注入すれば、他のPHPフレームワーク上のコードからも app:// URIで同じリソースを呼び出せます。

same independence, no network

// マイクロサービスなら:ネットワーク越し
$post = $http->get('https://blog.internal/posts/42');
// → タイムアウト・リトライ・直列化・別デプロイ

// BEAR.Sunday:別アプリを vendor に取り込み、URI で呼ぶだけ
composer require acme/blog
$post = $this->resource->get('app://blog/post', ['id' => 42]);
// → 同一プロセス・ネットワーク無し

without an HTTP wall

MyVendor\Cms\Resource

MyVendor\Blog\Resource

Acme\Inventory\Resource

独立したアプリケーションを、通信プロトコルではなくpackage、namespace、DI bindingで構成する。

Direction of technology

入口を作るのではなく、リソースを入口にする。

多くのフレームワークでは、HTMLとは別にAPI controllerを作り、CLIやAI向けにはまた別の実装を作ります。 BEAR.Sundayでは向きが逆です。アプリケーションの意味はResourceObjectにあり、HTTP、HTML、API、 CLI、Homebrewコマンド、Tool Use、多言語連携は、そのリソースへ接続するブリッジになります。

API / CLI / Homebrewを別実装にしない

APIサイトやCLIコマンドを追加実装するのではなく、既存のResourceObjectをAPIやコマンドとして転送します。Homebrewで配布するコマンドになっても、アプリケーションの意味は同じリソースに残ります。

Tool / MCPもリソースを入口にする

AI向けの関数群を別に作るのではなく、リソースをTool Useへブリッジします。MCPのような新しい入口が必要なら、作るべき中心は業務ロジックではなくリソースとのブリッジです。

IDLからcontrollerを作らない

先にIDLを置き、それに合わせてcontrollerを実装する向きではありません。リソースが先にあり、HTTP、Tool Use、ドキュメント、スキーマが、その意味を外へ運びます。

多言語もブリッジでつなぐ

BEAR.Thriftを使えば、BEAR.Sundayリソースを他言語や異なるPHPバージョンから利用できます。リソースの汎用性が、HTTPの外側にも伸びます。

Portable resources

他のPHPアプリケーションから、リソースとして呼べる。

BEAR.SundayのResourceObjectは、特定のWebフレームワークのcontroller actionではありません。 URIで識別され、Resource clientから呼び出されるアプリケーション部品です。そのため、 BEAR.Sundayで作った機能をvendor配下に取り込み、既存のPHPアプリケーションから利用できます。

これは単なるモジュラモノリスではありません。ネットワーク越しのマイクロサービスを作らずに、 リソース単位の独立性、再利用性、URIによる呼び出し境界を得る設計です。

call flow

  1. 01Composer packageとしてvendor配下に取り込む
  2. 02必要なcontextでResource clientを組み立てる
  3. 03app:// URIでリソースを呼び出す
  4. 04ResourceObjectの表現を既存アプリケーション側で利用する

フレームワーク境界を越える

BEAR.Sundayのリソースは、HTTP controllerに閉じた処理ではありません。Resource clientを注入できれば、他のPHPアプリケーションからもURIで呼び出せます。

microservice without network

独立したアプリケーションを別プロセスやHTTPサービスに分けなくても、package、namespace、DI bindingで独立性を保ったまま統合できます。

移行と共存に使える

既存アプリケーションの中からBEAR.Sundayの新しいリソースを呼び出し、必要な機能だけを段階的に導入できます。

Architecture

キャッシュは、後付けの最適化ではない。

BEAR.Sundayにおけるキャッシュは、レスポンスを速くするための補助機能ではありません。 リソースから本質的に静的なHTTP表現を生成し、その同一性をETagで示し、その依存関係をサーバーとCDNにまたがって 維持し、変更イベントで破棄する。これは、アプリケーションのRead ModelをWebの仕組みとして創り出す設計です。

Start with one resource

まずは小さく作り、構造を残す。