Skip to content

Commit

Permalink
Rule for magic repository method calls - findBy*, fineOneBy*, countBy*
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 13, 2019
1 parent 1b35d8a commit 5bf58a3
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 0 deletions.
1 change: 1 addition & 0 deletions rules.neon
@@ -1,2 +1,3 @@
rules:
- PHPStan\Rules\Doctrine\ORM\DqlRule
- PHPStan\Rules\Doctrine\ORM\MagicRepositoryMethodCallRule
95 changes: 95 additions & 0 deletions src/Rules/Doctrine/ORM/MagicRepositoryMethodCallRule.php
@@ -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.

Copy link
@mcfedr

mcfedr Feb 13, 2019

Contributor

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 in extension.neon, so its on by default, less options to configure.

This comment has been minimized.

Copy link
@ondrejmirtes

ondrejmirtes Feb 13, 2019

Author Member
  1. I can release it now with rules.neon because it's not a BC break.
  2. Other extensions follow the same convention - extension.neon + rules.neon
'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 tests/Rules/Doctrine/ORM/MagicRepositoryMethodCallRuleTest.php
@@ -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)),
];
}

}
48 changes: 48 additions & 0 deletions tests/Rules/Doctrine/ORM/data/magic-repository.php
@@ -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');
}

}

0 comments on commit 5bf58a3

Please sign in to comment.