Skip to content

1.17.0

Latest

Choose a tag to compare

@koriym koriym released this 16 Sep 15:36
· 4 commits to 1.x since this release

Added

  • Cache observability rebuilt on Koriym.SemanticLogger (#178): typed, schema-validated open/event/close log tree replaces free-text RepositoryLoggerInterface messages. Off by default - install DevQueryRepositoryLogModule (dev) or ProdQueryRepositoryLogModule (prod) to record. See docs/reading-the-log.md, docs/why-the-log-records-everything.md, docs/what-the-log-proves.md (Japanese translations included).
  • New log contexts: cache_policy (#186), cache_error, pool_error, put_skipped, pre_write_cleanup, cdn_headers, conditional_request, invalidate (tri-state cdn, plus durationMs), command (source field), semantic_logger_error (core). Schemas in docs/schemas/context/.
  • New fields on existing save contexts: saved, tags, requestedTtl (save_etag), durationMs on the cache_hit/cache_miss close.
  • #[CacheLog] qualifier resolves the cache logger. SafeSemanticLogger/SessionStoreInterface/ProcessSession keep it safe across a serialized injector (#179). TopLevelAwareInterface lets a custom logger opt into manual-call scope rooting.
  • Direct (non-AOP) put()/putStatic()/putDonut()/purge()/invalidateTags() calls now open manual_store/manual_purge/manual_invalidate scopes.
  • LogSinkInterface/ShutdownFlush, LogWriterInterface (LogFileWriter/LogStreamWriter/PsrLogWriter), ConcurrentRuntimeInterface/HostRuntime (refuses to arm under RoadRunner/a Swoole coroutine). Detection is not exhaustive: FrankenPHP worker mode, ReactPHP, Amp, a long-lived CLI consumer, and a Swoole worker whose logger is built outside a coroutine must bind their own ConcurrentRuntimeInterface/SessionStoreInterface/LogSinkInterface.
  • ProdQueryRepositoryLogModule: buffers a session and applies RetentionPolicyInterface/KeepMutationsAndFailures at flush.
  • DevQueryRepositoryLogModule: writes one file per request plus latest.json for vendor/bin/stree.
  • demo/run-degraded.php and DemoLogCoverageTest: the demos now cover every context, schema enum value and command source, and self-validate against docs/schemas/context.
  • UriScopedHttpCacheInterface::isNotModifiedFor() / ScopedValidatorInterface::hasEtagFor(): opt-in ETag scoped to the requested URI (#197, #201). Entries written before this version cannot be scoped: each client pays one full (non-304) response after the upgrade, once.
  • Negative TTL is clamped to 0 at the QueryRepository/ResourceStorage boundary.
  • Dependency evidence in docs/reading-the-log.md is now stated per parent declaration (#188).

Deprecated

  • RepositoryLogger, RepositoryLoggerInterface, StructuredRepositoryLoggerInterface, NullRepositoryLogger: bound for BC but receive no internal events.
  • The update parameter of #[Cacheable]: it has no effect.

Removed

  • docs/schemas/repository-log.json (superseded by per-context schemas in docs/schemas/context/).
  • BEAR\QueryRepository\Log\NullSemanticLogger (koriym/semantic-logger 0.9 ships its own).
  • skills/bear-cache-log/: folded into bear-observe.

Changed

  • Cache logging call sites now emit typed contexts through SemanticLoggerInterface/#[CacheLog] instead of RepositoryLoggerInterface::log().
  • SaveDonutContext/SaveDonutViewContext: sMaxAge field renamed to requestedTtl.
  • SaveEtagContext/SaveDonutViewContext: surrogateKeys field renamed to tags.
  • A failed command write (4xx) now closes command_result instead of vanishing from the log.
  • Removed the post-save assert() in ResourceStorage::saveDonut() (it threw after saved: false was already logged).
  • Pre-write cleanup is recorded at the source (pre_write_cleanup marker) instead of inferred from tag correlation.
  • A donut refresh no longer appends an r marker to the ETag, so an unchanged recomposition still revalidates via If-None-Match.
  • New runtime dependency: koriym/semantic-logger.
  • Breaking: recording is off by default - #[CacheLog] binds to NullSemanticLogger unless a log module is installed.
  • Breaking (#190): Exception\CacheStoreFailure now marks the cache-failure boundary. An unreadable ETag pool answers the conditional request in full instead of failing it, and a donut write the store refuses now serves the rendered page instead of a 500.
  • Breaking (#208): a CDN purge failure now fails closed. Local pools invalidate first, the outcome is logged as invalidate.cdn: failed, then the purge exception propagates instead of being swallowed - a CDN outage can no longer look like a successful purge.
  • #[Refresh] no longer double-writes its own #[Cacheable] destination after a regenerating GET.
  • Breaking: renamed ResourceDonut::FOMRAT → FORMAT, EtagSetter::getEtagByEitireView() → getEtagByEntireView(), and the misspelled parameters $httpCacche → $httpCache (EtagSetter::getEtagByPartialBody()), $concheControlMaxAge → $cacheControlMaxAge (HeaderSetter::__invoke()) - a named-argument caller has to follow.

Fixed

  • Donut caching could not handle a non-200 response: saveView() required a 200-only validator, and DonutRepository/ResourceDonut did not restore the stored status code (#206, #207). Behaviour change: a #[CacheableResponse]/#[DonutCache] page answering 2xx or 3xx is now served with that status instead of crashing or degrading to 200 - a redirect that was reachable only once now persists until its cache entry is invalidated, so such a page needs the surrogate keys that invalidate it.
  • onPost on a #[Cacheable] class ran with no interceptor at all, silently dropping #[Refresh]/#[Purge] (#212, #214).
  • onPost on a #[Cacheable]/donut class missing an onGet-required parameter threw UnmatchedQuery uncaught - a regression from #214 above (#219, #220).
  • A write could be answered from cache without running, when a command method carried #[RefreshCache] or a method-level #[CacheableResponse]; both now bind DonutCommandInterceptor. DonutCacheModule also missed onPost in its write matcher.
  • DevEtagSetter/MobileEtagSetter set a validator on a non-200 response, where EtagSetter always skipped it; CdnCacheControlHeaderSetterInterface likewise now applies to 200 only.
  • A client If-None-Match token containing a PSR-6 reserved character reached the ETag pool as a cache key and threw a 500; such tokens (and *) are now dropped and the request answered in full.
  • An embedded child was rendered twice per parent store instead of reusing the execution the renderer already paid for.
  • A donut refresh advanced Last-Modified even for byte-identical content; Age is now derived from storedAt instead of Last-Modified.
  • putStatic()/putDonut() logged a negative lifetime verbatim while storage clamped what it stored.
  • A donut write failure on the AOP path (pool outage, renderer error) is now recorded as cache_error{operation: write}.
  • Behaviour change for 1.16.x installations: fixed a 1.16.0 regression where a resource declaring its own Surrogate-Key lost embed dependency tracking and kept serving purged children.
  • Deduplicated the Surrogate-Key header when a resource is written twice in one request.
  • ResourceStorage::saveEtag() used a hard-coded new UriTag() instead of the injected UriTagInterface.
  • Behaviour change for existing installations (#185): a donut template with no Surrogate-Key was untagged and unreachable by purge(); it now rebuilds on its first purge. Entries written before this version need a rewrite or a cache clear to become reachable.
  • A #[Cacheable] value entry with no renderer previously degraded every store to a warning and left the cache empty; it is now stored without rendering. The ETag validator falls back from the view to the body, a stored value entry no longer carries the renderer's Content-Type, and a custom EtagSetterInterface now receives $ro->view === null on the value path and must read the body instead.
  • A value-entry body PHP cannot serialize() (a Closure, for instance) is a separate, known limitation: the write's serialize() Exception propagates uncaught rather than degrading to CacheStoreFailure (#208's rule: only the store's own failure is swallowed). Keep such values out of a value entry's body, or catch Exception at the call site.
  • CliHttpCache::isNotModifiedFor() ignored the CLI request form's If-None-Match argument.
  • MatchQuery threw for an onGet parameter with a default value that a write's query omitted.
  • Two donut placeholders on one template line were captured as a single URI by a greedy pattern.
  • CDN-Cache-Control directives are now comma-separated per RFC 9213 (were space-separated).
  • Under AkamaiModule, a #[CacheableResponse] page lost its embedded children's invalidation tags (ResourceStorageInterface::saveDonutView() gained an optional $tags parameter).
  • MobileEtagSetter serialized the raw body and threw on any #[Embed]; it now hashes the materialized copy the way EtagSetter does.