Skip to content

Commit fb801a9

Browse files
authored
[doctrine] improve repository handler (#353)
1 parent 353c9b0 commit fb801a9

File tree

6 files changed

+133
-48
lines changed

6 files changed

+133
-48
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ vendor/bin/psalm-plugin enable psalm/plugin-symfony
2323
### Features
2424

2525
- Detects the `ContainerInterface::get()` result type. Works better if you [configure](#configuration) a compiled container XML file.
26+
- Detects parameter return types from `ContainerInterface::getParameter()`.
2627
- Supports [Service Subscribers](https://github.com/psalm/psalm-plugin-symfony/issues/20). Works only if you [configure](#configuration) a compiled container XML file.
2728
- Detects return types from console arguments (`InputInterface::getArgument()`) and options (`InputInterface::getOption()`).
28-
Enforces to use "InputArgument" and "InputOption" constants as a best practise.
29+
Enforces to use "InputArgument" and "InputOption" constants as a best practice.
2930
- Detects Doctrine repository classes associated to entities when configured via annotations.
3031
- Fixes `PossiblyInvalidArgument` for `Symfony\Component\HttpFoundation\Request::getContent()`.
3132
The plugin determines the real return type by checking the given argument and marks it as either "string" or "resource".

src/Handler/DoctrineRepositoryHandler.php

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,69 +22,73 @@ class DoctrineRepositoryHandler implements AfterMethodCallAnalysisInterface, Aft
2222
{
2323
public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void
2424
{
25-
$expr = $event->getExpr();
2625
$declaring_method_id = $event->getDeclaringMethodId();
27-
$statements_source = $event->getStatementsSource();
26+
if (!in_array($declaring_method_id, ['Doctrine\ORM\EntityManagerInterface::getrepository', 'Doctrine\Persistence\ObjectManager::getrepository'])) {
27+
return;
28+
}
2829

29-
if (in_array($declaring_method_id, ['Doctrine\ORM\EntityManagerInterface::getrepository', 'Doctrine\Persistence\ObjectManager::getrepository'])) {
30-
if (!isset($expr->args[0]->value)) {
31-
return;
32-
}
30+
$expr = $event->getExpr();
31+
if (!isset($expr->args[0]->value)) {
32+
return;
33+
}
3334

34-
$entityName = $expr->args[0]->value;
35+
$entityName = $expr->args[0]->value;
36+
if (!$entityName instanceof Expr\ClassConstFetch) {
3537
if ($entityName instanceof String_) {
38+
$statements_source = $event->getStatementsSource();
3639
IssueBuffer::accepts(
3740
new RepositoryStringShortcut(new CodeLocation($statements_source, $entityName)),
3841
$statements_source->getSuppressedIssues()
3942
);
40-
} elseif ($entityName instanceof Expr\ClassConstFetch) {
41-
/** @psalm-var class-string|null $className */
42-
$className = $entityName->class->getAttribute('resolvedName');
43+
}
4344

44-
if (null === $className) {
45-
return;
46-
}
45+
return;
46+
}
4747

48-
try {
49-
$reflectionClass = new \ReflectionClass($className);
50-
51-
if (\PHP_VERSION_ID >= 80000 && method_exists(\ReflectionClass::class, 'getAttributes')) {
52-
$entityAttributes = $reflectionClass->getAttributes(EntityAnnotation::class);
53-
54-
foreach ($entityAttributes as $entityAttribute) {
55-
$arguments = $entityAttribute->getArguments();
56-
57-
if (isset($arguments['repositoryClass']) && is_string($arguments['repositoryClass'])) {
58-
$event->setReturnTypeCandidate(new Union([new TNamedObject($arguments['repositoryClass'])]));
59-
}
60-
}
61-
}
62-
63-
if (class_exists(AnnotationReader::class)) {
64-
$reader = new AnnotationReader();
65-
$entityAnnotation = $reader->getClassAnnotation(
66-
$reflectionClass,
67-
EntityAnnotation::class
68-
);
69-
70-
if ($entityAnnotation instanceof EntityAnnotation && $entityAnnotation->repositoryClass) {
71-
$event->setReturnTypeCandidate(new Union([new TNamedObject($entityAnnotation->repositoryClass)]));
72-
}
73-
}
74-
} catch (\ReflectionException $e) {
75-
}
48+
/** @psalm-var class-string|null $className */
49+
$className = $entityName->class->getAttribute('resolvedName');
50+
if (null === $className) {
51+
return;
52+
}
53+
54+
try {
55+
$reflectionClass = new \ReflectionClass($className);
56+
} catch (\ReflectionException) {
57+
return;
58+
}
59+
60+
$entityAttributes = $reflectionClass->getAttributes(EntityAnnotation::class);
61+
foreach ($entityAttributes as $entityAttribute) {
62+
$arguments = $entityAttribute->getArguments();
63+
64+
if (isset($arguments['repositoryClass']) && is_string($arguments['repositoryClass'])) {
65+
$event->setReturnTypeCandidate(new Union([new TNamedObject($arguments['repositoryClass'])]));
66+
67+
return;
68+
}
69+
}
70+
71+
if (class_exists(AnnotationReader::class)) {
72+
$reader = new AnnotationReader();
73+
$entityAnnotation = $reader->getClassAnnotation(
74+
$reflectionClass,
75+
EntityAnnotation::class
76+
);
77+
78+
if ($entityAnnotation instanceof EntityAnnotation && $entityAnnotation->repositoryClass) {
79+
$event->setReturnTypeCandidate(new Union([new TNamedObject($entityAnnotation->repositoryClass)]));
7680
}
7781
}
7882
}
7983

80-
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
84+
public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): void
8185
{
8286
$stmt = $event->getStmt();
8387
$statements_source = $event->getStatementsSource();
8488
$codebase = $event->getCodebase();
8589

8690
$docblock = $stmt->getDocComment();
87-
if ($docblock && false !== strpos((string) $docblock, 'repositoryClass')) {
91+
if ($docblock && str_contains((string) $docblock, 'repositoryClass')) {
8892
try {
8993
$parsedComment = DocComment::parsePreservingLength($docblock);
9094
if (isset($parsedComment->tags['Entity'])) {
@@ -96,7 +100,7 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event)
96100
$codebase->queueClassLikeForScanning($repositoryClassName);
97101
$file_storage->referenced_classlikes[strtolower($repositoryClassName)] = $repositoryClassName;
98102
}
99-
} catch (DocblockParseException $e) {
103+
} catch (DocblockParseException) {
100104
}
101105
}
102106
}

src/Plugin.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function __invoke(RegistrationInterface $registration, ?\SimpleXMLElement
3636
require_once __DIR__.'/Handler/ConsoleHandler.php';
3737
require_once __DIR__.'/Handler/ContainerDependencyHandler.php';
3838
require_once __DIR__.'/Handler/RequiredSetterHandler.php';
39-
require_once __DIR__.'/Handler/DoctrineQueryBuilderHandler.php';
4039
require_once __DIR__.'/Provider/FormGetErrorsReturnTypeProvider.php';
4140

4241
$registration->registerHooksFromClass(HeaderBagHandler::class);
@@ -45,16 +44,18 @@ public function __invoke(RegistrationInterface $registration, ?\SimpleXMLElement
4544
$registration->registerHooksFromClass(RequiredSetterHandler::class);
4645

4746
if (class_exists(\Doctrine\ORM\QueryBuilder::class)) {
47+
require_once __DIR__.'/Handler/DoctrineQueryBuilderHandler.php';
4848
$registration->registerHooksFromClass(DoctrineQueryBuilderHandler::class);
49+
50+
require_once __DIR__.'/Handler/DoctrineRepositoryHandler.php';
51+
$registration->registerHooksFromClass(DoctrineRepositoryHandler::class);
4952
}
5053

5154
if (class_exists(AnnotationRegistry::class)) {
52-
require_once __DIR__.'/Handler/DoctrineRepositoryHandler.php';
5355
if (method_exists(AnnotationRegistry::class, 'registerLoader')) {
5456
/** @psalm-suppress DeprecatedMethod */
5557
AnnotationRegistry::registerLoader('class_exists');
5658
}
57-
$registration->registerHooksFromClass(DoctrineRepositoryHandler::class);
5859

5960
require_once __DIR__.'/Handler/AnnotationHandler.php';
6061
$registration->registerHooksFromClass(AnnotationHandler::class);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@symfony-common
2+
Feature: RepositoryClass using attributes
3+
4+
Background:
5+
Given I have issue handlers "UndefinedClass,UnusedVariable" suppressed
6+
And I have Symfony plugin enabled
7+
And I have the following code preamble
8+
"""
9+
<?php
10+
namespace RepositoryClass;
11+
12+
use Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\EntityWithAttributes;
13+
use Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\EntityWithAttributesRepository;
14+
use Doctrine\ORM\EntityManagerInterface;
15+
"""
16+
17+
Scenario: The plugin can find correct repository class from entity
18+
Given I have the following code
19+
"""
20+
class SomeService
21+
{
22+
public function __construct(EntityManagerInterface $entityManager)
23+
{
24+
/** @psalm-trace $repository */
25+
$repository = $entityManager->getRepository(EntityWithAttributes::class);
26+
}
27+
}
28+
"""
29+
When I run Psalm
30+
Then I see these errors
31+
| Type | Message |
32+
| Trace | $repository: Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\EntityWithAttributesRepository |
33+
And I see no other errors
34+
35+
Scenario: Passing variable class does not crash the plugin
36+
Given I have the following code
37+
"""
38+
class SomeService
39+
{
40+
public function __construct(EntityManagerInterface $entityManager)
41+
{
42+
$entity = 'Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine\EntityWithAttributes';
43+
/** @psalm-trace $repository */
44+
$repository = $entityManager->getRepository($entity::class);
45+
}
46+
}
47+
"""
48+
When I run Psalm
49+
Then I see these errors
50+
| Type | Message |
51+
| Trace | $repository: Doctrine\ORM\EntityRepository<object> |
52+
| MixedArgument | Argument 1 of Doctrine\ORM\EntityManagerInterface::getRepository cannot be mixed, expecting class-string |
53+
And I see no other errors
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine;
6+
7+
use Doctrine\ORM\Mapping\Entity;
8+
9+
#[Entity(repositoryClass: EntityWithAttributesRepository::class)]
10+
class EntityWithAttributes
11+
{
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Psalm\SymfonyPsalmPlugin\Tests\Fixture\Doctrine;
6+
7+
use Doctrine\ORM\EntityRepository;
8+
9+
/**
10+
* @extends EntityRepository<EntityWithAttributes>
11+
*/
12+
class EntityWithAttributesRepository extends EntityRepository
13+
{
14+
}

0 commit comments

Comments
 (0)