diff --git a/README.md b/README.md index 266b828..fa974d1 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, // this is equal to 1 + 'priority' => \Dot\Log\Manager\Logger::ALERT, // this is equal to 1 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', ], @@ -113,7 +113,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', 'filters' => [ @@ -121,7 +121,7 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '>=', - 'priority' => \Laminas\Log\Logger::EMERG, + 'priority' => \Dot\Log\Manager\Logger::EMERG, ] ], ], @@ -130,7 +130,7 @@ return [ // Only warnings 'OnlyWarningsWriter' => [ 'name' => 'stream', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/warnings_only.log', 'filters' => [ @@ -138,7 +138,7 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '==', - 'priority' => \Laminas\Log\Logger::WARN, + 'priority' => \Dot\Log\Manager\Logger::WARN, ], ], ], @@ -147,7 +147,7 @@ return [ // Warnings and more important messages 'WarningOrHigherWriter' => [ 'name' => 'stream', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/important_messages.log', 'filters' => [ @@ -157,7 +157,7 @@ return [ // note, the smaller the priority, the more important is the message // 0 - emergency, 1 - alert, 2- error, 3 - warn. .etc 'operator' => '<=', - 'priority' => \Laminas\Log\Logger::WARN, + 'priority' => \Dot\Log\Manager\Logger::WARN, ], ], ], @@ -197,7 +197,7 @@ The following formats the message as JSON data: ```php 'formatter' => [ - 'name' => \Laminas\Log\Formatter\Json::class, + 'name' => \Dot\Log\Manager\Formatter\Json::class, ], ``` @@ -220,7 +220,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', // explicitly log all messages @@ -229,12 +229,12 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '>=', - 'priority' => \Laminas\Log\Logger::EMERG, + 'priority' => \Dot\Log\Manager\Logger::EMERG, ], ], ], 'formatter' => [ - 'name' => \Laminas\Log\Formatter\Json::class, + 'name' => \Dot\Log\Manager\Formatter\Json::class, ], ], ], @@ -252,7 +252,7 @@ Basic usage of the logger is illustrated below. The messages are written to see which logs are written and which are not written. ```php -use Laminas\Log\Logger; +use Dot\Log\Manager\Logger; ``` ... diff --git a/composer.json b/composer.json index 70cae91..200ae39 100644 --- a/composer.json +++ b/composer.json @@ -20,10 +20,8 @@ ], "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "psr/http-message": "^1.0 || ^2.0", "laminas/laminas-servicemanager": "^3.22", - "laminas/laminas-log": "^2.17", - "dotkernel/dot-mail": "^4.1" + "laminas/laminas-validator": "^2.64" }, "require-dev": { "phpunit/phpunit": "^10.2", diff --git a/docs/book/v3/configuring-writer.md b/docs/book/v3/configuring-writer.md index a9ed716..3531f0e 100644 --- a/docs/book/v3/configuring-writer.md +++ b/docs/book/v3/configuring-writer.md @@ -18,7 +18,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, // this is equal to 1 + 'priority' => \Dot\Log\Manager\Logger::ALERT, // this is equal to 1 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', ], diff --git a/docs/book/v3/example-with-formatter.md b/docs/book/v3/example-with-formatter.md index ed8b278..de6460b 100644 --- a/docs/book/v3/example-with-formatter.md +++ b/docs/book/v3/example-with-formatter.md @@ -15,7 +15,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', // explicitly log all messages @@ -24,12 +24,12 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '>=', - 'priority' => \Laminas\Log\Logger::EMERG, + 'priority' => \Dot\Log\Manager\Logger::EMERG, ], ], ], 'formatter' => [ - 'name' => \Laminas\Log\Formatter\Json::class, + 'name' => \Dot\Log\Manager\Formatter\Json::class, ], ], ], diff --git a/docs/book/v3/filtering-log-messages.md b/docs/book/v3/filtering-log-messages.md index 32c185f..da1aac7 100644 --- a/docs/book/v3/filtering-log-messages.md +++ b/docs/book/v3/filtering-log-messages.md @@ -31,7 +31,7 @@ return [ 'writers' => [ 'FileWriter' => [ 'name' => 'FileWriter', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/dk.log', 'filters' => [ @@ -39,7 +39,7 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '>=', - 'priority' => \Laminas\Log\Logger::EMERG, + 'priority' => \Dot\Log\Manager\Logger::EMERG, ] ], ], @@ -48,7 +48,7 @@ return [ // Only warnings 'OnlyWarningsWriter' => [ 'name' => 'stream', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/warnings_only.log', 'filters' => [ @@ -56,7 +56,7 @@ return [ 'name' => 'priority', 'options' => [ 'operator' => '==', - 'priority' => \Laminas\Log\Logger::WARN, + 'priority' => \Dot\Log\Manager\Logger::WARN, ], ], ], @@ -65,7 +65,7 @@ return [ // Warnings and more important messages 'WarningOrHigherWriter' => [ 'name' => 'stream', - 'priority' => \Laminas\Log\Logger::ALERT, + 'priority' => \Dot\Log\Manager\Logger::ALERT, 'options' => [ 'stream' => __DIR__ . '/../../log/important_messages.log', 'filters' => [ @@ -75,7 +75,7 @@ return [ // note, the smaller the priority, the more important is the message // 0 - emergency, 1 - alert, 2- error, 3 - warn etc. 'operator' => '<=', - 'priority' => \Laminas\Log\Logger::WARN, + 'priority' => \Dot\Log\Manager\Logger::WARN, ], ], ], diff --git a/docs/book/v3/usage.md b/docs/book/v3/usage.md index fea781e..e479b9c 100644 --- a/docs/book/v3/usage.md +++ b/docs/book/v3/usage.md @@ -5,7 +5,7 @@ Basic usage of the logger is illustrated below. The messages are written to see which logs are written and which are not written. ```php -use Laminas\Log\Logger; +use Dot\Log\Manager\Logger; ``` ... diff --git a/log.global.php.dist b/log.global.php.dist index a4342c5..7b401f6 100644 --- a/log.global.php.dist +++ b/log.global.php.dist @@ -2,11 +2,8 @@ return [ 'dot_log' => [ - 'loggers' => [ //define log services here - - //example 1 - stream logger 'stream_logger' => [ 'writers' => [ 'name' => 'stream', @@ -24,63 +21,6 @@ return [ ], ], ], - - //example 2 - db logger - 'db_logger' => [ - 'writers' => [ - 'name' => 'db', - 'priority' => \Laminas\Log\Logger::INFO, - 'options' => [ - //service name of the database adapter - 'db' => 'database', - 'table' => 'log_table', - - //optional, column map - 'column' => [ - 'timestamp' => 'date', - 'priority' => 'type', - 'message' => 'event', - ], - - //optional, separator - 'separator' => '-', - - 'formatter' => [ - 'name' => 'MyFormatter', - ], - - 'filters' => [ - [ - 'name' => 'MyFilter', - ] - ] - ], - ], - ], - - //example 3 - mail logger using a mail service from dot-mail library - 'mail_logger' => [ - 'writers' => [ - 'name' => 'mail', - 'priority' => \Laminas\Log\Logger::ERR, - 'options' => [ - 'mail_service' => 'dot-mail.mail-service.service_name', - - 'subject_prepend_text' => '', - - 'formatter' => [ - 'name' => 'MyFormatter', - ], - - 'filters' => [ - [ - 'name' => 'MyFilter' - ], - ], - ] - ], - ], - ], ], ]; diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index bf64ab2..0805b44 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -9,12 +9,10 @@ use Dot\Log\Factory\LoggerAbstractServiceFactory; use Dot\Log\Factory\ProcessorPluginManagerFactory; use Dot\Log\Factory\WriterPluginManagerFactory; -use Laminas\Log\FilterPluginManager; -use Laminas\Log\FormatterPluginManager; -use Laminas\Log\Logger; -use Laminas\Log\LoggerServiceFactory; -use Laminas\Log\ProcessorPluginManager; -use Laminas\Log\WriterPluginManager; +use Dot\Log\Manager\FilterPluginManager; +use Dot\Log\Manager\FormatterPluginManager; +use Dot\Log\Manager\ProcessorPluginManager; +use Dot\Log\Manager\WriterPluginManager; class ConfigProvider { diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..bdc611f --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,9 @@ +configKey = $configKey; } /** - * @param string $requestedName * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ @@ -46,22 +46,40 @@ public function canCreate(ContainerInterface $container, $requestedName): bool return false; } - return parent::canCreate($container, $parts[1]); + $config = $this->getConfig($container); + if (empty($config)) { + return false; + } + + return isset($config[$parts[1]]); } - /** - * @param string $requestedName - * @throws ContainerExceptionInterface - */ public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): Logger { $parts = explode('.', $requestedName); - return parent::__invoke($container, $parts[1], $options); + + $config = $this->getConfig($container); + $config = $config[$parts[1]]; + + $this->processConfig($config, $container); + + return new Logger($config); } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ protected function getConfig(ContainerInterface $services): array { - parent::getConfig($services); + if (! $services->has('config')) { + $this->config = []; + } + + $config = $services->get('config'); + if (isset($config[$this->configKey])) { + $this->config = $config[$this->configKey]; + } if ( ! empty($this->config) @@ -74,10 +92,6 @@ protected function getConfig(ContainerInterface $services): array return $this->config; } - /** - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ protected function processConfig(array &$config, ContainerInterface $services): void { if (isset($config['writers'])) { @@ -87,21 +101,6 @@ protected function processConfig(array &$config, ContainerInterface $services): $writerConfig['options']['stream'] ); } - if ( - isset($writerConfig['name']) - && in_array($writerConfig['name'], ['mail', Mail::class, 'laminaslogwritermail']) - && isset($writerConfig['options']['mail_service']) - && is_string($writerConfig['options']['mail_service']) - && $services->has($writerConfig['options']['mail_service']) - ) { - /** @var MailServiceInterface $mailService */ - $mailService = $services->get($writerConfig['options']['mail_service']); - $mail = $mailService->getMessage(); - $transport = $mailService->getTransport(); - - $config['writers'][$index]['options']['mail'] = $mail; - $config['writers'][$index]['options']['transport'] = $transport; - } } } diff --git a/src/Factory/ProcessorPluginManagerFactory.php b/src/Factory/ProcessorPluginManagerFactory.php index 61ee17f..006ea7b 100644 --- a/src/Factory/ProcessorPluginManagerFactory.php +++ b/src/Factory/ProcessorPluginManagerFactory.php @@ -5,7 +5,7 @@ namespace Dot\Log\Factory; use Exception; -use Laminas\Log\ProcessorPluginManager; +use Dot\Log\Manager\ProcessorPluginManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; diff --git a/src/Factory/WriterFactory.php b/src/Factory/WriterFactory.php new file mode 100644 index 0000000..9ebb970 --- /dev/null +++ b/src/Factory/WriterFactory.php @@ -0,0 +1,107 @@ +setCreationOptions($creationOptions); + } + } + + public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): object + { + $options = (array) $options; + + $options = $this->populateOptions($options, $container, 'filter_manager', 'LogFilterManager'); + $options = $this->populateOptions($options, $container, 'formatter_manager', 'LogFormatterManager'); + + return new $requestedName($options); + } + + /** + * Populates the options array with the correct container value. + */ + private function populateOptions( + array $options, + ContainerInterface $container, + string $name, + string $defaultService + ): array + { + if (isset($options[$name]) && is_string($options[$name])) { + $options[$name] = $container->get($options[$name]); + return $options; + } + + if (! isset($options[$name]) && $container->has($defaultService)) { + $options[$name] = $container->get($defaultService); + return $options; + } + + return $options; + } + + /** + * Create an instance of the named service. + * + * First, it checks if `$canonicalName` resolves to a class, and, if so, uses + * that value to proxy to `__invoke()`. + * + * Next, if `$requestedName` is non-empty and resolves to a class, this + * method uses that value to proxy to `__invoke()`. + * + * Finally, if the above each fail, it raises an exception. + * + * The approach above is performed as version 2 has two distinct behaviors + * under which factories are invoked: + * + * - If an alias was used, $canonicalName is the resolved name, and + * $requestedName is the service name requested, in which case $canonicalName + * is likely the qualified class name; + * - Otherwise, $canonicalName is the normalized name, and $requestedName + * is the original service name requested (typically the qualified class name). + * + * @throws ContainerExceptionInterface + */ + public function createService( + ServiceLocatorInterface $serviceLocator, + ?string $canonicalName = null, + ?string $requestedName = null + ): object + { + if (is_string($canonicalName) && class_exists($canonicalName)) { + return $this($serviceLocator->getServiceLocator(), $canonicalName, $this->creationOptions); + } + + if (is_string($requestedName) && class_exists($requestedName)) { + return $this($serviceLocator->getServiceLocator(), $requestedName, $this->creationOptions); + } + + throw new InvalidServiceException(sprintf( + '%s requires that the requested name is provided on invocation; ' + . 'please update your tests or consuming container', + self::class + )); + } + + public function setCreationOptions(array $creationOptions): void + { + $this->creationOptions = $creationOptions; + } +} diff --git a/src/Factory/WriterPluginManagerFactory.php b/src/Factory/WriterPluginManagerFactory.php index e628460..fcec022 100644 --- a/src/Factory/WriterPluginManagerFactory.php +++ b/src/Factory/WriterPluginManagerFactory.php @@ -5,7 +5,7 @@ namespace Dot\Log\Factory; use Exception; -use Laminas\Log\WriterPluginManager; +use Dot\Log\Manager\WriterPluginManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php new file mode 100644 index 0000000..3ceea26 --- /dev/null +++ b/src/Filter/FilterInterface.php @@ -0,0 +1,13 @@ +priority = (int) $priority; + $this->operator = $operator ?? '<='; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + */ + public function filter(array $event): bool|int + { + return version_compare((string) $event['priority'], (string) $this->priority, $this->operator); + } +} diff --git a/src/Filter/Regex.php b/src/Filter/Regex.php new file mode 100644 index 0000000..fafbfeb --- /dev/null +++ b/src/Filter/Regex.php @@ -0,0 +1,54 @@ +regex = $regex; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + */ + public function filter(array $event): bool + { + $message = $event['message']; + if (is_array($event['message'])) { + $message = var_export($message, true); + } + return preg_match($this->regex, $message) > 0; + } +} diff --git a/src/Filter/SuppressFilter.php b/src/Filter/SuppressFilter.php new file mode 100644 index 0000000..895f2f3 --- /dev/null +++ b/src/Filter/SuppressFilter.php @@ -0,0 +1,52 @@ +suppress($suppress); + } + + /** + * This is a simple boolean filter. + * + * Call suppress(true) to suppress all log events. + * Call suppress(false) to accept all log events. + */ + public function suppress(bool $suppress): void + { + $this->accept = !$suppress; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + */ + public function filter(array $event): bool + { + return $this->accept; + } +} diff --git a/src/Filter/Validator.php b/src/Filter/Validator.php new file mode 100644 index 0000000..4de1695 --- /dev/null +++ b/src/Filter/Validator.php @@ -0,0 +1,45 @@ +validator = $validator; + } + + /** + * Returns TRUE to accept the message, FALSE to block it. + */ + public function filter(array $event) + { + return $this->validator->isValid($event['message']); + } +} diff --git a/src/Formatter/Base.php b/src/Formatter/Base.php new file mode 100644 index 0000000..b650eba --- /dev/null +++ b/src/Formatter/Base.php @@ -0,0 +1,125 @@ +dateTimeFormat = $dateTimeFormat; + } + } + + /** + * Formats data to be written by the writer. + */ + public function format($event): array|string + { + foreach ($event as $key => $value) { + // Keep extra as an array + if ('extra' === $key && is_array($value)) { + $event[$key] = self::format($value); + } else { + $event[$key] = $this->normalize($value); + } + } + + return $event; + } + + /** + * Normalize all non-scalar data types (except null) in a string value + */ + protected function normalize(mixed $value): mixed + { + if (is_scalar($value) || null === $value) { + return $value; + } + + // better readable JSON + static $jsonFlags; + if ($jsonFlags === null) { + $jsonFlags = 0; + $jsonFlags |= defined('JSON_UNESCAPED_SLASHES') ? JSON_UNESCAPED_SLASHES : 0; + $jsonFlags |= defined('JSON_UNESCAPED_UNICODE') ? JSON_UNESCAPED_UNICODE : 0; + } + + // Error suppression is used in several of these cases as a fix for each of + // #5383 and #4616. Without it, #4616 fails whenever recursion occurs during + // json_encode() operations; usage of a dedicated error handler callback + // causes #5383 to fail when the Logger is being used as an error handler. + // The only viable solution here is error suppression, ugly as it may be. + if ($value instanceof DateTime) { + $value = $value->format($this->getDateTimeFormat()); + } elseif ($value instanceof Traversable) { + $value = @json_encode(iterator_to_array($value), $jsonFlags); + } elseif (is_array($value)) { + $value = @json_encode($value, $jsonFlags); + } elseif (is_object($value) && ! method_exists($value, '__toString')) { + $value = sprintf('object(%s) %s', get_class($value), @json_encode($value)); + } elseif (is_resource($value)) { + $value = sprintf('resource(%s)', get_resource_type($value)); + } elseif (! is_object($value)) { + $value = gettype($value); + } + + return (string) $value; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat() + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat): static + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} diff --git a/src/Formatter/FormatterInterface.php b/src/Formatter/FormatterInterface.php new file mode 100644 index 0000000..8d90a8f --- /dev/null +++ b/src/Formatter/FormatterInterface.php @@ -0,0 +1,34 @@ +format($this->getDateTimeFormat()); + } + + return @json_encode( + $event, + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK | JSON_PRESERVE_ZERO_FRACTION + ); + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormat(): string + { + return $this->dateTimeFormat; + } + + /** + * {@inheritDoc} + */ + public function setDateTimeFormat($dateTimeFormat): static + { + $this->dateTimeFormat = (string) $dateTimeFormat; + return $this; + } +} diff --git a/src/Formatter/Simple.php b/src/Formatter/Simple.php new file mode 100644 index 0000000..73645d4 --- /dev/null +++ b/src/Formatter/Simple.php @@ -0,0 +1,65 @@ +format = $format ?? static::DEFAULT_FORMAT; + + parent::__construct($dateTimeFormat); + } + + /** + * Formats data into a single line to be written by the writer. + */ + public function format($event): array|string + { + $output = $this->format; + + $event = parent::format($event); + foreach ($event as $name => $value) { + if ('extra' === $name && is_array($value) && count($value)) { + $value = $this->normalize($value); + } elseif ('extra' === $name) { + // Don't print an empty array + $value = ''; + } + $output = str_replace("%$name%", (string) $value, $output); + } + + if ( + array_key_exists('extra', $event) && empty($event['extra']) + && false !== strpos($this->format, '%extra%') + ) { + $output = rtrim($output, ' '); + } + return $output; + } +} diff --git a/src/Logger.php b/src/Logger.php new file mode 100644 index 0000000..e114b28 --- /dev/null +++ b/src/Logger.php @@ -0,0 +1,544 @@ + self::NOTICE, + E_USER_NOTICE => self::NOTICE, + E_WARNING => self::WARN, + E_CORE_WARNING => self::WARN, + E_USER_WARNING => self::WARN, + E_ERROR => self::ERR, + E_USER_ERROR => self::ERR, + E_CORE_ERROR => self::ERR, + E_RECOVERABLE_ERROR => self::ERR, + E_PARSE => self::ERR, + E_COMPILE_ERROR => self::ERR, + E_COMPILE_WARNING => self::ERR, + E_STRICT => self::DEBUG, + E_DEPRECATED => self::DEBUG, + E_USER_DEPRECATED => self::DEBUG, + ]; + + /** + * Registered error handler + */ + protected static bool $registeredErrorHandler = false; + + /** + * Registered shutdown error handler + */ + protected static bool $registeredFatalErrorShutdownFunction = false; + + /** + * Registered exception handler + */ + protected static bool $registeredExceptionHandler = false; + + /** + * List of priority code => priority (short) name + */ + protected array $priorities = [ + self::EMERG => 'EMERG', + self::ALERT => 'ALERT', + self::CRIT => 'CRIT', + self::ERR => 'ERR', + self::WARN => 'WARN', + self::NOTICE => 'NOTICE', + self::INFO => 'INFO', + self::DEBUG => 'DEBUG', + ]; + + protected SplPriorityQueue $writers; + + protected SplPriorityQueue $processors; + + protected WriterPluginManager $writerPlugins; + + protected ProcessorPluginManager $processorPlugins; + + /** + * Constructor + * + * Set options for a logger. Accepted options are: + * - writers: array of writers to add to this logger + * - exceptionhandler: if true register this logger as exceptionhandler + * - errorhandler: if true register this logger as errorhandler + */ + public function __construct(iterable $options = null) + { + $this->writers = new SplPriorityQueue(); + $this->processors = new SplPriorityQueue(); + + if ($options instanceof Traversable) { + $options = ArrayUtils::iteratorToArray($options); + } + + if (! $options) { + return; + } + + if (! is_array($options)) { + throw new InvalidArgumentException( + 'Options must be an array or an object implementing \Traversable ' + ); + } + + // Inject writer plugin manager, if available + if ( + isset($options['writer_plugin_manager']) + && $options['writer_plugin_manager'] instanceof AbstractPluginManager + ) { + $this->setWriterPluginManager($options['writer_plugin_manager']); + } + + // Inject processor plugin manager, if available + if ( + isset($options['processor_plugin_manager']) + && $options['processor_plugin_manager'] instanceof AbstractPluginManager + ) { + $this->setProcessorPluginManager($options['processor_plugin_manager']); + } + + if (isset($options['writers']) && is_array($options['writers'])) { + foreach ($options['writers'] as $writer) { + if (! isset($writer['name'])) { + throw new InvalidArgumentException('Options must contain a name for the writer'); + } + + $priority = $writer['priority'] ?? null; + $writerOptions = $writer['options'] ?? null; + + $this->addWriter($writer['name'], $priority, $writerOptions); + } + } + + if (isset($options['processors']) && is_array($options['processors'])) { + foreach ($options['processors'] as $processor) { + if (! isset($processor['name'])) { + throw new InvalidArgumentException('Options must contain a name for the processor'); + } + + $priority = $processor['priority'] ?? null; + $processorOptions = $processor['options'] ?? null; + + $this->addProcessor($processor['name'], $priority, $processorOptions); + } + } + + if (isset($options['exceptionhandler']) && $options['exceptionhandler'] === true) { + static::registerExceptionHandler($this); + } + + if (isset($options['errorhandler']) && $options['errorhandler'] === true) { + static::registerErrorHandler($this); + } + + if (isset($options['fatal_error_shutdownfunction']) && $options['fatal_error_shutdownfunction'] === true) { + static::registerFatalErrorShutdownFunction($this); + } + } + + /** + * Shutdown all writers + */ + public function __destruct() + { + foreach ($this->writers as $writer) { + try { + $writer->shutdown(); + } catch (Exception $e) { + } + } + } + + public function getWriterPluginManager(): WriterPluginManager + { + if (null === $this->writerPlugins) { + $this->setWriterPluginManager(new WriterPluginManager(new ServiceManager())); + } + return $this->writerPlugins; + } + + public function setWriterPluginManager(WriterPluginManager $writerPlugins): static + { + $this->writerPlugins = $writerPlugins; + return $this; + } + + /** + * Get writer instance + */ + public function writerPlugin(string $name, ?array $options = null): WriterInterface + { + return $this->getWriterPluginManager()->get($name, $options); + } + + /** + * Add a writer to a logger + */ + public function addWriter(WriterInterface|string $writer, int $priority = 1, ?array $options = null): static + { + if (is_string($writer)) { + $writer = $this->writerPlugin($writer, $options); + } elseif (! $writer instanceof Writer\WriterInterface) { + throw new InvalidArgumentException(sprintf( + 'Writer must implement %s\Writer\WriterInterface; received "%s"', + __NAMESPACE__, + is_object($writer) ? get_class($writer) : gettype($writer) + )); + } + $this->writers->insert($writer, $priority); + + return $this; + } + + public function getWriters(): SplPriorityQueue + { + return $this->writers; + } + + public function setWriters(SplPriorityQueue $writers): static + { + foreach ($writers->toArray() as $writer) { + if (! $writer instanceof Writer\WriterInterface) { + throw new InvalidArgumentException( + 'Writers must be a SplPriorityQueue of Laminas\Log\Writer' + ); + } + } + $this->writers = $writers; + return $this; + } + + public function getProcessorPluginManager(): ProcessorPluginManager + { + if (null === $this->processorPlugins) { + $this->setProcessorPluginManager(new ProcessorPluginManager(new ServiceManager())); + } + return $this->processorPlugins; + } + + public function setProcessorPluginManager($plugins): static + { + if (is_string($plugins)) { + $plugins = new $plugins(); + } + if (! $plugins instanceof ProcessorPluginManager) { + throw new InvalidArgumentException(sprintf( + 'processor plugin manager must extend %s\ProcessorPluginManager; received %s', + __NAMESPACE__, + is_object($plugins) ? get_class($plugins) : gettype($plugins) + )); + } + + $this->processorPlugins = $plugins; + return $this; + } + + public function processorPlugin(string $name, ?array $options = null): ProcessorInterface + { + return $this->getProcessorPluginManager()->get($name, $options); + } + + public function addProcessor(ProcessorInterface|string $processor, int $priority = 1, ?array $options = null): static + { + if (is_string($processor)) { + $processor = $this->processorPlugin($processor, $options); + } elseif (! $processor instanceof Processor\ProcessorInterface) { + throw new InvalidArgumentException(sprintf( + 'Processor must implement Laminas\Log\ProcessorInterface; received "%s"', + is_object($processor) ? get_class($processor) : gettype($processor) + )); + } + $this->processors->insert($processor, $priority); + + return $this; + } + + public function getProcessors(): SplPriorityQueue + { + return $this->processors; + } + + public function log(int $priority, mixed $message, iterable $extra = []): static + { + if (! is_int($priority) || ($priority < 0) || ($priority >= count($this->priorities))) { + throw new InvalidArgumentException(sprintf( + '$priority must be an integer >= 0 and < %d; received %s', + count($this->priorities), + var_export($priority, true) + )); + } + if (is_object($message) && ! method_exists($message, '__toString')) { + throw new InvalidArgumentException( + '$message must implement magic __toString() method' + ); + } + + if (! is_array($extra) && ! $extra instanceof Traversable) { + throw new InvalidArgumentException( + '$extra must be an array or implement Traversable' + ); + } elseif ($extra instanceof Traversable) { + $extra = ArrayUtils::iteratorToArray($extra); + } + + if ($this->writers->count() === 0) { + throw new RuntimeException('No log writer specified'); + } + + $timestamp = new DateTime(); + + if (is_array($message)) { + $message = var_export($message, true); + } + + $event = [ + 'timestamp' => $timestamp, + 'priority' => $priority, + 'priorityName' => $this->priorities[$priority], + 'message' => (string) $message, + 'extra' => $extra, + ]; + + /** @var ProcessorInterface $processor */ + foreach ($this->processors->toArray() as $processor) { + $event = $processor->process($event); + } + + /** @var WriterInterface $writer */ + foreach ($this->writers->toArray() as $writer) { + $writer->write($event); + } + + return $this; + } + + public function emerg(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::EMERG, $message, $extra); + } + + public function alert(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::ALERT, $message, $extra); + } + + public function crit(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::CRIT, $message, $extra); + } + + public function err(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::ERR, $message, $extra); + } + + public function warn(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::WARN, $message, $extra); + } + + public function notice(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::NOTICE, $message, $extra); + } + + public function info(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::INFO, $message, $extra); + } + + public function debug(string $message, iterable $extra = []): LoggerInterface + { + return $this->log(self::DEBUG, $message, $extra); + } + + /** + * Register logging system as an error handler to log PHP errors + * + * @link http://www.php.net/manual/function.set-error-handler.php + */ + public static function registerErrorHandler(Logger $logger, bool $continueNativeHandler = false): bool|null|callable + { + // Only register once per instance + if (static::$registeredErrorHandler) { + return false; + } + + $errorPriorityMap = static::$errorPriorityMap; + + $previous = set_error_handler( + function ($level, $message, $file, $line) use ($logger, $errorPriorityMap, $continueNativeHandler) { + $iniLevel = error_reporting(); + + if ($iniLevel & $level) { + if (isset($errorPriorityMap[$level])) { + $priority = $errorPriorityMap[$level]; + } else { + $priority = Logger::INFO; + } + $logger->log($priority, $message, [ + 'errno' => $level, + 'file' => $file, + 'line' => $line, + ]); + } + + return ! $continueNativeHandler; + } + ); + + static::$registeredErrorHandler = true; + return $previous; + } + + public static function unregisterErrorHandler(): void + { + restore_error_handler(); + static::$registeredErrorHandler = false; + } + + /** + * Register a shutdown handler to log fatal errors + * + * @link http://www.php.net/manual/function.register-shutdown-function.php + */ + public static function registerFatalErrorShutdownFunction(Logger $logger): bool + { + // Only register once per instance + if (static::$registeredFatalErrorShutdownFunction) { + return false; + } + + $errorPriorityMap = static::$errorPriorityMap; + + register_shutdown_function(function () use ($logger, $errorPriorityMap) { + $error = error_get_last(); + + if ( + null === $error + || ! in_array( + $error['type'], + [ + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_CORE_WARNING, + E_COMPILE_ERROR, + E_COMPILE_WARNING, + ], + true + ) + ) { + return; + } + + $logger->log( + $errorPriorityMap[$error['type']], + $error['message'], + [ + 'file' => $error['file'], + 'line' => $error['line'], + ] + ); + }); + + static::$registeredFatalErrorShutdownFunction = true; + + return true; + } + + /** + * Register logging system as an exception handler to log PHP exceptions + * + * @link http://www.php.net/manual/en/function.set-exception-handler.php + */ + public static function registerExceptionHandler(Logger $logger): bool + { + // Only register once per instance + if (static::$registeredExceptionHandler) { + return false; + } + + $errorPriorityMap = static::$errorPriorityMap; + + set_exception_handler(function ($exception) use ($logger, $errorPriorityMap) { + $logMessages = []; + + do { + $priority = Logger::ERR; + if ($exception instanceof ErrorException && isset($errorPriorityMap[$exception->getSeverity()])) { + $priority = $errorPriorityMap[$exception->getSeverity()]; + } + + $extra = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTrace(), + ]; + if (isset($exception->xdebug_message)) { + $extra['xdebug'] = $exception->xdebug_message; + } + + $logMessages[] = [ + 'priority' => $priority, + 'message' => $exception->getMessage(), + 'extra' => $extra, + ]; + $exception = $exception->getPrevious(); + } while ($exception); + + foreach (array_reverse($logMessages) as $logMessage) { + $logger->log($logMessage['priority'], $logMessage['message'], $logMessage['extra']); + } + }); + + static::$registeredExceptionHandler = true; + return true; + } + + public static function unregisterExceptionHandler(): void + { + restore_exception_handler(); + static::$registeredExceptionHandler = false; + } +} + diff --git a/src/LoggerInterface.php b/src/LoggerInterface.php new file mode 100644 index 0000000..125bce6 --- /dev/null +++ b/src/LoggerInterface.php @@ -0,0 +1,24 @@ +get('config'); + $logConfig = $config['log'] ?? []; + + $this->processConfig($logConfig, $container); + + return new Logger($logConfig); + } + + /** + * Process and return the configuration from the container. + */ + protected function processConfig(array &$config, ContainerInterface $services): void + { + if ( + isset($config['writer_plugin_manager']) + && is_string($config['writer_plugin_manager']) + && $services->has($config['writer_plugin_manager']) + ) { + $config['writer_plugin_manager'] = $services->get($config['writer_plugin_manager']); + } + + if ( + (! isset($config['writer_plugin_manager']) + || ! $config['writer_plugin_manager'] instanceof AbstractPluginManager) + && $services->has('LogWriterManager') + ) { + $config['writer_plugin_manager'] = $services->get('LogWriterManager'); + } + + if ( + isset($config['processor_plugin_manager']) + && is_string($config['processor_plugin_manager']) + && $services->has($config['processor_plugin_manager']) + ) { + $config['processor_plugin_manager'] = $services->get($config['processor_plugin_manager']); + } + + if ( + (! isset($config['processor_plugin_manager']) + || ! $config['processor_plugin_manager'] instanceof AbstractPluginManager) + && $services->has('LogProcessorManager') + ) { + $config['processor_plugin_manager'] = $services->get('LogProcessorManager'); + } + + if (! isset($config['writers']) || ! is_iterable($config['writers'])) { + return; + } + + if (! is_array($config['writers'])) { + $config['writers'] = iterator_to_array($config['writers']); + } + + foreach ($config['writers'] as $writerConfig) { + if (! is_array($writerConfig) && ! $writerConfig instanceof ArrayAccess) { + $type = is_object($writerConfig) ? get_class($writerConfig) : gettype($writerConfig); + throw new InvalidArgumentException( + 'config log.writers[] must contain array or ArrayAccess, ' . $type . ' provided' + ); + } + } + } +} diff --git a/src/Manager/FilterPluginManager.php b/src/Manager/FilterPluginManager.php new file mode 100644 index 0000000..931f1d8 --- /dev/null +++ b/src/Manager/FilterPluginManager.php @@ -0,0 +1,61 @@ + Priority::class, + 'regex' => Regex::class, + 'suppress' => SuppressFilter::class, + 'suppressfilter' => SuppressFilter::class, + 'validator' => Validator::class, + ]; + + protected $factories = [ + Priority::class => InvokableFactory::class, + Regex::class => InvokableFactory::class, + SuppressFilter::class => InvokableFactory::class, + Validator::class => InvokableFactory::class, + ]; + + protected $instanceOf = FilterInterface::class; + + /** + * Allow many filters of the same type + */ + protected $sharedByDefault = false; + + /** + * Validate the plugin is of the expected type. + * + * Validates against `$instanceOf`. + */ + public function validate($instance): void + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + '%s can only create instances of %s; %s is invalid', + static::class, + $this->instanceOf, + is_object($instance) ? get_class($instance) : gettype($instance) + )); + } + } +} diff --git a/src/Manager/FormatterPluginManager.php b/src/Manager/FormatterPluginManager.php new file mode 100644 index 0000000..2083f9f --- /dev/null +++ b/src/Manager/FormatterPluginManager.php @@ -0,0 +1,46 @@ + Simple::class, + ]; + + protected $factories = [ + Simple::class => InvokableFactory::class, + ]; + + protected $instanceOf = FormatterInterface::class; + + /** + * Allow many formatters of the same type + */ + protected $sharedByDefault = false; + + /** + * Validate the plugin is of the expected type. + * + * Validates against `$instanceOf`. + */ + public function validate($instance): void + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + '%s can only create instances of %s; %s is invalid', + static::class, + $this->instanceOf, + is_object($instance) ? get_class($instance) : gettype($instance) + )); + } + } +} diff --git a/src/Manager/ProcessorPluginManager.php b/src/Manager/ProcessorPluginManager.php new file mode 100644 index 0000000..c91834c --- /dev/null +++ b/src/Manager/ProcessorPluginManager.php @@ -0,0 +1,56 @@ + Backtrace::class, + 'psrplaceholder' => PsrPlaceholder::class, + 'referenceid' => ReferenceId::class, + 'requestid' => RequestId::class, + ]; + + protected $factories = [ + Backtrace::class => InvokableFactory::class, + PsrPlaceholder::class => InvokableFactory::class, + ReferenceId::class => InvokableFactory::class, + RequestId::class => InvokableFactory::class, + ]; + + protected $instanceOf = ProcessorInterface::class; + + /** + * Allow many processors of the same type + */ + protected $sharedByDefault = false; + + /** + * Validate the plugin is of the expected type. + * + * Validates against `$instanceOf`. + */ + public function validate($instance): void + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + '%s can only create instances of %s; %s is invalid', + static::class, + $this->instanceOf, + is_object($instance) ? get_class($instance) : gettype($instance) + )); + } + } +} + diff --git a/src/Manager/WriterPluginManager.php b/src/Manager/WriterPluginManager.php new file mode 100644 index 0000000..a1be1dc --- /dev/null +++ b/src/Manager/WriterPluginManager.php @@ -0,0 +1,55 @@ + Noop::class, + 'stream' => Stream::class, + + // The following are for backwards compatibility only; users + // should update their code to use the noop writer instead. + 'null' => Noop::class, + 'laminaslogwriternull' => Noop::class, + ]; + + protected $factories = [ + Noop::class => WriterFactory::class, + Stream::class => WriterFactory::class, + ]; + + protected $instanceOf = WriterInterface::class; + + /** + * Allow many writers of the same type + */ + protected $sharedByDefault = false; + + /** + * Validate the plugin is of the expected type. + * + * Validates against `$instanceOf`. + */ + public function validate(mixed $instance): void + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + '%s can only create instances of %s; %s is invalid', + static::class, + $this->instanceOf, + is_object($instance) ? get_class($instance) : gettype($instance) + )); + } + } +} + diff --git a/src/Processor/Backtrace.php b/src/Processor/Backtrace.php new file mode 100644 index 0000000..b239446 --- /dev/null +++ b/src/Processor/Backtrace.php @@ -0,0 +1,91 @@ + 5.4.0) + */ + protected int $traceLimit = 10; + + /** + * Classes within these namespaces in the stack are ignored + */ + protected array $ignoredNamespaces = ['Laminas\\Log']; + + /** + * Set options for a backtrace processor. Accepted options are: + * - ignoredNamespaces: array of namespaces to be excluded from the logged backtrace + */ + public function __construct(?array $options = null) + { + if (! empty($options['ignoredNamespaces'])) { + $this->ignoredNamespaces = array_merge($this->ignoredNamespaces, (array) $options['ignoredNamespaces']); + } + } + + /** + * Adds the origin of the log() call to the event extras + */ + public function process(array $event): array + { + $trace = $this->getBacktrace(); + + array_shift($trace); // ignore $this->getBacktrace(); + array_shift($trace); // ignore $this->process() + + $i = 0; + while ( + isset($trace[$i]['class']) + && $this->shouldIgnoreFrame($trace[$i]['class']) + ) { + $i++; + } + + $origin = [ + 'file' => $trace[$i - 1]['file'] ?? null, + 'line' => $trace[$i - 1]['line'] ?? null, + 'class' => $trace[$i]['class'] ?? null, + 'function' => $trace[$i]['function'] ?? null, + ]; + + $extra = $origin; + if (isset($event['extra'])) { + $extra = array_merge($origin, $event['extra']); + } + $event['extra'] = $extra; + + return $event; + } + + public function getIgnoredNamespaces(): array + { + return $this->ignoredNamespaces; + } + + /** + * Provide backtrace as slim as possible + */ + protected function getBacktrace(): array + { + return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $this->traceLimit); + } + + /** + * Determine whether the current frame in the backtrace should be ignored based on the class name + */ + protected function shouldIgnoreFrame($class): bool + { + foreach ($this->ignoredNamespaces as $ignoredNamespace) { + if (false !== strpos($class, $ignoredNamespace)) { + return true; + } + } + + return false; + } +} + diff --git a/src/Processor/ProcessorInterface.php b/src/Processor/ProcessorInterface.php new file mode 100644 index 0000000..545bca2 --- /dev/null +++ b/src/Processor/ProcessorInterface.php @@ -0,0 +1,13 @@ + $val) { + if ( + $val === null + || is_scalar($val) + || (is_object($val) && method_exists($val, "__toString")) + ) { + $replacements['{' . $key . '}'] = $val; + continue; + } + + if (is_object($val)) { + $replacements['{' . $key . '}'] = '[object ' . get_class($val) . ']'; + continue; + } + + $replacements['{' . $key . '}'] = '[' . gettype($val) . ']'; + } + + $event['message'] = strtr($event['message'], $replacements); + return $event; + } +} + diff --git a/src/Processor/ReferenceId.php b/src/Processor/ReferenceId.php new file mode 100644 index 0000000..2920092 --- /dev/null +++ b/src/Processor/ReferenceId.php @@ -0,0 +1,41 @@ +getIdentifier(); + + return $event; + } + + public function setReferenceId($identifier): static + { + $this->identifier = $identifier; + + return $this; + } + + public function getReferenceId(): string + { + return $this->getIdentifier(); + } +} + diff --git a/src/Processor/RequestId.php b/src/Processor/RequestId.php new file mode 100644 index 0000000..4734a0f --- /dev/null +++ b/src/Processor/RequestId.php @@ -0,0 +1,52 @@ +getIdentifier(); + return $event; + } + + /** + * Provide unique identifier for a request + */ + protected function getIdentifier(): string + { + if ($this->identifier) { + return $this->identifier; + } + + $identifier = (string) $_SERVER['REQUEST_TIME_FLOAT']; + + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $identifier .= $_SERVER['HTTP_X_FORWARDED_FOR']; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $identifier .= $_SERVER['REMOTE_ADDR']; + } + + $this->identifier = md5($identifier); + + return $this->identifier; + } +} + diff --git a/src/Writer/AbstractWriter.php b/src/Writer/AbstractWriter.php new file mode 100644 index 0000000..19846dd --- /dev/null +++ b/src/Writer/AbstractWriter.php @@ -0,0 +1,266 @@ +setFilterPluginManager($options['filter_manager']); + } + + if (isset($options['formatter_manager'])) { + $this->setFormatterPluginManager($options['formatter_manager']); + } + + if (isset($options['filters'])) { + $filters = $options['filters']; + if (is_int($filters) || is_string($filters) || $filters instanceof FilterInterface) { + $this->addFilter($filters); + } elseif (is_array($filters)) { + foreach ($filters as $filter) { + if (is_int($filter) || is_string($filter) || $filter instanceof FilterInterface) { + $this->addFilter($filter); + } elseif (is_array($filter)) { + if (! isset($filter['name'])) { + throw new InvalidArgumentException( + 'Options must contain a name for the filter' + ); + } + $filterOptions = $filter['options'] ?? null; + $this->addFilter($filter['name'], $filterOptions); + } + } + } + } + + if (isset($options['formatter'])) { + $formatter = $options['formatter']; + if (is_string($formatter) || $formatter instanceof FormatterInterface) { + $this->setFormatter($formatter); + } elseif (is_array($formatter)) { + if (! isset($formatter['name'])) { + throw new InvalidArgumentException('Options must contain a name for the formatter'); + } + $formatterOptions = $formatter['options'] ?? null; + $this->setFormatter($formatter['name'], $formatterOptions); + } + } + } + } + + /** + * Add a filter specific to this writer. + */ + public function addFilter(int|string|FilterInterface $filter, ?array $options = null): WriterInterface + { + if (is_int($filter)) { + $filter = new Priority($filter); + } + + if (is_string($filter)) { + $filter = $this->filterPlugin($filter, $options); + } + + if (! $filter instanceof FilterInterface) { + throw new InvalidArgumentException(sprintf( + 'Filter must implement %s\Filter\FilterInterface; received "%s"', + __NAMESPACE__, + is_object($filter) ? get_class($filter) : gettype($filter) + )); + } + + $this->filters[] = $filter; + return $this; + } + + public function getFilterPluginManager(): FilterPluginManager + { + if (null === $this->filterPlugins) { + $this->setFilterPluginManager(new FilterPluginManager(new ServiceManager())); + } + return $this->filterPlugins; + } + + public function setFilterPluginManager($plugins): static + { + if (is_string($plugins)) { + $plugins = new $plugins(); + } + if (! $plugins instanceof FilterPluginManager) { + throw new InvalidArgumentException(sprintf( + 'Writer plugin manager must extend %s; received %s', + FilterPluginManager::class, + is_object($plugins) ? get_class($plugins) : gettype($plugins) + )); + } + + $this->filterPlugins = $plugins; + return $this; + } + + public function filterPlugin($name, ?array $options = null) + { + return $this->getFilterPluginManager()->get($name, $options); + } + + public function getFormatterPluginManager(): FormatterPluginManager + { + if (null === $this->formatterPlugins) { + $this->setFormatterPluginManager(new FormatterPluginManager(new ServiceManager())); + } + return $this->formatterPlugins; + } + + public function setFormatterPluginManager($plugins): static + { + if (is_string($plugins)) { + $plugins = new $plugins(); + } + if (! $plugins instanceof FormatterPluginManager) { + throw new InvalidArgumentException( + sprintf( + 'Writer plugin manager must extend %s; received %s', + FormatterPluginManager::class, + is_object($plugins) ? get_class($plugins) : gettype($plugins) + ) + ); + } + + $this->formatterPlugins = $plugins; + return $this; + } + + public function formatterPlugin($name, ?array $options = null) + { + return $this->getFormatterPluginManager()->get($name, $options); + } + + /** + * Log a message to this writer. + * @throws ErrorException + */ + public function write(array $event): void + { + foreach ($this->filters as $filter) { + if (! $filter->filter($event)) { + return; + } + } + + $errorHandlerStarted = false; + + if ($this->convertWriteErrorsToExceptions && ! ErrorHandler::started()) { + ErrorHandler::start($this->errorsToExceptionsConversionLevel); + $errorHandlerStarted = true; + } + + try { + $this->doWrite($event); + } catch (Exception $e) { + if ($errorHandlerStarted) { + ErrorHandler::stop(); + } + throw $e; + } + + if ($errorHandlerStarted) { + $error = ErrorHandler::stop(); + if ($error) { + throw new RuntimeException("Unable to write", 0, $error); + } + } + } + + public function setFormatter(FormatterInterface|string $formatter, ?array $options = null): WriterInterface + { + if (is_string($formatter)) { + $formatter = $this->formatterPlugin($formatter, $options); + } + + if (! $formatter instanceof FormatterInterface) { + throw new InvalidArgumentException(sprintf( + 'Formatter must implement %s\Formatter\FormatterInterface; received "%s"', + __NAMESPACE__, + is_object($formatter) ? get_class($formatter) : gettype($formatter) + )); + } + + $this->formatter = $formatter; + return $this; + } + + protected function getFormatter(): FormatterInterface + { + return $this->formatter; + } + + protected function hasFormatter(): bool + { + return $this->formatter instanceof FormatterInterface; + } + + public function setConvertWriteErrorsToExceptions(bool $convertErrors): void + { + $this->convertWriteErrorsToExceptions = $convertErrors; + } + + /** + * Perform shutdown activities such as closing open resources + */ + public function shutdown() + { + } + + /** + * Write a message to the log + */ + abstract protected function doWrite(array $event): void; +} + diff --git a/src/Writer/Noop.php b/src/Writer/Noop.php new file mode 100644 index 0000000..701bb3f --- /dev/null +++ b/src/Writer/Noop.php @@ -0,0 +1,15 @@ +stream = $streamOrUrl; + } else { + ErrorHandler::start(); + if (isset($filePermissions) && ! file_exists($streamOrUrl) && is_writable(dirname($streamOrUrl))) { + touch($streamOrUrl); + chmod($streamOrUrl, $filePermissions); + } + $this->stream = fopen($streamOrUrl, $mode, false); + $error = ErrorHandler::stop(); + } + + if (! $this->stream) { + throw new RuntimeException(sprintf( + '"%s" cannot be opened with mode "%s"', + $streamOrUrl, + $mode + ), 0, $error); + } + + if (null !== $logSeparator) { + $this->setLogSeparator($logSeparator); + } + + if ($this->formatter === null) { + $this->formatter = new Simple(); + } + } + + /** + * Write a message to the log. + */ + protected function doWrite(array $event): void + { + $line = $this->formatter->format($event) . $this->logSeparator; + fwrite($this->stream, $line); + } + + /** + * Set log separator string + */ + public function setLogSeparator(string $logSeparator): static + { + $this->logSeparator = $logSeparator; + return $this; + } + + /** + * Get log separator string + */ + public function getLogSeparator(): string + { + return $this->logSeparator; + } + + /** + * Close the stream resource. + */ + public function shutdown(): void + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } +} diff --git a/src/Writer/WriterInterface.php b/src/Writer/WriterInterface.php new file mode 100644 index 0000000..6621fc5 --- /dev/null +++ b/src/Writer/WriterInterface.php @@ -0,0 +1,31 @@ +