Skip to content

Commit

Permalink
Merge pull request #1102: Expose LoggerChannel attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
spiralbot committed Apr 25, 2024
1 parent 2077e77 commit 39879ad
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 1 deletion.
23 changes: 23 additions & 0 deletions src/Attribute/LoggerChannel.php
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Spiral\Logger\Attribute;

use Psr\Log\LoggerInterface;

/**
* Used to specify the channel name for the logger when it injected as {@see LoggerInterface} on auto-wiring.
*
* Note: {@see \Spiral\Logger\LoggerInjector} should be registered in the container to support this attribute.
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class LoggerChannel
{
/**
* @param non-empty-string $name
*/
public function __construct(public readonly string $name)
{
}
}
30 changes: 30 additions & 0 deletions src/Bootloader/LoggerBootloader.php
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Spiral\Logger\Bootloader;

use Psr\Log\LoggerInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Container;
use Spiral\Logger\Attribute\LoggerChannel;
use Spiral\Logger\LogFactory;
use Spiral\Logger\LoggerInjector;
use Spiral\Logger\LogsInterface;
use Spiral\Logger\NullLogger;

/**
* Register {@see LoggerInterface} injector with support for {@see LoggerChannel} attribute.
* Register default {@see LogsInterface} implementation that produces {@see NullLogger}.
*/
final class LoggerBootloader extends Bootloader
{
protected const SINGLETONS = [
LogsInterface::class => LogFactory::class,
];

public function init(Container $container): void
{
$container->bindInjector(LoggerInterface::class, LoggerInjector::class);
}
}
62 changes: 62 additions & 0 deletions src/LoggerInjector.php
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Spiral\Logger;

use Psr\Log\LoggerInterface;
use Spiral\Core\Container\InjectorInterface;
use Spiral\Logger\Attribute\LoggerChannel;

/**
* Container injector for {@see LoggerInterface}.
* Supports {@see LoggerChannel} attribute.
*
* @implements InjectorInterface<LoggerInterface>
*/
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<class-string<LogsInterface>, 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<LoggerChannel>[] $attributes */
$attributes = $parameter->getAttributes(LoggerChannel::class);

return $attributes[0]?->newInstance()->name;
}
}
2 changes: 1 addition & 1 deletion src/NullLogger.php
Expand Up @@ -18,7 +18,7 @@ final class NullLogger implements LoggerInterface

public function __construct(
callable $receptor,
private string $channel
private readonly string $channel
) {
$this->receptor = $receptor(...);
}
Expand Down
87 changes: 87 additions & 0 deletions tests/FactoryTest.php
Expand Up @@ -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();
Expand Down

0 comments on commit 39879ad

Please sign in to comment.