Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.18.0] - 2026-10-28

### Changed
- Deploy this version with a new `CacheVersionModule` version (an app that binds its own cache adapter rather than this package's `Storage*Module`s must clear its pools instead): entries written by earlier versions may hold per-user content, and an earlier version reading this version's donut cache shows raw `[le2:...]` placeholders during a rolling deploy.
- A `#[Cacheable]`/`#[CacheableResponse]`/`#[DonutCache]` response whose own code, including a `$headers` property default, sets `Cache-Control` to `private` or `no-store` is not stored server-side. To keep server-side caching with a client-facing `no-store`, declare `#[HttpCache(noStore: true)]` instead.
- `ResourceDonut::FORMAT` is no longer public; use `ResourceDonut::placeholder()`.
- Requires `koriym/semantic-logger` `^0.11`. Written logs use its `log` document shape, and diagnostic `kind` is now `code`.
- Requires `bear/resource` `^1.22.4`.
- Allows `symfony/cache` 8 (PHP 8.4 or later), and requires `ray/psr-cache-module` `^1.6`, which supports it.
- `ResourceDonut::refresh()`: a hole that throws while an already-cached `#[DonutCache]` page is recomposed now degrades to a fresh render instead of failing the request directly from the refresh, the same way a cache-store failure already does. A hole that keeps throwing still fails the request, now from the fresh render instead of the refresh.
- `CacheDependency::depends()` no longer unsets the child's own `Surrogate-Key` header after copying it into the parent: a child that is both read by the parent and later rendered as a hole keeps its own key for its own independent cache entry.
- `KeepMutationsAndFailures` no longer keeps a session only because it contains a `semantic_logger_error` diagnostic; a session is kept for a mutation, a failed effect or the sample.
Expand Down
13 changes: 7 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@
"psr/log": "^1.1 || ^2.0 || ^3.0",
"ray/aop": "^2.19.1",
"ray/di": "^2.20",
"ray/psr-cache-module": "^1.5.1",
"symfony/cache": "^5.3 || ^6.0 || ^7.3",
"symfony/cache-contracts": "^2.4 || ^3.0",
"ray/psr-cache-module": "^1.6",
"symfony/cache": "^6.0 || ^7.3 || ^8.0",
"symfony/polyfill-php83": "^v1.32.0"
},
"require-dev": {
Expand All @@ -35,17 +34,19 @@
"mobiledetect/mobiledetectlib": "^3.74 || ^4.8",
"phpunit/phpunit": "^11.0",
"predis/predis": "^2.4",
"symfony/process": "^6.1 || ^7.1",
"symfony/process": "^6.1 || ^7.1 || ^8.0",
"twig/twig": "^3.4.3"
},
"suggest": {
"ext-redis": "Required for Redis cache storage backend",
"ext-memcached": "Required for Memcached cache storage backend"
"ext-memcached": "Required for Memcached cache storage backend",
"bear/fastly-module": "Required for FastlyModule (Fastly CDN purge)",
"mobiledetect/mobiledetectlib": "Required for MobileEtagModule (device-aware ETags)"
},
"autoload": {
"psr-4": {
"BEAR\\QueryRepository\\": ["src/", "src-deprecated/"],
"BEAR\\RepositoryModule\\Annotation\\": ["src-annotation/", "src-annotation-deprecated"]
"BEAR\\RepositoryModule\\Annotation\\": ["src-annotation/", "src-annotation-deprecated/"]
}
},
"autoload-dev": {
Expand Down
12 changes: 6 additions & 6 deletions docs/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -133,24 +133,24 @@ new AkamaiModule($host, $clientToken, $clientSecret, $accessToken);

## Cache Dependencies

Resources automatically track dependencies. When resource A embeds resource B, invalidating B also invalidates A.
A child taken in with `#[Embed]` becomes a dependency of its parent when the child is itself cached, i.e. it carries an ETag or a `Surrogate-Key`: invalidating the child then invalidates the parent. A child without a cache attribute carries neither header and adds no dependency, so purging its URI leaves the parent as stored; give such a child a cache attribute. A value fetched eagerly in `onGet()` (`$this->resource->get(...)`) is not an embedded request and is not tracked.

```php
#[Cacheable]
class BlogPost extends ResourceObject
{
#[Embed(rel: 'comments', src: 'app://self/comments{?id}')]
public function onGet(int $id): static
{
$this->body = [
'post' => $this->getPost($id),
'comments' => $this->resource->get('app://self/comments', ['post_id' => $id]),
];
$this->body['post'] = $this->getPost($id);
return $this;
}
}
```

When comments are updated, BlogPost cache is automatically invalidated via surrogate keys.
With `app://self/comments` itself `#[Cacheable]`, updating the comments invalidates the BlogPost entry through its surrogate keys.

On a `#[CacheableResponse]`/`#[DonutCache]` page the same rule applies to a child whose value `onGet()` reads: the whole template is dropped on purge. A child the page only renders as a hole is recomposed instead.

## Donut Caching

Expand Down
5 changes: 5 additions & 0 deletions tests/FakeCountingPurger.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ public function __invoke(string $tag): void
{
$this->tags[] = $tag;
}

public function reset(): void
{
$this->tags = [];
}
}
4 changes: 3 additions & 1 deletion tests/RefreshAnnotatedCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ public function testRefreshToCacheableDestinationWritesExactlyOnce(): void
{
$this->resource->get('app://self/user/profile', ['user_id' => 1]);
$this->logger->events = [];
$this->purger->tags = [];
// A method call, not an assignment: PHPStan would keep the property narrowed to
// array{} across put(), which it cannot see appending to it.
$this->purger->reset();

$this->resource->put('app://self/user', ['id' => 1, 'name' => 'x', 'age' => 5]);

Expand Down
21 changes: 16 additions & 5 deletions vendor-bin/tools/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading