Catching Exceptions in Zend Framework 3 - php

I use the ZF3 skeletion application.
I wonder where I am supposed to catch exceptions globally.
Example:
Right now, if I access an invalid route (mysite.com/invalid-route), the application reports an uncaught expection and HTTP response code 200
Fatal error: Uncaught Zend\View\Exception\RuntimeException: No RouteMatch instance provided
I would expect the build-in 404 error page to be triggered.
What am I missing? Can someone point me to the right direction?
The exception is logged properly using the following code:
class Module implements ConfigProviderInterface
{
const VERSION = '3.0.3-dev';
public function onBootstrap()
{
$logger = new Logger();
$writer = new Writer\Stream(__DIR__ . '/../../../data/log/error.log');
$logger->addWriter($writer);
// Log PHP errors
Logger::registerErrorHandler($logger, true);
// Log exceptions
Logger::registerExceptionHandler($logger);
}

This is something you can catch using a Listener, triggered on the early event MvcEvent::EVENT_ROUTE.
I would suggest using a dedicated class & Factory to separate concerns over the usage of the onBootstrap function. You'd do this by registering and "activating" a Listener class, like so:
'listeners' => [
// This makes sure it "gets listened to" from the very start of the application (onBootstrap)
RouteExistsListener::class,
],
'service_manager' => [
'factories' => [
// This is just what you think it is
RouteExistsListener::class => InvokableFactory::class,
],
],
You can use just the InvokableFactory for this Listener, as there are no special requirements.
class RouteExistsListener implements ListenerAggregateInterface
{
/**
* #var array
*/
protected $listeners = [];
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'doesRouteExist'], 100);
}
/**
* #param MvcEvent $event
*
* #return void|Response
* #throws Exception
*/
public function doesRouteExist(MvcEvent $event)
{
/** #var TranslatorAwareTreeRouteStack|TreeRouteStack $router */
$router = $event->getRouter();
/** #var Request $request */
$request = $event->getRequest();
/** #var RouteMatch|null $routeExists */
$routeExists = $router->match($request); // Return RouteMatch|null
if ($routeExists instanceof RouteMatch) {
return; // Route exists - nothing to do
}
$url = $router->assemble([], ['name' => 'home']); // Name of your redirect route (ie. not_found/404, or something)
/** #var Response $response */
$response = $event->getResponse();
$response->getHeaders()->addHeaderLine('Location', $url);
$response->setStatusCode(302);
$response->sendHeaders();
$event->getApplication()->getEventManager()->attach(
MvcEvent::EVENT_ROUTE,
function (MvcEvent $event) use ($response) {
$event->stopPropagation();
return $response;
},
-10000
);
return $response;
}
}
NOTE: Used an existing class of my own for the above and modified as I think it should work. However, might contain an error or two ;-) Still, should point you in the right direction I think.

Related

Mock Non-existent Interface in PHP

I am new to PHP unit testing and I need some clarity on how can we mock non-existent Interface. Basically, In my project, I want to test one protected method which is expecting two arguments one is Interface and another is a string.
I want to understand how can I pass Interface as an argument in a method. I tried to Mock the Interface but it's not working for me.
This is my main class
namespace App\Models;
class VerifyNs {
protected function NsCheck(SomeInterface $url, string $domain) {
$answer= dns_get_record($domain, DNS_NS);
if (!empty($answer[0]['target']), ('example.com' == substr($answer[0]['target'], -11)) {
$result = 'NS verification for #url passed., ['#url ' => $url ->getName()]';
return $result
}
return 'failed;
}
}
Here is the test case
class VerifyNsTest extends \PHPUnit\Framework\TestCase
{
/**
* To call protected method
* #param $object
* #param string $method
* #param array $parameters
* #return mixed
* #throws \Exception
*/
private function callMethod($object, string $method , array $parameters = [])
{
try {
$className = get_class($object);
$reflection = new \ReflectionClass($className);
} catch (\ReflectionException $e) {
throw new \Exception($e->getMessage());
}
$method = $reflection->getMethod($method);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
/**
* Tests for verifying NS of the domain.
*
* #dataProvider providerTestNsCheck
*/
public function testCheckNs($site, $url, $expected_result) {
$site_name = $this->getMockBuilder(SomeInterface::class)
->setMethods(['getName'])
->getMock();
$site_name->method('getName')->willReturn($site);
$verifyNs = new \App\Models\VerifyNs();
$this->assertEquals($expected_result, $this->callMethod($verifyNs, 'NsCheck', [ $site_name, $url]));
}
/**
* Data provider for testCheckNs().
*
*/
public function providerTestNsCheck() {
return [
[
'mysite',
'example.com',
'NS verification for mysite passed.',
], [
'google',
'google.com',
'failed.'
]
],
];
}
}
After running this, it's giving me an error
TypeError: Argument 1 passed to App\Models\VerifyNs::NsCheck() must be an instance of App\Models\SomeInterface, instance of Mock_SomeInterface_1ee04960 given
Can someone please help here? Is it possible to mock non_existent Interface and pass it as an argument to the method?
I was able to resolve this issue using prophesize
$site = $this->prophesize(SomeInterface::class);
$object =$site->reveal()
then passing $object in the callMethod.

ZF3 Dependency injection in Module.php

I'm currently migrating a ZF2 application to ZF3.
Mostly everything is going smoothly but I'm stuck on one thing.
In my Module.php, I have an ACL management using zend-permissions-acl.
class Module
{
protected $defaultLang = 'fr';
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
if (!$e->getRequest() instanceof ConsoleRequest){
$eventManager->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'onRenderError'));
$eventManager->attach(MvcEvent::EVENT_RENDER, array($this, 'onRender'));
$eventManager->attach(MvcEvent::EVENT_FINISH, array($this, 'onFinish'));
$this->initAcl($e);
$eventManager->attach('route', array($this, 'checkAcl'));
}
}
public function checkAcl(MvcEvent $e) {
$app = $e->getApplication();
$sm = $app->getServiceManager();
$route = $e -> getRouteMatch() -> getMatchedRouteName();
$authService = $sm->get('AuthenticationService');
$jwtService = $sm->get('JwtService');
$translator = $sm->get('translator');
$identity = null;
try {
$identity = $jwtService->getIdentity($e->getRequest());
} catch(\Firebase\JWT\ExpiredException $exception) {
$response = $e->getResponse();
$response->setStatusCode(401);
return $response;
}
if(is_null($identity) && $authService->hasIdentity()) { // no header being passed on... we try to use standard validation
$authService->setJwtMode(false);
$identity = $authService->getIdentity();
}
$userRole = 'default';
$translator->setLocale($this->defaultLang);
if(!is_null($identity))
{
$userRole = $identity->getType();
//check if client or prospect
if($userRole >= User::TYPE_CLIENT)
{
$userManagementRight = UserRight::CREATE_USERS;
if($identity->hasRight($userManagementRight))
$userRole = 'userManagement';
}
$translator->setLocale($identity->getLang());
}
if (!$e->getViewModel()->acl->isAllowed($userRole, null, $route)) {
$response = $e -> getResponse();
$response->setStatusCode(403);
return $response;
}
public function initAcl(MvcEvent $e) {
//here is list of routes allowed
}
}
My issue here is that I'm still using the getServiceManager and therefore getting the deprecated warning : Usage of Zend\ServiceManager\ServiceManager::getServiceLocator is deprecated since v3.0.0;
Basically, I just need to inject dependencies into Module.php.
I guess otherwise I would have to move the checkAcl to the Controller directly and inject the ACL in them ? Not sure what is the proper way of doing this.
Any feedback on this would be greatly appreciated.
Regards,
Robert
To solve the issue you should use a Listener class and Factory. It would also help you with more separation of concerns :)
You seem quite capable of figuring stuff out, judging by your code. As such, I'm just going to give you an example of my own, so you should fill yours in with your own code (I'm also a bit lazy and do not wish to rewrite everything when I can copy/paste my code in ;) )
In your module.config.php:
'listeners' => [
// Listing class here will automatically have them "activated" as listeners
ActiveSessionListener::class,
],
'service_manager' => [
'factories' => [
// The class (might need a) Factory
ActiveSessionListener::class => ActiveSessionListenerFactory::class,
],
],
The Factory
<?php
namespace User\Factory\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use User\Listener\ActiveSessionListener;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
class ActiveSessionListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager $entityManager */
$entityManager = $container->get(EntityManager::class);
/** #var AuthenticationService $authenticationService */
$authenticationService = $container->get(AuthenticationService::class);
return new ActiveSessionListener($authenticationService, $entityManager);
}
}
The Listener
<?php
namespace User\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use User\Entity\User;
use Zend\Authentication\AuthenticationService;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Mvc\MvcEvent;
/**
* Class ActiveSessionListener
*
* #package User\Listener
*
* Purpose of this class is to make sure that the identity of an active session becomes managed by the EntityManager.
* A User Entity must be in a managed state in the event of any changes to the Entity itself or in relations to/from it.
*/
class ActiveSessionListener implements ListenerAggregateInterface
{
/**
* #var AuthenticationService
*/
protected $authenticationService;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
/**
* #var array
*/
protected $listeners = [];
/**
* CreatedByUserListener constructor.
*
* #param AuthenticationService $authenticationService
* #param ObjectManager $objectManager
*/
public function __construct(AuthenticationService $authenticationService, ObjectManager $objectManager)
{
$this->setAuthenticationService($authenticationService);
$this->setObjectManager($objectManager);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'haveDoctrineManagerUser'], 1000);
}
/**
* #param Event $event
*
* #throws \Doctrine\Common\Persistence\Mapping\MappingException
* #throws \Doctrine\ORM\ORMException
*/
public function haveDoctrineManagerUser(Event $event)
{
if ($this->getAuthenticationService()->hasIdentity()) {
// Get current unmanaged (by Doctrine) session User
$identity = $this->getAuthenticationService()->getIdentity();
// Merge back into a managed state
$this->getObjectManager()->merge($identity);
$this->getObjectManager()->clear();
// Get the now managed Entity & replace the unmanaged session User by the managed User
$this->getAuthenticationService()->getStorage()->write(
$this->getObjectManager()->find(User::class, $identity->getId())
);
}
}
/**
* #return AuthenticationService
*/
public function getAuthenticationService() : AuthenticationService
{
return $this->authenticationService;
}
/**
* #param AuthenticationService $authenticationService
*
* #return ActiveSessionListener
*/
public function setAuthenticationService(AuthenticationService $authenticationService) : ActiveSessionListener
{
$this->authenticationService = $authenticationService;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return ActiveSessionListener
*/
public function setObjectManager($objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
}
The important bits:
The Listener class must implement ListenerAggregateInterface
Must be activated in the listeners key of the module configuration
That's it really. You then have the basic building blocks for a Listener.
Apart from the attach function you could take the rest and make that into an abstract class if you'd like. Would save a few lines (read: duplicate code) with multiple Listeners.
NOTE: Above example uses the normal EventManager. With a simple change to the above code you could create "generic" listeners, by attaching them to the SharedEventManager, like so:
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(SomeClass::class, EventConstantClass::SOME_STRING_CONSTANT, [$this, 'callbackFunction']);
}
public function callbackFunction (MvcEvent $event) {...}

SOAP Client - Class not found

I got a problem using SOAP client in Laravel. I want to send salesorders/customers from an shop to an ERP system. The Structure looks like this: In routes->web.php I call my Controller
Route::get('/show_orders', 'SalesOrdersController#store');
I created an controller SalesOrderController and it looks like this
<?php
namespace App\Http\Controllers;
class SalesOrdersController extends Controller
{
private $soapClient;
public function __construct()
{
$this->middleware('auth');
$this->soapClient = app('soap_client_sales_order');
}
public function store()
{
$soapClient->__setLocation(env('BYD_DOMAIN') . $serviceUrl);
$parameters['BasicMessageHeader'] = array(
'ID' => '00000000000102dcade9bcb0aa000c68',
);
$parameters['Customer'] = array(
'CategoryCode' => '1',
'CustomerIndicator' => 'true',
'Person' => array(
'GivenName' => 'Frank',
'FamilyName' => 'Sent',
),
);
$soapResult = $this->soapClient->MaintainBundle_V1($parameters);
}
}
I created an ServiceProvider SoapServiceProvider – looks like this:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class SoapServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->singleton('soap_client_sales_order', function () {
$serviceUrl = '/sap/bc/srt/scs/sap/managecustomerin1';
$wsdlPath = 'soap/managecustomerin1.wsdl';
$soapClient = new \SoapClient(
storage_path($wsdlPath),
array(
'trace' => 1,
'soap_version' => SOAP_1_2,
'exceptions' => 1,
'login' => env('SOAP_USER'),
'password' => env('SOAP_PASSWORD'),
)
);
});
$soapClient->__setLocation(env('BYD_DOMAIN') . $serviceUrl);
return $soapClient;
}
}
When I call my route http://shop.test/show_orders I get the following
Exception:"Class soap_client_sales_order does not exist"
Dump:
/home/vagrant/code/shop/vendor/laravel/framework/src/Illuminate/Container/Container.php
}
/**
* Instantiate a concrete instance of the given type.
*
* #param string $concrete
* #return mixed
*
* #throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface of Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
Hope you can give me a hint.
Thanks,
Manu
Well first of all you never defined $soapClient outside of $app->singleton scope so $soapClient was null and it was a wrong singleton implementation, here is an official guide from laravel docs and the next thing is that you gotta return the singleton inside of singleton scope meaning something like this
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->singleton('soap_client_sales_order', function () {
...
...
$soapClient->__setLocation(env('BYD_DOMAIN') . $serviceUrl);
return $soapClient;
});
}

Laravel request validation throwing HttpResponseException in version 5.2

I'm trying to update Laravel from version 5.1 to 5.2 in my project, I have followed this upgrade guide from the documentation, but now I'm getting this HttpResponseException when a validation fails
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
* #return mixed
*
* #throws \Illuminate\Http\Exception\HttpResponseException
*/
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException($this->response(
$this->formatErrors($validator)
));
}
In 5.1, the framework redirected to the previous url automatically with the validation errors.
This is my validation request
namespace domain\funcao\formRequest;
use autodoc\Http\Requests\Request;
class StoreFuncaoRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'codigo' => 'required|max:255|unique:funcao,codigo,'.$this->input('id').',id,deleted_at,NULL',
'nome' => 'required|max:255|unique:funcao,nome,'.$this->input('id').',id,deleted_at,NULL'
];
}
}
I have already updated my Exceptions Handler according to the guide
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* #var array
*/
protected $dontReport = [
\Illuminate\Auth\Access\AuthorizationException\AuthorizationException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Foundation\ValidationException\ValidationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
];
...
}
Did someone had this problem??
I found the cause for this error, I was calling PrettyPageHandler from Whoops manually in my Exception Handler class.
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* #var array
*/
protected $dontReport = [
\Illuminate\Auth\Access\AuthorizationException\AuthorizationException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Foundation\ValidationException\ValidationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
];
...
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
// I just needed to remove this call to get rid of the problem
if (config('app.debug'))
{
return $this->renderExceptionWithWhoops($e);
}
return parent::render($request, $e);
}
/**
* Render an exception using Whoops.
*
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
protected function renderExceptionWithWhoops(Exception $e)
{
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler());
return new \Illuminate\Http\Response(
$whoops->handleException($e),
$e->getStatusCode(),
$e->getHeaders()
);
}
}
I'm still using Whoops, but now automatically through Laravel Exceptions
This was the first question I came across when I had this problem although for me it was with Sentry not Whoops.
Here's a couple of exceptions and how I dealt with them:
public function render($request, Exception $e)
{
if ($e instanceof TokenMismatchException) {
return redirect()->back()->withInput()->with('error', 'Your Session has Expired');
}
if ($e instanceof HttpResponseException) {
return $e->getResponse();
}
return response()->view('errors.500', [
'sentryID' => $this->sentryID,
], 500);
}
I handle the HttpResponseException by simply returning the response the same way the ExceptionHandler class does.
All you have to do is, just write your business logics inside the protected failedValidation() inside your custom FormRequest class like follows
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Contracts\Validation\Validator;
/**
* [failedValidation [Overriding the event validator for custom error response]]
* #param Validator $validator [description]
* #return [object][object of various validation errors]
*/
public function failedValidation(Validator $validator) {
// write your business logic here otherwise it will give same old JSON response
throw new HttpResponseException(response()->json($validator->errors(), 422));
}

Refactor some calls on each Zf2 controller action

I need to do a custom isGranted method (not using Rbac or acl module from community). So I have a service which provides the functionality. But this code:
if (!$this->userService->isGrantedCustom($this->session->offsetGet('cod_lvl'), 'ZF_INV_HOM')) {
throw new \Exception("you_are_not_allowed", 1);
}
...is duplicated in each controller and each action I have. Parameters are changing of course depends on the permission ('ZF_INV_HOM', 'ZF_TODO_DELETE' ...).
I think it's not a bad idea to do this code before the controller is called, but I can't figure what is the best solution (best architecture), and how to pass those parameters to it (I thought about annotation on controllers but how to handle this ?).
The point is, if I have to modify this code I can't imagine to do that hundreds of times, for each controllers, each action I have I need to have this code in one place.
If you don't want to pollute your Module with all this code you can also make a listener class and attach only the listener in your bootstrap method:
<?php
namespace Application\Listener;
use Application\Service\UserService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\SharedEventManagerInterface;
use Zend\EventManager\SharedListenerAggregateInterface;
use Zend\Authentication\AuthenticationServiceInterface;
class IsAllowedListener implements SharedListenerAggregateInterface
{
/**
* #var AuthenticationServiceInterface
*/
protected $authService;
/**
* #var UserService
*/
protected $userService;
/**
* #var \Zend\Stdlib\CallbackHandler[]
*/
protected $sharedListeners = array();
/**
* #param SharedEventManagerInterface $events
*/
public function attachShared(SharedEventManagerInterface $events)
{
$this->sharedListeners[] = $events->attach(AbstractActionController::class, MvcEvent::EVENT_DISPATCH, array($this, 'isAllowed'), 1000);
}
public function __construct(AuthenticationServiceInterface $authService, UserService $userService ){
$this->authService = $authService;
$this->userService = $userService;
}
/**
* #param MvcEvent $event
*/
protected function isAllowed(MvcEvent $event)
{
$authService = $this->getAuthService();
$identity = $authService->getIdentity();
$userService = $this->getUserService();
if($userService->isGrantedCustom()){
// User is granted we can return
return;
}
// Return not allowed response
}
/**
* #return AuthenticationServiceInterface
*/
public function getAuthService()
{
return $this->authService;
}
/**
* #param AuthenticationServiceInterface $authService
*/
public function setAuthService(AuthenticationServiceInterface $authService)
{
$this->authService = $authService;
}
/**
* #return UserService
*/
public function getUserService()
{
return $this->userService;
}
/**
* #param UserService $userService
*/
public function setUserService(AuthenticationServiceInterface $userService)
{
$this->userService = $userService;
}
}
You need to setup a factory to inject your dependencies:
<?php
namespace Application\Listener;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Factory for creating the IsAllowedListener
*/
class IsAllowedListenerFactory implements FactoryInterface
{
/**
* Create the IsAllowedListener
*
* #param ServiceLocatorInterface $serviceLocator
* #return RenderLinksListener
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$userService = $serviceLocator->get('Application\Service\UserService');
return new IsAllowedListener($authService, $userService );
}
}
And register all this in config:
'service_manager' => array(
'factories' => array(
'Application\Listener\IsAllowedListener' => 'Application\Listener\IsAllowedListenerFactory'
)
)
And then in bootstrap:
public function onBootstrap(EventInterface $event)
{
$application = $event->getTarget();
$serviceManager = $application->getServiceManager();
$eventManager = $application->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
$isAllowedListener = $serviceManager->get('Application\Listener\IsAllowedListener')
$sharedEventManager->attachAggregate($isAllowedListener);
}
Instead of using AbstractActionController::class, you could also make a specific class, so you will only listen to instances of that class.
So for example AbstractIsAllowedActionController::class or something like that.
By attaching an event listener to the SharedEventManager you can target all controllers and have the authorization check in just one place.
In this case the target is Zend\Mvc\Controller\AbstractActionController which means any controller extending it will execute the listener. The high priority of this listener will mean that it is executed prior to the target controller action, giving you the chance to handle any requests that have not been authorized.
public function onBootstrap(MvcEvent $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager()->getSharedManager();
$eventManager->attach(
\Zend\Mvc\Controller\AbstractActionController::class, // Identity of the target controller
MvcEvent::EVENT_DISPATCH,
[$this, 'isAllowed'],
1000 // high priority
);
}
In each controller there would need to be some way that you can determine which 'resource' is being accessed.
As an example it could implement this interface
interface ResourceInterface
{
// Return a unique key representing the resource
public function getResourceId();
}
The listener could then look like this.
public function isAllowed(MvcEvent $event)
{
$serviceManager = $event->getApplication()->getServiceManager();
// We need the 'current' user identity
$authService = $serviceManager->get('Zend\Authentication\AuthenticationService');
$identity = $authService->getIdentity();
// The service that performs the authorization
$userService = $serviceManager->get('MyModule\Service\UserService');
// The target controller is itself a resource (the thing we want to access)
// in this example it returns an resource id so we know what we want to access
// but you could also get this 'id' from the request or config etc
$controller = $event->getTarget();
if ($controller instanceof ResourceInterface) {
$resourceName = $controller->getResourceId();
// Test the authorization, is UserX allowed resource ID Y
if (empty($resourceName) || $userService->isGrantedCustom($identity, $resourceName)) {
// early exit for success
return;
} else {
// Denied; perhaps trigger a new custom event or return a response
}
}
}

Categories