Skip to content

Commit

Permalink
Inherit attributes/annotations from parent classes
Browse files Browse the repository at this point in the history
  • Loading branch information
HypeMC committed Sep 18, 2021
1 parent dfa91eb commit 062727e
Show file tree
Hide file tree
Showing 19 changed files with 262 additions and 31 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,33 @@ class MyMessageHandler implements MessageHandlerInterface, LoggableOutputInterfa
}
```

Attribute options are inherited from all parent classes that have the PHP attribute declared. In case both a parent & a
child class have the same option defined, the one from the child class has precedence.

```php
namespace App;
use Bizkit\LoggableCommandBundle\ConfigurationProvider\Attribute\LoggableOutput;
use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputInterface;
use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputTrait;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
#[LoggableOutput(path: '%kernel.logs_dir%/messenger/{filename}.log')]
abstract class MyBaseMessageHandler implements MessageHandlerInterface, LoggableOutputInterface
{
use LoggableOutputTrait;
}
#[LoggableOutput(filename: 'my_log_name')]
class MyMessageHandler extends MyBaseMessageHandler
{
public function __invoke(MyMessage $myMessage): void
{
// ...
}
}
```

### Doctrine annotations

If you're using a version of PHP prior to 8,
Expand Down Expand Up @@ -269,6 +296,8 @@ class MyLoggableCommand extends LoggableCommand
}
```
Annotation options are also inherited from all parent classes.
### Adding other Monolog handlers to the output logger
To add other Monolog handlers to the output logger, in case of an inclusive channel list, add the Monolog channel
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ parameters:
- phpstan-autoload.php
ignoreErrors:
-
message: '#^Call to an undefined method ReflectionObject\:\:getAttributes\(\)\.$#'
message: '#^Call to an undefined method ReflectionClass\:\:getAttributes\(\)\.$#'
count: 1
path: src/ConfigurationProvider/AttributeConfigurationProvider.php

Expand Down
31 changes: 31 additions & 0 deletions src/ConfigurationProvider/AbstractConfigurationProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\ConfigurationProvider;

abstract class AbstractConfigurationProvider implements ConfigurationProviderInterface
{
/**
* Appends configuration options from the second configuration to the first configuration
* while not overwriting the options from the first configuration.
*/
protected static function mergeConfigurations(array $firstConfiguration, array $secondConfiguration): array
{
$extraOptions = $firstConfiguration['extra_options'] ?? [];
unset($firstConfiguration['extra_options']);

if (isset($secondConfiguration['extra_options'])) {
$extraOptions += $secondConfiguration['extra_options'];
unset($secondConfiguration['extra_options']);
}

$firstConfiguration += $secondConfiguration;

if (!empty($extraOptions)) {
$firstConfiguration['extra_options'] = $extraOptions;
}

return $firstConfiguration;
}
}
19 changes: 12 additions & 7 deletions src/ConfigurationProvider/AnnotationConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Doctrine\Common\Annotations\Reader as AnnotationsReaderInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;

final class AnnotationConfigurationProvider implements ConfigurationProviderInterface
final class AnnotationConfigurationProvider extends AbstractConfigurationProvider
{
/**
* @var AnnotationsReaderInterface
Expand All @@ -30,14 +30,19 @@ public function __construct(AnnotationsReaderInterface $annotationsReader, Conta
public function __invoke(LoggableOutputInterface $loggableOutput): array
{
$reflection = new \ReflectionObject($loggableOutput);
$configuration = [];

/** @var LoggableOutput|null $annotation */
$annotation = $this->annotationsReader->getClassAnnotation($reflection, LoggableOutput::class);
do {
/** @var LoggableOutput|null $annotation */
$annotation = $this->annotationsReader->getClassAnnotation($reflection, LoggableOutput::class);

if (null === $annotation) {
return [];
}
if (null === $annotation) {
continue;
}

return $this->containerBag->resolveValue($annotation->getOptions());
$configuration = self::mergeConfigurations($configuration, $annotation->getOptions());
} while (false !== $reflection = $reflection->getParentClass());

return empty($configuration) ? $configuration : $this->containerBag->resolveValue($configuration);
}
}
21 changes: 13 additions & 8 deletions src/ConfigurationProvider/AttributeConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;

final class AttributeConfigurationProvider implements ConfigurationProviderInterface
final class AttributeConfigurationProvider extends AbstractConfigurationProvider
{
/**
* @var ContainerBagInterface
Expand All @@ -23,16 +23,21 @@ public function __construct(ContainerBagInterface $containerBag)
public function __invoke(LoggableOutputInterface $loggableOutput): array
{
$reflectionObject = new \ReflectionObject($loggableOutput);
$configuration = [];

$reflectionAttributes = $reflectionObject->getAttributes(LoggableOutput::class);
do {
$reflectionAttributes = $reflectionObject->getAttributes(LoggableOutput::class);

if (null === $reflectionAttribute = $reflectionAttributes[0] ?? null) {
return [];
}
if (null === $reflectionAttribute = $reflectionAttributes[0] ?? null) {
continue;
}

/** @var LoggableOutput $attribute */
$attribute = $reflectionAttribute->newInstance();
/** @var LoggableOutput $attribute */
$attribute = $reflectionAttribute->newInstance();

return $this->containerBag->resolveValue($attribute->getOptions());
$configuration = self::mergeConfigurations($configuration, $attribute->getOptions());
} while (false !== $reflectionObject = $reflectionObject->getParentClass());

return empty($configuration) ? $configuration : $this->containerBag->resolveValue($configuration);
}
}
2 changes: 1 addition & 1 deletion src/ConfigurationProvider/DefaultConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputInterface;

final class DefaultConfigurationProvider implements ConfigurationProviderInterface
final class DefaultConfigurationProvider extends AbstractConfigurationProvider
{
/**
* @var array
Expand Down
11 changes: 2 additions & 9 deletions src/ConfigurationProvider/MergedConfigurationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputInterface;

final class MergedConfigurationProvider implements ConfigurationProviderInterface
final class MergedConfigurationProvider extends AbstractConfigurationProvider
{
/**
* @var ConfigurationProviderInterface[]
Expand All @@ -21,20 +21,13 @@ public function __construct(iterable $configurationProviders)
public function __invoke(LoggableOutputInterface $loggableOutput): array
{
$mergedConfiguration = [];
$extraOptions = [];

foreach ($this->configurationProviders as $configurationProvider) {
if ([] !== $configuration = $configurationProvider($loggableOutput)) {
if (!empty($configuration['extra_options'])) {
$extraOptions += $configuration['extra_options'];
unset($configuration['extra_options']);
}
$mergedConfiguration += $configuration;
$mergedConfiguration = self::mergeConfigurations($mergedConfiguration, $configuration);
}
}

$mergedConfiguration['extra_options'] = $extraOptions;

return $mergedConfiguration;
}
}
64 changes: 64 additions & 0 deletions tests/ConfigurationProvider/AbstractConfigurationProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider;

use Bizkit\LoggableCommandBundle\ConfigurationProvider\AbstractConfigurationProvider;
use Bizkit\LoggableCommandBundle\LoggableOutput\LoggableOutputInterface;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutput;
use Bizkit\LoggableCommandBundle\Tests\TestCase;

/**
* @covers \Bizkit\LoggableCommandBundle\ConfigurationProvider\AbstractConfigurationProvider
*/
final class AbstractConfigurationProviderTest extends TestCase
{
/**
* @dataProvider configurationsToMerge
*/
public function testConfigurationsAreMergedAsExpected(array $config1, array $config2, array $mergedConfig): void
{
$configurationProvider = new class($config1, $config2) extends AbstractConfigurationProvider {
private $config1;
private $config2;

public function __construct(array $config1, array $config2)
{
$this->config1 = $config1;
$this->config2 = $config2;
}

public function __invoke(LoggableOutputInterface $loggableOutput): array
{
return self::mergeConfigurations($this->config1, $this->config2);
}
};

$this->assertSame($mergedConfig, $configurationProvider(new DummyLoggableOutput()));
}

public function configurationsToMerge(): iterable
{
yield 'Extra options in none' => [
['opt1' => 'opt1val', 'opt2' => 'opt2val'],
['opt1' => 'opt1otherVal', 'opt3' => 'opt3otherVal'],
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'opt3' => 'opt3otherVal'],
];
yield 'Extra options in first' => [
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val']],
['opt1' => 'opt1otherVal', 'opt3' => 'opt3otherVal'],
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'opt3' => 'opt3otherVal', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val']],
];
yield 'Extra options in second' => [
['opt1' => 'opt1val', 'opt2' => 'opt2val'],
['opt1' => 'opt1otherVal', 'opt3' => 'opt3otherVal', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val']],
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'opt3' => 'opt3otherVal', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val']],
];
yield 'Extra options in both' => [
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val']],
['opt1' => 'opt1otherVal', 'opt3' => 'opt3otherVal', 'extra_options' => ['extra1' => 'extra1otherVal', 'extra3' => 'extra3otherVal']],
['opt1' => 'opt1val', 'opt2' => 'opt2val', 'opt3' => 'opt3otherVal', 'extra_options' => ['extra1' => 'extra1val', 'extra2' => 'extra2val', 'extra3' => 'extra3otherVal']],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Bizkit\LoggableCommandBundle\ConfigurationProvider\AnnotationConfigurationProvider;
use Bizkit\LoggableCommandBundle\ConfigurationProvider\ConfigurationProviderInterface;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyChildLoggableOutputWithAnnotation;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyChildLoggableOutputWithParentAnnotation;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutput;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutputWithAnnotation;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutputWithAnnotationAndParam;
Expand Down Expand Up @@ -44,6 +46,34 @@ public function testProviderReturnsExpectedConfigWhenAnnotationIsFound(): void
);
}

public function testProviderReturnsExpectedConfigWhenParentAndChildAnnotationsAreFound(): void
{
$handlerOptions = ['filename' => 'child-annotation-test', 'level' => Logger::CRITICAL, 'max_files' => 4];

$provider = $this->createConfigurationProvider(
$this->createContainerBagWithResolveValueMethodCalled($handlerOptions)
);

$this->assertSame(
$handlerOptions,
$provider(new DummyChildLoggableOutputWithAnnotation())
);
}

public function testProviderReturnsExpectedConfigWhenParentAnnotationIsFound(): void
{
$handlerOptions = ['filename' => 'annotation-test', 'level' => Logger::CRITICAL, 'max_files' => 4];

$provider = $this->createConfigurationProvider(
$this->createContainerBagWithResolveValueMethodCalled($handlerOptions)
);

$this->assertSame(
$handlerOptions,
$provider(new DummyChildLoggableOutputWithParentAnnotation())
);
}

public function testProviderReturnsEmptyConfigWhenAnnotationIsNotFound(): void
{
$provider = $this->createConfigurationProvider(
Expand Down
30 changes: 30 additions & 0 deletions tests/ConfigurationProvider/AttributeConfigurationProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Bizkit\LoggableCommandBundle\ConfigurationProvider\AttributeConfigurationProvider;
use Bizkit\LoggableCommandBundle\ConfigurationProvider\ConfigurationProviderInterface;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyChildLoggableOutputWithAttribute;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyChildLoggableOutputWithParentAttribute;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutput;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutputWithAttribute;
use Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures\DummyLoggableOutputWithAttributeAndParam;
Expand Down Expand Up @@ -36,6 +38,34 @@ public function testProviderReturnsExpectedConfigWhenAttributeIsFound(): void
);
}

public function testProviderReturnsExpectedConfigWhenParentAndChildAttributesAreFound(): void
{
$handlerOptions = ['filename' => 'child-attribute-test', 'level' => Logger::EMERGENCY, 'bubble' => true];

$provider = $this->createConfigurationProvider(
$this->createContainerBagWithResolveValueMethodCalled($handlerOptions)
);

$this->assertSame(
$handlerOptions,
$provider(new DummyChildLoggableOutputWithAttribute())
);
}

public function testProviderReturnsExpectedConfigWhenParentAttributeIsFound(): void
{
$handlerOptions = ['filename' => 'attribute-test', 'level' => Logger::EMERGENCY, 'bubble' => true];

$provider = $this->createConfigurationProvider(
$this->createContainerBagWithResolveValueMethodCalled($handlerOptions)
);

$this->assertSame(
$handlerOptions,
$provider(new DummyChildLoggableOutputWithParentAttribute())
);
}

public function testProviderReturnsEmptyConfigWhenAttributeIsNotFound(): void
{
$provider = $this->createConfigurationProvider(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures;

use Bizkit\LoggableCommandBundle\ConfigurationProvider\Attribute\LoggableOutput;

/**
* @LoggableOutput(filename="child-annotation-test")
*/
class DummyChildLoggableOutputWithAnnotation extends DummyLoggableOutputWithAnnotation
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures;

use Bizkit\LoggableCommandBundle\ConfigurationProvider\Attribute\LoggableOutput;

#[LoggableOutput(filename: 'child-attribute-test')]
class DummyChildLoggableOutputWithAttribute extends DummyLoggableOutputWithAttribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures;

class DummyChildLoggableOutputWithParentAnnotation extends DummyLoggableOutputWithAnnotation
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Bizkit\LoggableCommandBundle\Tests\ConfigurationProvider\Fixtures;

class DummyChildLoggableOutputWithParentAttribute extends DummyLoggableOutputWithAttribute
{
}
Loading

0 comments on commit 062727e

Please sign in to comment.