Repository: spiral/framework (php)

Issue:
Add finalizer for Monolog
Monolog provides the `ResettableInterface` for this use case, allowing you to end a log cycle and get things back to their initial state.

See https://github.com/Seldaek/monolog/blob/main/doc/01-usage.md#long-running-processes-and-avoiding-memory-leaks

Pull request:
Add finalizer for Monolog to prevent memory leaks.
| Q             | A
| ------------- | ---
| Bugfix?       | ❌
| Breaks BC?    | ❌
| New feature?  | ✔️
| Issues        | #583


See https://github.com/Seldaek/monolog/blob/main/doc/01-usage.md#long-running-processes-and-avoiding-memory-leaks


Patch:
diff --git a/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php b/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php
index bc8c441f0..d8ab18b3e 100644
--- a/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php
+++ b/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php
@@ -15,18 +15,21 @@ use Monolog\Formatter\LineFormatter;
 use Monolog\Handler\HandlerInterface;
 use Monolog\Handler\RotatingFileHandler;
 use Monolog\Logger;
+use Monolog\ResettableInterface;
 use Psr\Log\LoggerInterface;
 use Spiral\Boot\Bootloader\Bootloader;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\Patch\Append;
 use Spiral\Core\Container;
 use Spiral\Logger\LogsInterface;
+use Spiral\Monolog\Config\MonologConfig;
 use Spiral\Monolog\LogFactory;
 
 final class MonologBootloader extends Bootloader implements Container\SingletonInterface
 {
     protected const SINGLETONS = [
-        LogsInterface::class   => LogFactory::class,
+        LogsInterface::class => LogFactory::class,
         LoggerInterface::class => Logger::class,
     ];
 
@@ -37,51 +40,57 @@ final class MonologBootloader extends Bootloader implements Container\SingletonI
     /** @var ConfiguratorInterface */
     private $config;
 
-    /**
-     * @param ConfiguratorInterface $config
-     */
     public function __construct(ConfiguratorInterface $config)
     {
         $this->config = $config;
     }
 
-    /**
-     * @param Container $container
-     */
-    public function boot(Container $container): void
+    public function boot(Container $container, FinalizerInterface $finalizer): void
     {
-        $this->config->setDefaults('monolog', [
+        $finalizer->addFinalizer(static function () use ($container): void {
+            if ($container->hasInstance(LoggerInterface::class)) {
+                $logger = $container->get(LoggerInterface::class);
+
+                if ($logger instanceof ResettableInterface) {
+                    $logger->reset();
+                }
+            }
+
+            if ($container->hasInstance(LogsInterface::class)) {
+                $factory = $container->get(LogsInterface::class);
+
+                if ($factory instanceof ResettableInterface) {
+                    $factory->reset();
+                }
+            }
+        });
+
+        $this->config->setDefaults(MonologConfig::CONFIG, [
             'globalLevel' => Logger::DEBUG,
-            'handlers'    => [],
+            'handlers' => [],
         ]);
 
         $container->bindInjector(Logger::class, LogFactory::class);
     }
 
-    /**
-     * @param string           $channel
-     * @param HandlerInterface $handler
-     */
     public function addHandler(string $channel, HandlerInterface $handler): void
     {
-        if (!isset($this->config->getConfig('monolog')['handlers'][$channel])) {
-            $this->config->modify('monolog', new Append('handlers', $channel, []));
+        $name = MonologConfig::CONFIG;
+
+        if (! isset($this->config->getConfig($name)['handlers'][$channel])) {
+            $this->config->modify($name, new Append('handlers', $channel, []));
         }
 
-        $this->config->modify('monolog', new Append(
-            'handlers.' . $channel,
-            null,
-            $handler
-        ));
+        $this->config->modify(
+            $name,
+            new Append(
+                'handlers.' . $channel,
+                null,
+                $handler
+            )
+        );
     }
 
-    /**
-     * @param string $filename
-     * @param int    $level
-     * @param int    $maxFiles
-     * @param bool   $bubble
-     * @return HandlerInterface
-     */
     public function logRotate(
         string $filename,
         int $level = Logger::DEBUG,
diff --git a/src/Bridge/Monolog/src/LogFactory.php b/src/Bridge/Monolog/src/LogFactory.php
index 726896cdb..a531d718e 100644
--- a/src/Bridge/Monolog/src/LogFactory.php
+++ b/src/Bridge/Monolog/src/LogFactory.php
@@ -14,6 +14,7 @@ namespace Spiral\Monolog;
 use Monolog\Handler\HandlerInterface;
 use Monolog\Logger;
 use Monolog\Processor\PsrLogMessageProcessor;
+use Monolog\ResettableInterface;
 use Psr\Container\ContainerExceptionInterface;
 use Psr\Log\LoggerInterface;
 use Spiral\Core\Container\Autowire;
@@ -24,7 +25,7 @@ use Spiral\Logger\LogsInterface;
 use Spiral\Monolog\Config\MonologConfig;
 use Spiral\Monolog\Exception\ConfigException;
 
-final class LogFactory implements LogsInterface, InjectorInterface
+final class LogFactory implements LogsInterface, InjectorInterface, ResettableInterface
 {
     // Default logger channel (supplied via injection)
     public const DEFAULT = 'default';
@@ -42,9 +43,9 @@ final class LogFactory implements LogsInterface, InjectorInterface
     private $eventHandler;
 
     /**
-     * @param MonologConfig             $config
+     * @param MonologConfig $config
      * @param ListenerRegistryInterface $listenerRegistry
-     * @param FactoryInterface          $factory
+     * @param FactoryInterface $factory
      */
     public function __construct(
         MonologConfig $config,
@@ -104,7 +105,7 @@ final class LogFactory implements LogsInterface, InjectorInterface
         $handlers = [];
 
         foreach ($this->config->getHandlers($channel) as $handler) {
-            if (!$handler instanceof Autowire) {
+            if (! $handler instanceof Autowire) {
                 $handlers[] = $handler;
                 continue;
             }
@@ -131,7 +132,7 @@ final class LogFactory implements LogsInterface, InjectorInterface
     {
         $processors = [];
         foreach ($this->config->getProcessors($channel) as $processor) {
-            if (!$processor instanceof Autowire) {
+            if (! $processor instanceof Autowire) {
                 $processors[] = $processor;
                 continue;
             }
@@ -149,4 +150,11 @@ final class LogFactory implements LogsInterface, InjectorInterface
 
         return $processors;
     }
+
+    public function reset()
+    {
+        if ($this->default instanceof ResettableInterface) {
+            $this->default->reset();
+        }
+    }
 }


Test patch:
diff --git a/src/Bridge/Monolog/tests/FactoryTest.php b/src/Bridge/Monolog/tests/FactoryTest.php
index 0009b4963..dcdf63db5 100644
--- a/src/Bridge/Monolog/tests/FactoryTest.php
+++ b/src/Bridge/Monolog/tests/FactoryTest.php
@@ -11,21 +11,30 @@ declare(strict_types=1);
 
 namespace Spiral\Tests\Monolog;
 
+use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
+use Monolog\Handler\HandlerInterface;
 use Monolog\Logger;
+use Monolog\Processor\ProcessorInterface;
+use Monolog\ResettableInterface;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
 use Spiral\Boot\BootloadManager;
+use Spiral\Boot\Finalizer;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfigManager;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\LoaderInterface;
 use Spiral\Core\Container;
 use Spiral\Logger\ListenerRegistry;
+use Spiral\Logger\LogsInterface;
 use Spiral\Monolog\Bootloader\MonologBootloader;
 use Spiral\Monolog\Config\MonologConfig;
 use Spiral\Monolog\LogFactory;
 
 class FactoryTest extends TestCase
 {
+    use MockeryPHPUnitIntegration;
+
     public function testDefaultLogger(): void
     {
         $factory = new LogFactory(new MonologConfig([]), new ListenerRegistry(), new Container());
@@ -55,10 +64,55 @@ class FactoryTest extends TestCase
                 }
             }
         ));
+
+        $container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class));
+        $finalizer->shouldReceive('addFinalizer')->once();
+
         $container->get(BootloadManager::class)->bootload([MonologBootloader::class]);
         $container->bind(LogFactory::class, $factory);
 
         $this->assertSame($logger, $container->get(Logger::class));
         $this->assertSame($logger, $container->get(LoggerInterface::class));
     }
+
+    public function testFinalizerShouldResetDefaultLogger()
+    {
+        $container = new Container();
+        $container->bind(ConfiguratorInterface::class, new ConfigManager(
+            new class() implements LoaderInterface {
+                public function has(string $section): bool
+                {
+                    return false;
+                }
+
+                public function load(string $section): array
+                {
+                    return [];
+                }
+            }
+        ));
+
+        $container->bind(FinalizerInterface::class, $finalizer = new Finalizer());
+
+        $factory = new LogFactory(new MonologConfig([
+            'handlers' => [
+                'default' => [
+                    $handler = \Mockery::mock(HandlerInterface::class, ResettableInterface::class)
+                ]
+            ],
+            'processors' => [
+                'default' => [
+                    $processor = \Mockery::mock(ProcessorInterface::class, ResettableInterface::class)
+                ]
+            ]
+        ]), new ListenerRegistry(), $container);
+
+        $handler->shouldReceive('reset')->once();
+        $processor->shouldReceive('reset')->once();
+
+        $container->bind(LogFactory::class, $factory);
+        $container->get(BootloadManager::class)->bootload([MonologBootloader::class]);
+        $container->get(LogsInterface::class)->getLogger();
+        $finalizer->finalize();
+    }
 }
diff --git a/src/Bridge/Monolog/tests/HandlersTest.php b/src/Bridge/Monolog/tests/HandlersTest.php
index 80d1975a9..86f0b4fc6 100644
--- a/src/Bridge/Monolog/tests/HandlersTest.php
+++ b/src/Bridge/Monolog/tests/HandlersTest.php
@@ -15,6 +15,7 @@ use Monolog\Handler\NullHandler;
 use Monolog\Logger;
 use PHPUnit\Framework\TestCase;
 use Spiral\Boot\BootloadManager;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfigManager;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\LoaderInterface;
@@ -34,6 +35,10 @@ class HandlersTest extends TestCase
     public function setUp(): void
     {
         $this->container = new Container();
+
+        $this->container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class));
+        $finalizer->shouldReceive('addFinalizer')->once();
+
         $this->container->bind(ConfiguratorInterface::class, new ConfigManager(
             new class() implements LoaderInterface {
                 public function has(string $section): bool
diff --git a/src/Bridge/Monolog/tests/LoggerTest.php b/src/Bridge/Monolog/tests/LoggerTest.php
new file mode 100644
index 000000000..420cfefde
--- /dev/null
+++ b/src/Bridge/Monolog/tests/LoggerTest.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Spiral\Tests\Monolog;
+
+use Mockery as m;
+use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
+use Monolog\Logger;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Spiral\Boot\BootloadManager;
+use Spiral\Boot\Finalizer;
+use Spiral\Boot\FinalizerInterface;
+use Spiral\Config\ConfigManager;
+use Spiral\Config\ConfiguratorInterface;
+use Spiral\Config\LoaderInterface;
+use Spiral\Core\Container;
+use Spiral\Monolog\Bootloader\MonologBootloader;
+use Spiral\Monolog\LogFactory;
+
+class LoggerTest extends TestCase
+{
+    use MockeryPHPUnitIntegration;
+
+    public function testLoggerShouldBeReset()
+    {
+        $container = new Container();
+        $container->bind(ConfiguratorInterface::class, new ConfigManager(
+            new class() implements LoaderInterface {
+                public function has(string $section): bool
+                {
+                    return false;
+                }
+
+                public function load(string $section): array
+                {
+                    return [];
+                }
+            }
+        ));
+
+        $container->bind(FinalizerInterface::class, $finalizer = new Finalizer());
+        $container->bind(LogFactory::class, $injector = m::mock(Container\InjectorInterface::class));
+
+        $logger = m::mock(Logger::class);
+        $logger->shouldReceive('reset')->once();
+
+        $injector->shouldReceive('createInjection')->once()->andReturn($logger);
+
+        $container->get(BootloadManager::class)->bootload([MonologBootloader::class]);
+        $container->get(LoggerInterface::class);
+
+        $finalizer->finalize();
+    }
+}
diff --git a/src/Bridge/Monolog/tests/ProcessorsTest.php b/src/Bridge/Monolog/tests/ProcessorsTest.php
index ea4c9bc7e..9458aa72b 100644
--- a/src/Bridge/Monolog/tests/ProcessorsTest.php
+++ b/src/Bridge/Monolog/tests/ProcessorsTest.php
@@ -7,6 +7,7 @@ namespace Spiral\Tests\Monolog;
 use Monolog\Processor\PsrLogMessageProcessor;
 use PHPUnit\Framework\TestCase;
 use Spiral\Boot\BootloadManager;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfigManager;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\LoaderInterface;
@@ -26,6 +27,10 @@ class ProcessorsTest extends TestCase
     public function setUp(): void
     {
         $this->container = new Container();
+
+        $this->container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class));
+        $finalizer->shouldReceive('addFinalizer')->once();
+
         $this->container->bind(ConfiguratorInterface::class, new ConfigManager(
             new class() implements LoaderInterface {
                 public function has(string $section): bool
diff --git a/src/Bridge/Monolog/tests/RotateHandlerTest.php b/src/Bridge/Monolog/tests/RotateHandlerTest.php
index 9308034fa..35c36b5b3 100644
--- a/src/Bridge/Monolog/tests/RotateHandlerTest.php
+++ b/src/Bridge/Monolog/tests/RotateHandlerTest.php
@@ -15,6 +15,7 @@ use Monolog\Handler\RotatingFileHandler;
 use Monolog\Logger;
 use PHPUnit\Framework\TestCase;
 use Spiral\Boot\BootloadManager;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfigManager;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\LoaderInterface;
@@ -26,6 +27,10 @@ class RotateHandlerTest extends TestCase
     public function testRotateHandler(): void
     {
         $container = new Container();
+
+        $container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class));
+        $finalizer->shouldReceive('addFinalizer')->once();
+
         $container->bind(ConfiguratorInterface::class, new ConfigManager(
             new class() implements LoaderInterface {
                 public function has(string $section): bool
diff --git a/src/Bridge/Monolog/tests/TraitTest.php b/src/Bridge/Monolog/tests/TraitTest.php
index 09e6e987b..32d2bb43c 100644
--- a/src/Bridge/Monolog/tests/TraitTest.php
+++ b/src/Bridge/Monolog/tests/TraitTest.php
@@ -15,6 +15,7 @@ use Monolog\Logger;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\NullLogger;
 use Spiral\Boot\BootloadManager;
+use Spiral\Boot\FinalizerInterface;
 use Spiral\Config\ConfigManager;
 use Spiral\Config\ConfiguratorInterface;
 use Spiral\Config\LoaderInterface;
@@ -52,6 +53,10 @@ class TraitTest extends TestCase
     public function testScope(): void
     {
         $container = new Container();
+
+        $container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class));
+        $finalizer->shouldReceive('addFinalizer')->once();
+
         $container->bind(ConfiguratorInterface::class, new ConfigManager(
             new class() implements LoaderInterface {
                 public function has(string $section): bool