Skip to content

Commit 7938fac

Browse files
committed
feat: add NoEntityRepositoryInLoopRule to PHPStan rules
1 parent ba0d38b commit 7938fac

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed

rules.neon

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ rules:
1313
- Shopware\PhpStan\Rule\NoUserEntityGetStoreTokenRule
1414
- Shopware\PhpStan\Rule\ScheduledTaskTooLowIntervalRule
1515
- Shopware\PhpStan\Rule\SetForeignKeyRule
16+
- Shopware\PhpStan\Rule\NoEntityRepositoryInLoopRule
1617

1718
services:
1819
-
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopware\PhpStan\Rule;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Stmt\Foreach_;
10+
use PhpParser\Node\Stmt\For_;
11+
use PHPStan\Analyser\Scope;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
16+
17+
/**
18+
* @implements Rule<Node>
19+
*/
20+
class NoEntityRepositoryInLoopRule implements Rule
21+
{
22+
public function getNodeType(): string
23+
{
24+
return Node::class;
25+
}
26+
27+
/**
28+
* @return list<IdentifierRuleError>
29+
*/
30+
public function processNode(Node $node, Scope $scope): array
31+
{
32+
if (!$node instanceof For_ && !$node instanceof Foreach_) {
33+
return [];
34+
}
35+
36+
/** @var list<IdentifierRuleError> $errors */
37+
$errors = [];
38+
$this->checkNode($node, $scope, $errors);
39+
40+
return $errors;
41+
}
42+
43+
/**
44+
* @param list<IdentifierRuleError> $errors
45+
*/
46+
private function checkNode(Node $node, Scope $scope, array &$errors): void
47+
{
48+
if ($node instanceof MethodCall) {
49+
$callerType = $scope->getType($node->var);
50+
51+
if ($callerType->isObject()->yes() && in_array(EntityRepository::class, $callerType->getObjectClassNames(), true)) {
52+
$errors[] = RuleErrorBuilder::message('EntityRepository method calls are not allowed within loops. This can lead to unexpected N:1 queries.')
53+
->identifier('shopware.noEntityRepositoryInLoop')
54+
->build();
55+
}
56+
}
57+
58+
foreach ($node->getSubNodeNames() as $subNodeName) {
59+
$subNode = $node->$subNodeName;
60+
61+
if ($subNode instanceof Node) {
62+
$this->checkNode($subNode, $scope, $errors);
63+
}
64+
65+
if (is_array($subNode)) {
66+
foreach ($subNode as $subSubNode) {
67+
if ($subSubNode instanceof Node) {
68+
$this->checkNode($subSubNode, $scope, $errors);
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopware\PhpStan\Tests\Rule;
6+
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use Shopware\PhpStan\Rule\NoEntityRepositoryInLoopRule;
10+
11+
/**
12+
* @extends RuleTestCase<NoEntityRepositoryInLoopRule>
13+
*/
14+
class NoEntityRepositoryInLoopRuleTest extends RuleTestCase
15+
{
16+
protected function getRule(): Rule
17+
{
18+
return new NoEntityRepositoryInLoopRule();
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/Fixtures/NoEntityRepositoryInLoop/NoEntityRepositoryInLoop.php'], [
24+
[
25+
'EntityRepository method calls are not allowed within loops. This can lead to unexpected N:1 queries.',
26+
20,
27+
],
28+
[
29+
'EntityRepository method calls are not allowed within loops. This can lead to unexpected N:1 queries.',
30+
28,
31+
],
32+
]);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Shopware\PhpStan\Tests\Rule\Fixtures;
6+
7+
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
8+
9+
class NoEntityRepositoryInLoop
10+
{
11+
private EntityRepository $repository;
12+
13+
public function __construct(EntityRepository $repository)
14+
{
15+
$this->repository = $repository;
16+
}
17+
18+
public function badForLoop(): void
19+
{
20+
for ($i = 0; $i < 10; $i++) {
21+
$this->repository->search(/* some criteria */);
22+
}
23+
}
24+
25+
public function badForeachLoop(): void
26+
{
27+
$items = ['a', 'b', 'c'];
28+
foreach ($items as $item) {
29+
$this->repository->search(/* some criteria */);
30+
}
31+
}
32+
33+
public function goodUsage(): void
34+
{
35+
// This is fine, not in a loop
36+
$this->repository->search(/* some criteria */);
37+
}
38+
}

0 commit comments

Comments
 (0)