Logging with Zend Framework and PSR-3

Logging with Zend Framework and PSR-3

Recently I encountered a library that requires its logger dependencies to implement the psr-3 standard. A common interface for logging libraries. I wanted to use the zend loggers because they are easy to configure and already used in my application.

General configuration

Zend has a LoggerAbstractServiceFactory which is capable of instanciating multiple loggers configured via configuration files. Simply registering the LoggerAbstractServiceFactory as abstract factory dependency enables it.

// config/autoload/logging.global.php
return [
    'dependencies' => [
        'abstract_factories' => [
            \Zend\Log\LoggerAbstractServiceFactory::class,
        ],
    ],
];

Next we can add a top level log key to the configuration which the LoggerAbstractServiceFactory will reference to create and configure zend loggers.

// config/autoload/logging.global.php
return [
  'log' => [
    'StreamLogger' => [
      'writers' => [
        [
          'name'     => 'stream',
          'priority' => Zend\Log\Logger::DEBUG,
          'options'  => [
            'stream' => 'php://output',
          ],
        ],
      ],
    ],
   'dependencies' => ...
];

At this point you would think I could configure said library to pull this logger from the ServiceManager. Simply give it the service name 'StreamLogger'. However the zend loggers do not implement psr-3 and I would get an error due to incompatible dependencies.

zend-log comes with Zend\Log\PsrLoggerAdapter that is capable of wrapping a zend logger instance and is itself psr-3 compatible.

So I figured I could add a second service called PsrStreamLogger that would do just that and configure said library to use that service name instead.

PsrLoggerAdapter configuration

// config/autoload/logging.global.php
return [
    'dependencies' => [
        'abstract_factories' => [
            \Zend\Log\LoggerAbstractServiceFactory::class,
        ],
        'factories'          => [
            'PsrStreamLogger' => function($container) {
                return new \Zend\Log\PsrLoggerAdapter($container->get('StreamLogger'));
            }
        ],
    ],

    'log' => ...
];

Although this will work another approach is to use a delegator factory. This is a feature of the zend-servicemanager that enables it to decorate services with a different service via a factory. Here's a good article about it.

This is how you need to configure delegators;

// config/autoload/logging.global.php
return [
    'dependencies' => [
        'abstract_factories' => [...
        'delegators'         => [
            'StreamLogger' => [
                \My\Service\Log\PsrLoggerDelegator::class,
            ],
        ]
    ],

    'log' => [...
];

And here is my implementation of \My\Service\Log\PsrLoggerDelegator written in such a way it is backwards compatible with zend-servicemanager 2.

namespace My\Service\Service\Log;

use Interop\Container\ContainerInterface;
use Zend\Log\PsrLoggerAdapter;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class PsrLoggerDelegator implements DelegatorFactoryInterface
{
    /**
     * @param ContainerInterface $container
     * @param                    $requestedName
     * @param callable           $callback
     * @param array|null         $options
     * @return PsrLoggerAdapter
     */
    public function __invoke(ContainerInterface $container, $requestedName, callable $callback, array $options = null)
    {
        return new PsrLoggerAdapter($callback());
    }

    /**
     * SM2 Compatibility
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param string                  $name
     * @param string                  $requestedName
     * @param callable                $callback
     * @return mixed
     */
    public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
    {
        return $this($serviceLocator, $requestedName, $callback);
    }
}

This approach allows you to continue using the log name you've configured, and just ensures that it is decorated as a PSR-3-compliant logger.

Final note: As you now you will get a zend-log Logger decorated with the PsrLoggerDelegator whenever you pull StreamLogger existing services that currently require Zend\Log\LoggerInterface might now be problematic. Since psr-3 is supposed to improve interoperability you are encouraged to change its dependencies to the psr-3 interfaces.