Access control
Authentication answers "who is this", and access control answers "what are they allowed to do". The most common task is keeping guests away from the pages that add and delete posts. Checking this in every action is inconvenient, so the check is placed in one spot that every request passes through.
Where to put the check
The module class has an onBootstrap() method: Laminas calls it on startup. Inside it you can subscribe to an event — a step of request handling. The MvcEvent::EVENT_ROUTE event happens when the route is already known but the controller has not been called yet. A separate lesson covers events in more detail.
Protected routes
Open module/Blog/src/Module.php and add the method (if it already exists after the sessions lesson, extend it):
// module/Blog/src/Module.php
use Laminas\Authentication\AuthenticationService;
use Laminas\Mvc\MvcEvent;
public function onBootstrap(MvcEvent $event): void
{
$events = $event->getApplication()->getEventManager();
$events->attach(MvcEvent::EVENT_ROUTE, function (MvcEvent $e) {
$match = $e->getRouteMatch();
if ($match === null) {
return;
}
$protected = ['blog/add', 'blog/edit', 'blog/delete'];
$auth = $e->getApplication()->getServiceManager()->get(AuthenticationService::class);
if (!in_array($match->getMatchedRouteName(), $protected, true) || $auth->hasIdentity()) {
return;
}
$response = $e->getResponse();
$response->getHeaders()->addHeaderLine('Location', $e->getRouter()->assemble([], ['name' => 'login']));
$response->setStatusCode(302);
return $response;
}, -100);
}The listener takes the name of the matched route. If it is in the protected list and the visitor is not logged in, the listener returns a redirect response and the controller is not called. The number -100 is the priority: a negative value puts the listener after the built-in route matching.
Log out through /logout and open /blog/add — the browser lands on /login.
Roles and permissions
When there are several kinds of users, "logged in or not" is not enough. The laminas-permissions-acl package describes an access control list (ACL): roles, resources and permissions.
composer require laminas/laminas-permissions-acluse Laminas\Permissions\Acl\Acl;
$acl = new Acl();
$acl->addRole('guest');
$acl->addRole('editor', 'guest');
$acl->addRole('admin', 'editor');
$acl->addResource('post');
$acl->allow('guest', 'post', 'view');
$acl->allow('editor', 'post', ['add', 'edit']);
$acl->allow('admin');
$acl->isAllowed('editor', 'post', 'edit'); // true
$acl->isAllowed('editor', 'post', 'delete'); // falseThe second argument of addRole() is the parent: editor can do everything guest can. Calling allow('admin') without a resource allows the role everything.
| Term | What it is | Example |
|---|---|---|
| Role | A kind of user | guest, editor, admin |
| Resource | The thing access is restricted to | post |
| Privilege | An action on the resource | view, edit, delete |
The Acl object is created in a factory and registered as a service, and the listener calls isAllowed() with the role from the user's identity instead of hasIdentity(). For large projects there is a similar package, laminas-permissions-rbac, where permissions are tied to roles without resources.
Common mistake
Hiding the "Delete" link in a view script is not protection: the address /blog/delete/5 can be typed by hand. Permissions are checked on the server — in a listener or in the action — and hiding links only makes the page more convenient.