ACL and Controllers
First of all: These are different things / layers most often. As you criticize the exemplary controller code, it puts both together - most obviously too tight.
tereško already outlined a way how you could decouple this more with the decorator pattern.
I'd go one step back first to look for the original problem you're facing and discuss that a bit then.
On the one hand you want to have controllers that just do the work they're commanded to (command or action, let's call it command).
On the other hand you want to be able to put ACL in your application. The field of work of these ACLs should be - if I understood your question right - to control access to certain commands of your applications.
This kind of access control therefore needs something else that brings these two together. Based on the context in which a command is executed in, ACL kicks in and decisions need to be done whether or not a specific command can be executed by a specific subject (e.g. the user).
Let's summarize to this point what we have:
The ACL component is central here: It needs to know at least something about the command (to identify the command to be precise) and it needs to be able to identify the user. Users are normally easily identified by a unique ID. But often in webapplications there are users that are not identified at all, often called guest, anonymous, everybody etc.. For this example we assume that the ACL can consume a user object and encapsulate these details away. The user object is bound to the application request object and the ACL can consume it.
What about identifying a command? Your interpretation of the MVC pattern suggests that a command is compound of a classname and a method name. If we look more closely there are even arguments (parameters) for a command. So it's valid to ask what exactly identifies a command? The classname, the methodname, the number or names of arguments, even the data inside any of the arguments or a mixture of all this?
Depending on which level of detail you need to identify a command in your ACL'ing, this can vary a lot. For the example let's keep it simply and specify that a command is identified by the classname and method name.
So the context of how these three parts (ACL, Command and User) are belonging to each other is now more clear.
We could say, with an imaginary ACL compontent we can already do the following:
$acl->commandAllowedForUser($command, $user);
Just see what is happeninig here: By making both the command and the user identifiable, the ACL can do it's work. The job of the ACL is unrelated to the work of both the user object and the concrete command.
There is only one part missing, this can't live in the air. And it doesn't. So you need to locate the place where the access control needs to kick in. Let's take a look what happens in a standard webapplication:
User -> Browser -> Request (HTTP)
-> Request (Command) -> Action (Command) -> Response (Command)
-> Response(HTTP) -> Browser -> User
To locate that place, we know it must be before the concrete command is executed, so we can reduce that list and only need to look into the following (potential) places:
User -> Browser -> Request (HTTP)
-> Request (Command)
At some point in your application you know that a specific user has requested to perform a concrete command. You already do some sort of ACL'ing here: If a user requests a command which does not exists, you don't allow that command to execute. So where-ever that happens in your application might be a good place to add the "real" ACL checks:
The command has been located and we can create the identification of it so the ACL can deal with it. In case the command is not allowed for a user, the command will not be executed (action). Maybe a CommandNotAllowedResponse
instead of the CommandNotFoundResponse
for the case a request could not be resolved onto a concrete command.
The place where the mapping of a concrete HTTPRequest is mapped onto a command is often called Routing. As the Routing already has the job to locate a command, why not extend it to check if the command is actually allowed per ACL? E.g. by extending the Router
to a ACL aware router: RouterACL
. If your router does not yet know the User
, then the Router
is not the right place, because for the ACL'ing to work not only the command but also the user must be identified. So this place can vary, but I'm sure you can easily locate the place you need to extend, because it's the place that fullfills the user and command requirement:
User -> Browser -> Request (HTTP)
-> Request (Command)
User is available since the beginning, Command first with Request(Command)
.
So instead of putting your ACL checks inside each command's concrete implementation, you place it before it. You don't need any heavy patterns, magic or whatever, the ACL does it's job, the user does it's job and especially the command does it's job: Just the command, nothing else. The command has no interest to know whether or not roles apply to it, if it's guarded somewhere or not.
So just keep things apart that don't belong to each other. Use a slightly rewording of the Single Responsibility Principle (SRP): There should be only one reason to change a command - because the command has changed. Not because you now introduce ACL'ing in your application. Not because you switch the User object. Not because you migrate from an HTTP/HTML interface to a SOAP or command-line interface.
The ACL in your case controls the access to a command, not the command itself.
if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()
(else, display "You do not have access to this user's profile" or something like that? I don't get it. – Havre