Skip to content

Commit 0eae48b

Browse files
authored
PHPStan: infer targetEntity from property type for ManyToOne associations (#38)
Previously, when using associations without explicit targetEntity attribute, PHPStan would only infer the target entity from the property type for OneToOne associations. This left ManyToOne associations without targetEntity attribute incorrectly reported as "not a valid Doctrine association". This change extends type inference to also work for ManyToOne associations, matching the behavior users expect when not using phpstan-doctrine. Fixes #37
1 parent 7cbc70e commit 0eae48b

File tree

4 files changed

+90
-1
lines changed

4 files changed

+90
-1
lines changed

src/PHPStan/EntityPreloaderCore.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ReflectionNamedType;
2222
use ReflectionProperty;
2323
use function count;
24+
use function in_array;
2425
use function is_string;
2526

2627
abstract class EntityPreloaderCore
@@ -214,14 +215,17 @@ private function getAssociationTargetTypeFromPropertyReflection(
214215
ManyToMany::class,
215216
];
216217

218+
$toOneAssociations = [OneToOne::class, ManyToOne::class];
219+
217220
foreach ($associationAttributes as $attributeClass) {
218221
foreach ($propertyReflection->getAttributes($attributeClass) as $attributeReflection) {
219222
$attribute = $attributeReflection->newInstance();
220223

221224
if ($attribute->targetEntity !== null) {
222225
return new ObjectType($attribute->targetEntity);
226+
}
223227

224-
} elseif ($attributeClass === OneToOne::class && $propertyReflection->getType() instanceof ReflectionNamedType) {
228+
if (in_array($attributeClass, $toOneAssociations, true) && $propertyReflection->getType() instanceof ReflectionNamedType) {
225229
return new ObjectType($propertyReflection->getType()->getName());
226230
}
227231
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonkTests\DoctrineEntityPreloader\Fixtures\Issue37;
4+
5+
use Doctrine\ORM\Mapping\Entity;
6+
use Doctrine\ORM\Mapping\ManyToOne;
7+
use Doctrine\ORM\Mapping\OneToOne;
8+
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Synthetic\TestEntityWithId;
9+
10+
/**
11+
* @see https://github.com/shipmonk-rnd/doctrine-entity-preloader/issues/37
12+
*/
13+
#[Entity]
14+
class Employee extends TestEntityWithId
15+
{
16+
17+
/**
18+
* ManyToOne WITHOUT explicit targetEntity
19+
*/
20+
#[ManyToOne]
21+
private ?Employee $supervisor;
22+
23+
/**
24+
* OneToOne WITHOUT explicit targetEntity
25+
*/
26+
#[OneToOne]
27+
private ?EmployeeSettings $settings;
28+
29+
public function __construct(
30+
?int $number,
31+
?Employee $supervisor = null,
32+
?EmployeeSettings $settings = null,
33+
)
34+
{
35+
$this->number = $number;
36+
$this->supervisor = $supervisor;
37+
$this->settings = $settings;
38+
}
39+
40+
public function getSupervisor(): ?Employee
41+
{
42+
return $this->supervisor;
43+
}
44+
45+
public function getSettings(): ?EmployeeSettings
46+
{
47+
return $this->settings;
48+
}
49+
50+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonkTests\DoctrineEntityPreloader\Fixtures\Issue37;
4+
5+
use Doctrine\ORM\Mapping\Entity;
6+
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Synthetic\TestEntityWithId;
7+
8+
/**
9+
* @see https://github.com/shipmonk-rnd/doctrine-entity-preloader/issues/37
10+
*/
11+
#[Entity]
12+
class EmployeeSettings extends TestEntityWithId
13+
{
14+
15+
public function __construct(?int $number)
16+
{
17+
$this->number = $number;
18+
}
19+
20+
}

tests/PHPStan/Data/EntityPreloaderRuleTestData.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\PasswordVerifier;
1414
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Tag;
1515
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\User;
16+
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Issue37\Employee;
1617
use function PHPStan\Testing\assertType;
1718

1819
final class EntityPreloaderRuleTestData
@@ -114,4 +115,18 @@ public function preloadWithObject(array $entities): void
114115
assertType('list<object>', $this->entityPreloader->preload($entities, 'foo')); // error: Property 'object::$foo' not found.
115116
}
116117

118+
/**
119+
* @see https://github.com/shipmonk-rnd/doctrine-entity-preloader/issues/37
120+
*/
121+
public function preloadWithoutExplicitTargetEntity(): void
122+
{
123+
$employees = $this->entityManager->getRepository(Employee::class)->findAll();
124+
125+
// ManyToOne WITHOUT targetEntity attribute
126+
assertType('list<ShipMonkTests\DoctrineEntityPreloader\Fixtures\Issue37\Employee>', $this->entityPreloader->preload($employees, 'supervisor'));
127+
128+
// OneToOne WITHOUT targetEntity attribute
129+
assertType('list<ShipMonkTests\DoctrineEntityPreloader\Fixtures\Issue37\EmployeeSettings>', $this->entityPreloader->preload($employees, 'settings'));
130+
}
131+
117132
}

0 commit comments

Comments
 (0)