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
-
-
+
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();