Skip to content

feat(page-cache): full-page HTTP cache with file driver, tag invalidation, and entity bridge#58

Merged
markshust merged 6 commits into
marko-php:developfrom
michalbiarda:feature/page-cache-package
May 12, 2026
Merged

feat(page-cache): full-page HTTP cache with file driver, tag invalidation, and entity bridge#58
markshust merged 6 commits into
marko-php:developfrom
michalbiarda:feature/page-cache-package

Conversation

@michalbiarda
Copy link
Copy Markdown

Summary

Introduces three new packages for full-page HTTP response caching:

  • marko/page-cache — interface package with PageCacheInterface, #[Cacheable(ttl, tags, provider)] attribute, PageCacheMiddleware (registered globally), CacheabilityChecker, CLI commands (page-cache:clear, page-cache:purge, page-cache:status), CacheTagProviderInterface for dynamic per-request tags, IdentityInterface for entity-driven invalidation, and a boot-time IdentityBridgeValidator that fails loudly when the bridge is missing
  • marko/page-cache-file — file driver implementing PageCacheInterface with atomic writes and tag reverse-index for purgeTag()
  • marko/page-cache-entity — bridge package: three auto-discovered observers (PurgeOnEntityCreated/Updated/Deleted) delegate to IdentityPurger, which purges all tags returned by entities implementing IdentityInterface when saved or deleted

Test plan

  • composer test passes
  • ./vendor/bin/phpcs && ./vendor/bin/php-cs-fixer fix passes with no changes
  • Routes with #[Cacheable] are served from cache on second request
  • page-cache:purge <url> and page-cache:purge --tag <tag> clear the correct entries
  • #[Cacheable(provider: SomeProvider::class)] merges dynamic tags with static tags
  • Saving an entity implementing IdentityInterface purges its tags from the page cache
  • Boot throws PageCacheException::missingEntityBridge() when IdentityInterface is implemented without the bridge installed

🤖 Generated with Claude Code

michalbiarda and others added 4 commits May 9, 2026 20:31
…ages

Introduces full-page HTTP response caching with attribute-driven opt-in,
tag-based invalidation, and a global middleware that intercepts requests
to serve or store cached responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move helper functions from Pest.php into helpers.php (package-scoped
  require_once) so they load correctly under the root phpunit.xml runner
- Rename createDriver/cleanupDir to createPageCacheFileDriver/cleanupPageCacheDir
  to avoid global function collisions with cache-redis and database tests
- Rename makeRequest/makeResponse to makeCacheCheckerRequest/makeCacheCheckerResponse
  in CacheabilityCheckerTest to avoid collision with layout package tests
- Fix PackageStructureTest dirname depth (2→3) for root composer.json lookup
- Add missing .gitattributes, LICENSE to page-cache and page-cache-file packages
- Add page-cache and page-cache-file to GitHub issue templates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…event public/ exposure

FilePageCacheDriver now injects ProjectPaths and resolves relative paths
against the project base directory, matching the DebugbarStorage pattern.
This prevents cache files from landing in public/ when PHP's CWD is set
to the web root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…age-cache-entity bridge

- CacheTagProviderInterface + provider param on #[Cacheable] for per-request dynamic tags
- IdentityInterface in marko/page-cache for entities to declare their cache identities
- PageCacheMiddleware resolves provider via container, merges static + dynamic tags (deduplicated)
- marko/page-cache-entity bridge package: IdentityPurger + three observers (Created/Updated/Deleted)
- IdentityBridgeValidator: loud-fail boot check when IdentityInterface is used without the bridge installed
- README and docs updates for all new extension points

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label May 11, 2026
@michalbiarda
Copy link
Copy Markdown
Author

@markshust The whole feature was implemented using your Claude Code automation. When you decide to merge it, I'll work on implementing more drivers (Varnish, etc.). File driver was a proof of concept.

markshust and others added 2 commits May 12, 2026 09:36
Maintainer follow-up to PR marko-php#58. All changes are mechanical / non-behavioural:

1. **Slim READMEs to match Marko convention.**
   Per `docs/DOCS-STANDARDS.md`, package READMEs are slim pointers
   (Title + one-liner, Installation, Quick Example, Documentation link)
   and the docs site is the source of truth. Sibling packages
   (`cache`, `cache-file`, `database`) already follow this format.

2. **Remove vestigial README-content assertions.**
   Deleted `packages/page-cache/tests/ReadmeTest.php` and dropped two
   tests in `page-cache-entity/tests/PackageStructureTest.php` that
   asserted the README contained `IdentityPurger` signatures, observer
   class names, and `## API Reference`. Replaced with a docs-link
   assertion. Sibling slim packages do not assert README content.

3. **Fix incorrect FQCN namespaces in docs pages.**
   `Marko\PageCache\Service\CacheabilityChecker` → `Marko\PageCache\CacheabilityChecker`
   `Marko\PageCache\ValueObjects\CachePolicy` → `Marko\PageCache\CachePolicy`
   `Marko\PageCache\ValueObjects\CacheKey`    → `Marko\PageCache\CacheKey`
   Affects `page-cache.md` (4 occurrences) and `page-cache-file.md` (1).
   Copy-pasting the docs page examples now matches the real namespaces.

4. **Lint cleanup (`phpcbf` + `php-cs-fixer`).**
   Resolved all 53 sniff violations across 17 files in the three new
   packages — multiline method signatures, multiline function calls,
   and php-cs-fixer normalization. No semantic changes.

Verified: `composer test` passes (5038 / 0 failed), `phpcs` and
`php-cs-fixer` both clean.

Co-Authored-By: Michał Biarda <1135380+michalbiarda@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@markshust
Copy link
Copy Markdown
Collaborator

Hi Michał — thanks for the page-cache packages, this is a really nice addition. I've pushed a maintainer follow-up commit (32d823d) directly to your branch with three categories of polish that are common to all new Marko packages. Sharing the reasoning so it's useful for future PRs:

1. Slim READMEs. Marko's docs convention (see docs/DOCS-STANDARDS.md) is that the docs site is the source of truth and package READMEs are slim pointers — title + one-liner, composer require, one quick example, link to the docs page. Sibling packages like marko/cache, marko/cache-file, and marko/database already follow this format. Your original READMEs had the full content (Usage subsections, CLI commands, API Reference, Customization) — I confirmed everything was already captured in the docs pages and trimmed the READMEs accordingly. Nothing was lost.

2. Vestigial README-content tests. ReadmeTest.php and a couple of tests in PackageStructureTest.php were asserting the README contained ## API Reference, IdentityPurger signatures, observer class names, etc. — content that now lives only in the docs page. Slim sibling packages don't carry these tests, so I removed them and added one assertion that the README links to the docs page.

3. Docs-page namespace bugs. A few use statements in the docs pages referenced namespaces that don't exist:

  • Marko\PageCache\Service\CacheabilityCheckerMarko\PageCache\CacheabilityChecker
  • Marko\PageCache\ValueObjects\CachePolicyMarko\PageCache\CachePolicy
  • Marko\PageCache\ValueObjects\CacheKeyMarko\PageCache\CacheKey

A user copy-pasting from the docs would have hit class-not-found errors. Fixed in both page-cache.md and page-cache-file.md.

4. Lint. 53 phpcs sniff violations (multiline method signatures, multiline function calls) and a few php-cs-fixer normalizations across 17 files. Project policy is to ship touched files lint-clean. All auto-fixable via composer exec phpcbf + composer exec php-cs-fixer fix.

Also flagging one architectural observation, not addressed here: PageCacheMiddleware and the router both call RouteMatcherInterface::match() for the same request, so a cacheable request resolves the route twice. The cleanest fix needs changes in marko/routing (memoize inside RouteMatcher::match() and have Router resolve RouteMatcherInterface from the container instead of constructing a private instance). I'll open a follow-up issue against marko/routing for that — it's outside the scope of this PR.

Verified locally: composer test → 5038 passed / 0 failed; phpcs + php-cs-fixer clean. Thanks again — looking forward to merging.

@michalbiarda
Copy link
Copy Markdown
Author

Cool. Thanks for the fixes and reasoning behind them. I'm happy to have my first PR merged to Marko 😄. Now it's time to implement other drivers 👿.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants