Skip to content

Commit

Permalink
BREAKING New TypesInterface::loadType() #10525
Browse files Browse the repository at this point in the history
This should be used to declare typeLoader in `GraphQL\Type\Schema` with
something similar to:

```php
$types = new Types(...);
$schema = new GraphQL\Type\Schema([
    'typeLoader' => fn (string $name) => $types->loadType($name, 'Application\Model') ?? $types->loadType($name, 'OtherApplication\Model')
    // ...
]);
```

While this method could technically replace of uses of dedicated `get*()
`methods, we suggest to only use `loadType` with the `typeLoader`.
Because dedicated `get*()` methods are easier to use, and provide
stronger typing.

This is a breaking change because of the new method on `TypesInterface`,
but pre-existing behavior remains unchanged.
  • Loading branch information
PowerKiKi committed Jun 17, 2024
1 parent 5812837 commit 087f559
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 7 deletions.
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
ignoreErrors:
-
message: "#^Method GraphQL\\\\Doctrine\\\\Types\\:\\:getOperator\\(\\) should return GraphQL\\\\Doctrine\\\\Definition\\\\Operator\\\\AbstractOperator but returns GraphQL\\\\Type\\\\Definition\\\\NamedType&GraphQL\\\\Type\\\\Definition\\\\Type\\.$#"
count: 1
path: src/Types.php
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ parameters:
- '~^Parameter \#1 \$type of static method GraphQL\\Type\\Definition\\Type\:\:nonNull\(\) expects~'
- '~^Parameter \#1 \$config of class GraphQL\\Type\\Definition\\InputObjectType constructor expects~'
- '~^Parameter \#1 \$config of method GraphQL\\Type\\Definition\\InputObjectType\:\:__construct~'

includes:
- phpstan-baseline.neon
2 changes: 1 addition & 1 deletion src/Factory/Type/AbstractTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract class AbstractTypeFactory extends AbstractFactory
* @param class-string $className class name of Doctrine entity
* @param string $typeName GraphQL type name
*/
abstract public function create(string $className, string $typeName): NamedType;
abstract public function create(string $className, string $typeName): Type&NamedType;

/**
* Get the description of a class from the doc block.
Expand Down
45 changes: 40 additions & 5 deletions src/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
final class Types implements TypesInterface
{
/**
* @var array mapping of type name to type instances
* @var array<string, NamedType&Type> mapping of type name to type instances
*/
private array $types = [];

Expand Down Expand Up @@ -84,10 +84,10 @@ public function has(string $key): bool
return $this->customTypes && $this->customTypes->has($key) || array_key_exists($key, $this->types);
}

public function get(string $key): NamedType
public function get(string $key): Type&NamedType
{
if ($this->customTypes && $this->customTypes->has($key)) {
/** @var NamedType $t */
/** @var NamedType&Type $t */
$t = $this->customTypes->get($key);
$this->registerInstance($t);

Expand All @@ -106,7 +106,7 @@ public function get(string $key): NamedType
*
* @param class-string $className
*/
private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type
private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type&NamedType
{
$this->throwIfNotEntity($className);

Expand Down Expand Up @@ -239,7 +239,7 @@ public function getOperator(string $className, LeafType $type): AbstractOperator
*
* This is for internal use only. You should declare custom types via the constructor, not this method.
*/
public function registerInstance(NamedType $instance): void
public function registerInstance(Type&NamedType $instance): void
{
$this->types[$instance->name()] = $instance;
}
Expand Down Expand Up @@ -294,4 +294,39 @@ public function createFilteredQueryBuilder(string $className, array $filter, arr
{
return $this->filteredQueryBuilderFactory->create($className, $filter, $sorting);
}

public function loadType(string $typeName, string $namespace): ?Type
{
if ($this->has($typeName)) {
return $this->get($typeName);
}

if (preg_match('~^(?<shortName>.*)(?<kind>PartialInput)$~', $typeName, $m)
|| preg_match('~^(?<shortName>.*)(?<kind>Input|PartialInput|Filter|Sorting|FilterGroupJoin|FilterGroupCondition|ID)$~', $typeName, $m)
|| preg_match('~^(?<kind>JoinOn)(?<shortName>.*)$~', $typeName, $m)
|| preg_match('~^(?<shortName>.*)$~', $typeName, $m)) {
$shortName = $m['shortName'];
$kind = $m['kind'] ?? '';

/** @var class-string $className */
$className = $namespace . '\\' . $shortName;

if ($this->isEntity($className)) {
return match ($kind) {
'Input' => $this->getViaFactory($className, $typeName, $this->inputTypeFactory),
'PartialInput' => $this->getViaFactory($className, $typeName, $this->partialInputTypeFactory),
'Filter' => $this->getViaFactory($className, $typeName, $this->filterTypeFactory),
'Sorting' => $this->getViaFactory($className, $typeName, $this->sortingTypeFactory),
'JoinOn' => $this->getViaFactory($className, $typeName, $this->joinOnTypeFactory),
'FilterGroupJoin' => $this->getViaFactory($className, $typeName, $this->filterGroupJoinTypeFactory),
'FilterGroupCondition' => $this->getViaFactory($className, $typeName, $this->filterGroupConditionTypeFactory),
'ID' => $this->getViaFactory($className, $typeName, $this->entityIDTypeFactory),
'' => $this->getViaFactory($className, $typeName, $this->objectTypeFactory),
default => throw new Exception("Unsupported kind of type `$kind` when trying to load type `$typeName`"),
};
}
}

return null;
}
}
24 changes: 23 additions & 1 deletion src/TypesInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

/**
* Registry of types to manage all GraphQL types.
Expand All @@ -33,7 +34,7 @@ public function has(string $key): bool;
*
* @param string $key the key the type was registered with (eg: "Post", "PostInput", "PostPartialInput" or "PostStatus")
*/
public function get(string $key): NamedType;
public function get(string $key): Type&NamedType;

/**
* Returns an output type for the given entity.
Expand Down Expand Up @@ -116,4 +117,25 @@ public function getId(string $className): EntityIDType;
* @param class-string $className
*/
public function createFilteredQueryBuilder(string $className, array $filter, array $sorting): QueryBuilder;

/**
* Load a type from its name.
*
* This should be used to declare typeLoader in `GraphQL\Type\Schema` with something similar to:
*
* ```php
* $types = new Types(...);
* $schema = new GraphQL\Type\Schema([
* 'typeLoader' => fn (string $name) => $types->loadType($name, 'Application\Model') ?? $types->loadType($name, 'OtherApplication\Model')
* // ...
* ]);
* ```
*
* While this method could technically replace of uses of dedicated `get*()` methods, we suggest to only use
* `loadType` with the `typeLoader`. Because dedicated `get*()` methods are easier to use, and provide
* stronger typing.
*
* @return null|(Type&NamedType)
*/
public function loadType(string $typeName, string $namespace): ?Type;
}
36 changes: 36 additions & 0 deletions tests/TypesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,40 @@ public function testHas(): void
$this->types->get(stdClass::class);
self::assertTrue($this->types->has('customName'), 'should have custom registered type by its name, even if custom key was different, once type is created');
}

/**
* @dataProvider provideLoadType
*/
public function testLoadType(string $typeName): void
{
$type = $this->types->loadType($typeName, 'GraphQLTests\Doctrine\Blog\Model');
self::assertNotNull($type, 'should be able to lazy load a generated type by its name only');
self::assertSame($typeName, $type->name(), 'loaded type must have same name');
}

public static function provideLoadType(): iterable
{
yield 'PostInput' => ['PostInput'];
yield 'PostPartialInput' => ['PostPartialInput'];
yield 'Post' => ['Post'];
yield 'PostID' => ['PostID'];
yield 'PostFilter' => ['PostFilter'];
yield 'PostFilterGroupJoin' => ['PostFilterGroupJoin'];
yield 'PostSorting' => ['PostSorting'];
yield 'PostStatus' => ['PostStatus'];
yield 'PostFilterGroupCondition' => ['PostFilterGroupCondition'];
yield 'JoinOnPost' => ['JoinOnPost'];
}

public function testLoadUnknownType(): void
{
$type = $this->types->loadType('unknown-type-name', 'GraphQLTests\Doctrine\Blog\Model');
self::assertNull($type, 'should return null if type is not found to be chainable');
}

public function testLoadTypeInUnknownNamespace(): void
{
$type = $this->types->loadType('Post', 'Unknown\Model');
self::assertNull($type, 'should return null if namespace is not found to be chainable');
}
}

0 comments on commit 087f559

Please sign in to comment.