Skip to content

Commit 1d077a9

Browse files
committed
Allow class annotations in the entire class hierarchy, including traits
This allow to declare common Filter and Sorting parent classes or traits used by the classes.
1 parent f4a9830 commit 1d077a9

12 files changed

+242
-25
lines changed

src/Factory/Type/AbstractTypeFactory.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use GraphQL\Doctrine\Exception;
88
use GraphQL\Doctrine\Factory\AbstractFactory;
99
use GraphQL\Type\Definition\Type;
10-
use ReflectionClass;
1110

1211
/**
1312
* A factory to create an ObjectType from a Doctrine entity
@@ -53,15 +52,15 @@ final protected function getDescription(string $className): ?string
5352
/**
5453
* Throw an exception if the given type does not inherit expected type
5554
*
56-
* @param ReflectionClass $class
55+
* @param string $classWithAnnotation
5756
* @param string $annotation
58-
* @param string $expected
59-
* @param string $className
57+
* @param string $expectedClassName
58+
* @param string $actualClassName
6059
*/
61-
final protected function throwIfInvalidAnnotation(ReflectionClass $class, string $annotation, string $expected, string $className): void
60+
final protected function throwIfInvalidAnnotation(string $classWithAnnotation, string $annotation, string $expectedClassName, string $actualClassName): void
6261
{
63-
if (!is_a($className, $expected, true)) {
64-
throw new Exception('On class `' . $class->getName() . '` the annotation `@API\\' . $annotation . '` expects a FQCN implementing `' . $expected . '`, but instead got: ' . $className);
62+
if (!is_a($actualClassName, $expectedClassName, true)) {
63+
throw new Exception('On class `' . $classWithAnnotation . '` the annotation `@API\\' . $annotation . '` expects a FQCN implementing `' . $expectedClassName . '`, but instead got: ' . $actualClassName);
6564
}
6665
}
6766
}

src/Factory/Type/FilterTypeFactory.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ private function getConditionFieldsType(string $className, string $typeName): In
175175
$metadata = $this->entityManager->getClassMetadata($className);
176176

177177
// Get custom operators
178-
$this->customOperators = [];
179178
$this->readCustomOperatorsFromAnnotation($metadata->reflClass);
180179

181180
// Get all scalar fields
@@ -294,24 +293,21 @@ private function getOperators(string $fieldName, LeafType $leafType, bool $isAss
294293
*/
295294
private function readCustomOperatorsFromAnnotation(ReflectionClass $class): void
296295
{
297-
$filters = $this->getAnnotationReader()->getClassAnnotation($class, Filters::class);
298-
if ($filters) {
296+
$allFilters = Utils::getRecursiveClassAnnotations($this->getAnnotationReader(), $class, Filters::class);
297+
$this->customOperators = [];
298+
foreach ($allFilters as $classWithAnnotation => $filters) {
299299

300300
/** @var Filter $filter */
301301
foreach ($filters->filters as $filter) {
302302
$className = $filter->operator;
303-
$this->throwIfInvalidAnnotation($class, 'Filter', AbstractOperator::class, $className);
303+
$this->throwIfInvalidAnnotation($classWithAnnotation, 'Filter', AbstractOperator::class, $className);
304304

305305
if (!isset($this->customOperators[$filter->field])) {
306306
$this->customOperators[$filter->field] = [];
307307
}
308308
$this->customOperators[$filter->field][] = $filter;
309309
}
310310
}
311-
312-
if ($class->getParentClass()) {
313-
$this->readCustomOperatorsFromAnnotation($class->getParentClass());
314-
}
315311
}
316312

317313
/**

src/Factory/Type/SortingTypeFactory.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,18 @@ private function fillCache(string $className): void
132132
*/
133133
private function getFromAnnotation(ReflectionClass $class): array
134134
{
135-
$result = [];
135+
$sortings = Utils::getRecursiveClassAnnotations($this->getAnnotationReader(), $class, Sorting::class);
136136

137-
$sorting = $this->getAnnotationReader()->getClassAnnotation($class, Sorting::class);
138-
if ($sorting) {
137+
$result = [];
138+
foreach ($sortings as $classWithAnnotation => $sorting) {
139139
foreach ($sorting->classes as $className) {
140-
$this->throwIfInvalidAnnotation($class, 'Sorting', SortingInterface::class, $className);
140+
$this->throwIfInvalidAnnotation($classWithAnnotation, 'Sorting', SortingInterface::class, $className);
141141

142142
$name = lcfirst(preg_replace('~Type$~', '', Utils::getTypeName($className)));
143143
$result[$name] = new $className();
144144
}
145145
}
146146

147-
if ($class->getParentClass()) {
148-
return array_merge($result, $this->getFromAnnotation($class->getParentClass()));
149-
}
150-
151147
return $result;
152148
}
153149
}

src/Utils.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
namespace GraphQL\Doctrine;
66

7+
use Doctrine\Common\Annotations\Reader;
78
use GraphQL\Type\Definition\EnumType;
89
use GraphQL\Type\Definition\LeafType;
910
use GraphQL\Type\Definition\ScalarType;
11+
use ReflectionClass;
1012

1113
/**
1214
* A few utils
@@ -39,4 +41,34 @@ public static function getOperatorTypeName(string $className, LeafType $type): s
3941
{
4042
return preg_replace('~Type$~', '', self::getTypeName($className)) . ucfirst($type->name);
4143
}
44+
45+
/**
46+
* Return an array of all annotations found in the class hierarchy, including its traits, indexed by the class name
47+
*
48+
* @param Reader $reader
49+
* @param ReflectionClass $class
50+
* @param string $annotationName
51+
*
52+
* @return array annotations indexed by the class name where they were found
53+
*/
54+
public static function getRecursiveClassAnnotations(Reader $reader, ReflectionClass $class, string $annotationName): array
55+
{
56+
$result = [];
57+
58+
$annotation = $reader->getClassAnnotation($class, $annotationName);
59+
if ($annotation) {
60+
$result[$class->getName()] = $annotation;
61+
}
62+
63+
foreach ($class->getTraits() as $trait) {
64+
$result = array_merge($result, self::getRecursiveClassAnnotations($reader, $trait, $annotationName));
65+
}
66+
67+
$parent = $class->getParentClass();
68+
if ($parent) {
69+
$result = array_merge($result, self::getRecursiveClassAnnotations($reader, $parent, $annotationName));
70+
}
71+
72+
return $result;
73+
}
4274
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQLTests\Doctrine\Blog\Model\Special;
6+
7+
use Doctrine\ORM\Mapping as ORM;
8+
9+
/**
10+
* @ORM\MappedSuperclass
11+
*/
12+
final class ModelWithTraits
13+
{
14+
use TraitWithSortingAndFilter;
15+
16+
/**
17+
* @var int
18+
*
19+
* @ORM\Column(type="integer", options={"unsigned" = true})
20+
* @ORM\Id
21+
* @ORM\GeneratedValue(strategy="IDENTITY")
22+
*/
23+
private $id;
24+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQLTests\Doctrine\Blog\Model\Special;
6+
7+
use GraphQL\Doctrine\Annotation as API;
8+
9+
/**
10+
* @API\Sorting({"GraphQLTests\Doctrine\Blog\Sorting\UserName"})
11+
* @API\Filters({
12+
* @API\Filter(field="customFromTrait", operator="GraphQLTests\Doctrine\Blog\Filtering\SearchOperatorType", type="string"),
13+
* })
14+
*/
15+
trait TraitWithSortingAndFilter
16+
{
17+
}

tests/Blog/Sorting/PseudoRandom.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ public function __invoke(QueryBuilder $queryBuilder, string $order): void
1818
$alias = $queryBuilder->getRootAliases()[0];
1919

2020
$queryBuilder->addSelect('MOD(' . $alias . '.id, 5) AS HIDDEN score');
21-
$queryBuilder->orderBy('score', $order);
21+
$queryBuilder->addOrderBy('score', $order);
2222
}
2323
}

tests/Blog/Sorting/UserName.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ public function __invoke(QueryBuilder $queryBuilder, string $order): void
1818
$alias = $queryBuilder->getRootAliases()[0];
1919

2020
$queryBuilder->join($alias . '.user', 'sortingUser');
21-
$queryBuilder->orderBy('sortingUser.name', $order);
21+
$queryBuilder->addOrderBy('sortingUser.name', $order);
2222
}
2323
}

tests/FilterTypesTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GraphQL\Type\Definition\Type;
88
use GraphQLTests\Doctrine\Blog\Model\Post;
99
use GraphQLTests\Doctrine\Blog\Model\Special\InvalidFilter;
10+
use GraphQLTests\Doctrine\Blog\Model\Special\ModelWithTraits;
1011

1112
final class FilterTypesTest extends \PHPUnit\Framework\TestCase
1213
{
@@ -18,6 +19,12 @@ public function testCanGetPostFilter(): void
1819
$this->assertAllTypes('tests/data/PostFilter.graphqls', $actual);
1920
}
2021

22+
public function testCanInheritSortingFromTraits(): void
23+
{
24+
$actual = $this->types->getFilter(ModelWithTraits::class);
25+
$this->assertAllTypes('tests/data/ModelWithTraitsFilter.graphqls', $actual);
26+
}
27+
2128
public function providerFilteredQueryBuilder(): array
2229
{
2330
$values = [];

tests/SortingTypesTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace GraphQLTests\Doctrine;
66

77
use GraphQLTests\Doctrine\Blog\Model\Post;
8+
use GraphQLTests\Doctrine\Blog\Model\Special\ModelWithTraits;
89

910
final class SortingTypesTest extends \PHPUnit\Framework\TestCase
1011
{
@@ -15,4 +16,10 @@ public function testCanGetPostSorting(): void
1516
$actual = $this->types->getSorting(Post::class);
1617
$this->assertAllTypes('tests/data/PostSorting.graphqls', $actual);
1718
}
19+
20+
public function testCanInheritSortingFromTraits(): void
21+
{
22+
$actual = $this->types->getSorting(ModelWithTraits::class);
23+
$this->assertAllTypes('tests/data/ModelWithTraitsSorting.graphqls', $actual);
24+
}
1825
}

0 commit comments

Comments
 (0)