Skip to content

Commit fbbe606

Browse files
committed
Check non-magic Repository calls like findBy/findOneBy/count
1 parent fdff97e commit fbbe606

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
rules:
22
- PHPStan\Rules\Doctrine\ORM\DqlRule
33
- PHPStan\Rules\Doctrine\ORM\MagicRepositoryMethodCallRule
4+
- PHPStan\Rules\Doctrine\ORM\RepositoryMethodCallRule
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Type\Constant\ConstantArrayType;
9+
use PHPStan\Type\Constant\ConstantStringType;
10+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
11+
use PHPStan\Type\Doctrine\ObjectRepositoryType;
12+
use PHPStan\Type\VerbosityLevel;
13+
14+
class RepositoryMethodCallRule implements Rule
15+
{
16+
17+
/** @var ObjectMetadataResolver */
18+
private $objectMetadataResolver;
19+
20+
public function __construct(ObjectMetadataResolver $objectMetadataResolver)
21+
{
22+
$this->objectMetadataResolver = $objectMetadataResolver;
23+
}
24+
25+
public function getNodeType(): string
26+
{
27+
return Node\Expr\MethodCall::class;
28+
}
29+
30+
/**
31+
* @param \PhpParser\Node\Expr\MethodCall $node
32+
* @param Scope $scope
33+
* @return string[]
34+
*/
35+
public function processNode(Node $node, Scope $scope): array
36+
{
37+
if (!isset($node->args[0])) {
38+
return [];
39+
}
40+
$argType = $scope->getType($node->args[0]->value);
41+
if (!$argType instanceof ConstantArrayType) {
42+
return [];
43+
}
44+
if (count($argType->getKeyTypes()) === 0) {
45+
return [];
46+
}
47+
$calledOnType = $scope->getType($node->var);
48+
if (!$calledOnType instanceof ObjectRepositoryType) {
49+
return [];
50+
}
51+
52+
$methodNameIdentifier = $node->name;
53+
if (!$methodNameIdentifier instanceof Node\Identifier) {
54+
return [];
55+
}
56+
57+
$methodName = $methodNameIdentifier->toString();
58+
if (!in_array($methodName, [
59+
'findBy',
60+
'findOneBy',
61+
'count',
62+
], true)) {
63+
return [];
64+
}
65+
66+
$objectManager = $this->objectMetadataResolver->getObjectManager();
67+
if ($objectManager === null) {
68+
throw new \PHPStan\ShouldNotHappenException(sprintf(
69+
'Please provide the "objectManagerLoader" setting for magic repository %s::%s() method validation.',
70+
$calledOnType->getClassName(),
71+
$methodName
72+
));
73+
}
74+
75+
$entityClass = $calledOnType->getEntityClass();
76+
$classMetadata = $objectManager->getClassMetadata($entityClass);
77+
78+
$messages = [];
79+
foreach ($argType->getKeyTypes() as $keyType) {
80+
if (!$keyType instanceof ConstantStringType) {
81+
continue;
82+
}
83+
84+
$fieldName = $keyType->getValue();
85+
if (
86+
$classMetadata->hasField($fieldName)
87+
|| $classMetadata->hasAssociation($fieldName)
88+
) {
89+
continue;
90+
}
91+
92+
$messages[] = sprintf(
93+
'Call to method %s::%s() - entity %s does not have a field named $%s.',
94+
$calledOnType->describe(VerbosityLevel::typeOnly()),
95+
$methodName,
96+
$entityClass,
97+
$fieldName
98+
);
99+
}
100+
101+
return $messages;
102+
}
103+
104+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\Doctrine\GetRepositoryDynamicReturnTypeExtension;
8+
use PHPStan\Type\Doctrine\ObjectMetadataResolver;
9+
10+
class RepositoryMethodCallRuleTest extends RuleTestCase
11+
{
12+
13+
protected function getRule(): Rule
14+
{
15+
return new RepositoryMethodCallRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null));
16+
}
17+
18+
public function testRule(): void
19+
{
20+
$this->analyse([__DIR__ . '/data/repository-findBy-etc.php'], [
21+
[
22+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
23+
23,
24+
],
25+
[
26+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
27+
24,
28+
],
29+
[
30+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
31+
25,
32+
],
33+
[
34+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
35+
25,
36+
],
37+
[
38+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
39+
33,
40+
],
41+
[
42+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
43+
34,
44+
],
45+
[
46+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
47+
35,
48+
],
49+
[
50+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneBy() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
51+
35,
52+
],
53+
[
54+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::count() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
55+
43,
56+
],
57+
[
58+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::count() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
59+
44,
60+
],
61+
[
62+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::count() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.',
63+
45,
64+
],
65+
[
66+
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::count() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.',
67+
45,
68+
],
69+
]);
70+
}
71+
72+
/**
73+
* @return \PHPStan\Type\DynamicMethodReturnTypeExtension[]
74+
*/
75+
public function getDynamicMethodReturnTypeExtensions(): array
76+
{
77+
return [
78+
new GetRepositoryDynamicReturnTypeExtension(\Doctrine\ORM\EntityManager::class, new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)),
79+
];
80+
}
81+
82+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Doctrine\ORM;
4+
5+
use Doctrine\ORM\EntityManager;
6+
7+
class MagicRepositoryCalls
8+
{
9+
10+
/** @var EntityManager */
11+
private $entityManager;
12+
13+
public function __construct(EntityManager $entityManager)
14+
{
15+
$this->entityManager = $entityManager;
16+
}
17+
18+
public function doFindBy(): void
19+
{
20+
$entityRepository = $this->entityManager->getRepository(MyEntity::class);
21+
$entityRepository->findBy(['id' => 1]);
22+
$entityRepository->findBy(['title' => 'test']);
23+
$entityRepository->findBy(['transient' => 'test']);
24+
$entityRepository->findBy(['nonexistent' => 'test']);
25+
$entityRepository->findBy(['nonexistent' => 'test', 'transient' => 'test']);
26+
}
27+
28+
public function doFindOneBy(): void
29+
{
30+
$entityRepository = $this->entityManager->getRepository(MyEntity::class);
31+
$entityRepository->findOneBy(['id' => 1]);
32+
$entityRepository->findOneBy(['title' => 'test']);
33+
$entityRepository->findOneBy(['transient' => 'test']);
34+
$entityRepository->findOneBy(['nonexistent' => 'test']);
35+
$entityRepository->findOneBy(['nonexistent' => 'test', 'transient' => 'test']);
36+
}
37+
38+
public function doCountBy(): void
39+
{
40+
$entityRepository = $this->entityManager->getRepository(MyEntity::class);
41+
$entityRepository->count(['id' => 1]);
42+
$entityRepository->count(['title' => 'test']);
43+
$entityRepository->count(['transient' => 'test']);
44+
$entityRepository->count(['nonexistent' => 'test']);
45+
$entityRepository->count(['nonexistent' => 'test', 'transient' => 'test']);
46+
}
47+
48+
}

0 commit comments

Comments
 (0)