diff --git a/src/Bridge/Monolog/phpunit.xml b/src/Bridge/Monolog/phpunit.xml index fbd70dd46..7d3302032 100644 --- a/src/Bridge/Monolog/phpunit.xml +++ b/src/Bridge/Monolog/phpunit.xml @@ -1,28 +1,27 @@ - + stderr="true" + cacheDirectory=".phpunit.cache" + backupStaticProperties="false"> tests - - - src - - + + + src + + diff --git a/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php b/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php index e5b2b7ace..955505d68 100644 --- a/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php +++ b/src/Bridge/Monolog/src/Bootloader/MonologBootloader.php @@ -18,6 +18,7 @@ use Spiral\Config\Patch\Append; use Spiral\Core\Attribute\Singleton; use Spiral\Core\Container; +use Spiral\Logger\Bootloader\LoggerBootloader; use Spiral\Logger\LogsInterface; use Spiral\Monolog\Config\MonologConfig; use Spiral\Monolog\LogFactory; @@ -27,13 +28,17 @@ final class MonologBootloader extends Bootloader { protected const SINGLETONS = [ LogsInterface::class => LogFactory::class, - LoggerInterface::class => Logger::class, + Logger::class => Logger::class, ]; protected const BINDINGS = [ 'log.rotate' => [self::class, 'logRotate'], ]; + protected const DEPENDENCIES = [ + LoggerBootloader::class, + ]; + private const DEFAULT_FORMAT = "[%datetime%] %level_name%: %message% %context%\n"; public function __construct( @@ -71,8 +76,6 @@ public function init(Container $container, FinalizerInterface $finalizer): void 'globalLevel' => Logger::DEBUG, 'handlers' => [], ]); - - $container->bindInjector(Logger::class, LogFactory::class); } public function addHandler(string $channel, HandlerInterface $handler): void diff --git a/src/Bridge/Monolog/src/LogFactory.php b/src/Bridge/Monolog/src/LogFactory.php index 455715003..c62399bdb 100644 --- a/src/Bridge/Monolog/src/LogFactory.php +++ b/src/Bridge/Monolog/src/LogFactory.php @@ -14,6 +14,7 @@ use Spiral\Core\Container\InjectorInterface; use Spiral\Core\FactoryInterface; use Spiral\Logger\ListenerRegistryInterface; +use Spiral\Logger\LoggerInjector; use Spiral\Logger\LogsInterface; use Spiral\Monolog\Config\MonologConfig; use Spiral\Monolog\Exception\ConfigException; @@ -34,7 +35,7 @@ public function __construct( $this->eventHandler = new EventHandler($listenerRegistry, $config->getEventLevel()); } - public function getLogger(string $channel = null): LoggerInterface + public function getLogger(?string $channel = null): LoggerInterface { $default = $this->config->getDefault(); @@ -58,9 +59,11 @@ public function getLogger(string $channel = null): LoggerInterface ); } - public function createInjection(\ReflectionClass $class, string $context = null): LoggerInterface + /** + * @deprecated use {@see LoggerInjector} as an injector instead. + */ + public function createInjection(\ReflectionClass $class, ?string $context = null): LoggerInterface { - // always return default logger as injection return $this->getLogger(); } diff --git a/src/Bridge/Monolog/tests/FactoryTest.php b/src/Bridge/Monolog/tests/FactoryTest.php index 086c4c93e..30ade9901 100644 --- a/src/Bridge/Monolog/tests/FactoryTest.php +++ b/src/Bridge/Monolog/tests/FactoryTest.php @@ -17,6 +17,7 @@ use Spiral\Config\ConfiguratorInterface; use Spiral\Config\LoaderInterface; use Spiral\Core\Container; +use Spiral\Logger\Attribute\LoggerChannel; use Spiral\Logger\ListenerRegistry; use Spiral\Logger\LogsInterface; use Spiral\Monolog\Bootloader\MonologBootloader; @@ -74,9 +75,39 @@ public function load(string $section): array $this->container->bind(LogFactory::class, $factory); $this->assertSame($logger, $this->container->get(Logger::class)); + $this->assertInstanceOf(LoggerInterface::class, $this->container->get(LoggerInterface::class)); $this->assertSame($logger, $this->container->get(LoggerInterface::class)); } + public function testInjectionWithAttribute(): void + { + $factory = new LogFactory(new MonologConfig([]), new ListenerRegistry(), new Container()); + + $this->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 []; + } + } + )); + + $this->container->bind(FinalizerInterface::class, $finalizer = \Mockery::mock(FinalizerInterface::class)); + $finalizer->shouldReceive('addFinalizer')->once(); + + $this->container->get(StrategyBasedBootloadManager::class)->bootload([MonologBootloader::class]); + $this->container->bind(LogFactory::class, $factory); + + $this->container->invoke(function (#[LoggerChannel('foo')] LoggerInterface $logger) { + $this->assertSame('foo', $logger->getName()); + }); + } + public function testFinalizerShouldResetDefaultLogger() { $this->container->bind(ConfiguratorInterface::class, new ConfigManager( diff --git a/src/Bridge/Monolog/tests/LoggerTest.php b/src/Bridge/Monolog/tests/LoggerTest.php index 338204af2..6a5a44a7f 100644 --- a/src/Bridge/Monolog/tests/LoggerTest.php +++ b/src/Bridge/Monolog/tests/LoggerTest.php @@ -15,6 +15,7 @@ use Spiral\Config\ConfiguratorInterface; use Spiral\Config\LoaderInterface; use Spiral\Core\Container; +use Spiral\Logger\LogsInterface; use Spiral\Monolog\Bootloader\MonologBootloader; use Spiral\Monolog\LogFactory; @@ -39,15 +40,14 @@ public function load(string $section): array )); $this->container->bind(FinalizerInterface::class, $finalizer = new Finalizer()); - $this->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); - $this->container->get(StrategyBasedBootloadManager::class)->bootload([MonologBootloader::class]); - $this->container->get(LoggerInterface::class); + + $this->container->bind(LogsInterface::class, $factory = m::mock(LogsInterface::class)); + $factory->shouldReceive('getLogger')->once()->andReturn($logger); $finalizer->finalize(); } diff --git a/src/Logger/src/Attribute/LoggerChannel.php b/src/Logger/src/Attribute/LoggerChannel.php new file mode 100644 index 000000000..9e6feffff --- /dev/null +++ b/src/Logger/src/Attribute/LoggerChannel.php @@ -0,0 +1,23 @@ + LogFactory::class, + ]; + + public function init(Container $container): void + { + $container->bindInjector(LoggerInterface::class, LoggerInjector::class); + } +} diff --git a/src/Logger/src/LoggerInjector.php b/src/Logger/src/LoggerInjector.php new file mode 100644 index 000000000..6c68f9290 --- /dev/null +++ b/src/Logger/src/LoggerInjector.php @@ -0,0 +1,62 @@ + + */ +final class LoggerInjector implements InjectorInterface +{ + public const DEFAULT_CHANNEL = 'default'; + + public function __construct( + private readonly LogsInterface $factory, + ) { + } + + /** + * @param \ReflectionParameter|string|null $context may use extended context if possible. + */ + public function createInjection( + \ReflectionClass $class, + \ReflectionParameter|null|string $context = null, + ): LoggerInterface { + $channel = \is_object($context) ? $this->extractChannelAttribute($context) : null; + + if ($channel === null) { + /** + * Array of flags to check if the logger allows null argument + * + * @var array, bool> $cache + */ + static $cache = []; + + $cache[$this->factory::class] = (new \ReflectionMethod($this->factory, 'getLogger')) + ->getParameters()[0]->allowsNull(); + + $channel = $cache[$this->factory::class] ? null : self::DEFAULT_CHANNEL; + } + + return $this->factory->getLogger($channel); + } + + /** + * @return non-empty-string|null + */ + private function extractChannelAttribute(\ReflectionParameter $parameter): ?string + { + /** @var \ReflectionAttribute[] $attributes */ + $attributes = $parameter->getAttributes(LoggerChannel::class); + + return $attributes[0]?->newInstance()->name; + } +} diff --git a/src/Logger/src/NullLogger.php b/src/Logger/src/NullLogger.php index 02d34257c..c75f95f9f 100644 --- a/src/Logger/src/NullLogger.php +++ b/src/Logger/src/NullLogger.php @@ -18,7 +18,7 @@ final class NullLogger implements LoggerInterface public function __construct( callable $receptor, - private string $channel + private readonly string $channel ) { $this->receptor = $receptor(...); } diff --git a/src/Logger/tests/FactoryTest.php b/src/Logger/tests/FactoryTest.php index 6a31aed88..2a8cbbd03 100644 --- a/src/Logger/tests/FactoryTest.php +++ b/src/Logger/tests/FactoryTest.php @@ -4,14 +4,101 @@ namespace Spiral\Tests\Logger; +use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Spiral\Boot\BootloadManager\DefaultInvokerStrategy; +use Spiral\Boot\BootloadManager\Initializer; +use Spiral\Boot\BootloadManager\InitializerInterface; +use Spiral\Boot\BootloadManager\InvokerStrategyInterface; +use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager; +use Spiral\Boot\Environment; +use Spiral\Boot\EnvironmentInterface; +use Spiral\Core\Container; +use Spiral\Logger\Attribute\LoggerChannel; +use Spiral\Logger\Bootloader\LoggerBootloader; use Spiral\Logger\Event\LogEvent; use Spiral\Logger\ListenerRegistry; use Spiral\Logger\LogFactory; +use Spiral\Logger\LoggerInjector; +use Spiral\Logger\LogsInterface; class FactoryTest extends TestCase { + use MockeryPHPUnitIntegration; + + protected Container $container; + + protected function setUp(): void + { + $this->container = new Container(); + $this->container->bind(EnvironmentInterface::class, new Environment()); + $this->container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class); + $this->container->bind(InitializerInterface::class, Initializer::class); + } + + #[DoesNotPerformAssertions] + public function testDefaultLogger(): void + { + $factory = new LogFactory(new ListenerRegistry()); + $factory->getLogger('default'); + } + + public function testInjection(): void + { + $factory = new class () implements LogsInterface { + public function getLogger(string $channel): LoggerInterface + { + $mock = \Mockery::mock(LoggerInterface::class); + $mock->shouldReceive('getName')->andReturn($channel); + return $mock; + } + }; + $this->container->get(StrategyBasedBootloadManager::class)->bootload([LoggerBootloader::class]); + $this->container->bindSingleton(LogsInterface::class, $factory); + + $this->assertInstanceOf(LoggerInterface::class, $logger = $this->container->get(LoggerInterface::class)); + $this->assertSame(LoggerInjector::DEFAULT_CHANNEL, $logger->getName()); + } + + public function testInjectionNullableChannel(): void + { + $factory = new class () implements LogsInterface { + public function getLogger(?string $channel): LoggerInterface + { + $mock = \Mockery::mock(LoggerInterface::class); + $mock->shouldReceive('getName')->andReturn($channel); + return $mock; + } + }; + $this->container->get(StrategyBasedBootloadManager::class)->bootload([LoggerBootloader::class]); + $this->container->bindSingleton(LogsInterface::class, $factory); + + $this->assertInstanceOf(LoggerInterface::class, $logger = $this->container->get(LoggerInterface::class)); + $this->assertNull($logger->getName()); + } + + public function testInjectionWithAttribute(): void + { + $factory = new class () implements LogsInterface { + public function getLogger(?string $channel): LoggerInterface + { + $mock = \Mockery::mock(LoggerInterface::class); + $mock->shouldReceive('getName')->andReturn($channel); + return $mock; + } + }; + $this->container->get(StrategyBasedBootloadManager::class)->bootload([LoggerBootloader::class]); + $this->container->bindSingleton(LogsInterface::class, $factory); + + $this->container->invoke(function (#[LoggerChannel('foo')] LoggerInterface $logger) { + $this->assertSame('foo', $logger->getName()); + }); + } + + public function testEvent(): void { $l = new ListenerRegistry();