Skip to content

Commit

Permalink
Merge branch '2.11.x' into 3.0.x
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
derrabus committed Jan 9, 2022
2 parents 1af202b + 656f881 commit bee1b53
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 40 deletions.
11 changes: 5 additions & 6 deletions lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,32 @@

/**
* Base class for EntityManager decorators
*
* @extends ObjectManagerDecorator<EntityManagerInterface>
*/
abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
{
/** @var EntityManagerInterface */
protected $wrapped;

public function __construct(EntityManagerInterface $wrapped)
{
$this->wrapped = $wrapped;
}

public function getRepository($className): EntityRepository
{
return parent::getRepository($className);
return $this->wrapped->getRepository($className);
}

public function getMetadataFactory(): ClassMetadataFactory
{
return parent::getMetadataFactory();
return $this->wrapped->getMetadataFactory();
}

/**
* {@inheritdoc}
*/
public function getClassMetadata($className): ClassMetadata
{
return parent::getClassMetadata($className);
return $this->wrapped->getClassMetadata($className);
}

public function getConnection(): Connection
Expand Down
31 changes: 23 additions & 8 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ class ClassMetadataInfo implements ClassMetadata
/**
* The ReflectionProperty instances of the mapped class.
*
* @var ReflectionProperty[]|null[]
* @var array<string, ReflectionProperty|null>
*/
public $reflFields = [];

Expand Down Expand Up @@ -976,7 +976,8 @@ public function wakeupReflection($reflService)

foreach ($this->embeddedClasses as $property => $embeddedClass) {
if (isset($embeddedClass['declaredField'])) {
$childProperty = $reflService->getAccessibleProperty(
$childProperty = $this->getAccessibleProperty(
$reflService,
$this->embeddedClasses[$embeddedClass['declaredField']]['class'],
$embeddedClass['originalField']
);
Expand All @@ -990,7 +991,8 @@ public function wakeupReflection($reflService)
continue;
}

$fieldRefl = $reflService->getAccessibleProperty(
$fieldRefl = $this->getAccessibleProperty(
$reflService,
$embeddedClass['declared'] ?? $this->name,
$property
);
Expand All @@ -1003,15 +1005,15 @@ public function wakeupReflection($reflService)
if (isset($mapping['declaredField']) && isset($parentReflFields[$mapping['declaredField']])) {
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
$parentReflFields[$mapping['declaredField']],
$reflService->getAccessibleProperty($mapping['originalClass'], $mapping['originalField']),
$this->getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField']),
$mapping['originalClass']
);
continue;
}

$this->reflFields[$field] = isset($mapping['declared'])
? $reflService->getAccessibleProperty($mapping['declared'], $field)
: $reflService->getAccessibleProperty($this->name, $field);
? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
: $this->getAccessibleProperty($reflService, $this->name, $field);

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

foreach ($this->associationMappings as $field => $mapping) {
$this->reflFields[$field] = isset($mapping['declared'])
? $reflService->getAccessibleProperty($mapping['declared'], $field)
: $reflService->getAccessibleProperty($this->name, $field);
? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
: $this->getAccessibleProperty($reflService, $this->name, $field);
}
}

Expand Down Expand Up @@ -3719,4 +3721,17 @@ private function assertMappingOrderBy(array $mapping): void
throw new InvalidArgumentException("'orderBy' is expected to be an array, not " . gettype($mapping['orderBy']));
}
}

/**
* @psalm-param class-string $class
*/
private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ?ReflectionProperty
{
$reflectionProperty = $reflService->getAccessibleProperty($class, $field);
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
}

return $reflectionProperty;
}
}
51 changes: 51 additions & 0 deletions lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use InvalidArgumentException;
use LogicException;
use ReflectionProperty;

use function assert;
use function func_get_args;
use function func_num_args;
use function is_object;
use function sprintf;

/**
* @internal
*/
final class ReflectionReadonlyProperty extends ReflectionProperty
{
public function __construct(
private ReflectionProperty $wrappedProperty
) {
if (! $wrappedProperty->isReadOnly()) {
throw new InvalidArgumentException('Given property is not readonly.');
}

parent::__construct($wrappedProperty->class, $wrappedProperty->name);
}

public function getValue(?object $object = null): mixed
{
return $this->wrappedProperty->getValue(...func_get_args());
}

public function setValue(mixed $objectOrValue, mixed $value = null): void
{
if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) {
$this->wrappedProperty->setValue(...func_get_args());

return;
}

assert(is_object($objectOrValue));

if (parent::getValue($objectOrValue) !== $value) {
throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
}
}
}
6 changes: 2 additions & 4 deletions lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
use function reset;

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

/**
* Replaces the whereClause in the AST.
*
* Generates a clause equivalent to WHERE IN (:dpid_1, :dpid_2, ...)
* Appends a condition equivalent to "WHERE IN (:dpid_1, :dpid_2, ...)" to the whereClause of the AST.
*
* The parameter namespace (dpid) is defined by
* the PAGINATOR_ID_ALIAS
Expand Down
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ parameters:
path: lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php

-
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\\(\\)$#"
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\\(\\)$#"
count: 1
path: lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php

Expand Down
25 changes: 4 additions & 21 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -330,32 +330,12 @@
</RedundantCastGivenDocblockType>
</file>
<file src="lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php">
<ArgumentTypeCoercion occurrences="1">
<code>$className</code>
</ArgumentTypeCoercion>
<DeprecatedClass occurrences="1">
<code>ProxyFactory</code>
</DeprecatedClass>
<InvalidReturnStatement occurrences="1">
<code>parent::getMetadataFactory()</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>ClassMetadataFactory</code>
</InvalidReturnType>
<LessSpecificReturnStatement occurrences="2">
<code>parent::getClassMetadata($className)</code>
<code>parent::getRepository($className)</code>
</LessSpecificReturnStatement>
<MissingParamType occurrences="1">
<code>$entity</code>
</MissingParamType>
<MoreSpecificReturnType occurrences="2">
<code>ClassMetadata</code>
<code>EntityRepository</code>
</MoreSpecificReturnType>
<NonInvariantDocblockPropertyType occurrences="1">
<code>$wrapped</code>
</NonInvariantDocblockPropertyType>
<TooManyArguments occurrences="1">
<code>flush</code>
</TooManyArguments>
Expand Down Expand Up @@ -468,6 +448,9 @@
<code>?T</code>
<code>Collection&lt;int, T&gt;</code>
</InvalidReturnType>
<LessSpecificImplementedReturnType occurrences="1">
<code>string</code>
</LessSpecificImplementedReturnType>
<PropertyTypeCoercion occurrences="1">
<code>$em</code>
</PropertyTypeCoercion>
Expand Down Expand Up @@ -749,7 +732,7 @@
<code>$parentReflFields[$embeddedClass['declaredField']]</code>
<code>$parentReflFields[$mapping['declaredField']]</code>
<code>$queryMapping['resultClass']</code>
<code>$reflService-&gt;getAccessibleProperty($mapping['originalClass'], $mapping['originalField'])</code>
<code>$this-&gt;getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField'])</code>
</PossiblyNullArgument>
<PossiblyNullPropertyFetch occurrences="2">
<code>$embeddable-&gt;reflClass-&gt;name</code>
Expand Down
6 changes: 6 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
<referencedClass name="Doctrine\DBAL\Platforms\PostgreSQLPlatform" />
</errorLevel>
</InvalidClass>
<MethodSignatureMismatch>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/7357 -->
<file name="lib/Doctrine/ORM/Mapping/ReflectionReadonlyProperty.php"/>
</errorLevel>
</MethodSignatureMismatch>
<MissingDependency>
<errorLevel type="suppress">
<!-- DBAL 3.2 forward compatibility -->
Expand Down
31 changes: 31 additions & 0 deletions tests/Doctrine/Tests/Models/ReadonlyProperties/Author.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\ReadonlyProperties;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;

#[Entity, Table(name: 'author')]
class Author
{
#[Column, Id, GeneratedValue]
private readonly int $id;

#[Column]
private readonly string $name;

public function getId(): int
{
return $this->id;
}

public function getName(): string
{
return $this->name;
}
}
51 changes: 51 additions & 0 deletions tests/Doctrine/Tests/Models/ReadonlyProperties/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\ReadonlyProperties;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinTable;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\Table;

#[Entity, Table(name: 'book')]
class Book
{
#[Column, Id, GeneratedValue]
private readonly int $id;

#[Column]
private readonly string $title;

#[ManyToMany(targetEntity: Author::class), JoinTable(name: 'book_author')]
private readonly Collection $authors;

public function __construct()
{
$this->authors = new ArrayCollection();
}

public function getId(): int
{
return $this->id;
}

public function getTitle(): string
{
return $this->title;
}

/**
* @return list<Author>
*/
public function getAuthors(): array
{
return $this->authors->getValues();
}
}
41 changes: 41 additions & 0 deletions tests/Doctrine/Tests/Models/ReadonlyProperties/SimpleBook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\ReadonlyProperties;

use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\JoinColumn;
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\Table;

#[Entity, Table(name: 'simple_book')]
class SimpleBook
{
#[Column, Id, GeneratedValue]
private readonly int $id;

#[Column]
private readonly string $title;

#[ManyToOne, JoinColumn(nullable: false)]
private readonly Author $author;

public function getId(): int
{
return $this->id;
}

public function getTitle(): string
{
return $this->title;
}

public function getAuthor(): Author
{
return $this->author;
}
}
Loading

0 comments on commit bee1b53

Please sign in to comment.