Skip to content

Commit a1ccee4

Browse files
Remove ListenerNoBroadcastInHandleRule and refine NoServiceLocatorRule
Listeners can legitimately broadcast different events from handle() — the blanket ban was too broad. Also exempt Facade classes from the service locator rule and add test coverage for NoServiceLocatorRule. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 76a5ecd commit a1ccee4

14 files changed

Lines changed: 143 additions & 179 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ includes:
4343
| `phpnomad.event.notFinal` | Event classes should be declared `final`. |
4444
| `phpnomad.event.notReadonly` | Event properties and promoted constructor parameters should be `readonly`. |
4545
| `phpnomad.listener.notImplementing` | Classes in `Listeners` namespaces or named `*Listener` must implement `CanHandle`. |
46-
| `phpnomad.listener.broadcastInHandle` | Listeners must not call `EventStrategy::broadcast()` from within their `handle()` method. Prevents infinite loops. |
4746

4847
### DI / Container
4948

extension.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ rules:
2323
- PHPNomad\PhpstanRules\Rules\Adapters\AdaptersMustImplementRule
2424
- PHPNomad\PhpstanRules\Rules\Di\CanSetContainerMustUseTraitRule
2525
- PHPNomad\PhpstanRules\Rules\Initializers\InitializerUseHasInterfacesRule
26-
- PHPNomad\PhpstanRules\Rules\Events\ListenerNoBroadcastInHandleRule
2726
- PHPNomad\PhpstanRules\Rules\Database\DatastoreTypeHintInterfaceRule
2827
- PHPNomad\PhpstanRules\Rules\Database\DatabaseScopeRule
2928
- PHPNomad\PhpstanRules\Rules\Controllers\ControllerMustSetStatusRule

lib/Rules/Di/NoServiceLocatorRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPNomad\PhpstanRules\Enums\ClassType;
1213
use PHPStan\Type\ObjectType;
1314

1415
/**
@@ -56,6 +57,10 @@ public function processNode(Node $node, Scope $scope): array
5657
return [];
5758
}
5859

60+
if ($this->resolver->resolve($classReflection) === ClassType::Facade) {
61+
return [];
62+
}
63+
5964
foreach ($classReflection->getTraits(true) as $trait) {
6065
if ((string) $trait->getName() === 'PHPNomad\Loader\Traits\CanLoadInitializers') {
6166
return [];

lib/Rules/Events/ListenerNoBroadcastInHandleRule.php

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di;
4+
5+
use PHPNomad\PhpstanRules\Rules\Di\NoServiceLocatorRule;
6+
use PHPNomad\PhpstanRules\Services\ClassTypeResolver;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<NoServiceLocatorRule>
12+
*/
13+
class NoServiceLocatorRuleTest extends RuleTestCase
14+
{
15+
protected function getRule(): Rule
16+
{
17+
return new NoServiceLocatorRule(new ClassTypeResolver());
18+
}
19+
20+
public static function getAdditionalConfigFiles(): array
21+
{
22+
return [__DIR__ . '/../../phpstan.neon'];
23+
}
24+
25+
public function testBusinessClassWithInstanceProviderViolation(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/BusinessClassWithInstanceProvider.php'], [
28+
['Avoid using Container::get() as a service locator in business classes. Use constructor injection instead.', 15],
29+
]);
30+
}
31+
32+
public function testBusinessClassWithCanSetContainerViolation(): void
33+
{
34+
$this->analyse([__DIR__ . '/data/BusinessClassWithCanSetContainer.php'], [
35+
['Avoid using Container::get() as a service locator in business classes. Use constructor injection instead.', 14],
36+
]);
37+
}
38+
39+
public function testInitializerExempt(): void
40+
{
41+
$this->analyse([__DIR__ . '/data/InitializerWithInstanceProvider.php'], []);
42+
}
43+
44+
public function testFacadeWithContainerGet(): void
45+
{
46+
$this->analyse([__DIR__ . '/data/FacadeWithContainerGet.php'], []);
47+
}
48+
49+
public function testRegularClassNoContainerPasses(): void
50+
{
51+
$this->analyse([__DIR__ . '/data/RegularClassNoContainer.php'], []);
52+
}
53+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di\data;
4+
5+
use PHPNomad\Di\Interfaces\CanSetContainer;
6+
use PHPNomad\Di\Traits\HasSettableContainer;
7+
8+
class BusinessClassWithCanSetContainer implements CanSetContainer
9+
{
10+
use HasSettableContainer;
11+
12+
public function doSomething(): void
13+
{
14+
$service = $this->container->get(SomeService::class);
15+
}
16+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di\data;
4+
5+
use PHPNomad\Di\Interfaces\InstanceProvider;
6+
7+
class BusinessClassWithInstanceProvider
8+
{
9+
public function __construct(private InstanceProvider $container)
10+
{
11+
}
12+
13+
public function doSomething(): void
14+
{
15+
$service = $this->container->get(SomeService::class);
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di\data;
4+
5+
use PHPNomad\Facade\Abstracts\Facade;
6+
7+
class FacadeWithContainerGet extends Facade
8+
{
9+
protected function abstractInstance(): string
10+
{
11+
return SomeService::class;
12+
}
13+
14+
public function doSomething(): object
15+
{
16+
return $this->container->get(SomeService::class);
17+
}
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di\data;
4+
5+
use PHPNomad\Di\Interfaces\InstanceProvider;
6+
use PHPNomad\Events\Interfaces\HasListeners;
7+
8+
class InitializerWithInstanceProvider implements HasListeners
9+
{
10+
public function __construct(private InstanceProvider $container)
11+
{
12+
}
13+
14+
public function getListeners(): array
15+
{
16+
return [];
17+
}
18+
19+
public function doSomething(): void
20+
{
21+
$service = $this->container->get(SomeService::class);
22+
}
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace PHPNomad\PhpstanRules\Tests\Rules\Di\data;
4+
5+
class RegularClassNoContainer
6+
{
7+
public function doSomething(): void
8+
{
9+
// No container usage at all
10+
}
11+
}

0 commit comments

Comments
 (0)