Last-Modified is a promise that the rendered representation has not changed since a specific time. If a page also depends on related records, permissions, feature flags, or the current clock, the page entity’s update time alone is not a valid cache validator.
Last updated: November 6, 2026.
<?php
final class PageController extends AbstractController
{
public function show(Page $page, Request $request): Response
{
$changedAt = maxDate(
$page->getUpdatedAt(),
$this->forms->latestChangeFor($page),
);
$response = new Response();
$response->setPrivate();
$response->setLastModified($changedAt);
if ($response->isNotModified($request)) {
return $response;
}
return $this->render('page/show.html.twig', ['page' => $page], $response);
}
}Determine the validator with cheap metadata queries before rendering. For personalized pages, keep the response private. If the output changes merely because time passes, remove the validator or include a deliberate time boundary.
Validate the whole representation
Symfony’s HTTP cache validation guide recommends using the latest modification date of every object required to compute the response. isNotModified() compares that value with If-Modified-Since and converts a matching response to HTTP 304 with no body.
A 304 tells the browser or intermediary to reuse its stored body. If a related form, navigation item, or query result changes without advancing the validator, that reused body is expected behavior, not a frozen PHP process. Track a reliable aggregate update time, use an ETag derived from all relevant versions, or disable conditional caching for that representation.
Separate validation from expiration
The HTTP caching specification’s conditional request rules distinguish cache validation from freshness lifetime. Setting Last-Modified does not by itself specify how long a response is fresh, but proxies, framework attributes, and existing Cache-Control headers can affect when validation occurs.
Inspect the request’s If-Modified-Since header and the final response’s status, Cache-Control, Age, and Vary headers. Test once with browser caching disabled and once with a normal conditional request. Never mark user-specific output public unless its cache key varies safely by every authorization dimension.
For clock-driven content such as “available after now,” compute the next change boundary and use a short explicit lifetime, or return no-store. Continue with debugging PHP response headers, logging HTTP exchanges safely, and passing controller data to views.