Without any doubt, Monolog and Zend Service Manager are two libraries that are almost always found in the composer.json
file require
section of my projects. In case you didn't know, Monolog is a PSR-3 compliant logging library that allows you to save logs to various storage types and web services, while Zend Service Manager is a PSR-11 compliant dependency injection container and a service locator implementation that facilitates management of application dependencies.
In this post I'm gonna show you how the two can work together.
Configuration
Preferred way of configuring Zend Service Manager is using an associative array containing definitions of services. Typically, this configuration lives in a file, along with other configurations of your application.
Monolog is based on a concept of creating Logger
instances, whereas each one is identified by a channel (name) and equipped with a stack of handlers. And just like a router, database handler, cache, or any other application dependency of that kind, Logger is a service that you register with the DI container and inject it into other application services.
Here's a basic example of a Logger service usage:
config.php
use Interop\Container\ContainerInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
use Monolog\Logger;
return [
'dependencies' => [
'factories' => [
'AppLogger' => function (ContainerInterface $container) {
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__ . '/../data/log/app.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
return $logger;
},
],
],
];
index.php
use Zend\ServiceManager\ServiceManager;
$config = require 'config.php';
$serviceManager = new ServiceManager($config['dependencies']);
$logger = $serviceManager->get('AppLogger');
$logger->info('Hello world');
Separate configuration and object construction logic
While this is all that it takes to glue Monolog and Zend Service Manager together, configuration itself looks bulky and can become hard to maintain as you add more loggers. I prefer my configuration files to be light, plain PHP arrays, and keep object construction logic in separate classes. Besides a callable, Zend Service Manager also supports specifying factories as class names, so let's change our example accordingly:
config.php
return [
'dependencies' => [
'factories' => [
'AppLogger' => My\AppLoggerFactory::class,
],
],
];
AppLoggerFactory.php
namespace My;
use Interop\Container\ContainerInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
use Monolog\Logger;
class AppLoggerFactory
{
public function __invoke(ContainerInterface $container)
{
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__ . '/../data/log/app.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
return $logger;
}
}
index.php
use Zend\ServiceManager\ServiceManager;
$config = require 'config.php';
$serviceManager = new ServiceManager($config['dependencies']);
$logger = $serviceManager->get('AppLogger');
$logger->info('Hello world');
Keep environment-specific configuration away from code
By solving one problem, we've introduced another one. Our code now contains something that varies between environments (development, staging, production). In this particular example it is a path to the log file (data/log/app.log
), but this includes everything that is specific for the environment on which application is running. Things like database connection parameters, credentials for external services, and as we now know logging configuration, should not be kept in code and put under version control by any cost! Note that configuration file with real values should not be versioned neither, but only as a template, usually named config.php.dist
by a convention.
Bearing all this mind, let's apply appropriate changes:
config.php
return [
'logger' => [
'app' => [
'file' => __DIR__ . '/../data/log/app.log',
],
],
'dependencies' => [
'factories' => [
'AppLogger' => My\AppLoggerFactory::class,
],
],
];
AppLoggerFactory.php
namespace My;
use Interop\Container\ContainerInterface;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;
use Monolog\Logger;
class AppLoggerFactory
{
public function __invoke(ContainerInterface $container)
{
$config = $container->get('Config');
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler($config['logger']['app']['file'], Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());
return $logger;
}
}
index.php
$config = require 'config.php';
$serviceManager = new ServiceManager($config['dependencies']);
$serviceManager->set('Config', $config);
$logger = $serviceManager->get('AppLogger');
$logger->info('Hello world');
Two things to note here:
- Additional service was registered with the Service Manager -
'Config'
, which holds entire configuration array. - Logger factory retrieves configuration from the provided Container (Service Manager) instance to obtain environment-specific logger configuration.
Generic logger factory
As you add more and more loggers, therefore writing more factories, you start thinking about a generic factory that can instantiate loggers based on their array-like configuration, containing handlers, formatter and processors definitions.
Zend Service Manager facilitates such an idea in particular by featuring a concept of mapping multiple services to the same factory. This was made possible through $requestedName
that is passed as the second parameter of a factory.
To give you an idea of how a generic logger factory may look like:
namespace My;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
final class LoggerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('Config');
$loggerConfig = $config['logger'];
if (! array_key_exists($requestedName, $loggerConfig)) {
throw new ServiceNotFoundException(sprintf(
'Configuration for "%s" is missing',
$requestedName
));
}
return $this->createLoggerFromConfig($loggerConfig[$requestedName]);
}
private function createLoggerFromConfig(array $config)
{
// ...
}
}
Configuration file becomes a lot more cleaner and readable:
return [
'logger' => [
'AppLogger' => [
'name' => 'app',
'handlers' => [
[
'name' => Monolog\Handler\StreamHandler::class,
'options' => [
'stream' => __DIR__ . '/../data/log/app.log',
'level' => Monolog\Logger::INFO,
],
],
[
'name' => Monolog\Handler\FirePHPHandler::class,
],
],
],
'NotificationsLogger' => [
'name' => 'notifications',
[
'name' => Monolog\Handler\LogglyHandler::class,
'options' => [
'token' => '123',
],
],
],
],
'dependencies' => [
'factories' => [
'AppLogger' => My\LoggerFactory::class,
'NotificationsLogger' => My\LoggerFactory::class,
],
],
];
Monolog Factory
After repeating these things through projects, I eventually wrote a generic logger factory whose essence I've omitted in the previous example, with the aim of letting you know at this point that I've made Monolog Factory - library that facilitates creation of Monolog logger objects in both generic and container-interop contexts. It works nicely with Zend Service Manager, but also any other PSR-11 compliant dependency injection container.
Comments