-
Notifications
You must be signed in to change notification settings - Fork 101
Native enum support #409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
oojacoboo
merged 25 commits into
thecodingmachine:master
from
dsavina:feature/native-enum-support
Apr 7, 2022
Merged
Native enum support #409
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
6dcec26
Implement native enum type + mapper
dsavina 73f3420
Ignore test output in directory 'build/'
dsavina 02769b2
Run tests on PHP 8.1
dsavina d4439e0
Test native enum support if available
dsavina 453b85c
Disable static code analysis on 8.1-specific code files
dsavina c3a6920
[wip] Use default Type annotation for enums instead of EnumType
dsavina d85227e
Merge remote-tracking branch 'dsavina/test/native-enum-support' into …
oojacoboo 178ee95
Added symfony/var-dumper so we can actually debug
oojacoboo 8cd54b8
Excluded a bad rule for 8.1 code style
oojacoboo 8cb5138
CS fixes
oojacoboo 408db3f
Additional comments
oojacoboo db91015
Ignore Enum type mapping outside root
oojacoboo 380d61e
Define UnitEnum namespace
oojacoboo bca078e
Moved logic into more optimal location
oojacoboo 185bc9c
Removed unused use statement
oojacoboo 127694b
Merge branch 'master' into feature/native-enum-support
oojacoboo 9c418d0
Support v5 of var-dumper for < PHP8
oojacoboo 7a22c45
Remove root namespace for `UnitEnum` interface check
oojacoboo 8a88dfc
Ignore PHPStan check for isEnum
oojacoboo f3018f1
Added root namespce back for UnitEnum
oojacoboo c778661
Deprecated EnumType annotation and updated docs
oojacoboo 2109afa
CS fixes
oojacoboo 6256fbc
Fixed failing test
oojacoboo 3e01865
Fixed bug causing useEnumValues to not never be assigned
oojacoboo 351619a
Improved documentation around the class attribute for @Type
oojacoboo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
/build/ | ||
/vendor/ | ||
/composer.lock | ||
/src/Tests/ | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace TheCodingMachine\GraphQLite\Mappers\Root; | ||
|
||
use GraphQL\Type\Definition\InputType; | ||
use GraphQL\Type\Definition\NamedType; | ||
use GraphQL\Type\Definition\OutputType; | ||
use GraphQL\Type\Definition\Type as GraphQLType; | ||
use phpDocumentor\Reflection\DocBlock; | ||
use phpDocumentor\Reflection\Type; | ||
use phpDocumentor\Reflection\Types\Object_; | ||
use ReflectionClass; | ||
use ReflectionEnum; | ||
use ReflectionMethod; | ||
use ReflectionProperty; | ||
use Symfony\Contracts\Cache\CacheInterface; | ||
use TheCodingMachine\GraphQLite\AnnotationReader; | ||
use TheCodingMachine\GraphQLite\Types\EnumType; | ||
use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; | ||
use UnitEnum; | ||
|
||
use function assert; | ||
use function enum_exists; | ||
|
||
/** | ||
* Maps an enum class to a GraphQL type (only available in PHP>=8.1) | ||
*/ | ||
class EnumTypeMapper implements RootTypeMapperInterface | ||
{ | ||
/** @var array<class-string<UnitEnum>, EnumType> */ | ||
private $cache = []; | ||
/** @var array<string, EnumType> */ | ||
private $cacheByName = []; | ||
/** @var array<string, class-string<UnitEnum>> */ | ||
private $nameToClassMapping; | ||
/** @var RootTypeMapperInterface */ | ||
private $next; | ||
/** @var AnnotationReader */ | ||
private $annotationReader; | ||
/** @var array|NS[] */ | ||
private $namespaces; | ||
/** @var CacheInterface */ | ||
private $cacheService; | ||
|
||
/** | ||
* @param NS[] $namespaces List of namespaces containing enums. Used when searching an enum by name. | ||
*/ | ||
public function __construct( | ||
RootTypeMapperInterface $next, | ||
AnnotationReader $annotationReader, | ||
CacheInterface $cacheService, | ||
array $namespaces | ||
) { | ||
$this->next = $next; | ||
$this->annotationReader = $annotationReader; | ||
$this->cacheService = $cacheService; | ||
$this->namespaces = $namespaces; | ||
} | ||
|
||
/** | ||
* @param (OutputType&GraphQLType)|null $subType | ||
* @param ReflectionMethod|ReflectionProperty $reflector | ||
* | ||
* @return OutputType&GraphQLType | ||
*/ | ||
public function toGraphQLOutputType( | ||
Type $type, | ||
?OutputType $subType, | ||
$reflector, | ||
DocBlock $docBlockObj | ||
): OutputType { | ||
$result = $this->map($type); | ||
if ($result === null) { | ||
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Maps into the appropriate InputType | ||
* | ||
* @param InputType|GraphQLType|null $subType | ||
* @param ReflectionMethod|ReflectionProperty $reflector | ||
* | ||
* @return InputType|GraphQLType | ||
*/ | ||
public function toGraphQLInputType( | ||
Type $type, | ||
?InputType $subType, | ||
string $argumentName, | ||
$reflector, | ||
DocBlock $docBlockObj | ||
): InputType | ||
{ | ||
$result = $this->map($type); | ||
if ($result === null) { | ||
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
private function map(Type $type): ?EnumType | ||
{ | ||
if (! $type instanceof Object_) { | ||
return null; | ||
} | ||
$fqsen = $type->getFqsen(); | ||
if ($fqsen === null) { | ||
return null; | ||
} | ||
|
||
/** @var class-string<object> $enumClass */ | ||
$enumClass = (string) $fqsen; | ||
|
||
return $this->mapByClassName($enumClass); | ||
} | ||
|
||
/** | ||
* @param class-string $enumClass | ||
*/ | ||
private function mapByClassName(string $enumClass): ?EnumType | ||
{ | ||
if (isset($this->cache[$enumClass])) { | ||
return $this->cache[$enumClass]; | ||
} | ||
|
||
if (! enum_exists($enumClass)) { | ||
return null; | ||
} | ||
|
||
// phpcs:disable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable | ||
/** @var class-string<UnitEnum> $enumClass */ | ||
// phpcs:enable SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable | ||
|
||
$reflectionEnum = new ReflectionEnum($enumClass); | ||
|
||
$typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionEnum); | ||
$typeName = ($typeAnnotation !== null ? $typeAnnotation->getName() : null) ?? $reflectionEnum->getShortName(); | ||
|
||
// Expose values instead of names if specifically configured to and if enum is string-backed | ||
$useValues = $typeAnnotation !== null && | ||
$typeAnnotation->useEnumValues() && | ||
$reflectionEnum->isBacked() && | ||
(string) $reflectionEnum->getBackingType() === 'string'; | ||
|
||
$type = new EnumType($enumClass, $typeName, $useValues); | ||
|
||
return $this->cacheByName[$typeName] = $this->cache[$enumClass] = $type; | ||
} | ||
|
||
private function getTypeName(ReflectionClass $reflectionClass): string | ||
{ | ||
$typeAnnotation = $this->annotationReader->getTypeAnnotation($reflectionClass); | ||
|
||
return ($typeAnnotation !== null ? $typeAnnotation->getName() : null) ?? $reflectionClass->getShortName(); | ||
} | ||
|
||
/** | ||
* Returns a GraphQL type by name. | ||
* If this root type mapper can return this type in "toGraphQLOutputType" or "toGraphQLInputType", it should | ||
* also map these types by name in the "mapNameToType" method. | ||
* | ||
* @param string $typeName The name of the GraphQL type | ||
*/ | ||
public function mapNameToType(string $typeName): NamedType | ||
{ | ||
// This is a hack to make sure "$schema->assertValid()" returns true. | ||
// The mapNameToType will fail if the mapByClassName method was not called before. | ||
// This is actually not an issue in real life scenarios where enum types are never queried by type name. | ||
if (isset($this->cacheByName[$typeName])) { | ||
return $this->cacheByName[$typeName]; | ||
} | ||
|
||
$nameToClassMapping = $this->getNameToClassMapping(); | ||
if (isset($this->nameToClassMapping[$typeName])) { | ||
$className = $nameToClassMapping[$typeName]; | ||
$type = $this->mapByClassName($className); | ||
assert($type !== null); | ||
return $type; | ||
} | ||
|
||
return $this->next->mapNameToType($typeName); | ||
} | ||
|
||
/** | ||
* Go through all classes in the defined namespaces and loads the cache. | ||
* | ||
* @return array<string, class-string<UnitEnum>> | ||
*/ | ||
private function getNameToClassMapping(): array | ||
{ | ||
if ($this->nameToClassMapping === null) { | ||
$this->nameToClassMapping = $this->cacheService->get('enum_name_to_class', function () { | ||
$nameToClassMapping = []; | ||
foreach ($this->namespaces as $ns) { | ||
foreach ($ns->getClassList() as $className => $classRef) { | ||
if (! enum_exists($className)) { | ||
continue; | ||
} | ||
|
||
$nameToClassMapping[$this->getTypeName($classRef)] = $className; | ||
} | ||
} | ||
return $nameToClassMapping; | ||
}); | ||
} | ||
|
||
return $this->nameToClassMapping; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we getting much value out of this? I'd just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PHPStan (before I disabled it) required a
class-string<UnitEnum>
, as signed inEnumType
constructor, and wasn't able to determine$enumClass
complies at this point (after theenum_exist
check).On the other side, CodeSniffer doesn't like precising the type subsequently to a variable declaration (which I think is a shame, but I guess I understand the rule), therefore the
phpcs:disable
directive.I could simply soften the typecheck in
EnumType::__constructor
, fromclass-string<UnitEnum>
toclass-string
, but I must say I'm not so fond of this option.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, we should be doing better validation in the constructor for this string, instead of relying on static type analysis. What kind of feedback is a developer going to here here if an incorrect class-string is provided? It's nice to provide these additional static checks for IDE completion and lib development testing, but provides little runtime, or even development, assurances.
Is this error not referring to the fact that it's a
param
and not avar
in terms of phpcs parsing? Why aren't we applying this in the docblock of the current function?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation lies precisely in the
enum_exists()
check above. The ideal would have been for PHPStan to exploit this check in order to infer aclass-string<UnitEnum>
, just likeis_int()
would lead to inferring an integer value, I'm merely giving it a little push here.The input argument of the method doesn't strictly have to be a valid enum class name, it simply won't be mapped as an
EnumType
if not;null
will be returned, and the mapper will fallback to the next in line (once again, behaviour copied fromMyCLabsTypeMapper
). Typing the argument asclass-string<UnitEnum>
means the check must be done outside of the method, both inEnumTypeMapper::map(Type $type)
andEnumTypeMapper::mapNameToType(string $typeName)
, but even then PHPStan would need this little push as it's unable to inferclass-string<UnitEnum>
fromenum_exists()
.