Skip to content

Commit fa7ca68

Browse files
[WIP] UnitOfWork::getOriginalEntityData
1 parent fad886a commit fa7ca68

8 files changed

+270
-2
lines changed

extension.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ services:
163163
descriptorRegistry: @doctrineTypeDescriptorRegistry
164164
tags:
165165
- phpstan.broker.dynamicMethodReturnTypeExtension
166+
-
167+
class: PHPStan\Type\Doctrine\UnitOfWorkGetOriginalEntityDataDynamicReturnTypeExtension
168+
arguments:
169+
metadataResolver: @PHPStan\Type\Doctrine\ObjectMetadataResolver
170+
descriptorRegistry: @doctrineTypeDescriptorRegistry
171+
tags:
172+
- phpstan.broker.dynamicMethodReturnTypeExtension
166173
-
167174
class: PHPStan\Type\Doctrine\Query\QueryResultDynamicReturnTypeExtension
168175
tags:
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use Doctrine\ORM\Mapping\ClassMetadata;
6+
use Doctrine\ORM\PersistentCollection;
7+
use Doctrine\ORM\UnitOfWork;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\MixedType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use function count;
20+
use function is_array;
21+
use function is_string;
22+
23+
final class UnitOfWorkGetOriginalEntityDataDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
26+
private ObjectMetadataResolver $metadataResolver;
27+
28+
private DescriptorRegistry $descriptorRegistry;
29+
30+
public function __construct(
31+
ObjectMetadataResolver $metadataResolver,
32+
DescriptorRegistry $descriptorRegistry
33+
)
34+
{
35+
$this->metadataResolver = $metadataResolver;
36+
$this->descriptorRegistry = $descriptorRegistry;
37+
}
38+
39+
public function getClass(): string
40+
{
41+
return UnitOfWork::class;
42+
}
43+
44+
public function isMethodSupported(MethodReflection $methodReflection): bool
45+
{
46+
return $methodReflection->getName() === 'getOriginalEntityData';
47+
}
48+
49+
public function getTypeFromMethodCall(
50+
MethodReflection $methodReflection,
51+
MethodCall $methodCall,
52+
Scope $scope
53+
): Type
54+
{
55+
if (count($methodCall->getArgs()) === 0) {
56+
return $this->getDefaultReturnType($methodReflection, $scope, $methodCall);
57+
}
58+
59+
$entityType = $scope->getType($methodCall->getArgs()[0]->value);
60+
$objectClassNames = $entityType->getObjectClassNames();
61+
62+
if (count($objectClassNames) === 0) {
63+
return $this->getDefaultReturnType($methodReflection, $scope, $methodCall);
64+
}
65+
66+
$dataTypes = [];
67+
68+
foreach ($objectClassNames as $className) {
69+
$metadata = $this->metadataResolver->getClassMetadata($className);
70+
if ($metadata === null) {
71+
return $this->getDefaultReturnType($methodReflection, $scope, $methodCall);
72+
}
73+
74+
$dataTypes[] = $this->createOriginalEntityDataType($metadata);
75+
}
76+
77+
return TypeCombinator::union(...$dataTypes);
78+
}
79+
80+
private function createOriginalEntityDataType(ClassMetadata $metadata): Type
81+
{
82+
$builder = ConstantArrayTypeBuilder::createEmpty();
83+
$collectionType = new ObjectType(PersistentCollection::class);
84+
85+
foreach ($metadata->fieldMappings as $fieldName => $mapping) {
86+
if ($metadata->isIdentifier($fieldName) && $metadata->isIdGeneratorIdentity()) {
87+
continue;
88+
}
89+
90+
if ($metadata->versionField === $fieldName) {
91+
continue;
92+
}
93+
94+
if (!isset($mapping['type'])) {
95+
continue;
96+
}
97+
98+
try {
99+
$type = $this->descriptorRegistry->get($mapping['type'])->getWritableToPropertyType();
100+
} catch (DescriptorNotRegisteredException $exception) {
101+
$type = new MixedType();
102+
}
103+
104+
if (($mapping['nullable'] ?? false) === true) {
105+
$type = TypeCombinator::addNull($type);
106+
}
107+
108+
$builder->setOffsetValueType(
109+
new ConstantStringType($fieldName),
110+
$type,
111+
);
112+
}
113+
114+
foreach ($metadata->associationMappings as $fieldName => $mapping) {
115+
if (($mapping['type'] & ClassMetadata::TO_ONE) !== 0) {
116+
$targetEntity = $mapping['targetEntity'] ?? null;
117+
if (!is_string($targetEntity)) {
118+
continue;
119+
}
120+
121+
$type = new ObjectType($targetEntity);
122+
if ($this->isAssociationNullable($mapping)) {
123+
$type = TypeCombinator::addNull($type);
124+
}
125+
126+
$builder->setOffsetValueType(
127+
new ConstantStringType($fieldName),
128+
$type,
129+
);
130+
continue;
131+
}
132+
133+
if (($mapping['type'] & ClassMetadata::TO_MANY) === 0) {
134+
continue;
135+
}
136+
137+
$builder->setOffsetValueType(
138+
new ConstantStringType($fieldName),
139+
$collectionType,
140+
);
141+
}
142+
143+
return $builder->getArray();
144+
}
145+
146+
/**
147+
* @param array<string, mixed> $association
148+
*/
149+
private function isAssociationNullable(array $association): bool
150+
{
151+
$joinColumns = $association['joinColumns'] ?? null;
152+
if (!is_array($joinColumns)) {
153+
return true;
154+
}
155+
156+
foreach ($joinColumns as $joinColumn) {
157+
if (!is_array($joinColumn)) {
158+
continue;
159+
}
160+
if (($joinColumn['nullable'] ?? true) === false) {
161+
return false;
162+
}
163+
}
164+
165+
return true;
166+
}
167+
168+
private function getDefaultReturnType(MethodReflection $methodReflection, Scope $scope, MethodCall $methodCall): Type
169+
{
170+
return ParametersAcceptorSelector::selectFromArgs(
171+
$scope,
172+
$methodCall->getArgs(),
173+
$methodReflection->getVariants(),
174+
)->getReturnType();
175+
}
176+
177+
}

tests/Type/Doctrine/UnitOfWorkGetEntityChangeSetDynamicReturnTypeExtensionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ final class UnitOfWorkGetEntityChangeSetDynamicReturnTypeExtensionTest extends T
1010
/** @return iterable<mixed> */
1111
public function dataFileAsserts(): iterable
1212
{
13-
yield from $this->gatherAssertTypes(__DIR__ . '/data/UnitOfWorkChangeSet/unitOfWork-change-set.php');
13+
yield from $this->gatherAssertTypes(__DIR__ . '/data/UnitOfWork/unitOfWork-change-set.php');
1414
}
1515

1616
/**
@@ -29,7 +29,7 @@ public function testFileAsserts(
2929
/** @return string[] */
3030
public static function getAdditionalConfigFiles(): array
3131
{
32-
return [__DIR__ . '/data/UnitOfWorkChangeSet/config.neon'];
32+
return [__DIR__ . '/data/UnitOfWork/config.neon'];
3333
}
3434

3535
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
final class UnitOfWorkGetOriginalEntityDataDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/** @return iterable<mixed> */
11+
public function dataFileAsserts(): iterable
12+
{
13+
yield from $this->gatherAssertTypes(__DIR__ . '/data/UnitOfWork/unitOfWork-original-entity-data.php');
14+
}
15+
16+
/**
17+
* @dataProvider dataFileAsserts
18+
* @param mixed ...$args
19+
*/
20+
public function testFileAsserts(
21+
string $assertType,
22+
string $file,
23+
...$args
24+
): void
25+
{
26+
$this->assertFileAsserts($assertType, $file, ...$args);
27+
}
28+
29+
/** @return string[] */
30+
public static function getAdditionalConfigFiles(): array
31+
{
32+
return [__DIR__ . '/data/UnitOfWork/config.neon'];
33+
}
34+
35+
}
File renamed without changes.

tests/Type/Doctrine/data/UnitOfWorkChangeSet/entity-manager.php renamed to tests/Type/Doctrine/data/UnitOfWork/entity-manager.php

File renamed without changes.

tests/Type/Doctrine/data/UnitOfWorkChangeSet/unitOfWork-change-set.php renamed to tests/Type/Doctrine/data/UnitOfWork/unitOfWork-change-set.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Doctrine\ORM\UnitOfWork;
66
use QueryResult\Entities\Many;
7+
use QueryResult\Entities\One;
78
use QueryResult\Entities\Simple;
89
use function PHPStan\Testing\assertType;
910

@@ -25,6 +26,12 @@ public function associations(UnitOfWork $unitOfWork, Many $entity): void
2526
);
2627
}
2728

29+
public function persistentCollection(UnitOfWork $unitOfWork, One $entity): void
30+
{
31+
$changeSet = $unitOfWork->getEntityChangeSet($entity);
32+
assertType('array{Doctrine\\ORM\\PersistentCollection, Doctrine\\ORM\\PersistentCollection}', $changeSet['manies']);
33+
}
34+
2835
public function unknownEntity(UnitOfWork $unitOfWork, object $entity): void
2936
{
3037
assertType(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace UnitOfWorkOriginalEntityData;
4+
5+
use Doctrine\ORM\UnitOfWork;
6+
use QueryResult\Entities\Many;
7+
use QueryResult\Entities\One;
8+
use QueryResult\Entities\Simple;
9+
use function PHPStan\Testing\assertType;
10+
11+
final class UnitOfWorkOriginalEntityDataAssertions
12+
{
13+
public function simple(UnitOfWork $unitOfWork, Simple $entity): void
14+
{
15+
assertType(
16+
'array{id: lowercase-string&numeric-string&uppercase-string, intColumn: int, floatColumn: float, decimalColumn: numeric-string&uppercase-string, stringColumn: string, stringNullColumn: string|null, mixedColumn: mixed}',
17+
$unitOfWork->getOriginalEntityData($entity)
18+
);
19+
}
20+
21+
public function associations(UnitOfWork $unitOfWork, Many $entity): void
22+
{
23+
assertType(
24+
'array{id: lowercase-string&numeric-string&uppercase-string, intColumn: int, stringColumn: string, stringNullColumn: string|null, datetimeColumn: DateTime, datetimeImmutableColumn: DateTimeImmutable, simpleArrayColumn: list<string>, one: QueryResult\\Entities\\One, oneNull: QueryResult\\Entities\\One|null, oneDefaultNullability: QueryResult\\Entities\\One|null, compoundPk: QueryResult\\Entities\\CompoundPk|null, compoundPkAssoc: QueryResult\\Entities\\CompoundPkAssoc|null}',
25+
$unitOfWork->getOriginalEntityData($entity)
26+
);
27+
}
28+
29+
public function persistentCollection(UnitOfWork $unitOfWork, One $entity): void
30+
{
31+
$originalData = $unitOfWork->getOriginalEntityData($entity);
32+
assertType('Doctrine\\ORM\\PersistentCollection', $originalData['manies']);
33+
}
34+
35+
public function unknownEntity(UnitOfWork $unitOfWork, object $entity): void
36+
{
37+
assertType(
38+
'array<string, mixed>',
39+
$unitOfWork->getOriginalEntityData($entity)
40+
);
41+
}
42+
}

0 commit comments

Comments
 (0)