Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rule for magic repository method calls - findBy*, fineOneBy*, countBy*
- Loading branch information
1 parent
1b35d8a
commit 5bf58a3
Showing
4 changed files
with
202 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
rules: | ||
- PHPStan\Rules\Doctrine\ORM\DqlRule | ||
- PHPStan\Rules\Doctrine\ORM\MagicRepositoryMethodCallRule |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Doctrine\ORM; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Type\Doctrine\ObjectMetadataResolver; | ||
use PHPStan\Type\Doctrine\ObjectRepositoryType; | ||
use PHPStan\Type\VerbosityLevel; | ||
|
||
class MagicRepositoryMethodCallRule implements Rule | ||
{ | ||
|
||
/** @var ObjectMetadataResolver */ | ||
private $objectMetadataResolver; | ||
|
||
public function __construct(ObjectMetadataResolver $objectMetadataResolver) | ||
{ | ||
$this->objectMetadataResolver = $objectMetadataResolver; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node\Expr\MethodCall::class; | ||
} | ||
|
||
/** | ||
* @param \PhpParser\Node\Expr\MethodCall $node | ||
* @param Scope $scope | ||
* @return string[] | ||
*/ | ||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
$calledOnType = $scope->getType($node->var); | ||
if (!$calledOnType instanceof ObjectRepositoryType) { | ||
return []; | ||
} | ||
|
||
$methodNameIdentifier = $node->name; | ||
if (!$methodNameIdentifier instanceof Node\Identifier) { | ||
return []; | ||
} | ||
|
||
$methodName = $methodNameIdentifier->toString(); | ||
if ( | ||
strpos($methodName, 'findBy') === 0 | ||
&& strlen($methodName) > strlen('findBy') | ||
) { | ||
$methodFieldName = substr($methodName, strlen('findBy')); | ||
} elseif ( | ||
strpos($methodName, 'findOneBy') === 0 | ||
&& strlen($methodName) > strlen('findOneBy') | ||
) { | ||
$methodFieldName = substr($methodName, strlen('findOneBy')); | ||
} elseif ( | ||
strpos($methodName, 'countBy') === 0 | ||
&& strlen($methodName) > strlen('countBy') | ||
) { | ||
$methodFieldName = substr($methodName, strlen('countBy')); | ||
} else { | ||
return []; | ||
} | ||
|
||
$objectManager = $this->objectMetadataResolver->getObjectManager(); | ||
if ($objectManager === null) { | ||
throw new \PHPStan\ShouldNotHappenException(sprintf( | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
ondrejmirtes
Author
Member
|
||
'Please provide the "objectManagerLoader" setting for magic repository %s::%s() method validation.', | ||
$calledOnType->getClassName(), | ||
$methodName | ||
)); | ||
} | ||
|
||
$fieldName = $this->classify($methodFieldName); | ||
$entityClass = $calledOnType->getEntityClass(); | ||
$classMetadata = $objectManager->getClassMetadata($entityClass); | ||
if ($classMetadata->hasField($fieldName) || $classMetadata->hasAssociation($fieldName)) { | ||
return []; | ||
} | ||
|
||
return [sprintf( | ||
'Call to method %s::%s() - entity %s does not have a field named $%s.', | ||
$calledOnType->describe(VerbosityLevel::typeOnly()), | ||
$methodName, | ||
$entityClass, | ||
$fieldName | ||
)]; | ||
} | ||
|
||
private function classify(string $word): string | ||
{ | ||
return lcfirst(str_replace([' ', '_', '-'], '', ucwords($word, ' _-'))); | ||
} | ||
|
||
} |
58 changes: 58 additions & 0 deletions
58
tests/Rules/Doctrine/ORM/MagicRepositoryMethodCallRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Doctrine\ORM; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
use PHPStan\Type\Doctrine\GetRepositoryDynamicReturnTypeExtension; | ||
use PHPStan\Type\Doctrine\ObjectMetadataResolver; | ||
|
||
class MagicRepositoryMethodCallRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new MagicRepositoryMethodCallRule(new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/magic-repository.php'], [ | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findByTransient() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.', | ||
24, | ||
], | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findByNonexistent() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.', | ||
25, | ||
], | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneByTransient() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.', | ||
34, | ||
], | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::findOneByNonexistent() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.', | ||
35, | ||
], | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::countByTransient() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $transient.', | ||
44, | ||
], | ||
[ | ||
'Call to method Doctrine\ORM\EntityRepository<PHPStan\Rules\Doctrine\ORM\MyEntity>::countByNonexistent() - entity PHPStan\Rules\Doctrine\ORM\MyEntity does not have a field named $nonexistent.', | ||
45, | ||
], | ||
]); | ||
} | ||
|
||
/** | ||
* @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] | ||
*/ | ||
public function getDynamicMethodReturnTypeExtensions(): array | ||
{ | ||
return [ | ||
new GetRepositoryDynamicReturnTypeExtension(\Doctrine\ORM\EntityManager::class, new ObjectMetadataResolver(__DIR__ . '/entity-manager.php', null)), | ||
]; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Doctrine\ORM; | ||
|
||
use Doctrine\ORM\EntityManager; | ||
|
||
class MagicRepositoryCalls | ||
{ | ||
|
||
/** @var EntityManager */ | ||
private $entityManager; | ||
|
||
public function __construct(EntityManager $entityManager) | ||
{ | ||
$this->entityManager = $entityManager; | ||
} | ||
|
||
public function doFindBy(): void | ||
{ | ||
$entityRepository = $this->entityManager->getRepository(MyEntity::class); | ||
$entityRepository->findBy(['id' => 1]); | ||
$entityRepository->findById(1); | ||
$entityRepository->findByTitle('test'); | ||
$entityRepository->findByTransient('test'); | ||
$entityRepository->findByNonexistent('test'); | ||
} | ||
|
||
public function doFindOneBy(): void | ||
{ | ||
$entityRepository = $this->entityManager->getRepository(MyEntity::class); | ||
$entityRepository->findOneBy(['id' => 1]); | ||
$entityRepository->findOneById(1); | ||
$entityRepository->findOneByTitle('test'); | ||
$entityRepository->findOneByTransient('test'); | ||
$entityRepository->findOneByNonexistent('test'); | ||
} | ||
|
||
public function doCountBy(): void | ||
{ | ||
$entityRepository = $this->entityManager->getRepository(MyEntity::class); | ||
$entityRepository->countBy(['id' => 1]); | ||
$entityRepository->countById(1); | ||
$entityRepository->countByTitle('test'); | ||
$entityRepository->countByTransient('test'); | ||
$entityRepository->countByNonexistent('test'); | ||
} | ||
|
||
} |
What do you think about making it just return
[]
here, so its not a hard dependency on object manager, then this can just be included inextension.neon
, so its on by default, less options to configure.