Skip to content

Commit 6234e4c

Browse files
authored
[doctrine] Add NoListenerWithoutContractRule (#201)
1 parent ef4d30d commit 6234e4c

File tree

12 files changed

+262
-34
lines changed

12 files changed

+262
-34
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,50 @@ class SomeListener
17251725

17261726
<br>
17271727

1728+
### NoDoctrineListenerWithoutContractRule
1729+
1730+
There should be no Doctrine listeners modified in config. Implement "Document\Event\EventSubscriber" to provide events in the class itself
1731+
1732+
```yaml
1733+
rules:
1734+
- Symplify\PHPStanRules\Rules\Doctrine\NoDoctrineListenerWithoutContractRule
1735+
```
1736+
1737+
```php
1738+
class SomeListener
1739+
{
1740+
public function onFlush()
1741+
{
1742+
}
1743+
}
1744+
```
1745+
1746+
:x:
1747+
1748+
<br>
1749+
1750+
```php
1751+
use Doctrine\Common\EventSubscriber;
1752+
use Doctrine\ODM\MongoDB\Events;
1753+
1754+
class SomeListener implements EventSubscriber
1755+
{
1756+
public function onFlush()
1757+
{
1758+
}
1759+
1760+
public static function getSubscribedEvents(): array
1761+
{
1762+
return [
1763+
Events::onFlush
1764+
];
1765+
}
1766+
}
1767+
```
1768+
1769+
:+1:
1770+
1771+
17281772
### NoStringInGetSubscribedEventsRule
17291773

17301774
Symfony getSubscribedEvents() method must contain only event class references, no strings

config/doctrine-rules.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@ rules:
44
- Symplify\PHPStanRules\Rules\Doctrine\NoRepositoryCallInDataFixtureRule
55
- Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOnServiceRepositoryEntityRule
66

7+
- Symplify\PHPStanRules\Rules\Doctrine\NoDoctrineListenerWithoutContractRule
8+
79
# test fixtures
810
- Symplify\PHPStanRules\Rules\Doctrine\RequireQueryBuilderOnRepositoryRule
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Doctrine;
6+
7+
use PhpParser\Node\Stmt\Class_;
8+
use Symplify\PHPStanRules\Enum\DoctrineEvents;
9+
10+
final class DoctrineEventSubscriberAnalyzer
11+
{
12+
public static function detect(Class_ $class): bool
13+
{
14+
// skip doctrine, as this is handling symfony only
15+
foreach ($class->getMethods() as $classMethod) {
16+
if (in_array($classMethod->name->toString(), DoctrineEvents::ORM_LIST)) {
17+
return true;
18+
}
19+
20+
if (in_array($classMethod->name->toString(), DoctrineEvents::ODM_LIST)) {
21+
return true;
22+
}
23+
}
24+
25+
return false;
26+
}
27+
}

src/Enum/DoctrineEvents.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Enum;
6+
7+
final class DoctrineEvents
8+
{
9+
/**
10+
* @see https://www.doctrine-project.org/projects/doctrine-orm/en/3.3/reference/events.html
11+
*/
12+
public const ORM_LIST = [
13+
'preRemove',
14+
'postRemove',
15+
'prePersist',
16+
'postPersist',
17+
'preUpdate',
18+
'postUpdate',
19+
'postLoad',
20+
'loadClassMetadata',
21+
'onClassMetadataNotFound',
22+
'preFlush',
23+
'onFlush',
24+
'postFlush',
25+
'onClear',
26+
];
27+
28+
/**
29+
* @see https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/events.html#lifecycle-events
30+
*/
31+
public const ODM_LIST = [
32+
'documentNotFound',
33+
'onClear',
34+
'postCollectionLoad',
35+
];
36+
}

src/Enum/DoctrineRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ final class DoctrineRuleIdentifier
1717
public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
1818

1919
public const INJECT_SERVICE_REPOSITORY = 'doctrine.injectServiceRepository';
20+
21+
public const NO_LISTENER_WITHOUT_CONTRACT = 'doctrine.noListenerWithoutContract';
2022
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Doctrine;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\InClassNode;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symplify\PHPStanRules\Doctrine\DoctrineEventSubscriberAnalyzer;
14+
use Symplify\PHPStanRules\Enum\DoctrineRuleIdentifier;
15+
16+
/**
17+
* Based on https://tomasvotruba.com/blog/2019/07/22/how-to-convert-listeners-to-subscribers-and-reduce-your-configs
18+
* Subscribers have much better PHP support - IDE, PHPStan + Rector - than simple yaml files
19+
*
20+
* @implements Rule<InClassNode>
21+
*
22+
* @see \Symplify\PHPStanRules\Tests\Rules\Doctrine\NoDoctrineListenerWithoutContractRule\NoDoctrineListenerWithoutContractRuleTest
23+
*/
24+
final class NoDoctrineListenerWithoutContractRule implements Rule
25+
{
26+
/**
27+
* @var string
28+
*/
29+
public const ERROR_MESSAGE = 'There should be no Doctrine listeners modified in config. Implement "Document\Event\EventSubscriber" to provide events in the class itself';
30+
31+
public function getNodeType(): string
32+
{
33+
return InClassNode::class;
34+
}
35+
36+
/**
37+
* @param InClassNode $node
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
if (! $scope->isInClass()) {
42+
return [];
43+
}
44+
45+
$classReflection = $scope->getClassReflection();
46+
if (! str_ends_with($classReflection->getName(), 'Listener')) {
47+
return [];
48+
}
49+
50+
$classLike = $node->getOriginalNode();
51+
if (! $classLike instanceof Class_) {
52+
return [];
53+
}
54+
55+
if ($classLike->implements !== []) {
56+
return [];
57+
}
58+
59+
if (! DoctrineEventSubscriberAnalyzer::detect($classLike)) {
60+
return [];
61+
}
62+
63+
$identifierRuleError = RuleErrorBuilder::message(self::ERROR_MESSAGE)
64+
->identifier(DoctrineRuleIdentifier::NO_LISTENER_WITHOUT_CONTRACT)
65+
->build();
66+
67+
return [$identifierRuleError];
68+
}
69+
}

src/Rules/Symfony/NoListenerWithoutContractRule.php

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Node\InClassNode;
1313
use PHPStan\Rules\Rule;
1414
use PHPStan\Rules\RuleErrorBuilder;
15+
use Symplify\PHPStanRules\Doctrine\DoctrineEventSubscriberAnalyzer;
1516
use Symplify\PHPStanRules\Enum\SymfonyClass;
1617
use Symplify\PHPStanRules\Enum\SymfonyRuleIdentifier;
1718

@@ -30,27 +31,6 @@ final class NoListenerWithoutContractRule implements Rule
3031
*/
3132
public const ERROR_MESSAGE = 'There should be no listeners modified in config. Use EventSubscriberInterface contract or #[AsEventListener] attribute and native PHP instead';
3233

33-
/**
34-
* @see https://www.doctrine-project.org/projects/doctrine-orm/en/3.3/reference/events.html
35-
*/
36-
private const DOCTRINE_EVENT_NAMES = [
37-
'preRemove',
38-
'postRemove',
39-
'prePersist',
40-
'postPersist',
41-
'preUpdate',
42-
'postUpdate',
43-
'postLoad',
44-
'loadClassMetadata',
45-
'onClassMetadataNotFound',
46-
'preFlush',
47-
'onFlush',
48-
'postFlush',
49-
'onClear',
50-
// ODM
51-
'documentNotFound',
52-
];
53-
5434
public function getNodeType(): string
5535
{
5636
return InClassNode::class;
@@ -84,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array
8464
return [];
8565
}
8666

87-
if ($this->isDoctrineListener($classLike)) {
67+
if (DoctrineEventSubscriberAnalyzer::detect($classLike)) {
8868
return [];
8969
}
9070

@@ -107,18 +87,6 @@ public function processNode(Node $node, Scope $scope): array
10787
return [$identifierRuleError];
10888
}
10989

110-
private function isDoctrineListener(Class_ $class): bool
111-
{
112-
// skip doctrine, as this is handling symfony only
113-
foreach ($class->getMethods() as $classMethod) {
114-
if (in_array($classMethod->name->toString(), self::DOCTRINE_EVENT_NAMES)) {
115-
return true;
116-
}
117-
}
118-
119-
return false;
120-
}
121-
12290
private function hasAsListenerAttribute(Class_ $class): bool
12391
{
12492
foreach ($class->attrGroups as $attrGroup) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
<?php
3+
4+
namespace Doctrine\Common;
5+
6+
interface EventSubscriber
7+
{
8+
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Doctrine\NoDoctrineListenerWithoutContractRule\Fixture;
4+
5+
final class SimpleDoctrineListener
6+
{
7+
public function preFlush()
8+
{
9+
}
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Doctrine\NoDoctrineListenerWithoutContractRule\Fixture;
4+
5+
use Doctrine\Common\EventSubscriber;
6+
7+
final class SkipContractAwareListener implements EventSubscriber
8+
{
9+
public function preFlush()
10+
{
11+
}
12+
}

0 commit comments

Comments
 (0)