Skip to content

Commit

Permalink
Merge 5292908 into dcc68bd
Browse files Browse the repository at this point in the history
  • Loading branch information
moufmouf committed Apr 6, 2020
2 parents dcc68bd + 5292908 commit d249ff2
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 85 deletions.
5 changes: 5 additions & 0 deletions docs/type_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ class StatusEnum extends Enum
}
```

<div class="alert alert-warning">GraphQLite must be able to find all the classes extending the "MyCLabs\Enum" class
in your project. By default, GraphQLite will look for "Enum" classes in the namespaces declared for the types. For this
reason, <strong>your enum classes MUST be in one of the namespaces declared for the types in your GraphQLite
configuration file.</strong></div>


<div class="alert alert-info">There are many enumeration library in PHP and you might be using another library.
If you want to add support for your own library, this is not extremely difficult to do. You need to register a custom
Expand Down
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@

<rule ref="SlevomatCodingStandard.TypeHints.PropertyTypeHint">
<properties>
<property name="enableNativeTypeHint" value="0"/>
<property name="enableNativeTypeHint" value="false"/>
</properties>
</rule>

Expand Down
3 changes: 0 additions & 3 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ parameters:
-
message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array<int, T of object> but returns array<object>.#'
path: src/AnnotationReader.php
-
message: '#Parameter \#1 \$enumClass of method TheCodingMachine\\GraphQLite\\Mappers\\Root\\MyCLabsEnumTypeMapper::getTypeName\(\) expects class-string<MyCLabs\\Enum\\Enum>, class-string<object> given.#'
path: src/Mappers/Root/MyCLabsEnumTypeMapper.php
- '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage()#'
# Needed because of a bug in PHP-CS
- '#PHPDoc tag @param for parameter \$args with type mixed is not subtype of native type array<int, mixed>.#'
Expand Down
53 changes: 8 additions & 45 deletions src/Mappers/GlobTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,37 @@

namespace TheCodingMachine\GraphQLite\Mappers;

use Mouf\Composer\ClassNameMapper;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use TheCodingMachine\GraphQLite\AnnotationReader;
use TheCodingMachine\GraphQLite\InputTypeGenerator;
use TheCodingMachine\GraphQLite\InputTypeUtils;
use TheCodingMachine\GraphQLite\NamingStrategyInterface;
use TheCodingMachine\GraphQLite\TypeGenerator;
use function class_exists;
use function interface_exists;
use TheCodingMachine\GraphQLite\Utils\Namespaces\NS;
use function str_replace;

/**
* Scans all the classes in a given namespace of the main project (not the vendor directory).
* Analyzes all classes and uses the @Type annotation to find the types automatically.
*
* Assumes that the container contains a class whose identifier is the same as the class name.
*
* @internal
*/
final class GlobTypeMapper extends AbstractTypeMapper
{
/** @var string */
/** @var NS */
private $namespace;
/**
* The array of globbed classes.
* Only instantiable classes are returned.
* Key: fully qualified class name
*
* @var array<string,ReflectionClass<object>>
*/
private $classes;
/** @var bool */
private $recursive;
/** @var ClassNameMapper */
private $classNameMapper;

/**
* @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
* @param NS $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
*/
public function __construct(string $namespace, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, RecursiveTypeMapperInterface $recursiveTypeMapper, CacheInterface $cache, ?ClassNameMapper $classNameMapper = null, ?int $globTTL = 2, ?int $mapTTL = null, bool $recursive = true)
public function __construct(NS $namespace, TypeGenerator $typeGenerator, InputTypeGenerator $inputTypeGenerator, InputTypeUtils $inputTypeUtils, ContainerInterface $container, AnnotationReader $annotationReader, NamingStrategyInterface $namingStrategy, RecursiveTypeMapperInterface $recursiveTypeMapper, CacheInterface $cache, ?int $globTTL = 2, ?int $mapTTL = null)
{
$this->namespace = $namespace;
$this->recursive = $recursive;
$this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true);
$cachePrefix = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace);
$cachePrefix = str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace->getNamespace());
parent::__construct($cachePrefix, $typeGenerator, $inputTypeGenerator, $inputTypeUtils, $container, $annotationReader, $namingStrategy, $recursiveTypeMapper, $cache, $globTTL, $mapTTL);
}

Expand All @@ -61,28 +46,6 @@ public function __construct(string $namespace, TypeGenerator $typeGenerator, Inp
*/
protected function getClassList(): array
{
if ($this->classes === null) {
$this->classes = [];
$explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTTL, $this->classNameMapper, $this->recursive);
$classes = $explorer->getClassMap();
foreach ($classes as $className => $phpFile) {
if (! class_exists($className, false) && ! interface_exists($className, false)) {
// Let's try to load the file if it was not imported yet.
// We are importing the file manually to avoid triggering the autoloader.
// The autoloader might trigger errors if the file does not respect PSR-4 or if the
// Symfony DebugAutoLoader is installed. (see https://github.com/thecodingmachine/graphqlite/issues/216)
require_once $phpFile;
// Does it exists now?
if (! class_exists($className, false) && ! interface_exists($className, false)) {
continue;
}
}

$refClass = new ReflectionClass($className);
$this->classes[$className] = $refClass;
}
}

return $this->classes;
return $this->namespace->getClassList();
}
}
66 changes: 59 additions & 7 deletions src/Mappers/Root/MyCLabsEnumTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
use phpDocumentor\Reflection\Types\Object_;
use ReflectionClass;
use ReflectionMethod;
use Symfony\Contracts\Cache\CacheInterface;
use TheCodingMachine\GraphQLite\AnnotationReader;
use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType;
use TheCodingMachine\GraphQLite\Utils\Namespaces\NS;
use function assert;
use function is_a;
use function ltrim;

/**
* Maps an class extending MyCLabs enums to a GraphQL type
Expand All @@ -32,11 +36,20 @@ class MyCLabsEnumTypeMapper implements RootTypeMapperInterface
private $next;
/** @var AnnotationReader */
private $annotationReader;
/** @var array|NS[] */
private $namespaces;
/** @var CacheInterface */
private $cacheService;

public function __construct(RootTypeMapperInterface $next, AnnotationReader $annotationReader)
/**
* @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;
}

/**
Expand Down Expand Up @@ -94,20 +107,21 @@ private function mapByClassName(string $enumClass): ?EnumType
if (! is_a($enumClass, Enum::class, true)) {
return null;
}
/**
* @var class-string<Enum>
*/
$enumClass = ltrim($enumClass, '\\');
if (isset($this->cache[$enumClass])) {
return $this->cache[$enumClass];
}

$type = new MyCLabsEnumType($enumClass, $this->getTypeName($enumClass));
$refClass = new ReflectionClass($enumClass);
$type = new MyCLabsEnumType($enumClass, $this->getTypeName($refClass));
return $this->cacheByName[$type->name] = $this->cache[$enumClass] = $type;
}

/**
* @param class-string<Enum> $enumClass
*/
private function getTypeName(string $enumClass): string
private function getTypeName(ReflectionClass $refClass): string
{
$refClass = new ReflectionClass($enumClass);
$enumType = $this->annotationReader->getEnumTypeAnnotation($refClass);
if ($enumType !== null) {
$name = $enumType->getName();
Expand All @@ -133,6 +147,15 @@ public function mapNameToType(string $typeName): NamedType
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;
}

/*if (strpos($typeName, 'MyCLabsEnum_') === 0) {
$className = str_replace('__', '\\', substr($typeName, 12));
Expand All @@ -144,4 +167,33 @@ public function mapNameToType(string $typeName): NamedType

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

/** @var array<string, class-string<Enum>> */
private $nameToClassMapping;

/**
* Go through all classes in the defined namespaces and loads the cache.
*
* @return array<string, class-string<Enum>>
*/
private function getNameToClassMapping(): array
{
if ($this->nameToClassMapping === null) {
$this->nameToClassMapping = $this->cacheService->get('myclabsenum_name_to_class', function () {
$nameToClassMapping = [];
foreach ($this->namespaces as $ns) {
foreach ($ns->getClassList() as $className => $classRef) {
if (! $classRef->isSubclassOf(Enum::class)) {
continue;
}

$nameToClassMapping[$this->getTypeName($classRef)] = $className;
}
}
return $nameToClassMapping;
});
}

return $this->nameToClassMapping;
}
}
15 changes: 11 additions & 4 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Utils\NamespacedCache;
use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory;
use function array_map;
use function array_reverse;
use function crc32;
use function function_exists;
Expand Down Expand Up @@ -316,6 +318,12 @@ public function createSchema(): Schema
$cachedDocBlockFactory = new CachedDocBlockFactory($this->cache);
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
$typeRegistry = new TypeRegistry();
$symfonyCache = new Psr16Adapter($this->cache);

$namespaceFactory = new NamespaceFactory($this->cache, $this->classNameMapper, $this->globTTL);
$nsList = array_map(static function (string $namespace) use ($namespaceFactory) {
return $namespaceFactory->createNamespace($namespace);
}, $this->typeNamespaces);

$psr6Cache = new Psr16Adapter($this->cache);
$expressionLanguage = $this->expressionLanguage ?: new ExpressionLanguage($psr6Cache);
Expand All @@ -336,7 +344,7 @@ public function createSchema(): Schema

$errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper);
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper);
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader);
$rootTypeMapper = new MyCLabsEnumTypeMapper($rootTypeMapper, $annotationReader, $symfonyCache, $nsList);

if (! empty($this->rootTypeMapperFactories)) {
$rootSchemaFactoryContext = new RootTypeMapperFactoryContext(
Expand Down Expand Up @@ -391,9 +399,9 @@ public function createSchema(): Schema
throw new GraphQLRuntimeException('Cannot create schema: no namespace for types found (You must call the SchemaFactory::addTypeNamespace() at least once).');
}

foreach ($this->typeNamespaces as $typeNamespace) {
foreach ($nsList as $ns) {
$compositeTypeMapper->addTypeMapper(new GlobTypeMapper(
$typeNamespace,
$ns,
$typeGenerator,
$inputTypeGenerator,
$inputTypeUtils,
Expand All @@ -402,7 +410,6 @@ public function createSchema(): Schema
$namingStrategy,
$recursiveTypeMapper,
$this->cache,
$this->classNameMapper,
$this->globTTL
));
}
Expand Down
89 changes: 89 additions & 0 deletions src/Utils/Namespaces/NS.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Utils\Namespaces;

use Mouf\Composer\ClassNameMapper;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use function class_exists;
use function interface_exists;

/**
* The NS class represents a PHP Namespace and provides utility methods to explore those classes.
*
* @internal
*/
final class NS
{
/** @var string */
private $namespace;
/**
* The array of globbed classes.
* Only instantiable classes are returned.
* Key: fully qualified class name
*
* @var array<string,ReflectionClass<object>>
*/
private $classes;
/** @var bool */
private $recursive;
/** @var ClassNameMapper */
private $classNameMapper;
/** @var CacheInterface */
private $cache;
/** @var int|null */
private $globTTL;

/**
* @param string $namespace The namespace that contains the GraphQL types (they must have a `@Type` annotation)
*/
public function __construct(string $namespace, CacheInterface $cache, ClassNameMapper $classNameMapper, ?int $globTTL, bool $recursive)
{
$this->namespace = $namespace;
$this->recursive = $recursive;
$this->cache = $cache;
$this->classNameMapper = $classNameMapper;
$this->globTTL = $globTTL;
}

/**
* Returns the array of globbed classes.
* Only instantiable classes are returned.
*
* @return array<string,ReflectionClass<object>> Key: fully qualified class name
*/
public function getClassList(): array
{
if ($this->classes === null) {
$this->classes = [];
$explorer = new GlobClassExplorer($this->namespace, $this->cache, $this->globTTL, $this->classNameMapper, $this->recursive);
$classes = $explorer->getClassMap();
foreach ($classes as $className => $phpFile) {
if (! class_exists($className, false) && ! interface_exists($className, false)) {
// Let's try to load the file if it was not imported yet.
// We are importing the file manually to avoid triggering the autoloader.
// The autoloader might trigger errors if the file does not respect PSR-4 or if the
// Symfony DebugAutoLoader is installed. (see https://github.com/thecodingmachine/graphqlite/issues/216)
require_once $phpFile;
// Does it exists now?
if (! class_exists($className, false) && ! interface_exists($className, false)) {
continue;
}
}

$refClass = new ReflectionClass($className);
$this->classes[$className] = $refClass;
}
}

return $this->classes;
}

public function getNamespace(): string
{
return $this->namespace;
}
}

0 comments on commit d249ff2

Please sign in to comment.