Skip to content

Commit bee1b53

Browse files
committed
Merge branch '2.11.x' into 3.0.x
* 2.11.x: Leverage generic ObjectManagerDecorator (doctrine#9312) Fix WhereInWalker description to better describe the behaviour of this class (doctrine#9268) Regenerate Psalm baseline Update Psalm baseline for Persistence 2.3 (doctrine#9349) Support readonly properties for read operations (doctrine#9316)
2 parents 1af202b + 656f881 commit bee1b53

File tree

12 files changed

+385
-40
lines changed

12 files changed

+385
-40
lines changed

lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php

+5-6
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,32 @@
2525

2626
/**
2727
* Base class for EntityManager decorators
28+
*
29+
* @extends ObjectManagerDecorator<EntityManagerInterface>
2830
*/
2931
abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
3032
{
31-
/** @var EntityManagerInterface */
32-
protected $wrapped;
33-
3433
public function __construct(EntityManagerInterface $wrapped)
3534
{
3635
$this->wrapped = $wrapped;
3736
}
3837

3938
public function getRepository($className): EntityRepository
4039
{
41-
return parent::getRepository($className);
40+
return $this->wrapped->getRepository($className);
4241
}
4342

4443
public function getMetadataFactory(): ClassMetadataFactory
4544
{
46-
return parent::getMetadataFactory();
45+
return $this->wrapped->getMetadataFactory();
4746
}
4847

4948
/**
5049
* {@inheritdoc}
5150
*/
5251
public function getClassMetadata($className): ClassMetadata
5352
{
54-
return parent::getClassMetadata($className);
53+
return $this->wrapped->getClassMetadata($className);
5554
}
5655

5756
public function getConnection(): Connection

lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php

+23-8
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ class ClassMetadataInfo implements ClassMetadata
688688
/**
689689
* The ReflectionProperty instances of the mapped class.
690690
*
691-
* @var ReflectionProperty[]|null[]
691+
* @var array<string, ReflectionProperty|null>
692692
*/
693693
public $reflFields = [];
694694

@@ -976,7 +976,8 @@ public function wakeupReflection($reflService)
976976

977977
foreach ($this->embeddedClasses as $property => $embeddedClass) {
978978
if (isset($embeddedClass['declaredField'])) {
979-
$childProperty = $reflService->getAccessibleProperty(
979+
$childProperty = $this->getAccessibleProperty(
980+
$reflService,
980981
$this->embeddedClasses[$embeddedClass['declaredField']]['class'],
981982
$embeddedClass['originalField']
982983
);
@@ -990,7 +991,8 @@ public function wakeupReflection($reflService)
990991
continue;
991992
}
992993

993-
$fieldRefl = $reflService->getAccessibleProperty(
994+
$fieldRefl = $this->getAccessibleProperty(
995+
$reflService,
994996
$embeddedClass['declared'] ?? $this->name,
995997
$property
996998
);
@@ -1003,15 +1005,15 @@ public function wakeupReflection($reflService)
10031005
if (isset($mapping['declaredField']) && isset($parentReflFields[$mapping['declaredField']])) {
10041006
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
10051007
$parentReflFields[$mapping['declaredField']],
1006-
$reflService->getAccessibleProperty($mapping['originalClass'], $mapping['originalField']),
1008+
$this->getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField']),
10071009
$mapping['originalClass']
10081010
);
10091011
continue;
10101012
}
10111013

10121014
$this->reflFields[$field] = isset($mapping['declared'])
1013-
? $reflService->getAccessibleProperty($mapping['declared'], $field)
1014-
: $reflService->getAccessibleProperty($this->name, $field);
1015+
? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
1016+
: $this->getAccessibleProperty($reflService, $this->name, $field);
10151017

10161018
if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
10171019
$this->reflFields[$field] = new ReflectionEnumProperty(
@@ -1023,8 +1025,8 @@ public function wakeupReflection($reflService)
10231025

10241026
foreach ($this->associationMappings as $field => $mapping) {
10251027
$this->reflFields[$field] = isset($mapping['declared'])
1026-
? $reflService->getAccessibleProperty($mapping['declared'], $field)
1027-
: $reflService->getAccessibleProperty($this->name, $field);
1028+
? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
1029+
: $this->getAccessibleProperty($reflService, $this->name, $field);
10281030
}
10291031
}
10301032

@@ -3719,4 +3721,17 @@ private function assertMappingOrderBy(array $mapping): void
37193721
throw new InvalidArgumentException("'orderBy' is expected to be an array, not " . gettype($mapping['orderBy']));
37203722
}
37213723
}
3724+
3725+
/**
3726+
* @psalm-param class-string $class
3727+
*/
3728+
private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ?ReflectionProperty
3729+
{
3730+
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
3731+
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
3732+
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
3733+
}
3734+
3735+
return $reflectionProperty;
3736+
}
37223737
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Mapping;
6+
7+
use InvalidArgumentException;
8+
use LogicException;
9+
use ReflectionProperty;
10+
11+
use function assert;
12+
use function func_get_args;
13+
use function func_num_args;
14+
use function is_object;
15+
use function sprintf;
16+
17+
/**
18+
* @internal
19+
*/
20+
final class ReflectionReadonlyProperty extends ReflectionProperty
21+
{
22+
public function __construct(
23+
private ReflectionProperty $wrappedProperty
24+
) {
25+
if (! $wrappedProperty->isReadOnly()) {
26+
throw new InvalidArgumentException('Given property is not readonly.');
27+
}
28+
29+
parent::__construct($wrappedProperty->class, $wrappedProperty->name);
30+
}
31+
32+
public function getValue(?object $object = null): mixed
33+
{
34+
return $this->wrappedProperty->getValue(...func_get_args());
35+
}
36+
37+
public function setValue(mixed $objectOrValue, mixed $value = null): void
38+
{
39+
if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) {
40+
$this->wrappedProperty->setValue(...func_get_args());
41+
42+
return;
43+
}
44+
45+
assert(is_object($objectOrValue));
46+
47+
if (parent::getValue($objectOrValue) !== $value) {
48+
throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
49+
}
50+
}
51+
}

lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
use function reset;
2929

3030
/**
31-
* Replaces the whereClause of the AST with a WHERE id IN (:foo_1, :foo_2) equivalent.
31+
* Appends a condition "id IN (:foo_1, :foo_2)" to the whereClause of the AST.
3232
*/
3333
class WhereInWalker extends TreeWalkerAdapter
3434
{
@@ -43,9 +43,7 @@ class WhereInWalker extends TreeWalkerAdapter
4343
public const PAGINATOR_ID_ALIAS = 'dpid';
4444

4545
/**
46-
* Replaces the whereClause in the AST.
47-
*
48-
* Generates a clause equivalent to WHERE IN (:dpid_1, :dpid_2, ...)
46+
* Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST.
4947
*
5048
* The parameter namespace (dpid) is defined by
5149
* the PAGINATOR_ID_ALIAS

phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ parameters:
136136
path: lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php
137137

138138
-
139-
message: "#^Return type \\(Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataFactory\\) of method Doctrine\\\\ORM\\\\Decorator\\\\EntityManagerDecorator\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\<Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadata\\<object\\>\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManagerDecorator\\:\\:getMetadataFactory\\(\\)$#"
139+
message: "#^Return type \\(Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadataFactory\\) of method Doctrine\\\\ORM\\\\Decorator\\\\EntityManagerDecorator\\:\\:getMetadataFactory\\(\\) should be compatible with return type \\(Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadataFactory\\<Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadata\\<object\\>\\>\\) of method Doctrine\\\\Persistence\\\\ObjectManagerDecorator\\<Doctrine\\\\ORM\\\\EntityManagerInterface\\>\\:\\:getMetadataFactory\\(\\)$#"
140140
count: 1
141141
path: lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php
142142

psalm-baseline.xml

+4-21
Original file line numberDiff line numberDiff line change
@@ -330,32 +330,12 @@
330330
</RedundantCastGivenDocblockType>
331331
</file>
332332
<file src="lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php">
333-
<ArgumentTypeCoercion occurrences="1">
334-
<code>$className</code>
335-
</ArgumentTypeCoercion>
336333
<DeprecatedClass occurrences="1">
337334
<code>ProxyFactory</code>
338335
</DeprecatedClass>
339-
<InvalidReturnStatement occurrences="1">
340-
<code>parent::getMetadataFactory()</code>
341-
</InvalidReturnStatement>
342-
<InvalidReturnType occurrences="1">
343-
<code>ClassMetadataFactory</code>
344-
</InvalidReturnType>
345-
<LessSpecificReturnStatement occurrences="2">
346-
<code>parent::getClassMetadata($className)</code>
347-
<code>parent::getRepository($className)</code>
348-
</LessSpecificReturnStatement>
349336
<MissingParamType occurrences="1">
350337
<code>$entity</code>
351338
</MissingParamType>
352-
<MoreSpecificReturnType occurrences="2">
353-
<code>ClassMetadata</code>
354-
<code>EntityRepository</code>
355-
</MoreSpecificReturnType>
356-
<NonInvariantDocblockPropertyType occurrences="1">
357-
<code>$wrapped</code>
358-
</NonInvariantDocblockPropertyType>
359339
<TooManyArguments occurrences="1">
360340
<code>flush</code>
361341
</TooManyArguments>
@@ -468,6 +448,9 @@
468448
<code>?T</code>
469449
<code>Collection&lt;int, T&gt;</code>
470450
</InvalidReturnType>
451+
<LessSpecificImplementedReturnType occurrences="1">
452+
<code>string</code>
453+
</LessSpecificImplementedReturnType>
471454
<PropertyTypeCoercion occurrences="1">
472455
<code>$em</code>
473456
</PropertyTypeCoercion>
@@ -749,7 +732,7 @@
749732
<code>$parentReflFields[$embeddedClass['declaredField']]</code>
750733
<code>$parentReflFields[$mapping['declaredField']]</code>
751734
<code>$queryMapping['resultClass']</code>
752-
<code>$reflService-&gt;getAccessibleProperty($mapping['originalClass'], $mapping['originalField'])</code>
735+
<code>$this-&gt;getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField'])</code>
753736
</PossiblyNullArgument>
754737
<PossiblyNullPropertyFetch occurrences="2">
755738
<code>$embeddable-&gt;reflClass-&gt;name</code>

psalm.xml

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
<referencedClass name="Doctrine\DBAL\Platforms\PostgreSQLPlatform" />
5454
</errorLevel>
5555
</InvalidClass>
56+
<MethodSignatureMismatch>
57+
<errorLevel type="suppress">
58+
<!-- See https://github.com/vimeo/psalm/issues/7357 -->
59+
<file name="lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php"/>
60+
</errorLevel>
61+
</MethodSignatureMismatch>
5662
<MissingDependency>
5763
<errorLevel type="suppress">
5864
<!-- DBAL 3.2 forward compatibility -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\ReadonlyProperties;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
use Doctrine\ORM\Mapping\Table;
12+
13+
#[Entity, Table(name: 'author')]
14+
class Author
15+
{
16+
#[Column, Id, GeneratedValue]
17+
private readonly int $id;
18+
19+
#[Column]
20+
private readonly string $name;
21+
22+
public function getId(): int
23+
{
24+
return $this->id;
25+
}
26+
27+
public function getName(): string
28+
{
29+
return $this->name;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\ReadonlyProperties;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\ORM\Mapping\Column;
10+
use Doctrine\ORM\Mapping\Entity;
11+
use Doctrine\ORM\Mapping\GeneratedValue;
12+
use Doctrine\ORM\Mapping\Id;
13+
use Doctrine\ORM\Mapping\JoinTable;
14+
use Doctrine\ORM\Mapping\ManyToMany;
15+
use Doctrine\ORM\Mapping\Table;
16+
17+
#[Entity, Table(name: 'book')]
18+
class Book
19+
{
20+
#[Column, Id, GeneratedValue]
21+
private readonly int $id;
22+
23+
#[Column]
24+
private readonly string $title;
25+
26+
#[ManyToMany(targetEntity: Author::class), JoinTable(name: 'book_author')]
27+
private readonly Collection $authors;
28+
29+
public function __construct()
30+
{
31+
$this->authors = new ArrayCollection();
32+
}
33+
34+
public function getId(): int
35+
{
36+
return $this->id;
37+
}
38+
39+
public function getTitle(): string
40+
{
41+
return $this->title;
42+
}
43+
44+
/**
45+
* @return list<Author>
46+
*/
47+
public function getAuthors(): array
48+
{
49+
return $this->authors->getValues();
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\ReadonlyProperties;
6+
7+
use Doctrine\ORM\Mapping\Column;
8+
use Doctrine\ORM\Mapping\Entity;
9+
use Doctrine\ORM\Mapping\GeneratedValue;
10+
use Doctrine\ORM\Mapping\Id;
11+
use Doctrine\ORM\Mapping\JoinColumn;
12+
use Doctrine\ORM\Mapping\ManyToOne;
13+
use Doctrine\ORM\Mapping\Table;
14+
15+
#[Entity, Table(name: 'simple_book')]
16+
class SimpleBook
17+
{
18+
#[Column, Id, GeneratedValue]
19+
private readonly int $id;
20+
21+
#[Column]
22+
private readonly string $title;
23+
24+
#[ManyToOne, JoinColumn(nullable: false)]
25+
private readonly Author $author;
26+
27+
public function getId(): int
28+
{
29+
return $this->id;
30+
}
31+
32+
public function getTitle(): string
33+
{
34+
return $this->title;
35+
}
36+
37+
public function getAuthor(): Author
38+
{
39+
return $this->author;
40+
}
41+
}

0 commit comments

Comments
 (0)