Donut cache
When the whole has non-cacheable holes, reuse the unchanging surrounding parts.
Technology
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
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
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
As long as state doesn't change, the same URI yields the same representation. Changes only on events.
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 sentDependency resolution
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
depends on: Profile, Comments, Weather
depends on: Comment items
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
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.
When the whole has non-cacheable holes, reuse the unchanging surrounding parts.
When the hole itself is also cacheable, partial resource changes propagate to the overall cache and ETag.
Even when A contains B and B contains C, regenerate at minimal cost by reusing everything except the changed C.
Performance and delivery quality
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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
#[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 50msruntimes — 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
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).
Rather than hardcoding URIs, follow _links and Location (=affordances) that responses offer. Same navigation as a client. HATEOAS made into executable tests.
Each step is bound to ALPS transitions like #[Alps('goCheckout')]. The test procedure directly becomes a traversal of semantic state transitions.
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.
'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
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
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.
Modules are collections of bindings, just like Guice. Install and override to recombine functional units and create graphs per context.
Complex creation, lazy initialization, and singleton/prototype lifespans are confined to Providers and Scopes. Consumer objects don't know the creation details.
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.
No branching on globals like APP_DEBUG or APP_MODE. Behavioral differences are injected as bindings to interfaces.
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.
Not just the application—the framework's package structure also maintains dependency direction. Outer concerns don't reference inner ones.
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
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 networkwithout 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
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.
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.
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.
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.
BEAR.Thrift lets you use BEAR.Sunday resources from other languages or different PHP versions. Resource generality extends beyond HTTP.
Portable resources
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
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.
Independent applications can be integrated—preserving isolation through packages, namespaces, and DI binding—without splitting them into separate processes or HTTP services.
Call new BEAR.Sunday resources from within existing applications and introduce only the needed features incrementally.
Architecture
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