From 51b37035b3ec9befc869e629286b6f8ea4c29437 Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Wed, 22 Oct 2014 21:53:14 -0600 Subject: [PATCH] Make $wgMWLoggerDefaultSpi more expressive Allow $wgMWLoggerDefaultSpi to specify a more expressive object creation by introducing a new ObjectFactory class which can process an array of instructions to call either an object constructor or a factory method with an array of arguments. This allows removal of the $wgMWLoggerMonologSpiConfig global variable in favor of configuration using $wgMWLoggerDefaultSpi. New classes introduced: ; ObjectFactory : Construct objects from configuration instructions. Change-Id: If56cce5dcb1ad5712e238d6e2dab809a351f79be --- docs/mwlogger.txt | 6 +- includes/AutoLoader.php | 1 + includes/DefaultSettings.php | 39 ++---- includes/debug/logger/Logger.php | 8 +- includes/debug/logger/NullSpi.php | 7 + includes/debug/logger/monolog/Spi.php | 191 +++++++++++--------------- includes/libs/ObjectFactory.php | 89 ++++++++++++ 7 files changed, 190 insertions(+), 151 deletions(-) create mode 100644 includes/libs/ObjectFactory.php diff --git a/docs/mwlogger.txt b/docs/mwlogger.txt index 9964e8b671..5a3e249b3d 100644 --- a/docs/mwlogger.txt +++ b/docs/mwlogger.txt @@ -50,10 +50,8 @@ a more feature rich logging configuration. == Globals == ; $wgMWLoggerDefaultSpi -: Default service provider interface to use with MWLogger -; $wgMWLoggerMonologSpiConfig -: Configuration for MWLoggerMonologSpi describing how to configure the - Monolog logger instances. +: Specification for creating the default service provider interface to use + with MWLogger [0]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md [1]: https://github.com/Seldaek/monolog diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 6936570ae2..25bf47d1d0 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -132,6 +132,7 @@ $wgAutoloadLocalClasses = array( 'MWHookException' => 'includes/Hooks.php', 'MWHttpRequest' => 'includes/HttpFunctions.php', 'MWNamespace' => 'includes/MWNamespace.php', + 'ObjectFactory' => 'includes/libs/ObjectFactory.php', 'OutputPage' => 'includes/OutputPage.php', 'PathRouter' => 'includes/PathRouter.php', 'PathRouterPatternReplacer' => 'includes/PathRouter.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index af36a64d17..d828f38a7f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -5218,38 +5218,21 @@ $wgDebugLogGroups = array(); /** * Default service provider for creating MWLogger instances. * - * This can either be the name of a class implementing the MWLoggerSpi - * interface with a zero argument constructor or a callable that will return - * an MWLoggerSpi instance. Alternately the MWLogger::registerProvider method - * can be called to inject an MWLoggerSpi instance into MWLogger and bypass - * the use of this configuration variable. - * - * @since 1.25 - * @var $wgMWLoggerDefaultSpi string|callable - * @see MwLogger - */ -$wgMWLoggerDefaultSpi = 'MWLoggerNullSpi'; - -/** - * Configuration for MWLoggerMonologSpi logger factory. + * The value should be an array suitable for use with + * ObjectFactory::getObjectFromSpec(). The created object is expected to + * implement the MWLoggerSpi interface. See ObjectFactory for additional + * details. * - * Default configuration installs a null handler that will silently discard - * all logging events. + * Alternately the MWLogger::registerProvider method can be called to inject + * an MWLoggerSpi instance into MWLogger and bypass the use of this + * configuration variable entirely. * * @since 1.25 - * @see MWLoggerMonologSpi + * @var array $wgMWLoggerDefaultSpi + * @see MwLogger */ -$wgMWLoggerMonologSpiConfig = array( - 'loggers' => array( - '@default' => array( - 'handlers' => array( 'null' ), - ), - ), - 'handlers' => array( - 'null' => array( - 'class' => '\\Monolog\\Logger\\NullHandler', - ), - ), +$wgMWLoggerDefaultSpi = array( + 'class' => 'MWLoggerNullSpi', ); /** diff --git a/includes/debug/logger/Logger.php b/includes/debug/logger/Logger.php index f5dd1cf7c2..7164bfabc8 100644 --- a/includes/debug/logger/Logger.php +++ b/includes/debug/logger/Logger.php @@ -198,11 +198,9 @@ class MWLogger implements \Psr\Log\LoggerInterface { public static function getInstance( $channel ) { if ( self::$spi === null ) { global $wgMWLoggerDefaultSpi; - if ( is_callable( $wgMWLoggerDefaultSpi ) ) { - $provider = $wgMWLoggerDefaultSpi(); - } else { - $provider = new $wgMWLoggerDefaultSpi(); - } + $provider = ObjectFactory::getObjectFromSpec( + $wgMWLoggerDefaultSpi + ); self::registerProvider( $provider ); } diff --git a/includes/debug/logger/NullSpi.php b/includes/debug/logger/NullSpi.php index 6c38c329ee..33304fc696 100644 --- a/includes/debug/logger/NullSpi.php +++ b/includes/debug/logger/NullSpi.php @@ -23,6 +23,13 @@ * MWLogger service provider that creates \Psr\Log\NullLogger instances. * A NullLogger silently discards all log events sent to it. * + * Usage: + * @code + * $wgMWLoggerDefaultSpi = array( + * 'class' => 'MWLoggerNullSpi', + * ); + * @endcode + * * @see MWLogger * @since 1.25 * @author Bryan Davis diff --git a/includes/debug/logger/monolog/Spi.php b/includes/debug/logger/monolog/Spi.php index fc39b25bba..e5147153d9 100644 --- a/includes/debug/logger/monolog/Spi.php +++ b/includes/debug/logger/monolog/Spi.php @@ -29,73 +29,75 @@ * for any channel that isn't explicitly named in the 'loggers' configuration * section. * - * Configuration can be specified using the $wgMWLoggerMonologSpiConfig global - * variable. - * - * Example: + * Configuration will most typically be provided in the $wgMWLoggerDefaultSpi + * global configuration variable used by MWLogger to construct its default SPI + * provider: * @code - * $wgMWLoggerMonologSpiConfig = array( - * 'loggers' => array( - * '@default' => array( - * 'processors' => array( 'wiki', 'psr', 'pid', 'uid', 'web' ), - * 'handlers' => array( 'stream' ), - * ), - * 'runJobs' => array( - * 'processors' => array( 'wiki', 'psr', 'pid' ), - * 'handlers' => array( 'stream' ), - * ) - * ), - * 'processors' => array( - * 'wiki' => array( - * 'class' => 'MWLoggerMonologProcessor', - * ), - * 'psr' => array( - * 'class' => '\\Monolog\\Processor\\PsrLogMessageProcessor', - * ), - * 'pid' => array( - * 'class' => '\\Monolog\\Processor\\ProcessIdProcessor', - * ), - * 'uid' => array( - * 'class' => '\\Monolog\\Processor\\UidProcessor', - * ), - * 'web' => array( - * 'class' => '\\Monolog\\Processor\\WebProcessor', - * ), - * ), - * 'handlers' => array( - * 'stream' => array( - * 'class' => '\\Monolog\\Handler\\StreamHandler', - * 'args' => array( 'path/to/your.log' ), - * 'formatter' => 'line', - * ), - * 'redis' => array( - * 'class' => '\\Monolog\\Handler\\RedisHandler', - * 'args' => array( function() { - * $redis = new Redis(); - * $redis->connect( '127.0.0.1', 6379 ); - * return $redis; - * }, - * 'logstash' - * ), - * 'formatter' => 'logstash', - * ), - * 'udp2log' => array( - * 'class' => 'MWLoggerMonologHandler', - * 'args' => array( - * 'udp://127.0.0.1:8420/mediawiki - * ), - * 'formatter' => 'line', - * ), - * ), - * 'formatters' => array( - * 'line' => array( - * 'class' => '\\Monolog\\Formatter\\LineFormatter', - * ), - * 'logstash' => array( - * 'class' => '\\Monolog\\Formatter\\LogstashFormatter', - * 'args' => array( 'mediawiki', php_uname( 'n' ), null, '', 1 ), - * ), - * ), + * $wgMWLoggerDefaultSpi = array( + * 'class' => 'MWLoggerMonologSpi', + * 'args' => array( array( + * 'loggers' => array( + * '@default' => array( + * 'processors' => array( 'wiki', 'psr', 'pid', 'uid', 'web' ), + * 'handlers' => array( 'stream' ), + * ), + * 'runJobs' => array( + * 'processors' => array( 'wiki', 'psr', 'pid' ), + * 'handlers' => array( 'stream' ), + * ) + * ), + * 'processors' => array( + * 'wiki' => array( + * 'class' => 'MWLoggerMonologProcessor', + * ), + * 'psr' => array( + * 'class' => '\\Monolog\\Processor\\PsrLogMessageProcessor', + * ), + * 'pid' => array( + * 'class' => '\\Monolog\\Processor\\ProcessIdProcessor', + * ), + * 'uid' => array( + * 'class' => '\\Monolog\\Processor\\UidProcessor', + * ), + * 'web' => array( + * 'class' => '\\Monolog\\Processor\\WebProcessor', + * ), + * ), + * 'handlers' => array( + * 'stream' => array( + * 'class' => '\\Monolog\\Handler\\StreamHandler', + * 'args' => array( 'path/to/your.log' ), + * 'formatter' => 'line', + * ), + * 'redis' => array( + * 'class' => '\\Monolog\\Handler\\RedisHandler', + * 'args' => array( function() { + * $redis = new Redis(); + * $redis->connect( '127.0.0.1', 6379 ); + * return $redis; + * }, + * 'logstash' + * ), + * 'formatter' => 'logstash', + * ), + * 'udp2log' => array( + * 'class' => 'MWLoggerMonologHandler', + * 'args' => array( + * 'udp://127.0.0.1:8420/mediawiki + * ), + * 'formatter' => 'line', + * ), + * ), + * 'formatters' => array( + * 'line' => array( + * 'class' => '\\Monolog\\Formatter\\LineFormatter', + * ), + * 'logstash' => array( + * 'class' => '\\Monolog\\Formatter\\LogstashFormatter', + * 'args' => array( 'mediawiki', php_uname( 'n' ), null, '', 1 ), + * ), + * ), + * ) ), * ); * @endcode * @@ -119,14 +121,9 @@ class MWLoggerMonologSpi implements MWLoggerSpi { /** - * @param array $config Configuration data. Defaults to global - * $wgMWLoggerMonologSpiConfig + * @param array $config Configuration data. */ - public function __construct( $config = null ) { - if ( $config === null ) { - global $wgMWLoggerMonologSpiConfig; - $config = $wgMWLoggerMonologSpiConfig; - } + public function __construct( array $config ) { $this->config = $config; $this->reset(); } @@ -166,8 +163,8 @@ class MWLoggerMonologSpi implements MWLoggerSpi { $this->config['loggers'][$channel] : $this->config['loggers']['@default']; - $monolog = $this->createLogger( $channel, $spec ); - $this->singletons['loggers'][$channel] = new MWLogger( $monolog ); + $monolog = $this->createLogger( $channel, $spec ); + $this->singletons['loggers'][$channel] = new MWLogger( $monolog ); } return $this->singletons['loggers'][$channel]; @@ -206,7 +203,8 @@ class MWLoggerMonologSpi implements MWLoggerSpi { protected function getProcessor( $name ) { if ( !isset( $this->singletons['processors'][$name] ) ) { $spec = $this->config['processors'][$name]; - $this->singletons['processors'][$name] = $this->instantiate( $spec ); + $processor = ObjectFactory::getObjectFromSpec( $spec ); + $this->singletons['processors'][$name] = $processor; } return $this->singletons['processors'][$name]; } @@ -220,7 +218,7 @@ class MWLoggerMonologSpi implements MWLoggerSpi { protected function getHandler( $name ) { if ( !isset( $this->singletons['handlers'][$name] ) ) { $spec = $this->config['handlers'][$name]; - $handler = $this->instantiate( $spec ); + $handler = ObjectFactory::getObjectFromSpec( $spec ); $handler->setFormatter( $this->getFormatter( $spec['formatter'] ) ); $this->singletons['handlers'][$name] = $handler; } @@ -236,44 +234,9 @@ class MWLoggerMonologSpi implements MWLoggerSpi { protected function getFormatter( $name ) { if ( !isset( $this->singletons['formatters'][$name] ) ) { $spec = $this->config['formatters'][$name]; - $this->singletons['formatters'][$name] = $this->instantiate( $spec ); + $formatter = ObjectFactory::getObjectFromSpec( $spec ); + $this->singletons['formatters'][$name] = $formatter; } return $this->singletons['formatters'][$name]; } - - - /** - * Instantiate the requested object. - * - * The specification array must contain a 'class' key with string value that - * specifies the class name to instantiate. It can optionally contain an - * 'args' key that provides constructor arguments. - * - * @param array $spec Object specification - * @return object - */ - protected function instantiate( $spec ) { - $clazz = $spec['class']; - $args = isset( $spec['args'] ) ? $spec['args'] : array(); - // If an argument is a callable, call it. - // This allows passing things such as a database connection to a logger. - $args = array_map( function ( $value ) { - if ( is_callable( $value ) ) { - return $value(); - } else { - return $value; - } - }, $args ); - - if ( empty( $args ) ) { - $obj = new $clazz(); - - } else { - $ref = new ReflectionClass( $clazz ); - $obj = $ref->newInstanceArgs( $args ); - } - - return $obj; - } - } diff --git a/includes/libs/ObjectFactory.php b/includes/libs/ObjectFactory.php new file mode 100644 index 0000000000..ee696c3aa2 --- /dev/null +++ b/includes/libs/ObjectFactory.php @@ -0,0 +1,89 @@ + + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class ObjectFactory { + + /** + * Instantiate an object based on a specification array. + * + * The specification array must contain a 'class' key with string value + * that specifies the class name to instantiate or a 'factory' key with + * a callable (is_callable() === true). It can optionally contain + * an 'args' key that provides arguments to pass to the + * constructor/callable. + * + * Object construction using a specification having both 'class' and + * 'args' members will call the constructor of the class using + * ReflectionClass::newInstanceArgs. The use of ReflectionClass carries + * a performance penalty and should not be used to create large numbers of + * objects. If this is needed, consider introducing a factory method that + * can be called via call_user_func_array() instead. + * + * Values in the arguments collection which are Closure instances will be + * expanded by invoking them with no arguments before passing the + * resulting value on to the constructor/callable. This can be used to + * pass DatabaseBase instances or other live objects to the + * constructor/callable. + * + * @param array $spec Object specification + * @return object + * @throws InvalidArgumentException when object specification does not + * contain 'class' or 'factory' keys + * @throws ReflectionException when 'args' are supplied and 'class' + * constructor is non-public or non-existant + */ + public static function getObjectFromSpec( $spec ) { + $args = isset( $spec['args'] ) ? $spec['args'] : array(); + + $args = array_map( function ( $value ) { + if ( is_object( $value ) && $value instanceof Closure ) { + // If an argument is a Closure, call it. + return $value(); + } else { + return $value; + } + }, $args ); + + if ( isset( $spec['class'] ) ) { + $clazz = $spec['class']; + if ( !$args ) { + $obj = new $clazz(); + } else { + $ref = new ReflectionClass( $clazz ); + $obj = $ref->newInstanceArgs( $args ); + } + } elseif ( isset( $spec['factory'] ) ) { + $obj = call_user_func_array( $spec['factory'], $args ); + } else { + throw new InvalidArgumentException( + 'Provided specification lacks both factory and class parameters.' + ); + } + + return $obj; + } +} -- 2.20.1