Skip to content

Commit dd825e8

Browse files
authored
[doctrine] Add NoGetRepositoryOnServiceRepositoryEntityRule (#182)
1 parent abab32b commit dd825e8

25 files changed

+507
-31
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,71 @@ final class SomeRepository
12211221

12221222
<br>
12231223

1224+
### NoGetRepositoryOnServiceRepositoryEntityRule
1225+
1226+
Instead of calling "->getRepository(...::class)" service locator, inject service repository via constructor and use it directly
1227+
1228+
```yaml
1229+
rules:
1230+
- Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOnServiceRepositoryEntityRule
1231+
```
1232+
1233+
<br>
1234+
1235+
```php
1236+
use Doctrine\ORM\Mapping as ORM;
1237+
1238+
/**
1239+
* @ORM\Entity(repositoryClass=SomeRepository::class)
1240+
*/
1241+
class SomeEntity
1242+
{
1243+
}
1244+
```
1245+
1246+
<br>
1247+
1248+
```php
1249+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
1250+
1251+
final class SomeEntityRepository extends ServiceEntityRepository
1252+
{
1253+
}
1254+
```
1255+
1256+
<br>
1257+
1258+
```php
1259+
use Doctrine\ORM\EntityManagerInterface;
1260+
1261+
final class SomeService
1262+
{
1263+
public function run(EntityManagerInterface $entityManager)
1264+
{
1265+
return $this->entityManager->getRepository(SomeEntity::class);
1266+
}
1267+
}
1268+
```
1269+
1270+
:x:
1271+
1272+
<br>
1273+
1274+
```php
1275+
use Doctrine\ORM\EntityManagerInterface;
1276+
1277+
final class SomeService
1278+
{
1279+
public function __construct(private SomeEntityRepository $someEntityRepository)
1280+
{
1281+
}
1282+
}
1283+
```
1284+
1285+
:+1:
1286+
1287+
<br>
1288+
12241289
### NoRepositoryCallInDataFixtureRule
12251290

12261291
Repository should not be called in data fixtures, as it can lead to tight coupling

config/doctrine-rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ rules:
22
- Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOutsideServiceRule
33
- Symplify\PHPStanRules\Rules\Doctrine\NoParentRepositoryRule
44
- Symplify\PHPStanRules\Rules\Doctrine\NoRepositoryCallInDataFixtureRule
5+
- Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOnServiceRepositoryEntityRule
56

67
# test fixtures
78
- Symplify\PHPStanRules\Rules\Doctrine\RequireQueryBuilderOnRepositoryRule
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Doctrine;
6+
7+
use Nette\Utils\FileSystem;
8+
use Nette\Utils\Strings;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use Rector\Exception\ShouldNotHappenException;
11+
12+
final readonly class RepositoryClassResolver
13+
{
14+
/**
15+
* @var string
16+
*/
17+
private const QUOTED_REPOSITORY_CLASS_REGEX = '#repositoryClass=\"(?<repositoryClass>.*?)\"#';
18+
19+
/**
20+
* @var string
21+
*/
22+
private const USE_REPOSITORY_REGEX = '#use (?<repositoryClass>.*?Repository);#';
23+
24+
public function __construct(
25+
private ReflectionProvider $reflectionProvider
26+
) {
27+
}
28+
29+
public function resolveFromEntityClass(string $entityClassName): ?string
30+
{
31+
if (! $this->reflectionProvider->hasClass($entityClassName)) {
32+
throw new ShouldNotHappenException();
33+
}
34+
35+
$classReflection = $this->reflectionProvider->getClass($entityClassName);
36+
37+
$entityClassFileName = $classReflection->getFileName();
38+
if ($entityClassFileName === null) {
39+
return null;
40+
}
41+
42+
$entityFileContents = FileSystem::read($entityClassFileName);
43+
44+
// match repositoryClass="..." in entity
45+
$match = Strings::match($entityFileContents, self::QUOTED_REPOSITORY_CLASS_REGEX);
46+
47+
if (! isset($match['repositoryClass'])) {
48+
// try fallback to repository ::class + use import
49+
50+
$repositoryUseMatch = Strings::match($entityFileContents, self::USE_REPOSITORY_REGEX);
51+
if (isset($repositoryUseMatch['repositoryClass'])) {
52+
$repositoryClass = $repositoryUseMatch['repositoryClass'];
53+
} else {
54+
// unable to resolve
55+
return null;
56+
}
57+
} else {
58+
$repositoryClass = $match['repositoryClass'];
59+
}
60+
61+
if (! $this->reflectionProvider->hasClass($repositoryClass)) {
62+
throw new ShouldNotHappenException(
63+
sprintf('Repository class "%s" for entity "%s" does not exist', $repositoryClass, $entityClassName)
64+
);
65+
}
66+
67+
return $repositoryClass;
68+
}
69+
}

src/Enum/ClassName.php

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66

77
final class ClassName
88
{
9-
/**
10-
* @var string
11-
*/
12-
public const PHPUNIT_TEST_CASE = 'PHPUnit\Framework\TestCase';
13-
149
/**
1510
* @var string
1611
*/
@@ -36,16 +31,6 @@ final class ClassName
3631
*/
3732
public const RECTOR_ATTRIBUTE_KEY = 'Rector\NodeTypeResolver\Node\AttributeKey';
3833

39-
/**
40-
* @var string
41-
*/
42-
public const DOCTRINE_FIXTURE_INTERFACE = 'Doctrine\Common\DataFixtures\FixtureInterface';
43-
44-
/**
45-
* @var string
46-
*/
47-
public const ENTITY_REPOSITORY_CLASS = 'Doctrine\ORM\EntityRepository';
48-
4934
/**
5035
* @var string
5136
*/

src/Enum/DoctrineClass.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Enum;
6+
7+
final class DoctrineClass
8+
{
9+
/**
10+
* @var string
11+
*/
12+
public const ODM_SERVICE_REPOSITORY = 'Doctrine\Bundle\MongoDBBundle\Repository\ServiceDocumentRepository';
13+
14+
/**
15+
* @var string
16+
*/
17+
public const ORM_SERVICE_REPOSITORY = 'Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository';
18+
19+
/**
20+
* @var string
21+
*/
22+
public const FIXTURE_INTERFACE = 'Doctrine\Common\DataFixtures\FixtureInterface';
23+
24+
/**
25+
* @var string
26+
*/
27+
public const ENTITY_REPOSITORY = 'Doctrine\ORM\EntityRepository';
28+
}

src/Enum/DoctrineRuleIdentifier.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
final class DoctrineRuleIdentifier
88
{
9-
public const DOCTRINE_NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';
9+
public const NO_GET_REPOSITORY_OUTSIDE_SERVICE = 'doctrine.noGetRepositoryOutsideService';
1010

11-
public const DOCTRINE_NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';
11+
public const NO_REPOSITORY_CALL_IN_DATA_FIXTURES = 'doctrine.noRepositoryCallInDataFixtures';
1212

13-
public const DOCTRINE_NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';
13+
public const NO_PARENT_REPOSITORY = 'doctrine.noParentRepository';
1414

1515
public const NO_ENTITY_MOCKING = 'doctrine.noEntityMocking';
1616

1717
public const REQUIRE_QUERY_BUILDER_ON_REPOSITORY = 'doctrine.requireQueryBuilderOnRepository';
18+
19+
public const INJECT_SERVICE_REPOSITORY = 'doctrine.injectServiceRepository';
1820
}

src/Enum/TestClassName.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Enum;
6+
7+
final class TestClassName
8+
{
9+
/**
10+
* @var string
11+
*/
12+
public const PHPUNIT_TEST_CASE = 'PHPUnit\Framework\TestCase';
13+
14+
/**
15+
* @var string
16+
*/
17+
public const BEHAT_CONTEXT = 'Behat\Behat\Context\Context';
18+
}

src/Rules/ClassNameRespectsParentSuffixRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symplify\PHPStanRules\Enum\ClassName;
1717
use Symplify\PHPStanRules\Enum\RuleIdentifier;
1818
use Symplify\PHPStanRules\Enum\SymfonyClass;
19+
use Symplify\PHPStanRules\Enum\TestClassName;
1920
use Symplify\PHPStanRules\Naming\ClassToSuffixResolver;
2021

2122
/**
@@ -37,7 +38,7 @@ final class ClassNameRespectsParentSuffixRule implements Rule
3738
SymfonyClass::EVENT_SUBSCRIBER_INTERFACE,
3839
SymfonyClass::SYMFONY_ABSTRACT_CONTROLLER,
3940
ClassName::SNIFF,
40-
ClassName::PHPUNIT_TEST_CASE,
41+
TestClassName::PHPUNIT_TEST_CASE,
4142
Exception::class,
4243
'PhpCsFixer\Fixer\FixerInterface',
4344
Rule::class,

0 commit comments

Comments
 (0)