adsazad/symfony-etagging
Symfony bundle that adds easy ETag support to any Response. Inject EtaggingInterface and wrap rendered responses with etagResponse() to enable caching/conditional requests. Supports custom headers plus configurable max-age and shared max-age.
Installation
composer require adsazad/symfony-etagging
Add the bundle to config/bundles.php:
return [
// ...
Adsazad\SymfonyEtaggingBundle\SymfonyEtaggingBundle::class => ['all' => true],
];
First Use Case
Inject EtaggingInterface into a controller and wrap your response:
use Adsazad\SymfonyEtaggingBundle\Util\EtaggingInterface;
public function index(EtaggingInterface $etag)
{
$response = $this->render('home.html.twig');
return $etag->etagResponse($response);
}
Where to Look First
src/Util/EtaggingInterface.php for method signatures.Etagging Responses
Use etagResponse() on any Symfony Response object (e.g., render(), json(), redirect()).
$response = new Response('Content');
return $etag->etagResponse($response);
Custom Headers
Extend functionality by adding custom headers before calling etagResponse():
$etag->addCustom('X-My-Header', 'value');
return $etag->etagResponse($response);
Cache Control
$etag->setMaxAge(3600); // 1 hour
$etag->setSharedMax(86400); // 1 day
Integration with Twig Dynamically generate ETags based on template content:
{% set contentHash = render(contentBlock) | hash('sha256') %}
Then pass to etagResponse() via controller logic.
Middleware Approach For global ETagging, create a middleware:
public function handle(Request $request, callable $next)
{
$response = $next($request);
$etag->etagResponse($response);
return $response;
}
Register in config/routes.php:
$app->middleware(EtagMiddleware::class)->group(...);
ETag Collisions
Response content + headers. If two responses differ only in non-cached headers (e.g., Last-Modified), collisions may occur.$etag->addCustom('X-Content-Hash', hash('sha256', $response->getContent()));
Shared vs. Private Max-Age
setSharedMax() overrides setMaxAge() if both are called. Test edge cases:
$etag->setMaxAge(3600)->setSharedMax(1800); // Shared wins!
Conditional Requests
If-None-Match with ETags. Ensure your app handles 304 Not Modified:
if ($request->headers->has('If-None-Match') && $etag->isNotModified($request)) {
return new Response('', 304);
}
Debugging ETags
Network tab). Missing ETag or incorrect Cache-Control indicates misconfiguration.$etagValue = $etag->etagResponse($response)->headers->get('ETag');
$this->logger->info('Generated ETag:', ['etag' => $etagValue]);
Leverage Symfony’s Cache System
Combine with Cache component for dynamic ETag generation:
$cacheKey = 'response_'.md5($request->getPathInfo());
$content = $this->cache->get($cacheKey, function() use ($request) {
return $this->render('page.html.twig', ['data' => $this->fetchData()]);
});
Extension Points
EtaggingInterface:
class CustomEtagger implements EtaggingInterface {
public function etagResponse(Response $response): Response {
// Custom logic (e.g., database versioning)
return parent::etagResponse($response);
}
}
services.yaml:
Adsazad\SymfonyEtaggingBundle\Util\EtaggingInterface: '@custom_etagger'
Performance Tuning
setSharedMax() with long durations (e.g., 31536000 for 1 year).setMaxAge() (e.g., 60 seconds) with must-revalidate.Testing
EtaggingInterface in PHPUnit:
$mockEtag = $this->createMock(EtaggingInterface::class);
$mockEtag->method('etagResponse')->willReturn(new Response('Mocked'));
If-None-Match headers:
$client->request('GET', '/page', [], [], [
'HTTP_IF_NONE_MATCH' => 'expected-etag',
]);
$this->assertEquals(304, $client->getResponse()->getStatusCode());
Laravel-Specific Notes
AppServiceProvider:
$this->app->bind(EtaggingInterface::class, function($app) {
return new \Adsazad\SymfonyEtaggingBundle\Util\Etagging();
});
HandleEtagMiddleware in app/Http/Kernel.php:
protected $middleware = [
\App\Http\Middleware\HandleEtagMiddleware::class,
];
How can I help you explore Laravel packages today?