Skip to content

Commit a12cdd0

Browse files
authored
Implement AsDoctrineListener and AutoconfigureTag doctrine.event_listener (#258)
1 parent 5acf474 commit a12cdd0

File tree

3 files changed

+126
-1
lines changed

3 files changed

+126
-1
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ $ vendor/bin/phpstan
5252
- `onKernelResponse`, `onKernelRequest`, etc
5353
- `!php const` references in `config` yamls
5454
- `defaultIndexMethod` in `#[AutowireLocator]` and `#[AutowireIterator]`
55-
- Workflow event listener attributes: `#[AsAnnounceListener]`, `#[AsCompletedListener]`, `#[AsEnterListener]`, `#[AsEnteredListener]`, `#[AsGuardListener]`, `#[AsLeaveListener]`, `#[AsTransitionListener]`
55+
- Workflow event listener attributes: `#[AsAnnounceListener]`, ...
56+
- `#[AutoconfigureTag('doctrine.event_listener')]` attribute
57+
5658

5759
#### Doctrine:
5860
- `#[AsEntityListener]` attribute
61+
- `#[AsDoctrineListener]` attribute
5962
- `Doctrine\ORM\Events::*` events
6063
- `Doctrine\Common\EventSubscriber` methods
6164
- `repositoryMethod` in `#[UniqueEntity]` attribute

src/Provider/DoctrineUsageProvider.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ protected function shouldMarkMethodAsUsed(ReflectionMethod $method): ?string
164164
return 'Is part of AsEntityListener methods';
165165
}
166166

167+
if ($this->isPartOfAsDoctrineListener($class, $methodName)) {
168+
return 'Is part of AsDoctrineListener methods';
169+
}
170+
171+
if ($this->isPartOfAutoconfigureTagDoctrineListener($class, $methodName)) {
172+
return 'Is part of AutoconfigureTag doctrine.event_listener methods';
173+
}
174+
167175
if ($this->isProbablyDoctrineListener($methodName)) {
168176
return 'Is probable listener method';
169177
}
@@ -227,6 +235,55 @@ protected function isPartOfAsEntityListener(
227235
return false;
228236
}
229237

238+
protected function isPartOfAsDoctrineListener(
239+
ReflectionClass $class,
240+
string $methodName
241+
): bool
242+
{
243+
foreach ($class->getAttributes('Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener') as $attribute) {
244+
$eventName = $attribute->getArguments()['event'] ?? $attribute->getArguments()[0] ?? null;
245+
246+
// AsDoctrineListener doesn't have a 'method' parameter
247+
// Symfony looks for a method named after the event, or falls back to __invoke
248+
if ($eventName === $methodName || $methodName === '__invoke') {
249+
return true;
250+
}
251+
}
252+
253+
return false;
254+
}
255+
256+
protected function isPartOfAutoconfigureTagDoctrineListener(
257+
ReflectionClass $class,
258+
string $methodName
259+
): bool
260+
{
261+
foreach ($class->getAttributes('Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag') as $attribute) {
262+
$arguments = $attribute->getArguments();
263+
$tagName = $arguments[0] ?? $arguments['name'] ?? null;
264+
265+
// Only handle doctrine.event_listener tags
266+
if ($tagName !== 'doctrine.event_listener') {
267+
continue;
268+
}
269+
270+
$listenerMethodName = $arguments['method'] ?? null;
271+
272+
// If no method is specified, the listener method name is inferred from the event name
273+
if ($listenerMethodName === null) {
274+
$eventName = $arguments['event'] ?? null;
275+
276+
if ($eventName === $methodName) {
277+
return true;
278+
}
279+
} elseif ($listenerMethodName === $methodName) {
280+
return true;
281+
}
282+
}
283+
284+
return false;
285+
}
286+
230287
protected function isEntityRepositoryConstructor(
231288
ReflectionClass $class,
232289
ReflectionMethod $method

tests/Rule/data/providers/doctrine.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,68 @@ public function someMethod(): void {}
7070
public function someMethod2(): void {} // error: Unused Doctrine\MySubscriber::someMethod2
7171

7272
}
73+
74+
#[\Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener(event: 'postGenerateSchema')]
75+
class FixDoctrineMigrationTableSchema {
76+
77+
public function postGenerateSchema(): void {}
78+
79+
public function unusedMethod(): void {} // error: Unused Doctrine\FixDoctrineMigrationTableSchema::unusedMethod
80+
81+
}
82+
83+
#[\Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener(event: 'postLoad')]
84+
class AsDoctrineListenerWithInvoke {
85+
86+
public function __invoke(): void {}
87+
88+
public function unusedMethod(): void {} // error: Unused Doctrine\AsDoctrineListenerWithInvoke::unusedMethod
89+
90+
}
91+
92+
#[\Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag('doctrine.event_listener', event: 'postGenerateSchema')]
93+
class FixDoctrineMigrationTableSchemaWithAutoconfigureTag {
94+
95+
public function postGenerateSchema(): void {}
96+
97+
public function unusedMethod(): void {} // error: Unused Doctrine\FixDoctrineMigrationTableSchemaWithAutoconfigureTag::unusedMethod
98+
99+
}
100+
101+
#[\Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag('doctrine.event_listener', event: 'postGenerateSchema', method: 'onPostGenerateSchema')]
102+
class FixDoctrineMigrationTableSchemaWithAutoconfigureTagAndMethod {
103+
104+
public function onPostGenerateSchema(): void {}
105+
106+
public function unusedMethod(): void {} // error: Unused Doctrine\FixDoctrineMigrationTableSchemaWithAutoconfigureTagAndMethod::unusedMethod
107+
108+
}
109+
110+
// Test multiple AsDoctrineListener attributes on same class (IS_REPEATABLE)
111+
#[\Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener(event: 'postPersist')]
112+
#[\Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener(event: 'postUpdate')]
113+
#[\Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener(event: 'preRemove')]
114+
class MultipleAsDoctrineListeners {
115+
116+
public function postPersist(): void {}
117+
118+
public function postUpdate(): void {}
119+
120+
public function preRemove(): void {}
121+
122+
public function unusedMethod(): void {} // error: Unused Doctrine\MultipleAsDoctrineListeners::unusedMethod
123+
124+
}
125+
126+
// Test multiple AutoconfigureTag attributes
127+
#[\Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag('doctrine.event_listener', event: 'postPersist')]
128+
#[\Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag('doctrine.event_listener', event: 'postUpdate', method: 'afterUpdate')]
129+
class MultipleAutoconfigureTags {
130+
131+
public function postPersist(): void {}
132+
133+
public function afterUpdate(): void {}
134+
135+
public function unusedMethod(): void {} // error: Unused Doctrine\MultipleAutoconfigureTags::unusedMethod
136+
137+
}

0 commit comments

Comments
 (0)