Skip to content

Commit c3f524d

Browse files
authored
Merge pull request #235 from moufmouf/enum_issue
Changing the default name given to enum types in the GraphQL schema
2 parents 330b752 + 91cb7b0 commit c3f524d

File tree

13 files changed

+146
-20
lines changed

13 files changed

+146
-20
lines changed

docs/annotations_reference.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,15 @@ Attribute | Compulsory | Type | Definition
241241
---------------|------------|------|--------
242242
*for* | *yes* | string | The name of the PHP parameter
243243
*constraint* | *yes | annotation | One (or many) Symfony validation annotations.
244+
245+
## @EnumType annotation
246+
247+
The `@EnumType` annotation is used to change the name of a "Enum" type.
248+
Note that if you do not want to change the name, the annotation is optionnal. Any object extending `MyCLabs\Enum\Enum`
249+
is automatically mapped to a GraphQL enum type.
250+
251+
**Applies on**: classes extending the `MyCLabs\Enum\Enum` base class.
252+
253+
Attribute | Compulsory | Type | Definition
254+
---------------|------------|------|--------
255+
name | *no* | string | The name of the enum type (in the GraphQL schema)

docs/type_mapping.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ class StatusEnum extends Enum
223223
*/
224224
public function users(StatusEnum $status): array
225225
{
226-
if ($status == StatusEum::ON()) {
226+
if ($status == StatusEnum::ON()) {
227227
// Note that the "magic" ON() method returns an instance of the StatusEnum class.
228228
// Also, note that we are comparing this instance using "==" (using "===" would fail as we have 2 different instances here)
229229
// ...
@@ -232,6 +232,30 @@ public function users(StatusEnum $status): array
232232
}
233233
```
234234

235+
```graphql
236+
query users($status: StatusEnum!) {}
237+
users(status: $status) {
238+
id
239+
}
240+
}
241+
```
242+
243+
By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes
244+
that live in different namespaces with the same class name), you can solve it using the `@EnumType` annotation:
245+
246+
```php
247+
use TheCodingMachine\GraphQLite\Annotations\EnumType;
248+
249+
/**
250+
* @EnumType(name="UserStatus")
251+
*/
252+
class StatusEnum extends Enum
253+
{
254+
// ...
255+
}
256+
```
257+
258+
235259
<div class="alert alert-info">There are many enumeration library in PHP and you might be using another library.
236260
If you want to add support for your own library, this is not extremely difficult to do. You need to register a custom
237261
"RootTypeMapper" with GraphQLite. You can learn more about <em>type mappers</em> in the <a href="internals.md">"internals" documentation</a>

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ parameters:
2929
-
3030
message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array<int, T of object> but returns array<object>.#'
3131
path: src/AnnotationReader.php
32+
-
33+
message: '#Parameter \#1 \$enumClass of method TheCodingMachine\\GraphQLite\\Mappers\\Root\\MyCLabsEnumTypeMapper::getTypeName\(\) expects class-string<MyCLabs\\Enum\\Enum>, class-string<object> given.#'
34+
path: src/Mappers/Root/MyCLabsEnumTypeMapper.php
3235
- '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage()#'
3336
# Needed because of a bug in PHP-CS
3437
- '#PHPDoc tag @param for parameter \$args with type mixed is not subtype of native type array<int, mixed>.#'
38+
3539
#-
3640
# message: '#If condition is always true#'
3741
# path: src/Middlewares/SecurityFieldMiddleware.php

src/AnnotationReader.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
use Doctrine\Common\Annotations\AnnotationException;
88
use Doctrine\Common\Annotations\Reader;
99
use InvalidArgumentException;
10+
use MyCLabs\Enum\Enum;
1011
use ReflectionClass;
1112
use ReflectionMethod;
1213
use ReflectionParameter;
1314
use RuntimeException;
1415
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
1516
use TheCodingMachine\GraphQLite\Annotations\Decorate;
17+
use TheCodingMachine\GraphQLite\Annotations\EnumType;
1618
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
1719
use TheCodingMachine\GraphQLite\Annotations\Exceptions\InvalidParameterException;
1820
use TheCodingMachine\GraphQLite\Annotations\ExtendType;
@@ -85,7 +87,6 @@ public function getTypeAnnotation(ReflectionClass $refClass): ?Type
8587
{
8688
try {
8789
$type = $this->getClassAnnotation($refClass, Type::class);
88-
assert($type instanceof Type || $type === null);
8990
if ($type !== null && $type->isSelfType()) {
9091
$type->setClass($refClass->getName());
9192
}
@@ -105,7 +106,6 @@ public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType
105106
{
106107
try {
107108
$extendType = $this->getClassAnnotation($refClass, ExtendType::class);
108-
assert($extendType instanceof ExtendType || $extendType === null);
109109
} catch (ClassNotFoundException $e) {
110110
throw ClassNotFoundException::wrapExceptionForExtendTag($e, $refClass->getName());
111111
}
@@ -230,7 +230,12 @@ public function getMiddlewareAnnotations(ReflectionMethod $refMethod): Middlewar
230230
/**
231231
* Returns a class annotation. Does not look in the parent class.
232232
*
233-
* @param ReflectionClass<T> $refClass
233+
* @param ReflectionClass<object> $refClass
234+
* @param class-string<T> $annotationClass
235+
*
236+
* @return T|null
237+
*
238+
* @throws AnnotationException
234239
*
235240
* @template T of object
236241
*/
@@ -239,6 +244,7 @@ private function getClassAnnotation(ReflectionClass $refClass, string $annotatio
239244
$type = null;
240245
try {
241246
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
247+
assert($type === null || $type instanceof $annotationClass);
242248
} catch (AnnotationException $e) {
243249
switch ($this->mode) {
244250
case self::STRICT_MODE:
@@ -389,4 +395,12 @@ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annota
389395

390396
return $toAddAnnotations;
391397
}
398+
399+
/**
400+
* @param ReflectionClass<Enum> $refClass
401+
*/
402+
public function getEnumTypeAnnotation(ReflectionClass $refClass): ?EnumType
403+
{
404+
return $this->getClassAnnotation($refClass, EnumType::class);
405+
}
392406
}

src/Annotations/EnumType.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Annotations;
6+
7+
/**
8+
* The EnumType annotation is useful to change the name of the generated "enum" type.
9+
*
10+
* @Annotation
11+
* @Target({"CLASS"})
12+
* @Attributes({
13+
* @Attribute("name", type = "string"),
14+
* })
15+
*/
16+
class EnumType
17+
{
18+
/** @var string|null */
19+
private $name;
20+
21+
/**
22+
* @param mixed[] $attributes
23+
*/
24+
public function __construct(array $attributes = [])
25+
{
26+
$this->name = $attributes['name'] ?? null;
27+
}
28+
29+
/**
30+
* Returns the GraphQL name for this type.
31+
*/
32+
public function getName(): ?string
33+
{
34+
return $this->name;
35+
}
36+
}

src/Mappers/Root/MyCLabsEnumTypeMapper.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,30 @@
1313
use phpDocumentor\Reflection\DocBlock;
1414
use phpDocumentor\Reflection\Type;
1515
use phpDocumentor\Reflection\Types\Object_;
16+
use ReflectionClass;
1617
use ReflectionMethod;
18+
use TheCodingMachine\GraphQLite\AnnotationReader;
1719
use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType;
1820
use function is_a;
19-
use function str_replace;
20-
use function strpos;
21-
use function substr;
2221

2322
/**
2423
* Maps an class extending MyCLabs enums to a GraphQL type
2524
*/
2625
class MyCLabsEnumTypeMapper implements RootTypeMapperInterface
2726
{
28-
/** @var array<string, EnumType> */
27+
/** @var array<class-string<object>, EnumType> */
2928
private $cache = [];
29+
/** @var array<string, EnumType> */
30+
private $cacheByName = [];
3031
/** @var RootTypeMapperInterface */
3132
private $next;
33+
/** @var AnnotationReader */
34+
private $annotationReader;
3235

33-
public function __construct(RootTypeMapperInterface $next)
36+
public function __construct(RootTypeMapperInterface $next, AnnotationReader $annotationReader)
3437
{
3538
$this->next = $next;
39+
$this->annotationReader = $annotationReader;
3640
}
3741

3842
/**
@@ -74,11 +78,17 @@ private function map(Type $type): ?EnumType
7478
if ($fqsen === null) {
7579
return null;
7680
}
81+
/**
82+
* @var class-string<object>
83+
*/
7784
$enumClass = (string) $fqsen;
7885

7986
return $this->mapByClassName($enumClass);
8087
}
8188

89+
/**
90+
* @param class-string<object> $enumClass
91+
*/
8292
private function mapByClassName(string $enumClass): ?EnumType
8393
{
8494
if (! is_a($enumClass, Enum::class, true)) {
@@ -88,7 +98,24 @@ private function mapByClassName(string $enumClass): ?EnumType
8898
return $this->cache[$enumClass];
8999
}
90100

91-
return $this->cache[$enumClass] = new MyCLabsEnumType($enumClass);
101+
$type = new MyCLabsEnumType($enumClass, $this->getTypeName($enumClass));
102+
return $this->cacheByName[$type->name] = $this->cache[$enumClass] = $type;
103+
}
104+
105+
/**
106+
* @param class-string<Enum> $enumClass
107+
*/
108+
private function getTypeName(string $enumClass): string
109+
{
110+
$refClass = new ReflectionClass($enumClass);
111+
$enumType = $this->annotationReader->getEnumTypeAnnotation($refClass);
112+
if ($enumType !== null) {
113+
$name = $enumType->getName();
114+
if ($name !== null) {
115+
return $name;
116+
}
117+
}
118+
return $refClass->getShortName();
92119
}
93120

94121
/**
@@ -100,14 +127,20 @@ private function mapByClassName(string $enumClass): ?EnumType
100127
*/
101128
public function mapNameToType(string $typeName): NamedType
102129
{
103-
if (strpos($typeName, 'MyCLabsEnum_') === 0) {
130+
// This is a hack to make sure "$schema->assertValid()" returns true.
131+
// The mapNameToType will fail if the mapByClassName method was not called before.
132+
// This is actually not an issue in real life scenarios where enum types are never queried by type name.
133+
if (isset($this->cacheByName[$typeName])) {
134+
return $this->cacheByName[$typeName];
135+
}
136+
/*if (strpos($typeName, 'MyCLabsEnum_') === 0) {
104137
$className = str_replace('__', '\\', substr($typeName, 12));
105138
106139
$type = $this->mapByClassName($className);
107140
if ($type !== null) {
108141
return $type;
109142
}
110-
}
143+
}*/
111144

112145
return $this->next->mapNameToType($typeName);
113146
}

src/SchemaFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ public function createSchema(): Schema
334334

335335
$errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper);
336336
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper);
337-
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper);
337+
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader);
338338

339339
if (! empty($this->rootTypeMapperFactories)) {
340340
$rootSchemaFactoryContext = new RootTypeMapperFactoryContext(

src/Types/MyCLabsEnumType.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use GraphQL\Type\Definition\EnumType;
88
use InvalidArgumentException;
99
use MyCLabs\Enum\Enum;
10-
use function str_replace;
1110

1211
/**
1312
* An extension of the EnumType to support Myclabs enum.
@@ -17,7 +16,7 @@
1716
*/
1817
class MyCLabsEnumType extends EnumType
1918
{
20-
public function __construct(string $enumClassName)
19+
public function __construct(string $enumClassName, string $typeName)
2120
{
2221
$consts = $enumClassName::toArray();
2322
$constInstances = [];
@@ -26,7 +25,7 @@ public function __construct(string $enumClassName)
2625
}
2726

2827
parent::__construct([
29-
'name' => 'MyCLabsEnum_' . str_replace('\\', '__', $enumClassName),
28+
'name' => $typeName,
3029
'values' => $constInstances,
3130
]);
3231
}

tests/AbstractQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
329329

330330
$errorRootTypeMapper = new FinalRootTypeMapper($this->getTypeMapper());
331331
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $this->getTypeMapper(), $topRootTypeMapper);
332-
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper);
332+
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $this->getAnnotationReader());
333333
$rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $topRootTypeMapper, $this->getTypeRegistry(), $this->getTypeMapper());
334334
$rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper);
335335

tests/Fixtures/Integration/Models/ProductTypeEnum.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Models;
44

55
use MyCLabs\Enum\Enum;
6+
use TheCodingMachine\GraphQLite\Annotations\EnumType;
67

8+
/**
9+
* @EnumType(name="ProductTypes")
10+
*/
711
class ProductTypeEnum extends Enum
812
{
913
const FOOD = 'food';

tests/Integration/EndToEndTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
226226
'rootTypeMapper' => function(ContainerInterface $container) {
227227
$errorRootTypeMapper = new FinalRootTypeMapper($container->get(RecursiveTypeMapperInterface::class));
228228
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $container->get(RecursiveTypeMapperInterface::class), $container->get(RootTypeMapperInterface::class));
229-
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper);
229+
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $container->get(AnnotationReader::class));
230230
$rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class));
231231
$rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $container->get(RootTypeMapperInterface::class));
232232
return $rootTypeMapper;

tests/Mappers/Root/MyCLabsEnumTypeMapperTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class MyCLabsEnumTypeMapperTest extends AbstractQueryProviderTest
1313

1414
public function testObjectTypeHint(): void
1515
{
16-
$mapper = new MyCLabsEnumTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()));
16+
$mapper = new MyCLabsEnumTypeMapper(new FinalRootTypeMapper($this->getTypeMapper()), $this->getAnnotationReader());
1717

1818
$this->expectException(CannotMapTypeException::class);
1919
$this->expectExceptionMessage("don't know how to handle type object");

tests/Types/MyclabsEnumTypeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class MyclabsEnumTypeTest extends TestCase
1010
{
1111
public function testException()
1212
{
13-
$enumType = new MyCLabsEnumType(ProductTypeEnum::class);
13+
$enumType = new MyCLabsEnumType(ProductTypeEnum::class, 'foo');
1414
$this->expectException(InvalidArgumentException::class);
1515
$this->expectExceptionMessage('Expected a Myclabs Enum instance');
1616
$enumType->serialize('foo');

0 commit comments

Comments
 (0)