4

I want to build an error handling & logging mecanism into an Apigility Zend Framework 2 aplication and catch & log all exceptions.

After some research I found a Stack Overflow answer with a solution, that seemed exactly to meet this requirements. Here is the code from the answer (with some minor naming and formatting modifications):

Module.php

...

use Zend\Mvc\ModuleRouteListener;
use Zend\Log\Logger;
use Zend\Log\Writer\Stream;

...

class Module implements ApigilityProviderInterface
{

    public function onBootstrap(MvcEvent $mvcEvent)
    {
        $eventManager = $mvcEvent->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
        /**
         * Log any Uncaught Exceptions, including all Exceptions in the stack
         */
        $sharedEventManager = $mvcEvent->getApplication()->getEventManager()->getSharedManager();
        $serviceManager = $mvcEvent->getApplication()->getServiceManager();
        $sharedEventManager->attach('Zend\Mvc\Application', MvcEvent::EVENT_DISPATCH_ERROR,
            function($mvcEvent) use ($serviceManager) {
                if ($mvcEvent->getParam('exception')){
                    $exception = $mvcEvent->getParam('exception');
                    do {
                        $serviceManager->get('Logger')->crit(
                            sprintf(
                               "%s:%d %s (%d) [%s]\n", 
                                $exception->getFile(), 
                                $exception->getLine(), 
                                $exception->getMessage(), 
                                $exception->getCode(), 
                                get_class($exception)
                            )
                        );
                    }
                    while($exception = $exception->getPrevious());
                }
            }
        );
    }

    ...

    public function getServiceConfig() {
        return array(
            'factories' => array(
                // V1
                ...
                'Logger' => function($sm){
                    $logger = new Logger;
                    $writer = new Stream('/var/log/httpd/sandbox-log');
                    $logger->addWriter($writer);
                    return $logger;
                },
            ),
            ...
        );
    }

}

So now I've tried this out (with a simple throw new \Exception('foo')) at several places in the code (in a Resource, in a Service, and in a Mapper class) and expected to get the exceptions cached and logged into the file I defiden for. But it isn't working.

Am I doing something wrong? What? How to get it working? How to catch and log all exceptions in an Apigility driven Zend Framework 2 application?


Additional info: An example of a place in the code, where an exception gets thrown:

class AddressResource extends AbstractResourceListener ...
{
    public function fetch($id) {
        throw new \Exception('fetch_EXCEPTION');
        $service = $this->getAddressService();
        $entity = $service->getAddress($id);
        return $entity;
    }
}

Additional info: The trace in the respose (when if set throw new \Exception('fetch_EXCEPTION'); in the BarResource#fetch(...)):

{
    "trace": [
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/AbstractResourceListener.php",
            "line": 166,
            "function": "fetch",
            "class": "FooAPI\\V1\\Rest\\Bar\\BarResource",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "function": "dispatch",
            "class": "ZF\\Rest\\AbstractResourceListener",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "dispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "fetch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/Resource.php",
            "line": 541,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/RestController.php",
            "line": 483,
            "function": "fetch",
            "class": "ZF\\Rest\\Resource",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractRestfulController.php",
            "line": 366,
            "function": "get",
            "class": "ZF\\Rest\\RestController",
            "type": "->",
            "args": [
                "1"
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zfcampus/zf-rest/src/RestController.php",
            "line": 332,
            "function": "onDispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractRestfulController",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "function": "onDispatch",
            "class": "ZF\\Rest\\RestController",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "onDispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php",
            "line": 118,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractRestfulController.php",
            "line": 300,
            "function": "dispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractController",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php",
            "line": 93,
            "function": "dispatch",
            "class": "Zend\\Mvc\\Controller\\AbstractRestfulController",
            "type": "->",
            "args": [
                {},
                {}
            ]
        },
        {
            "function": "onDispatch",
            "class": "Zend\\Mvc\\DispatchListener",
            "type": "->",
            "args": [
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 444,
            "function": "call_user_func",
            "args": [
                [
                    {},
                    "onDispatch"
                ],
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php",
            "line": 205,
            "function": "triggerListeners",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php",
            "line": 314,
            "function": "trigger",
            "class": "Zend\\EventManager\\EventManager",
            "type": "->",
            "args": [
                "dispatch",
                {},
                {}
            ]
        },
        {
            "file": "/var/www/my-project/public/index.php",
            "line": 56,
            "function": "run",
            "class": "Zend\\Mvc\\Application",
            "type": "->",
            "args": []
        }
    ],
    "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
    "title": "Internal Server Error",
    "status": 500,
    "detail": "fetch_EXCEPTION"
}
Community
  • 1
  • 1
automatix
  • 14,018
  • 26
  • 105
  • 230
  • 1
    Is the problem that it's not reaching the callback function or the logging isn't working? Have you tried attaching your listener at a higher priority i.e. `> 1`? – Ankh Jun 09 '15 at 07:46
  • Exactly, the callback is not reached at all. It works in another ZF2 application, but here, in the "Apigility application", it seems to be ignored. I've just tried to change the priority (`1000` and `-1000`), but it hasn't helped. – automatix Jun 09 '15 at 10:05

3 Answers3

1

We are currently using the following code successfully to capture all error responses from Apigility:

$app = $event->getTarget();
$em = $app->getEventManager();

$sendResponseListener = $app->getServiceManager()->get('SendResponseListener');
$sendResponseListener->getEventManager()->attach(SendResponseEvent::EVENT_SEND_RESPONSE,  function(SendResponseEvent $event) {
    $response = $event->getResponse();
    if ($response instanceof ApiProblemResponse) {
          $error = $response->getApiProblem()->toArray();
          // inspect $error array and log the information you want
    }
});
dt1021
  • 39
  • 2
  • 1
    @automatix Unfortunately, due to poor design of Apigility, this is the only way to log exceptions that are raised from your API resource listeners. This is because `\ZF\Rest\RestController` catches any exceptions and _handles them_ by returning an `ApiProblemResponse`. So the `dispatch.error` event is never triggered and the normal ZF exception handling won't occur. – Glenn Schmidt Dec 14 '15 at 05:26
  • A slight improvement on this: You can actually just attach to the `finish` event on the standard Application object, rather than needing to get the SendResponseListener from the service manager. ie. `$e->getApplication()->getEventManager()->attach(MvcEvent::EVENT_FINISH, function() {...}` – Glenn Schmidt Dec 14 '15 at 09:00
0

You connected your listener to the MvcEvent::EVENT_DISPATCH_ERROR event. Are you sure that you throw your new \Exception('foo') during dispatch (i.e. in a controller). Also in the answer you linked to they mention that this solution is for catching errors/exceptions thrown in a controller.

If you for example throw an exception while rendering your listener will never be triggered. In those cases you would need to listen to MvcEvent::EVENT_RENDER_ERROR.

I wonder if this setup is the best way to do it. Maybe you should search for other examples instead of simply following/copying an answer from StackOverflow.

EDIT:

If you are also using the ApiProblem module for Apigility then it could be that the ApiProblemListener is triggered on a MvcEvent::EVENT_DISPATCH_ERROR event before your own listener.

In the onDispatchError method the ApiProblemListener returns a response object and that might be the reason that other events (with lower priority) are not triggered at all after this.

The ApiProblemListener is attached like this:

$this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onDispatchError'), 100);

Try to raise the priority for your listener to a value above the 100 from the ApiProblemListener. Then you will probably have success.

Community
  • 1
  • 1
Wilt
  • 41,477
  • 12
  • 152
  • 203
  • 1
    Thank you for your answer! I'll reply point by point: 1. `EVENT_DISPATCH_ERROR` is the correct event, at least it works in another applcation (without Apigility). 2. Controller: Not only the controller, it works (in another app without Apigility) in model classes as well. 3. "Copying an answer": I copied the code, that IMO should work; this approach is also described on other resources, e.g. [here](http://www.codeproject.com/Articles/869222/Zend-Framework-Error-Exception-Handling) and [here](https://samsonasik.wordpress.com/2012/09/23/1675-using-logger-to-save-exception-in-zend-framework-2/). – automatix Jun 09 '15 at 10:25
  • It's a very good hint, thank you very much! Yes, it seems so, that the `ApiProblemListener` catches exceptions and prohibits theor processing by other listeners. The `priority` is set to `100` (`class ApiProblemListener { ... public function attach(EventManagerInterface $events) { ... $this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, array($this, 'onDispatchError'), 100); ... } ... }`). But even if I set a higher `priority` for my listener (e.g. `10000`), the `ApiProblem` "wins" and my listener gets ignored. – automatix Jun 09 '15 at 20:26
  • Wait, it's not the `ApiProblem`. I set breakpoints into the `ApiProblemListener#onDispatchError(...)` and even write there a `die()` -- the method does not get called. So, it must be another listener. – automatix Jun 09 '15 at 20:37
  • Where are you throwing your exception? If I throw an exception inside my `RestController` instance it does end up calling the `onDispatchError` method in the `ApiProblemListener`, so somewhere you are doing something different. Maybe you can explain how and where you are testing your code. – Wilt Jun 10 '15 at 07:12
  • First of all thank you for still trying to help! To your question: Where I'm throwing the exception and how ym testing the code. In my application I'm using a structure (like [here](http://stackoverflow.com/a/30313118/2019043) shown) and the calls chain looks like `AddressResource#fetchAll(...) -> AddressService#getBar(...) -> AddressMapper#findAll(...)`. I've tried to throw the exception on each of these steps. See also the example in my question update. To test the logging I'm simply observing the file the `Logger` should storage to information to (`tail -f /var/log/httpd/sandbox-log`). – automatix Jun 10 '15 at 15:09
0

I face the same problem to logging any exception happened,

I try to attach my own listener to MvcEvent::EVENT_DISPATCH_ERROR even with high priority[3000] not working, after some research and read Apigility code I discover that any thrown exception will be catch by ApiProblemListener and create new ApiProblem from this exception information into onRender method

The workaround to solve problem and log any exception is override the AbstractResourceListener and create your own ResourceListener and force any Resource class extend it,

your own ResourceListener must override dispatch method and call parent method to catch the thrown exception and log it then return new Response

Example:

<?php

namespace API\V1\Listener;

class MyOwnListener  extends AbstractResourceListener
{

    /**
     * {@inheritdoc}
     */
    public function dispatch(ResourceEvent $event)
    {
        try {
            $response = parent::dispatch($event);
        } catch (\Throwable $exception) {
            // catch thrown exception
            // then return new APIProblem with message you want 
            $response = new ApiProblem(500, 'error message');
        }
        return $response;
    }
}
ahmed hamdy
  • 5,096
  • 1
  • 47
  • 58