phploggingzend-frameworkmezzio

How to log PHP errors into PHP's error file in Zend Expressive?


Using Zend Expressive 2.0.5, I want to log PHP errors into the PHP's error log file itself, I went through https://docs.zendframework.com/zend-expressive/features/error-handling/#handling-exceptions-and-errors

I also saw this post: Zend expressive - php error reporting. Which cleared a lot of things for me but still quite didn't solve the asked question.

Things I did: Defined my own ErrorHandlerFactory where I attached two listeners to the Zend-stratigility's ErrorHandler

  1. First Listener uses Zend Log to log into my application's log file.(Just thought it would be nice to have errors in my application.log too.)

  2. In the Second Listener, I want to log into PHP's error log file, so I have used error_log() method from php.

Questions:

  1. The error_log() is not printing the log the way a log appears when printed by php's error handler. What I mean:

    When an error is printed by the php's error handler, it looks something like this:

    [08-Feb-2018 08:22:51 US/Central] PHP Warning: array_push() expects at least 2 parameters, 1 given in C:\webserver\webroot\myapi\src\App\src\Action\PageAction.php on line 38

    While when I print the log using error_log() it looks something like this:

    [08-Feb-2018 09:03:49 US/Central] array_push() expects at least 2 parameters, 1 given in C:\webserver\webroot\myapi\src\App\src\Action\PageAction.php on line 38

    What am I missing here is the PHP's error type: PHP Warning, Is this the error code? The error code I get is an integer, how do I parse that code? should I map the error codes with PHP errors constants which appear in the logs, for example: WARNING, NOTICE, etc, I can even do that, but the problem is: I got the same error code of 0 both the times when php's error handler printed a WARNING and a Fatal error logs.

  2. Is it right to log errors in PHP's error log file like this? Should I do the job of PHP's error handler? The error handler could be doing a lot of things, for example: Logging the error message for few errors but for another also logging the stack trace. If this is not right, then how else can I send the error to the PHP's error_handler?

    From my understanding:

    My own Error Handler prevents users to look for exceptions and stack traces but rather returns a generic message. This also means that the Error Handler consumes the error and doesn't throw it further outside, i.e. will not throw it to the PHP's error handler.


Solution

  • Answering question 1:

    I am able to almost simulate the way PHP error handler logs PHP errors. Things I did:

    1. Went through the docs and this SO question. Using these I was able to attach listeners to Zend-stratigility's ErrorHandler
    2. Went through PHP's Error Constants and set_error_handler(), which gave me some ideas on how to find out which type of Error or Exception occured.

    Below is the code for my ErrorHandlerFactory where I attach the listeners.

    <?php
    // TODO: PHP 7.0.8 is giving strict erros eben if this directive is not enabled. And that too, it should be enabled per file from my understanding.
    //declare(strict_types = 1);
    namespace App\Factories;
    use Interop\Container\ContainerInterface;
    use Psr\Http\Message\RequestInterface;
    use Psr\Http\Message\ResponseInterface;
    use Zend\Log\Logger as ZendLogger;
    use Throwable;
    use Zend\Diactoros\Response;
    use Zend\Expressive\Middleware\ErrorResponseGenerator;
    use Zend\Stratigility\Middleware\ErrorHandler;
    class ErrorHandlerFactory
    {
        /**
         * @param ContainerInterface $container
         * @return ErrorHandler
         * @throws \Psr\Container\ContainerExceptionInterface
         * @throws \Psr\Container\NotFoundExceptionInterface
         */
        public function __invoke(ContainerInterface $container)
        {
            $generator = $container->has(ErrorResponseGenerator::class)
                ? $container->get(ErrorResponseGenerator::class)
                : null;
            $errorHandler = new ErrorHandler(new Response(), $generator);
    
             // attaching a listener for logging into application's log file.
            if ($container->has(ZendLogger::class)) {
                /** @var ZendLogger $logger */
                $logger = $container->get(ZendLogger::class);
                $errorHandler->attachListener(function (
                    Throwable $throwable,
                    RequestInterface $request,
                    ResponseInterface $response
                ) use ($logger) {
                    $logger->err(NULL, [
                        'method'  => $request->getMethod(),
                        'uri'     => (string) $request->getUri(),
                        'message' => $throwable->getMessage(),
                        'file'    => $throwable->getFile(),
                        'line'    => $throwable->getLine(),
                    ]);
                });
            }
    
            // Attaching second listener for logging the errors into the PHP's error log
            $errorHandler->attachListener(function (
                Throwable $throwable,
                RequestInterface $request,
                ResponseInterface $response
            ) {
                // Default Error type, when PHP Error occurs.
                $errorType = sprintf("Fatal error: Uncaught %s", get_class($throwable));
                if (get_class($throwable) === "ErrorException") {
    
                    // this is an Exception
                    /** @noinspection PhpUndefinedMethodInspection */
                    $severity = $throwable->getSeverity();
                    switch($severity) {
                        case E_ERROR:
                        case E_USER_ERROR:
                            $errorType = 'Fatal error';
                            break;
                        case E_USER_WARNING:
                        case E_WARNING:
                            $errorType = 'Warning';
                            break;
                        case E_USER_NOTICE:
                        case E_NOTICE:
                        case E_STRICT:
                            $errorType = 'Notice';
                            break;
                        case E_RECOVERABLE_ERROR:
                            $errorType = 'Catchable fatal error';
                            break;
                        case E_USER_DEPRECATED:
                        case E_DEPRECATED:
                            $errorType = "Deprecated";
                            break;
                        default:
                            $errorType = 'Unknown error';
                    }
    
                    error_log(sprintf("PHP %s: %s in %s on line %d", $errorType, $throwable->getMessage(), $throwable->getFile(), $throwable->getLine()), 0);
                }
                else {
                    // this is an Error.
                    error_log(sprintf("PHP %s: %s in %s on line %d \nStack trace:\n%s", $errorType, $throwable->getMessage(), $throwable->getFile(), $throwable->getLine(), $throwable->getTraceAsString()), 0);
                }
            });
    
            return $errorHandler;
        }
    }
    

    Apart from this, this Factory needs to be added to the dependencies. In the file: dependencies.global.php, in the factories array:

    Replace

    Zend\Stratigility\Middleware\ErrorHandler::class => Container\ErrorHandlerFactory::class,

    with

    Zend\Stratigility\Middleware\ErrorHandler::class => \App\Factories\ErrorHandlerFactory::class
    

    And this should almost simulate the logging behaviour how php error handler does.

    Answering Question 2:

    I think it is fine to do this since PHP by itself provides set_error_handler() and anyway we have to handle the errors by ourselves and not pass it to the PHP's error handler. If our ErrorHandler(listener) can replicate the messages and log into the PHP's error log using error_log(), then it is fine.