Skip to content

Commit 8983bd5

Browse files
authored
Merge pull request #2 from bijibox/codex/remove-unused-test-data-for-unitofwork-qq9b6t
Refine UnitOfWork change set assertions
2 parents 9b118ac + 319407c commit 8983bd5

File tree

5 files changed

+322
-9
lines changed

5 files changed

+322
-9
lines changed

extension.neon

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,22 @@ services:
156156
descriptorRegistry: @doctrineTypeDescriptorRegistry
157157
tags:
158158
- phpstan.broker.dynamicMethodReturnTypeExtension
159-
-
160-
class: PHPStan\Type\Doctrine\Query\QueryResultDynamicReturnTypeExtension
161-
tags:
162-
- phpstan.broker.dynamicMethodReturnTypeExtension
163-
-
164-
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\ExpressionBuilderDynamicReturnTypeExtension
165-
arguments:
166-
argumentsProcessor: @doctrineQueryBuilderArgumentsProcessor
167-
tags:
159+
-
160+
class: PHPStan\Type\Doctrine\Query\QueryResultDynamicReturnTypeExtension
161+
tags:
162+
- phpstan.broker.dynamicMethodReturnTypeExtension
163+
-
164+
class: PHPStan\Type\Doctrine\UnitOfWorkGetEntityChangeSetDynamicReturnTypeExtension
165+
arguments:
166+
objectMetadataResolver: @PHPStan\Type\Doctrine\ObjectMetadataResolver
167+
descriptorRegistry: @doctrineTypeDescriptorRegistry
168+
tags:
169+
- phpstan.broker.dynamicMethodReturnTypeExtension
170+
-
171+
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\ExpressionBuilderDynamicReturnTypeExtension
172+
arguments:
173+
argumentsProcessor: @doctrineQueryBuilderArgumentsProcessor
174+
tags:
168175
- phpstan.broker.dynamicMethodReturnTypeExtension
169176
-
170177
class: PHPStan\Type\Doctrine\QueryBuilder\Expr\BaseExpressionDynamicReturnTypeExtension
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use Doctrine\ORM\Mapping\ClassMetadata;
6+
use Doctrine\ORM\UnitOfWork;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
11+
use PHPStan\Type\Constant\ConstantIntegerType;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use PHPStan\Type\Doctrine\DescriptorNotRegisteredException;
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 PHPStan\Type\TypeUtils;
20+
use function count;
21+
use function array_key_exists;
22+
use function is_array;
23+
use function is_string;
24+
25+
final class UnitOfWorkGetEntityChangeSetDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
26+
{
27+
private ObjectMetadataResolver $objectMetadataResolver;
28+
29+
private DescriptorRegistry $descriptorRegistry;
30+
31+
public function __construct(ObjectMetadataResolver $objectMetadataResolver, DescriptorRegistry $descriptorRegistry)
32+
{
33+
$this->objectMetadataResolver = $objectMetadataResolver;
34+
$this->descriptorRegistry = $descriptorRegistry;
35+
}
36+
37+
public function getClass(): string
38+
{
39+
return UnitOfWork::class;
40+
}
41+
42+
public function isMethodSupported(MethodReflection $methodReflection): bool
43+
{
44+
return $methodReflection->getName() === 'getEntityChangeSet';
45+
}
46+
47+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
48+
{
49+
if (count($methodCall->getArgs()) === 0) {
50+
return $methodReflection->getVariants()[0]->getReturnType();
51+
}
52+
53+
$entityType = $scope->getType($methodCall->getArgs()[0]->value);
54+
$classNames = TypeUtils::getDirectClassNames($entityType);
55+
56+
if ($classNames === []) {
57+
return $methodReflection->getVariants()[0]->getReturnType();
58+
}
59+
60+
$types = [];
61+
62+
foreach ($classNames as $className) {
63+
$metadata = $this->objectMetadataResolver->getClassMetadata($className);
64+
if ($metadata === null) {
65+
continue;
66+
}
67+
68+
$types[] = $this->createChangeSetType($metadata);
69+
}
70+
71+
if ($types === []) {
72+
return $methodReflection->getVariants()[0]->getReturnType();
73+
}
74+
75+
return TypeCombinator::union(...$types);
76+
}
77+
78+
/**
79+
* @param ClassMetadata<object> $metadata
80+
*/
81+
private function createChangeSetType(ClassMetadata $metadata): Type
82+
{
83+
$builder = ConstantArrayTypeBuilder::createEmpty();
84+
85+
foreach ($metadata->fieldMappings as $mapping) {
86+
if (!is_array($mapping)) {
87+
continue;
88+
}
89+
90+
$fieldName = $mapping['fieldName'] ?? null;
91+
if (!is_string($fieldName)) {
92+
continue;
93+
}
94+
95+
$builder->setOffsetValueType(
96+
new ConstantStringType($fieldName),
97+
$this->createFieldChangeSetType($mapping),
98+
true
99+
);
100+
}
101+
102+
foreach ($metadata->associationMappings as $mapping) {
103+
if (!is_array($mapping)) {
104+
continue;
105+
}
106+
107+
$fieldName = $mapping['fieldName'] ?? null;
108+
if (!is_string($fieldName)) {
109+
continue;
110+
}
111+
112+
$builder->setOffsetValueType(
113+
new ConstantStringType($fieldName),
114+
$this->createAssociationChangeSetType($mapping),
115+
true
116+
);
117+
}
118+
119+
return $builder->getArray();
120+
}
121+
122+
/**
123+
* @param array<mixed> $mapping
124+
*/
125+
private function createFieldChangeSetType(array $mapping): Type
126+
{
127+
$typeName = $mapping['type'] ?? null;
128+
$propertyType = new MixedType();
129+
130+
if (is_string($typeName)) {
131+
try {
132+
$propertyType = $this->descriptorRegistry->get($typeName)->getWritableToPropertyType();
133+
} catch (DescriptorNotRegisteredException $e) {
134+
$propertyType = new MixedType();
135+
}
136+
}
137+
138+
if (($mapping['nullable'] ?? false) === true) {
139+
$propertyType = TypeCombinator::addNull($propertyType);
140+
}
141+
142+
$propertyType = TypeCombinator::addNull($propertyType);
143+
144+
return $this->createPairType($propertyType);
145+
}
146+
147+
/**
148+
* @param array<mixed> $mapping
149+
*/
150+
private function createAssociationChangeSetType(array $mapping): Type
151+
{
152+
$type = new MixedType();
153+
154+
$mappingType = $mapping['type'] ?? null;
155+
156+
if ($mappingType === ClassMetadata::ONE_TO_MANY || $mappingType === ClassMetadata::MANY_TO_MANY) {
157+
return $this->createPairType(new MixedType());
158+
}
159+
160+
if (is_string($mapping['targetEntity'] ?? null)) {
161+
$type = new ObjectType($mapping['targetEntity']);
162+
}
163+
164+
$nullable = true;
165+
if (array_key_exists('joinColumns', $mapping) && is_array($mapping['joinColumns'])) {
166+
$nullable = false;
167+
foreach ($mapping['joinColumns'] as $joinColumn) {
168+
if (($joinColumn['nullable'] ?? false) === true) {
169+
$nullable = true;
170+
break;
171+
}
172+
}
173+
}
174+
175+
if ($nullable) {
176+
$type = TypeCombinator::addNull($type);
177+
}
178+
179+
$type = TypeCombinator::addNull($type);
180+
181+
return $this->createPairType($type);
182+
}
183+
184+
private function createPairType(Type $valueType): Type
185+
{
186+
$builder = ConstantArrayTypeBuilder::createEmpty();
187+
$builder->setOffsetValueType(new ConstantIntegerType(0), $valueType);
188+
$builder->setOffsetValueType(new ConstantIntegerType(1), $valueType);
189+
190+
return $builder->getArray();
191+
}
192+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Doctrine;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class UnitOfWorkGetEntityChangeSetDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
/** @return iterable<mixed> */
10+
public function dataFileAsserts(): iterable
11+
{
12+
yield from $this->gatherAssertTypes(__DIR__ . '/data/UnitOfWorkChangeSet/unitOfWorkGetEntityChangeSet.php');
13+
}
14+
15+
/**
16+
* @dataProvider dataFileAsserts
17+
* @param mixed ...$args
18+
*/
19+
public function testFileAsserts(string $assertType, string $file, ...$args): void
20+
{
21+
$this->assertFileAsserts($assertType, $file, ...$args);
22+
}
23+
24+
/** @return string[] */
25+
public static function getAdditionalConfigFiles(): array
26+
{
27+
return [__DIR__ . '/data/UnitOfWorkChangeSet/config.neon'];
28+
}
29+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
includes:
2+
- ../../../../../extension.neon
3+
parameters:
4+
doctrine:
5+
objectManagerLoader: ../QueryResult/entity-manager.php
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace UnitOfWorkChangeSet;
4+
5+
use Doctrine\ORM\EntityManagerInterface;
6+
use Doctrine\ORM\UnitOfWork;
7+
use QueryResult\Entities\Many;
8+
use QueryResult\Entities\Simple;
9+
use function PHPStan\Testing\assertType;
10+
11+
class UnitOfWorkGetEntityChangeSet
12+
{
13+
public function describeSimple(EntityManagerInterface $em, Simple $simple): void
14+
{
15+
$unitOfWork = $em->getUnitOfWork();
16+
$changeSet = $unitOfWork->getEntityChangeSet($simple);
17+
18+
assertType(
19+
'array{
20+
intColumn?: array{0: int|null, 1: int|null},
21+
floatColumn?: array{0: float|null, 1: float|null},
22+
decimalColumn?: array{0: string|null, 1: string|null},
23+
stringColumn?: array{0: string|null, 1: string|null},
24+
stringNullColumn?: array{0: string|null, 1: string|null},
25+
mixedColumn?: array{0: mixed|null, 1: mixed|null}
26+
}',
27+
$changeSet
28+
);
29+
30+
if (array_key_exists('stringNullColumn', $changeSet)) {
31+
assertType('array{0: string|null, 1: string|null}', $changeSet['stringNullColumn']);
32+
}
33+
}
34+
35+
public function describeMany(EntityManagerInterface $em, Many $many): void
36+
{
37+
$unitOfWork = $em->getUnitOfWork();
38+
$changeSet = $unitOfWork->getEntityChangeSet($many);
39+
40+
assertType(
41+
'array{
42+
intColumn?: array{0: int|null, 1: int|null},
43+
stringColumn?: array{0: string|null, 1: string|null},
44+
stringNullColumn?: array{0: string|null, 1: string|null},
45+
datetimeColumn?: array{0: \DateTime|null, 1: \DateTime|null},
46+
datetimeImmutableColumn?: array{0: \DateTimeImmutable|null, 1: \DateTimeImmutable|null},
47+
one?: array{0: QueryResult\\Entities\\One|null, 1: QueryResult\\Entities\\One|null},
48+
oneNull?: array{0: QueryResult\\Entities\\One|null, 1: QueryResult\\Entities\\One|null},
49+
oneDefaultNullability?: array{0: QueryResult\\Entities\\One|null, 1: QueryResult\\Entities\\One|null},
50+
compoundPk?: array{0: QueryResult\\Entities\\CompoundPk|null, 1: QueryResult\\Entities\\CompoundPk|null},
51+
compoundPkAssoc?: array{0: QueryResult\\Entities\\CompoundPkAssoc|null, 1: QueryResult\\Entities\\CompoundPkAssoc|null},
52+
simpleArrayColumn?: array{0: list<string>|null, 1: list<string>|null}
53+
}',
54+
$changeSet
55+
);
56+
57+
if (array_key_exists('intColumn', $changeSet)) {
58+
assertType('array{0: int|null, 1: int|null}', $changeSet['intColumn']);
59+
}
60+
61+
if (array_key_exists('datetimeColumn', $changeSet)) {
62+
assertType('array{0: \DateTime|null, 1: \DateTime|null}', $changeSet['datetimeColumn']);
63+
}
64+
65+
if (array_key_exists('one', $changeSet)) {
66+
assertType('array{0: QueryResult\\Entities\\One|null, 1: QueryResult\\Entities\\One|null}', $changeSet['one']);
67+
}
68+
69+
assertType('array{0: list<string>|null, 1: list<string>|null}|null', $changeSet['simpleArrayColumn'] ?? null);
70+
71+
if (array_key_exists('simpleArrayColumn', $changeSet)) {
72+
assertType('array{0: list<string>|null, 1: list<string>|null}', $changeSet['simpleArrayColumn']);
73+
}
74+
}
75+
76+
public function describeDynamic(UnitOfWork $unitOfWork, Simple|Many $entity): void
77+
{
78+
assertType('array', $unitOfWork->getEntityChangeSet($entity));
79+
}
80+
}

0 commit comments

Comments
 (0)