Skip to content

Commit 93ab55a

Browse files
committed
Added attribute support
1 parent 6af3ff5 commit 93ab55a

14 files changed

+293
-98
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 0.4.0 - 2022-02-05
8+
### Added
9+
- Configuration of listeners using attributes (`\Tjovaisas\Bundle\DelayedEventBundle\Attribute\AsDelayedEventListener`)
10+
- Library will try to guess method's name based on the passed event when configuring listeners
11+
### Removed
12+
- Support for Symfony ^4.0
13+
- Support for Symfony >=5.0 <5.3
14+
715
## 0.3.0 - 2022-01-30
816
### Added
917
- Support for Symfony ^6.0

README.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,6 @@ composer require tjovaisas/delayed-event-bundle
2626

2727
### Register the bundle:
2828

29-
**Symfony 3.x version:**\
30-
Register bundle into `AppKernel.php`:
31-
```php
32-
public function registerBundles()
33-
{
34-
return [
35-
// ...
36-
new \Tjovaisas\Bundle\DelayedEventBundle\TjovaisasDelayedEventBundle(),
37-
];
38-
}
39-
```
40-
41-
**Symfony 4.x version:**\
4229
Register bundle into `config/bundles.php`:
4330
```php
4431
return [
@@ -49,13 +36,30 @@ return [
4936

5037
## Usage
5138

52-
The only thing that's needed is to change the default tag from `kernel.event_listener` to `tjovaisas.event_listener.post_flush`:
53-
```
39+
Bundle can be configured either using service's configuration's definition or by using attributes:
40+
- Default `kernel.event_listener` tag can be changed to `tjovaisas.event_listener.post_flush` to dispatch message to the given listener after flush occured:
41+
```xml
5442
<service class="Namespace\SomeListener"
5543
id="namespace.some_listener">
5644
<tag name="tjovaisas.event_listener.post_flush" event="some_event" method="onEvent" priority="1" />
5745
</service>
5846
```
47+
- Using attributes. Attribute can be defined either on the whole class
48+
```php
49+
#[AsDelayedEventListener(event: 'some_event', method: 'onEvent', priority: 1)]
50+
class SomeListener
51+
{
52+
//...
53+
}
54+
```
55+
or on classes method (only for Symfony ^6.0):
56+
```php
57+
#[AsDelayedEventListener(event: 'some_event')]
58+
public function onEvent(): void
59+
{
60+
//...
61+
}
62+
```
5963

6064
## Caviats
6165

composer.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
"php": "^8.0",
1515
"doctrine/doctrine-bundle": "^2.3",
1616
"doctrine/orm": "^2.7",
17-
"symfony/config": "^4.0 || ^5.0 || ^6.0",
18-
"symfony/dependency-injection": "^4.0 || ^5.0 || ^6.0",
19-
"symfony/event-dispatcher": "^4.0 || ^5.0 || ^6.0",
20-
"symfony/framework-bundle": "^4.0 || ^5.0 || ^6.0"
17+
"symfony/config": "^5.3 || ^6.0",
18+
"symfony/dependency-injection": "^5.3 || ^6.0",
19+
"symfony/event-dispatcher": "^5.3 || ^6.0",
20+
"symfony/framework-bundle": "^5.3 || ^6.0"
2121
},
2222
"require-dev": {
2323
"friendsofphp/php-cs-fixer": "^3.5",
2424
"phpunit/phpunit": "^9.0",
25-
"symfony/yaml": "^4.0 || ^5.0 || ^6.0",
25+
"symfony/yaml": "^5.3 || ^6.0",
2626
"vimeo/psalm": "^4.7"
2727
},
2828
"autoload": {
@@ -45,7 +45,7 @@
4545
"bin/php-cs-fixer fix --diff --dry-run --verbose"
4646
],
4747
"phpunit": "bin/phpunit",
48-
"fix-cs": "bin/php-cs-fixer fix --config=.php_cs",
49-
"test": ["@check-style", "@phpunit"]
48+
"fix-cs": "bin/php-cs-fixer fix",
49+
"test": ["@analyze", "@phpunit"]
5050
}
5151
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tjovaisas\Bundle\DelayedEventBundle\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
10+
class AsDelayedEventListener
11+
{
12+
public function __construct(
13+
public string $event,
14+
public ?string $method = null,
15+
public int $priority = 0,
16+
) {
17+
}
18+
}

src/DependencyInjection/RegisterListenersPass.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@
1212

1313
class RegisterListenersPass implements CompilerPassInterface
1414
{
15+
public const TAG_NAME = 'tjovaisas.event_listener.post_flush';
1516
private const EVENT_REGISTRAR_ID = 'tjovaisas.delayed_event.service.event_registrar';
1617

1718
public function process(ContainerBuilder $container): void
1819
{
1920
$registrarDefinition = $container->findDefinition(self::EVENT_REGISTRAR_ID);
2021
$delayedEvents = [];
2122

22-
foreach ($container->findTaggedServiceIds('tjovaisas.event_listener.post_flush') as $id => $events) {
23+
foreach ($container->findTaggedServiceIds(self::TAG_NAME) as $id => $events) {
2324
foreach ($events as $event) {
2425
if (!isset($event['event'])) {
2526
throw new InvalidArgumentException(sprintf('"%s" must have event defined', $id));
2627
}
2728

2829
$delayedEvents[] = $event['event'];
29-
$method = $event['method'] ?? '__invoke';
30+
$method = $this->attemptMethodExtraction($container, $event, $id);
3031
$priority = $event['priority'] ?? 0;
3132

3233
$this->checkMethodExists($container, $method, $id);
@@ -48,11 +49,36 @@ public function process(ContainerBuilder $container): void
4849
new ServiceClosureArgument(new Reference(self::EVENT_REGISTRAR_ID)),
4950
'onEvent',
5051
],
51-
]
52+
],
5253
);
5354
}
5455
}
5556

57+
private function attemptMethodExtraction(ContainerBuilder $container, array $event, string $id): string
58+
{
59+
if (isset($event['method'])) {
60+
$this->checkMethodExists($container, $event['method'], $id);
61+
62+
return $event['method'];
63+
}
64+
65+
$method = 'on' . preg_replace_callback([
66+
'/(?<=\b|_)[a-z]/i',
67+
'/[^a-z0-9]/i',
68+
], fn ($matches) => strtoupper($matches[0]), $event['event']);
69+
$method = preg_replace('/[^a-z0-9]/i', '', $method);
70+
71+
try {
72+
$this->checkMethodExists($container, $method, $id);
73+
} catch (InvalidArgumentException) {
74+
$method = '__invoke';
75+
76+
$this->checkMethodExists($container, $method, $id);
77+
} finally {
78+
return $method;
79+
}
80+
}
81+
5682
private function checkMethodExists(ContainerBuilder $container, string $method, string $id): void
5783
{
5884
$class = $container->getDefinition($id)->getClass();

src/DependencyInjection/TjovaisasDelayedEventExtension.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
namespace Tjovaisas\Bundle\DelayedEventBundle\DependencyInjection;
66

7+
use Reflector;
8+
use ReflectionMethod;
79
use Symfony\Component\Config\FileLocator;
810
use Symfony\Component\DependencyInjection\Loader;
11+
use Symfony\Component\DependencyInjection\ChildDefinition;
912
use Symfony\Component\DependencyInjection\ContainerBuilder;
1013
use Symfony\Component\DependencyInjection\Extension\Extension;
14+
use Symfony\Component\DependencyInjection\Exception\LogicException;
15+
use Tjovaisas\Bundle\DelayedEventBundle\Attribute\AsDelayedEventListener;
1116

1217
class TjovaisasDelayedEventExtension extends Extension
1318
{
@@ -18,5 +23,29 @@ public function load(array $configs, ContainerBuilder $container): void
1823

1924
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
2025
$loader->load('services.xml');
26+
27+
$container->registerAttributeForAutoconfiguration(
28+
AsDelayedEventListener::class,
29+
static function (
30+
ChildDefinition $definition,
31+
AsDelayedEventListener $attribute,
32+
Reflector $reflector,
33+
): void {
34+
$tagAttributes = get_object_vars($attribute);
35+
if ($reflector instanceof ReflectionMethod) {
36+
if (isset($tagAttributes['method'])) {
37+
throw new LogicException(
38+
sprintf(
39+
'AsDelayedEventListener attribute cannot declare a method on "%s::%s()".',
40+
$reflector->class,
41+
$reflector->name,
42+
),
43+
);
44+
}
45+
$tagAttributes['method'] = $reflector->getName();
46+
}
47+
$definition->addTag(RegisterListenersPass::TAG_NAME, $tagAttributes);
48+
},
49+
);
2150
}
2251
}

src/Service/QueueReleaser.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ public function __construct(private EventRegistrar $eventRegistrar)
1515

1616
public function release(): void
1717
{
18-
foreach ($this->eventRegistrar->getQueue() as $queue) {
19-
$event = $queue->getEvent();
18+
$queue = $this->eventRegistrar->getQueue();
19+
$this->eventRegistrar->resetQueue();
20+
21+
foreach ($queue as $queueItem) {
22+
$event = $queueItem->getEvent();
2023
$stoppable = $event instanceof Event;
2124

22-
foreach ($queue->getListeners() as $listeners) {
25+
foreach ($queueItem->getListeners() as $listeners) {
2326
foreach ($listeners as $listener) {
2427
if ($stoppable && $event->isPropagationStopped()) {
2528
break 2;
@@ -29,7 +32,5 @@ public function release(): void
2932
}
3033
}
3134
}
32-
33-
$this->eventRegistrar->resetQueue();
3435
}
3536
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tjovaisas\Bundle\DelayedEventBundle\Tests\Functional;
6+
7+
use stdClass;
8+
use Doctrine\ORM\EntityManagerInterface;
9+
use Symfony\Component\HttpKernel\Kernel;
10+
use PHPUnit\Framework\MockObject\MockObject;
11+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
12+
use Tjovaisas\Bundle\DelayedEventBundle\Tests\Functional\Fixtures\Events;
13+
use Tjovaisas\Bundle\DelayedEventBundle\Tests\Functional\Fixtures\Entity\Entity;
14+
use Tjovaisas\Bundle\DelayedEventBundle\Tests\Functional\Fixtures\Event\SomeEvent;
15+
use Tjovaisas\Bundle\DelayedEventBundle\Tests\Functional\Fixtures\Listener\AttributeEventListener;
16+
17+
class AttributeEventsTest extends FunctionalTestCase
18+
{
19+
private MockObject|AttributeEventListener $attributeEventListener;
20+
private EventDispatcherInterface $eventDispatcher;
21+
private EntityManagerInterface $entityManager;
22+
23+
protected function setUp(): void
24+
{
25+
$container = $this->setUpContainer();
26+
27+
$this->setUpDatabase();
28+
29+
$this->eventDispatcher = $container->get('event_dispatcher');
30+
$this->entityManager = $container->get('doctrine.orm.entity_manager');
31+
$this->attributeEventListener = $this->createMock(AttributeEventListener::class);
32+
33+
$container->set('attribute_event_listener', $this->attributeEventListener);
34+
}
35+
36+
public function testAttributesMethodIsNotDefined(): void
37+
{
38+
$this->attributeEventListener
39+
->expects(static::once())
40+
->method('onFoo')
41+
;
42+
43+
$this->eventDispatcher->dispatch(new stdClass(), 'foo');
44+
45+
$this->entityManager->flush();
46+
}
47+
48+
/**
49+
* @dataProvider dataProviderDefinedEvents
50+
*/
51+
public function testListenerIsTriggeredOnMultipleOccasions(?string $event, string $method): void
52+
{
53+
$entity = new Entity();
54+
55+
$this->entityManager->persist($entity);
56+
57+
$this->eventDispatcher->dispatch(new SomeEvent($entity), $event);
58+
59+
$this->attributeEventListener
60+
->expects(static::once())
61+
->method($method)
62+
->willReturnCallback(function (SomeEvent $someEvent): void {
63+
$this->assertSame(1, $someEvent->getEntity()->getId());
64+
})
65+
;
66+
67+
$this->entityManager->flush();
68+
}
69+
70+
public function dataProviderDefinedEvents(): array
71+
{
72+
$providers = [
73+
'listener is triggered on class defined event' => [
74+
null,
75+
'onClassDefinedEvent',
76+
],
77+
];
78+
79+
if (Kernel::VERSION_ID >= 60000) {
80+
$providers = array_merge(
81+
$providers,
82+
[
83+
'listener is triggered on method defined event' => [
84+
Events::ON_EVENT,
85+
'onMethodDefinedEvent',
86+
],
87+
],
88+
);
89+
}
90+
91+
return $providers;
92+
}
93+
}

0 commit comments

Comments
 (0)