Skip to content

Commit

Permalink
Merge pull request doctrine#9541 from greg0ire/improve-templating
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire authored Feb 24, 2022
2 parents 021444b + 40af1fc commit 947935e
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 47 deletions.
16 changes: 12 additions & 4 deletions lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ class AttributeDriver extends AnnotationDriver
Mapping\MappedSuperclass::class => 2,
];

/**
* The annotation reader.
*
* @var AttributeReader
*/
protected $reader;

/**
* @param array<string> $paths
*/
Expand All @@ -46,7 +53,8 @@ public function __construct(array $paths)
));
}

parent::__construct(new AttributeReader(), $paths);
$this->reader = new AttributeReader();
$this->addPaths($paths);
}

/**
Expand Down Expand Up @@ -271,7 +279,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void
// Check for JoinColumn/JoinColumns annotations
$joinColumns = [];

$joinColumnAttributes = $this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class);
$joinColumnAttributes = $this->reader->getPropertyAnnotationCollection($property, Mapping\JoinColumn::class);

foreach ($joinColumnAttributes as $joinColumnAttribute) {
$joinColumns[] = $this->joinColumnToArray($joinColumnAttribute);
Expand Down Expand Up @@ -376,11 +384,11 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void
];
}

foreach ($this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class) as $joinColumn) {
foreach ($this->reader->getPropertyAnnotationCollection($property, Mapping\JoinColumn::class) as $joinColumn) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
}

foreach ($this->reader->getPropertyAnnotation($property, Mapping\InverseJoinColumn::class) as $joinColumn) {
foreach ($this->reader->getPropertyAnnotationCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
}

Expand Down
79 changes: 59 additions & 20 deletions lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Attribute;
use Doctrine\ORM\Mapping\Annotation;
use LogicException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
Expand All @@ -14,58 +15,93 @@
use function assert;
use function is_string;
use function is_subclass_of;
use function sprintf;

/**
* @internal
*/
final class AttributeReader
{
/** @var array<string,bool> */
/** @var array<class-string<Annotation>,bool> */
private array $isRepeatableAttribute = [];

/** @return array<Annotation|RepeatableAttributeCollection> */
/**
* @psalm-return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of Annotation
*/
public function getClassAnnotations(ReflectionClass $class): array
{
return $this->convertToAttributeInstances($class->getAttributes());
}

/** @return Annotation|RepeatableAttributeCollection|null */
public function getClassAnnotation(ReflectionClass $class, $annotationName)
{
return $this->getClassAnnotations($class)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}

/** @return array<Annotation|RepeatableAttributeCollection> */
/**
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of Annotation
*/
public function getMethodAnnotations(ReflectionMethod $method): array
{
return $this->convertToAttributeInstances($method->getAttributes());
}

/** @return Annotation|RepeatableAttributeCollection|null */
public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
{
return $this->getMethodAnnotations($method)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}

/** @return array<Annotation|RepeatableAttributeCollection> */
/**
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of Annotation
*/
public function getPropertyAnnotations(ReflectionProperty $property): array
{
return $this->convertToAttributeInstances($property->getAttributes());
}

/** @return Annotation|RepeatableAttributeCollection|null */
/**
* @param class-string<T> $annotationName The name of the annotation.
*
* @return T|null
*
* @template T of Annotation
*/
public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
{
if ($this->isRepeatable($annotationName)) {
throw new LogicException(sprintf(
'The attribute "%s" is repeatable. Call getPropertyAnnotationCollection() instead.',
$annotationName
));
}

return $this->getPropertyAnnotations($property)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}

/**
* @param class-string<T> $annotationName The name of the annotation.
*
* @return RepeatableAttributeCollection<T>
*
* @template T of Annotation
*/
public function getPropertyAnnotationCollection(
ReflectionProperty $property,
string $annotationName
): RepeatableAttributeCollection {
if (! $this->isRepeatable($annotationName)) {
throw new LogicException(sprintf(
'The attribute "%s" is not repeatable. Call getPropertyAnnotation() instead.',
$annotationName
));
}

return $this->getPropertyAnnotations($property)[$annotationName] ?? new RepeatableAttributeCollection();
}

/**
* @param array<ReflectionAttribute> $attributes
*
* @return array<Annotation|RepeatableAttributeCollection>
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of Annotation
*/
private function convertToAttributeInstances(array $attributes): array
{
Expand Down Expand Up @@ -98,6 +134,9 @@ private function convertToAttributeInstances(array $attributes): array
return $instances;
}

/**
* @param class-string<Annotation> $attributeClassName
*/
private function isRepeatable(string $attributeClassName): bool
{
if (isset($this->isRepeatableAttribute[$attributeClassName])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use Doctrine\ORM\Mapping\Annotation;

/**
* @template-extends ArrayObject<int,Annotation>
* @template-extends ArrayObject<int, T>
* @template T of Annotation
*/
final class RepeatableAttributeCollection extends ArrayObject
{
Expand Down
6 changes: 4 additions & 2 deletions lib/Doctrine/ORM/Repository/RepositoryFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ interface RepositoryFactory
* Gets the repository for an entity class.
*
* @param EntityManagerInterface $entityManager The EntityManager instance.
* @param string $entityName The name of the entity.
* @param class-string<T> $entityName The name of the entity.
*
* @return ObjectRepository
* @return ObjectRepository<T>
*
* @template T of object
*/
public function getRepository(EntityManagerInterface $entityManager, $entityName);
}
12 changes: 1 addition & 11 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -261,15 +261,10 @@ parameters:
path: lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php

-
message: "#^Argument of an invalid type Doctrine\\\\ORM\\\\Mapping\\\\InverseJoinColumn supplied for foreach, only iterables are supported\\.$#"
message: "#^PHPDoc type Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeReader of property Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeDriver\\:\\:\\$reader is not covariant with PHPDoc type Doctrine\\\\Common\\\\Annotations\\\\Reader of overridden property Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:\\$reader\\.$#"
count: 1
path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php

-
message: "#^Argument of an invalid type Doctrine\\\\ORM\\\\Mapping\\\\JoinColumn supplied for foreach, only iterables are supported\\.$#"
count: 2
path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php

-
message: "#^PHPDoc type array\\<string, int\\> of property Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeDriver\\:\\:\\$entityAnnotationClasses is not covariant with PHPDoc type array\\<class\\-string, bool\\|int\\> of overridden property Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:\\$entityAnnotationClasses\\.$#"
count: 1
Expand All @@ -280,11 +275,6 @@ parameters:
count: 1
path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php

-
message: "#^Parameter \\#1 \\$reader of method Doctrine\\\\Persistence\\\\Mapping\\\\Driver\\\\AnnotationDriver\\:\\:__construct\\(\\) expects Doctrine\\\\Common\\\\Annotations\\\\Reader, Doctrine\\\\ORM\\\\Mapping\\\\Driver\\\\AttributeReader given\\.$#"
count: 1
path: lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php

-
message: "#^Access to an undefined property Doctrine\\\\Persistence\\\\Mapping\\\\ClassMetadata\\<T of object\\>\\:\\:\\$name\\.$#"
count: 1
Expand Down
11 changes: 2 additions & 9 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -772,9 +772,6 @@
<ArgumentTypeCoercion occurrences="1">
<code>$metadata</code>
</ArgumentTypeCoercion>
<InvalidArgument occurrences="1">
<code>new AttributeReader()</code>
</InvalidArgument>
<InvalidArrayAccess occurrences="4">
<code>$value[0]</code>
<code>$value[0]</code>
Expand All @@ -785,17 +782,13 @@
<code>$mapping</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType occurrences="1"/>
<NonInvariantDocblockPropertyType occurrences="1">
<NonInvariantDocblockPropertyType occurrences="2">
<code>$entityAnnotationClasses</code>
<code>$reader</code>
</NonInvariantDocblockPropertyType>
<PossiblyNullArgument occurrences="1">
<code>$listenerClassName</code>
</PossiblyNullArgument>
<PossiblyNullIterator occurrences="3">
<code>$joinColumnAttributes</code>
<code>$this-&gt;reader-&gt;getPropertyAnnotation($property, Mapping\InverseJoinColumn::class)</code>
<code>$this-&gt;reader-&gt;getPropertyAnnotation($property, Mapping\JoinColumn::class)</code>
</PossiblyNullIterator>
<RedundantCondition occurrences="3">
<code>assert($method instanceof ReflectionMethod)</code>
<code>assert($method instanceof ReflectionMethod)</code>
Expand Down
3 changes: 3 additions & 0 deletions tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ protected function loadDriverForCMSModels(): AnnotationDriver
return $annotationDriver;
}

/**
* @return AnnotationDriver
*/
protected function loadDriver(): MappingDriver
{
return $this->createAnnotationDriver();
Expand Down
48 changes: 48 additions & 0 deletions tests/Doctrine/Tests/ORM/Mapping/AttributeReaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Mapping;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Driver\AttributeReader;
use LogicException;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;

/**
* @requires PHP 8.0
*/
class AttributeReaderTest extends TestCase
{
public function testItThrowsWhenGettingRepeatableAnnotationWithTheWrongMethod(): void
{
$reader = new AttributeReader();
$property = new ReflectionProperty(TestEntity::class, 'id');
$this->expectException(LogicException::class);
$this->expectExceptionMessage(
'The attribute "Doctrine\ORM\Mapping\Index" is repeatable. Call getPropertyAnnotationCollection() instead.'
);
$reader->getPropertyAnnotation($property, ORM\Index::class);
}

public function testItThrowsWhenGettingNonRepeatableAnnotationWithTheWrongMethod(): void
{
$reader = new AttributeReader();
$property = new ReflectionProperty(TestEntity::class, 'id');
$this->expectException(LogicException::class);
$this->expectExceptionMessage(
'The attribute "Doctrine\ORM\Mapping\Id" is not repeatable. Call getPropertyAnnotation() instead.'
);
$reader->getPropertyAnnotationCollection($property, ORM\Id::class);
}
}

#[ORM\Entity]
#[ORM\Index(name: 'bar', columns: ['id'])]
class TestEntity
{
#[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue]
/** @var int */
public $id;
}

0 comments on commit 947935e

Please sign in to comment.