Technology

The natural mechanisms of the static Web, brought to dynamic applications.

BEAR.Sunday's caching is not a mechanism for temporarily storing responses. It is an architecture that generates inherently static resource representations as Read Models and maintains identity and dependency relationships across the server, CDN, and client layers.

Central idea

Not caching—Read Model generation.

Content like blog posts, product info, news articles, and profiles appears "dynamic" because each request passes through PHP and DB. But if the same resource state yields the same representation, it's inherently static. What changes isn't time—it's events.

BEAR.Sunday treats this property as architecture. ResourceObjects generate HTTP representations, retain dependency relationships as URI tags, and express identity through ETags. Generated representations are placed on server caches and CDNs and served as static content until a change event occurs.

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

Static content with unpredictable timing.

Shorten the TTL and pray it's not stale yet. In many apps, that's what passes for a "caching strategy."

The key insight of event-driven content is not conflating unpredictable change timing with constantly shifting content. You don't know when an article will be edited. You don't know when a comment will be added. But between events, the representation is static.

Fastly classified this kind of content as static for an unknown duration, but potentially changeable. What's needed isn't a short TTL—it's immediate, programmable purge delivered to the CDN from change events the application already knows about.

So instead of shortening TTLs to ease anxiety, the application that knows about changes invalidates the dependent representations. Without changes, the CDN keeps serving the same representation; clients check identity via ETag and take 304 Not Modified when unchanged.

Classification

Inherently static

As long as state doesn't change, the same URI yields the same representation. Changes only on events.

Inherently dynamic

Meaning changes with each request. Personalization, random numbers, current time—the computation process itself is the representation.

An actual exchange

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

2  GET /article/42                          CDN HIT          ← No PHP or DB activity

3  Author edits profile-7
   → PURGE  Surrogate-Key: profile-7        ← Cascading invalidation of dependent article-42

4  GET /article/42                          200  ETag:"b9"   ← Regenerated only this time

5  GET /article/42  If-None-Match:"b9"       304 Not Modified ← Body not sent

Dependency resolution

Cascading invalidation reaches both content and ETags.

If content A depends on B, and B depends on C, then a change in C doesn't stop at C's cache. B's and A's representations—and their ETags—all show stale identity and must be invalidated.

In BEAR.Sunday, #[Embed] resources and explicitly declared dependency URIs become tags. When AOP detects a change, server-side caches and ETags are cascading-invalidated, and where possible, the same dependency relationships propagate to CDN Surrogate-Keys. Dependency resolution doesn't stay confined inside the server.

Dependency graph

Article

depends on: Profile, Comments, Weather

Comments

depends on: Comment items

Weather

depends on: Forecast source

When the forecast source changes, Weather and Article caches—and their respective ETags—are invalidated across both the server and CDN layers.

Partial read models

Partial content also becomes part of the Read Model.

It's not a binary choice of whether the whole page can be cached. BEAR.Sunday has donut caching and donut hole caching, handling cacheable parts, non-cacheable parts, and parts that change on different cycles separately.

What matters is that partial representation dependencies are also reflected in overall identity. When the hole's content changes, only the necessary scope is regenerated, and the overall ETag is also updated. The static Web's caching model is extended to the composition of partial representations.

Donut cache

When the whole has non-cacheable holes, reuse the unchanging surrounding parts.

Donut hole cache

When the hole itself is also cacheable, partial resource changes propagate to the overall cache and ETag.

Recursive composition

Even when A contains B and B contains C, regenerate at minimal cost by reusing everything except the changed C.

Performance and delivery quality

Speed is a consequence of design, not optimization.

BEAR.Sunday places performance extremely deep in the design. Not after-the-fact optimization to go faster—the very existence of SQL, resource graphs, DI graphs, and root objects as explicit structures leads to performance. That's why they can be inspected before shipping, and at runtime switched to batching, DI compilation, root object caching, and parallelization.

Stop bad DB access before shipping

Because SQL files and parameters are independent, execution plans, full table scans, and inefficient JOINs can be analyzed in CI. Catch DB access problems before they become production slowness.

DataLoader batches N+1

When linkCrawl constructs a resource graph, per-child-resource DB access is batched by DataLoader. Multiple resource requests are converted into a single efficient query.

DI compiler curbs startup cost

The dependency graph isn't something to explore at runtime each time. ScriptInjector generates PHP factory code so production starts from a pre-built object graph without interpreting the DI container.

Root object cache

Serialize and reuse the application root object assembled for a given context across requests. Avoid regenerating the DI container and AOP configuration each time—keep it out of normal request processing.

Switch embed representation to parallel execution

BEAR.Async lets you switch #[Embed] resources from sequential to parallel fetching without changing resource code. Whether HTML or JSON representation, embedded resources are fetched in parallel—execution strategy changes through Module swap alone.

SQL as a first-class citizen

Don't hide SQL—make it a first-class citizen.

BEAR.Sunday prefers standard technology and doesn't hide SQL behind an ORM. In Ray.MediaQuery, SQL is an independent file; the entry point is a typed interface. SQL being a first-class citizen changes not just performance inspection (EXPLAIN before shipping) but the very way development is done. Contracts enable parallel work; SQL-specialized IDEs and AI both access the same SQL assets directly.

Don't hide SQL—make it a first-class citizen

In Ray.MediaQuery, SQL is an independent file in var/sql, and the entry point is a typed interface with #[DbQuery]. Not hidden behind an ORM, you can write JOINs, CTEs, window functions, and vendor-specific SQL directly.

Contracts enable parallel, divided work

The interface (signature, return type, SQL filename) becomes the contract. SQL and application developers proceed in parallel without waiting for each other; the app side can build use cases first with fakes even without a DB.

SQL-specialized tools work as-is

As independent .sql files, SQL-specialized IDEs like DataGrip can provide schema completion, execution, EXPLAIN, formatting, and refactoring directly. Examine execution plans and indexes decoupled from the PHP runtime.

AI can read and write without hidden generation

Without dynamic query generation hiding things, AI can directly read and write the interface contract and actual SQL. Human-specialized tools and AI access the same SQL assets the same way.

Transparent parallel execution

Don't rewrite code for parallelization.

#[Embed] doesn't embed the result of a resource—it embeds the request to a resource, i.e., the relationship between resources themselves. So whether to fetch sequentially, in parallel via ext-parallel threads, or via Swoole coroutines is the Linker's job. The resource class doesn't know it was called in parallel.

Because URIs represent intent (What) and execution method (How) is hidden in Modules, execution strategy can be swapped in later. A resource written ten years ago benefits from parallel execution just by adding a Module. Develop and debug with standard PHP; switch to parallel in production through configuration alone.

The "function color" problem often discussed in async programming—where a function calling an async function must itself become async, propagating throughout the codebase—is severed at the resource boundary. The code is the same whether sequential or parallel. Only the execution strategy changes.

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

Thread pool. For PHP-FPM / Apache. Just add bin/async.php.

Swoole

Coroutines. High concurrency on a persistent server. Install AsyncSwooleModule.

mysqli

DB queries only, parallelized. Minimal configuration.

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;
    }
}

These embed declarations change not a single character between sequential and parallel. While MVC writes "how to execute" procedurally, BEAR.Sunday declares "relationships between resources." Because the declaration is independent of execution strategy, swapping strategies doesn't affect the code.

* BEAR.Async is currently Alpha. ext-parallel requires ZTS PHP and the ext-parallel extension; Swoole requires ext-swoole.

Tests that follow links

One story travels from resource to HTTP, through the same code.

This is not a convenience feature. It's the Web principle (HATEOAS)—where clients follow links offered by resources rather than guessing the next operation—made into executable tests. Write user stories as link-following flows; swap transport via DI and the same scenario runs over real HTTP/JSON, continuing into HTML links/forms.

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
    {
        // Follow offered links, not hardcoded URIs
        $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

// Re-run the entire flow over real HTTP/JSON.
// Only newResource() changes. Scenario is inherited.
final class HttpPurchaseFlowTest extends PurchaseFlowTest
{
    protected function newResource(): ResourceInterface
    {
        return new HttpResource(
            '127.0.0.1:8080',
            __DIR__ . '/index.php',
        );
    }
}

The same test runs both as an in-process resource graph (milliseconds, no browser) and over real HTTP boundaries (including cookies and redirects).

Tests follow links

Rather than hardcoding URIs, follow _links and Location (=affordances) that responses offer. Same navigation as a client. HATEOAS made into executable tests.

Aligned to spec (ALPS)

Each step is bound to ALPS transitions like #[Alps('goCheckout')]. The test procedure directly becomes a traversal of semantic state transitions.

Transport is swapped via DI

Whether in-process resources or real HTTP, the resource is the same. Change newResource() to HttpResource and the same scenario runs over real HTTP/JSON.

E2E narrows to its strengths

'Does the API behave correctly?' is pushed down to the resource layer. Browser E2E shrinks to its proper domain: visual regression, real-browser JS, and auth flows.

* Using the same mechanism, pairing Fake and SQL implementations as "twins" with the same assertions turns migration from a "prayer" into equivalence verification—one example of "everything is injected."

Context-agnostic DI

Generated objects don't know their mode.

BEAR.Sunday doesn't just use DI as an application convenience feature. Built on Ray.Di, which inherits Google Guice's concepts, the framework itself follows DIP and ADP. It avoids changing behavior by referencing global modes or configuration at runtime.

Context, like prod-hal-api-app, is a matrix combining environment, representation, I/O surface, and application type. But it's used only to assemble the object graph. Post-construction objects have no need—and no means—to reference whether they're production, HTML, or API.

context string

prod-hal-api-app

prod: production constraints

hal: representation

api: application surface

app: resource namespace

Key = type + qualifier

The same interface is identified as distinct dependencies via Qualifier attributes. The meaning of a dependency is a typed Key, not a string-based runtime branch.

Module as binding map

Modules are collections of bindings, just like Guice. Install and override to recombine functional units and create graphs per context.

Provider and scope

Complex creation, lazy initialization, and singleton/prototype lifespans are confined to Providers and Scopes. Consumer objects don't know the creation details.

Compiled factories

ScriptInjector generates PHP factory code from the dependency graph. In production, startup doesn't interpret the container each time—it runs from the pre-built graph.

Don't read execution mode

No branching on globals like APP_DEBUG or APP_MODE. Behavioral differences are injected as bindings to interfaces.

Context only exists during assembly

Context strings like prod-hal-api-app are used to generate the object graph. Once created, objects don't know which context they were built for.

DIP and ADP applied throughout the framework

Not just the application—the framework's package structure also maintains dependency direction. Outer concerns don't reference inner ones.

Test the resource, not the representation

Both HTML applications and API applications go through the same ResourceObject, so tests call resources by app:// URIs and check body, headers, and links. No need to parse rendered HTML and guess business results.

Application composition

Integrate multiple applications without erecting HTTP walls.

The same set of resources can run as both an HTML application and an API application because behavior is recombined through context modules and DI bindings. The resource code itself doesn't need to know which representation it's being called under.

That means you don't need to build HTML sites and API sites as separate implementations. Both HTML and API representations emerge from the same resources, so even screen feature tests can check resource state, links, and headers the same way as APIs—without parsing HTML strings.

Furthermore, you can pull another application in as a vendor package and integrate it while preserving independence through namespace and dependency relationships. Without creating service boundaries over HTTP for separation, you can assemble independent applications into a single object graph following DIP and ADP.

This property also works for external calls. Pull BEAR.Sunday-built resources in as a package, inject a Resource client into an existing application, and you can call the same resources via app:// URIs from code in other PHP frameworks.

same independence, no network

// Microservices: across the network
$post = $http->get('https://blog.internal/posts/42');
// → timeouts, retries, serialization, separate deploys

// BEAR.Sunday: pull into vendor, call by URI
composer require acme/blog
$post = $this->resource->get('app://blog/post', ['id' => 42]);
// → same process, no network

without an HTTP wall

MyVendor\Cms\Resource

MyVendor\Blog\Resource

Acme\Inventory\Resource

Compose independent applications through packages, namespaces, and DI bindings—not communication protocols.

Direction of technology

Resources are the entry point—don't build separate entry points.

In many frameworks, you build separate API controllers alongside HTML, and yet another implementation for CLI or AI. In BEAR.Sunday, the direction is reversed. Application meaning resides in ResourceObjects; HTTP, HTML, API, CLI, Homebrew commands, Tool Use, and multi-language integration become bridges that connect to those resources.

Don't duplicate API / CLI / Homebrew implementations

Rather than adding separate implementations for API sites or CLI commands, transport existing ResourceObjects as APIs or commands. Even as a Homebrew-distributed command, the application's meaning stays in the same resource.

Resource is the entry point for Tool / MCP too

Rather than building separate function sets for AI, bridge resources to Tool Use. If a new entry point like MCP is needed, what you build is a bridge to resources, not separate business logic.

Don't build controllers from IDL

The direction isn't: place the IDL first and align controller implementations to it. Resources come first. HTTP, Tool Use, documentation, and schemas carry that meaning outward.

Connect multiple languages through bridges

BEAR.Thrift lets you use BEAR.Sunday resources from other languages or different PHP versions. Resource generality extends beyond HTTP.

Portable resources

Callable as resources from other PHP applications.

BEAR.Sunday ResourceObjects aren't controller actions tied to a specific web framework. They are application components identified by URI and called from a Resource client. That's why features built with BEAR.Sunday can be pulled into vendor and used from existing PHP applications.

This isn't merely a modular monolith. It's a design that achieves resource-level independence, reusability, and URI-based call boundaries without building network-spanning microservices.

call flow

  1. 01Pull into vendor as a Composer package
  2. 02Assemble the Resource client for the required context
  3. 03Call resources via app:// URIs
  4. 04Use ResourceObject representations from within the existing application

Crosses framework boundaries

BEAR.Sunday resources aren't HTTP controller actions confined to one framework. If you can inject a Resource client, you can call them by URI from other PHP applications.

Microservice without network

Independent applications can be integrated—preserving isolation through packages, namespaces, and DI binding—without splitting them into separate processes or HTTP services.

Usable for migration and coexistence

Call new BEAR.Sunday resources from within existing applications and introduce only the needed features incrementally.

Architecture

Caching is not an after-the-fact optimization.

In BEAR.Sunday, caching is not an auxiliary feature to speed up responses. It generates inherently static HTTP representations from resources, indicates their identity with ETags, maintains their dependency relationships across server and CDN, and invalidates them on change events. This is a design that creates the application's Read Model as a mechanism of the Web itself.

Start with one resource

Start small, keep the structure.