For the complete documentation index, see llms.txt. This page is also available as Markdown.

Controllers

Declaring the policy on the action

use AccessControl\Attribute\AccessPolicy;
use AccessControl\Attribute\Argument;

final class PostController extends AbstractController
{
    #[AccessPolicy('EDIT', new Argument('post'), message: 'Only the author may edit this post.')]
    public function edit(Post $post): Response
    {
        // ...
    }
}

The listener reads the attribute, resolves new Argument('post') against the controller arguments, asks the question, and throws before your code runs. A denial comes out as a 403, the exception carrying #[WithHttpStatus(403)].

On the class rather than the method, the policy applies to every action. The attribute is repeatable, and composes with All, AtLeastOneOf and When. See Access Policies.

#[IsGranted] keeps working

From the moment the bundle is registered, Symfony's #[IsGranted] is read by this component, not by Security. Nothing in your code changes.

#[IsGranted('EDIT', 'post')]
public function edit(Post $post): Response

The bridge listener matches Security's own behaviour, including the parts that are easy to get wrong: a subject given as an Expression is evaluated rather than taken as an argument name, and methods is matched case-insensitively.

Two listeners answering the same attribute would ask the requester twice for one question, and the profiler would show two. So the bundle removes Security's, rather than letting both run.

What is lost, and it is confined. Security's listener fills setAttributes(), setSubject() and setAccessDecision() on the exception it throws; this component's exception carries none of that. Nothing in Symfony reads those three accessors, so the loss only reaches application code that reads them in an AccessDeniedHandlerInterface or an error template.

isGranted() and denyAccessUnlessGranted() without Security

AbstractController::isGranted() and denyAccessUnlessGranted() raise a LogicException telling you to install SecurityBundle when Security is absent, whichever other stack the application registered. That check lives in FrameworkBundle, which this project does not patch.

Add the trait, and nothing else changes:

A trait method wins over the one inherited from the parent class, so the call sites stay as they are. With Security present, the trait defers to it.

Injecting the checker

For a controller that is not an AbstractController, or anywhere else in the application:

decide() on the same service hands back the full AccessDecision, with the reason and the votes, when a boolean is not enough.

Asking about somebody else

The checker binds the current requester. To ask about another one, build the request yourself:

Last updated

Was this helpful?