Check access to controller method

Hello,

I have several controllers with different actions. Let’s assume that in a few cases I would like for the whole controller to check if the user, based on the passed parameter (let’s assume category_id), can perform actions in it. What is the best method to not duplicate the code in each action, but to check based on the parameter from routing and let’s say a database query, if the user has access to this controller and the actions in it?

Hi!
Well, I am doing exactly what you mentioned (manually querying the ACL at the beginning of each action), but I think that can be replaced with an MvcEvent-Listener for the event ‘route’. (Using the EventManager - tutorials - Laminas Docs)

Laminas\Mvc\Application L307:

// Trigger route event
        $event->setName(MvcEvent::EVENT_ROUTE);
        $event->stopPropagation(false); // Clear before triggering
        $result = $events->triggerEventUntil($shortCircuit, $event);
        if ($result->stopped()) {
            $response = $result->last();
            if ($response instanceof ResponseInterface) {
                $event->setName(MvcEvent::EVENT_FINISH);
                $event->setTarget($this);
                $event->setResponse($response);
                $event->stopPropagation(false); // Clear before triggering
                $events->triggerEvent($event);
                $this->response = $response;
                return $this;
            }
        }

Hope this helps :slight_smile:

Correct, this is the recommended way.

An example of a listener can be found in the documentation of laminas-view:

https://docs.laminas.dev/laminas-view/cookbook/setting-module-specific-layouts/

The registration of listener can be simplified, module/MyModule/config/module.config.php:

return [
    'listeners' => [
        MyModule\Listener\MyListener::class,
    ],
    'service_manager' => [
        'factories' => [
             MyModule\Listener\MyListener::class => Laminas\ServiceManager\Factory\InvokableFactory::class,
        ],
    ],
];

Thanks for confirming :smiley:

'service_manager' => [
      MyModule\Listener\MyListener::class => Laminas\ServiceManager\Factory\InvokableFactory::class,
   ],

Should there be a “factories” key?

'service_manager' => [
 'factories' => [ //here?
    MyModule\Listener\MyListener::class => Laminas\ServiceManager\Factory\InvokableFactory::class,
 ]
]

Right, I have updated the code example! :+1:t3:

1 Like

Thank you for all your suggestions. Everyone has been helpful.

By the way, there was a question: if I check whether the user is the owner of the category, should I return the category object retrieved in the listener under some variable, so that in the controller in the methods I do not duplicate the query?
I suspect the listener is not a good place. Once because of the role, twice because the function has too much responsibility. But on the other hand it’s always fewer database queries.

I’m sorry but your question was to check something based on some parameter from routing and not to retrieve something.
Please provide a code example of your controller to illustrate what you need so we can better understand your use case.

First, thank you for your help and guidance.

Let me describe my problem in detail.

Users, who have accounts in the portal, have access to the content management panel. Due to the nature of the site there are several hundred of them. In the content management panel of their pages there are several controllers. Among them there is a group of say 10 controllers, which are used to manage the content created by the user’s own web pages. And here at the outset in each controller and each method I have a call to a class say websiteBuilder, which currently does two things: it checks access to the method based on the passed routing parameter and if the user has access to it also sets a parameter with the site object - it has to fetch it anyway to check access, so it returns it by the way.

I would like to do the checking in one place - here the listener is perfect. There is still the issue of calling the object retrieval controller in one place. In zend framework 1 there was init() method and here it would be perfect.

class WebsiteController extends AbstractController
{

	protected $websiteBuilder;
	...

	public function __construct(
		WebsiteBuilderInterface $websiteBuilder,
		...
	)
	{
		$this->websiteBuilder = $websiteBuilder;
		...
	}

	public function indexAction()
	{
		$websiteBuilder = $this->websiteBuilder;
		$websiteBuilder->setWebsiteId((int) $this->params()->fromRoute('website_id'));
		$websiteBuilder->run();
		$resultWebsiteBuilder = $websiteBuilder->getParams();

		if (! $resultWebsiteBuilder->isValid()) {
			// set message
			$this->flashMessenger()->addErrorMessage($resultWebsiteBuilder->getErrorMessage());
			// redirect
			return $this->redirect()->toRoute($resultWebsiteBuilder->getRedirect(), $params = $resultSiteBuilder->getRedirectAction());
		}
		
		$websiteRow = $resultWebsiteBuilder->getWebsiteObject();

		...
		[other code]
	}

	public function otherAction()
	{
		$websiteBuilder = $this->websiteBuilder;
		$websiteBuilder->setWebsiteId((int) $this->params()->fromRoute('website_id'));
		$websiteBuilder->run();
		$resultWebsiteBuilder = $websiteBuilder->getParams();

		if (! $resultWebsiteBuilder->isValid()) {
			// set message
			$this->flashMessenger()->addErrorMessage($resultWebsiteBuilder->getErrorMessage());
			// redirect
			return $this->redirect()->toRoute($resultWebsiteBuilder->getRedirect(), $params = $resultSiteBuilder->getRedirectAction());
		}
		
		$websiteRow = $resultWebsiteBuilder->getWebsiteObject();
		
		...
		[other code]
	}
	
	...

The WebsiteController uses a WebsiteControllerFactory to inject dependencies.

There is probably a very simple solution to this case… that I don’t know :smiley:

In the Laminas\Mvc\Controller\AbstractActionController you can find the onDispatch method:

This method can be overridden in own controllers and is executed with every request of the controller:

public function onDispatch(MvcEvent $e)
{
    $action = $e->getRouteMatch()->getParam('action');

    // …

    return parent::onDispatch($e);
}

Maybe this will already help you.


I will look at the rest of your example later and give you feedback.

Thank You @froschdesign.

Your hint was very helpful and I was able to solve the problem.