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.