methorz / http-cache-middleware
PSR-15 HTTP caching middleware with ETag support and RFC 7234 compliance
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/methorz/http-cache-middleware
Requires
- php: ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.0 || ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- psr/container: ^2.0
- slevomat/coding-standard: ^8.25
- squizlabs/php_codesniffer: ^4.0
Suggests
- laminas/laminas-servicemanager: For Mezzio integration (use Integration\Mezzio\ConfigProvider)
This package is auto-updated.
Last update: 2025-12-01 09:32:22 UTC
README
PSR-15 HTTP caching middleware with ETag support and RFC 7234 compliance
Automatic HTTP caching for PSR-15 applications with ETag generation, 304 Not Modified responses, and Cache-Control header management. Zero configuration, production-ready.
โจ Features
- ๐ท๏ธ Automatic ETag Generation - MD5/SHA256/custom algorithm support
- ๐ 304 Not Modified - Automatic conditional request handling
- ๐ Cache-Control Builder - Fluent interface for RFC 7234 directives
- โ RFC Compliant - RFC 7234 (caching) & RFC 7232 (conditional requests)
- ๐ฏ Conditional Requests -
If-None-Match, wildcard support - ๐ช Strong & Weak ETags - Full support for both ETag types
- ๐ง Zero Configuration - Sensible defaults, works out-of-the-box
- ๐จ Highly Customizable - Control caching behavior per-route
- ๐ฆ Framework Agnostic - Works with any PSR-15 application
๐ฆ Installation
composer require methorz/http-cache-middleware
๐ Quick Start
Basic Usage
use MethorZ\HttpCache\Middleware\CacheMiddleware; // Add to middleware pipeline $app->pipe(new CacheMiddleware());
That's it! All GET and HEAD requests will now have:
- Automatic ETag generation
- 304 Not Modified responses
- Proper caching headers
๐ Detailed Usage
With Cache-Control Directives
use MethorZ\HttpCache\Middleware\CacheMiddleware; use MethorZ\HttpCache\Directive\CacheControlDirective; $cacheControl = CacheControlDirective::create() ->public() ->maxAge(3600) ->mustRevalidate(); $middleware = new CacheMiddleware(cacheControl: $cacheControl);
Generated Headers:
ETag: "5d41402abc4b2a76b9719d911017c592"
Cache-Control: public, max-age=3600, must-revalidate
Configuration Options
$middleware = new CacheMiddleware( enabled: true, // Enable/disable caching cacheControl: $directive, // Cache-Control directive useWeakEtag: false, // Use weak ETags (W/) etagAlgorithm: 'md5', // Hash algorithm (md5, sha256, etc.) cacheableMethods: ['GET', 'HEAD'], // Cacheable HTTP methods cacheableStatuses: [200, 203], // Cacheable status codes );
Development Mode (Disable Caching)
For development, you want fresh data on every request. Simply disable the middleware:
// Option 1: Conditionally add middleware based on environment if (getenv('APP_ENV') !== 'development') { $app->pipe(new CacheMiddleware()); } // Option 2: Disable via constructor parameter $middleware = new CacheMiddleware( enabled: getenv('APP_ENV') !== 'development' ); // Option 3: Don't add middleware to pipeline in development // (recommended - cleanest approach)
Recommended approach: Only add CacheMiddleware to your production pipeline configuration, not in development.
๐ฏ Cache-Control Directive Builder
Fluent interface for building Cache-Control headers:
Common Patterns
Public, cacheable for 1 hour:
CacheControlDirective::create() ->public() ->maxAge(3600) ->mustRevalidate(); // Output: "public, max-age=3600, must-revalidate"
Private, no caching:
CacheControlDirective::create() ->private() ->noCache(); // Output: "private, no-cache"
Immutable assets (images, CSS, JS):
CacheControlDirective::create() ->public() ->maxAge(31536000) // 1 year ->immutable(); // Output: "public, max-age=31536000, immutable"
API responses with shared cache:
CacheControlDirective::create() ->public() ->maxAge(300) // Browser cache: 5 minutes ->sMaxAge(3600) // CDN cache: 1 hour ->staleWhileRevalidate(60); // Output: "public, max-age=300, s-maxage=3600, stale-while-revalidate=60"
All Directives
| Method | Description | Example |
|---|---|---|
public() |
Cache may be stored by any cache | public |
private() |
Cache only for single user | private |
noCache() |
Must revalidate before use | no-cache |
noStore() |
Must not be stored anywhere | no-store |
maxAge(int) |
Maximum freshness time | max-age=3600 |
sMaxAge(int) |
Shared cache max age | s-maxage=7200 |
mustRevalidate() |
Must revalidate when stale | must-revalidate |
proxyRevalidate() |
Proxy must revalidate | proxy-revalidate |
noTransform() |
Cache must not transform response | no-transform |
staleWhileRevalidate(int) |
Serve stale while fetching fresh | stale-while-revalidate=60 |
staleIfError(int) |
Serve stale if origin errors | stale-if-error=120 |
immutable() |
Response will never change | immutable |
๐ท๏ธ ETag Generation
Automatic ETag Generation
use MethorZ\HttpCache\Generator\ETagGenerator; // Strong ETag (exact match required) $etag = ETagGenerator::generate($response); // Output: "5d41402abc4b2a76b9719d911017c592" // Weak ETag (semantic equality) $weakEtag = ETagGenerator::generateWeak($response); // Output: W/"5d41402abc4b2a76b9719d911017c592" // Custom algorithm $sha256Etag = ETagGenerator::generateWithAlgorithm($response, 'sha256');
ETag Utilities
// Check if ETag is weak ETagGenerator::isWeak('W/"abc"'); // true ETagGenerator::isWeak('"abc"'); // false // Extract hash value ETagGenerator::extractHash('"abc123"'); // "abc123" ETagGenerator::extractHash('W/"abc123"'); // "abc123" // Compare ETags ETagGenerator::matches('"abc"', 'W/"abc"', weakComparison: true); // true ETagGenerator::matches('"abc"', 'W/"abc"', weakComparison: false); // false
๐ How It Works
1. First Request (Cache Miss)
Client โ GET /api/items
Server โ 200 OK
ETag: "abc123"
Cache-Control: public, max-age=3600
Body: {...}
Client caches response with ETag
2. Subsequent Request (Cache Validation)
Client โ GET /api/items
If-None-Match: "abc123"
Server โ 304 Not Modified
ETag: "abc123"
Cache-Control: public, max-age=3600
(empty body)
Benefits:
- โก Faster: No body transmission (~95% bandwidth reduction)
- ๐ฐ Cheaper: Reduced server CPU & network costs
- ๐ Better UX: Instant responses for unchanged resources
๐ฏ Use Cases
1. Static Asset Caching
// For images, CSS, JS with content hashing in filename $middleware = new CacheMiddleware( cacheControl: CacheControlDirective::create() ->public() ->maxAge(31536000) // 1 year ->immutable(), );
2. API Response Caching
// Cache API responses for 5 minutes $middleware = new CacheMiddleware( cacheControl: CacheControlDirective::create() ->public() ->maxAge(300) ->mustRevalidate(), );
3. Dynamic Content with Validation
// Always validate with server, but use weak ETags $middleware = new CacheMiddleware( useWeakEtag: true, cacheControl: CacheControlDirective::create() ->public() ->noCache() // Always revalidate ->maxAge(0), );
4. Private User Data
// Cache in browser only, not in shared caches $middleware = new CacheMiddleware( cacheControl: CacheControlDirective::create() ->private() ->maxAge(300), );
5. CDN Integration
// Different cache times for browser vs CDN $middleware = new CacheMiddleware( cacheControl: CacheControlDirective::create() ->public() ->maxAge(300) // Browser: 5 minutes ->sMaxAge(3600) // CDN: 1 hour ->staleWhileRevalidate(60), );
๐ง Configuration Examples
Mezzio / Laminas
// config/autoload/middleware.global.php use MethorZ\HttpCache\Middleware\CacheMiddleware; use MethorZ\HttpCache\Directive\CacheControlDirective; return [ 'dependencies' => [ 'factories' => [ CacheMiddleware::class => function (): CacheMiddleware { return new CacheMiddleware( cacheControl: CacheControlDirective::create() ->public() ->maxAge(3600), ); }, ], ], ]; // config/pipeline.php $app->pipe(CacheMiddleware::class);
Per-Route Configuration
// Apply different caching strategies per route $publicCaching = new CacheMiddleware( cacheControl: CacheControlDirective::create()->public()->maxAge(3600), ); $privateCaching = new CacheMiddleware( cacheControl: CacheControlDirective::create()->private()->maxAge(300), ); $app->get('/api/public', [$publicCaching, PublicHandler::class]); $app->get('/api/user/profile', [$privateCaching, ProfileHandler::class]);
๐ HTTP Headers Reference
Request Headers (Client โ Server)
| Header | Description | Example |
|---|---|---|
If-None-Match |
Conditional request with ETag | "abc123" or W/"abc123" or * |
If-Modified-Since |
Conditional request with date | Wed, 21 Oct 2015 07:28:00 GMT |
Response Headers (Server โ Client)
| Header | Description | Example |
|---|---|---|
ETag |
Entity tag for resource version | "abc123" or W/"abc123" |
Cache-Control |
Caching directives | public, max-age=3600 |
Expires |
Absolute expiration time | Wed, 21 Oct 2025 07:28:00 GMT |
Last-Modified |
Resource modification time | Wed, 21 Oct 2024 07:28:00 GMT |
๐งช Testing
# Run tests composer test # Static analysis composer analyze # Code style composer cs-check composer cs-fix
Test Coverage: 37 tests, 57 assertions, 100% passing
โก Performance Impact
Bandwidth Savings
Without caching:
GET /api/items โ 200 OK (10 KB body) โ 10 KB transferred
With caching (subsequent requests):
GET /api/items (If-None-Match: "abc123") โ 304 Not Modified โ ~500 bytes transferred
Savings: ~95% bandwidth reduction
Server Load Reduction
- โ 304 responses skip expensive body serialization
- โ ETag comparison is instant (simple hash check)
- โ Reduces database queries when responses haven't changed
- โ Lower CPU usage for repeated identical requests
๐ Security Considerations
Private vs Public
// โ Don't cache sensitive user data publicly CacheControlDirective::create()->public(); // Bad for /api/user/profile // โ Use private for user-specific data CacheControlDirective::create()->private(); // Good for /api/user/profile
Cache Invalidation
This middleware handles validation (304 responses), not invalidation. For cache invalidation:
- Change resource content โ new ETag โ cache miss โ fresh response
- Use
no-cachedirective โ always revalidate with server - Use CDN purge APIs for immediate invalidation
๐ Resources
๐ Related Packages
This package is part of the MethorZ HTTP middleware ecosystem:
| Package | Description |
|---|---|
| methorz/http-dto | Automatic HTTP โ DTO conversion with validation |
| methorz/http-problem-details | RFC 7807 error handling middleware |
| methorz/http-cache-middleware | HTTP caching with ETag support (this package) |
| methorz/http-request-logger | Structured logging with request tracking |
| methorz/openapi-generator | Automatic OpenAPI spec generation |
These packages work together seamlessly in PSR-15 applications.
๐ License
MIT License. See LICENSE for details.
๐ค Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.