Controller attributes like #[IsCsrfTokenValid], #[IsSignatureValid],
#[Lock] and #[RateLimit] keep your controllers short by moving common
checks out of them. When a check fails, Symfony throws an exception and the user
gets an error response.
This is fine for APIs, but in HTML applications you may want to handle things differently. For example, if an email confirmation link has expired, you might prefer to show a helpful message and redirect the user. Until now, handling this in the controller meant removing the attribute and doing the check yourself.
In Symfony 8.2, controllers can recover from attribute failures. Add a nullable argument typed with the attribute's exception. If the check fails, Symfony passes the exception to that argument and calls the controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Component\HttpFoundation\Exception\SignedUriException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\IsSignatureValid;
// ...
#[IsSignatureValid]
public function verifyEmail(?SignedUriException $signatureError = null): Response
{
if (null !== $signatureError) {
$this->addFlash('error', 'This link is invalid or has expired.');
return $this->redirectToRoute('app_login');
}
// ...
}
This works with the following attributes and exceptions:
#[IsCsrfTokenValid]:InvalidCsrfTokenException#[IsSignatureValid]:SignedUriException#[Lock]:LockConflictedException#[RateLimit]:RateLimitExceededException
The argument's type determines which failures you handle. Use a subclass to be
more specific (e.g. ?ExpiredSignedUriException, a subclass of
SignedUriException, handles only expired links) or a union type to handle
failures from several attributes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#[IsCsrfTokenValid('delete-post')]
#[RateLimit('delete_post')]
public function delete(
Post $post,
InvalidCsrfTokenException|RateLimitExceededException|null $failure = null,
): Response {
if (null !== $failure) {
$this->addFlash('error', match (true) {
$failure instanceof InvalidCsrfTokenException => 'Your session expired. Please try again.',
$failure instanceof RateLimitExceededException => 'Too many deletions. Please try again later.',
});
return $this->redirectToRoute('post_show', ['id' => $post->getId()]);
}
// ...
}
Controllers without these arguments behave as before. Keep in mind that when
the controller receives the exception, it runs anyway, so it must check the
argument. That's why #[IsGranted] never passes its failures to the
controller: forgetting that check would let users run actions they aren't
allowed to.
You can enable the same behavior for your own attributes with the new
#[RecoverableAttribute]. See the controller attributes documentation for
details.