Skip to content

Commit 087f559

Browse files
committedJun 17, 2024··
BREAKING New TypesInterface::loadType() #10525
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.
1 parent 5812837 commit 087f559

6 files changed

+109
-7
lines changed
 

‎phpstan-baseline.neon

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: "#^Method GraphQL\\\\Doctrine\\\\Types\\:\\:getOperator\\(\\) should return GraphQL\\\\Doctrine\\\\Definition\\\\Operator\\\\AbstractOperator but returns GraphQL\\\\Type\\\\Definition\\\\NamedType&GraphQL\\\\Type\\\\Definition\\\\Type\\.$#"
5+
count: 1
6+
path: src/Types.php

‎phpstan.neon.dist

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ parameters:
1717
- '~^Parameter \#1 \$type of static method GraphQL\\Type\\Definition\\Type\:\:nonNull\(\) expects~'
1818
- '~^Parameter \#1 \$config of class GraphQL\\Type\\Definition\\InputObjectType constructor expects~'
1919
- '~^Parameter \#1 \$config of method GraphQL\\Type\\Definition\\InputObjectType\:\:__construct~'
20+
21+
includes:
22+
- phpstan-baseline.neon

‎src/Factory/Type/AbstractTypeFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ abstract class AbstractTypeFactory extends AbstractFactory
2121
* @param class-string $className class name of Doctrine entity
2222
* @param string $typeName GraphQL type name
2323
*/
24-
abstract public function create(string $className, string $typeName): NamedType;
24+
abstract public function create(string $className, string $typeName): Type&NamedType;
2525

2626
/**
2727
* Get the description of a class from the doc block.

‎src/Types.php

+40-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
final class Types implements TypesInterface
4040
{
4141
/**
42-
* @var array mapping of type name to type instances
42+
* @var array<string, NamedType&Type> mapping of type name to type instances
4343
*/
4444
private array $types = [];
4545

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

87-
public function get(string $key): NamedType
87+
public function get(string $key): Type&NamedType
8888
{
8989
if ($this->customTypes && $this->customTypes->has($key)) {
90-
/** @var NamedType $t */
90+
/** @var NamedType&Type $t */
9191
$t = $this->customTypes->get($key);
9292
$this->registerInstance($t);
9393

@@ -106,7 +106,7 @@ public function get(string $key): NamedType
106106
*
107107
* @param class-string $className
108108
*/
109-
private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type
109+
private function getViaFactory(string $className, string $typeName, AbstractTypeFactory $factory): Type&NamedType
110110
{
111111
$this->throwIfNotEntity($className);
112112

@@ -239,7 +239,7 @@ public function getOperator(string $className, LeafType $type): AbstractOperator
239239
*
240240
* This is for internal use only. You should declare custom types via the constructor, not this method.
241241
*/
242-
public function registerInstance(NamedType $instance): void
242+
public function registerInstance(Type&NamedType $instance): void
243243
{
244244
$this->types[$instance->name()] = $instance;
245245
}
@@ -294,4 +294,39 @@ public function createFilteredQueryBuilder(string $className, array $filter, arr
294294
{
295295
return $this->filteredQueryBuilderFactory->create($className, $filter, $sorting);
296296
}
297+
298+
public function loadType(string $typeName, string $namespace): ?Type
299+
{
300+
if ($this->has($typeName)) {
301+
return $this->get($typeName);
302+
}
303+
304+
if (preg_match('~^(?<shortName>.*)(?<kind>PartialInput)$~', $typeName, $m)
305+
|| preg_match('~^(?<shortName>.*)(?<kind>Input|PartialInput|Filter|Sorting|FilterGroupJoin|FilterGroupCondition|ID)$~', $typeName, $m)
306+
|| preg_match('~^(?<kind>JoinOn)(?<shortName>.*)$~', $typeName, $m)
307+
|| preg_match('~^(?<shortName>.*)$~', $typeName, $m)) {
308+
$shortName = $m['shortName'];
309+
$kind = $m['kind'] ?? '';
310+
311+
/** @var class-string $className */
312+
$className = $namespace . '\\' . $shortName;
313+
314+
if ($this->isEntity($className)) {
315+
return match ($kind) {
316+
'Input' => $this->getViaFactory($className, $typeName, $this->inputTypeFactory),
317+
'PartialInput' => $this->getViaFactory($className, $typeName, $this->partialInputTypeFactory),
318+
'Filter' => $this->getViaFactory($className, $typeName, $this->filterTypeFactory),
319+
'Sorting' => $this->getViaFactory($className, $typeName, $this->sortingTypeFactory),
320+
'JoinOn' => $this->getViaFactory($className, $typeName, $this->joinOnTypeFactory),
321+
'FilterGroupJoin' => $this->getViaFactory($className, $typeName, $this->filterGroupJoinTypeFactory),
322+
'FilterGroupCondition' => $this->getViaFactory($className, $typeName, $this->filterGroupConditionTypeFactory),
323+
'ID' => $this->getViaFactory($className, $typeName, $this->entityIDTypeFactory),
324+
'' => $this->getViaFactory($className, $typeName, $this->objectTypeFactory),
325+
default => throw new Exception("Unsupported kind of type `$kind` when trying to load type `$typeName`"),
326+
};
327+
}
328+
}
329+
330+
return null;
331+
}
297332
}

‎src/TypesInterface.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use GraphQL\Type\Definition\ListOfType;
1111
use GraphQL\Type\Definition\NamedType;
1212
use GraphQL\Type\Definition\ObjectType;
13+
use GraphQL\Type\Definition\Type;
1314

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

3839
/**
3940
* Returns an output type for the given entity.
@@ -116,4 +117,25 @@ public function getId(string $className): EntityIDType;
116117
* @param class-string $className
117118
*/
118119
public function createFilteredQueryBuilder(string $className, array $filter, array $sorting): QueryBuilder;
120+
121+
/**
122+
* Load a type from its name.
123+
*
124+
* This should be used to declare typeLoader in `GraphQL\Type\Schema` with something similar to:
125+
*
126+
* ```php
127+
* $types = new Types(...);
128+
* $schema = new GraphQL\Type\Schema([
129+
* 'typeLoader' => fn (string $name) => $types->loadType($name, 'Application\Model') ?? $types->loadType($name, 'OtherApplication\Model')
130+
* // ...
131+
* ]);
132+
* ```
133+
*
134+
* While this method could technically replace of uses of dedicated `get*()` methods, we suggest to only use
135+
* `loadType` with the `typeLoader`. Because dedicated `get*()` methods are easier to use, and provide
136+
* stronger typing.
137+
*
138+
* @return null|(Type&NamedType)
139+
*/
140+
public function loadType(string $typeName, string $namespace): ?Type;
119141
}

‎tests/TypesTest.php

+36
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,40 @@ public function testHas(): void
156156
$this->types->get(stdClass::class);
157157
self::assertTrue($this->types->has('customName'), 'should have custom registered type by its name, even if custom key was different, once type is created');
158158
}
159+
160+
/**
161+
* @dataProvider provideLoadType
162+
*/
163+
public function testLoadType(string $typeName): void
164+
{
165+
$type = $this->types->loadType($typeName, 'GraphQLTests\Doctrine\Blog\Model');
166+
self::assertNotNull($type, 'should be able to lazy load a generated type by its name only');
167+
self::assertSame($typeName, $type->name(), 'loaded type must have same name');
168+
}
169+
170+
public static function provideLoadType(): iterable
171+
{
172+
yield 'PostInput' => ['PostInput'];
173+
yield 'PostPartialInput' => ['PostPartialInput'];
174+
yield 'Post' => ['Post'];
175+
yield 'PostID' => ['PostID'];
176+
yield 'PostFilter' => ['PostFilter'];
177+
yield 'PostFilterGroupJoin' => ['PostFilterGroupJoin'];
178+
yield 'PostSorting' => ['PostSorting'];
179+
yield 'PostStatus' => ['PostStatus'];
180+
yield 'PostFilterGroupCondition' => ['PostFilterGroupCondition'];
181+
yield 'JoinOnPost' => ['JoinOnPost'];
182+
}
183+
184+
public function testLoadUnknownType(): void
185+
{
186+
$type = $this->types->loadType('unknown-type-name', 'GraphQLTests\Doctrine\Blog\Model');
187+
self::assertNull($type, 'should return null if type is not found to be chainable');
188+
}
189+
190+
public function testLoadTypeInUnknownNamespace(): void
191+
{
192+
$type = $this->types->loadType('Post', 'Unknown\Model');
193+
self::assertNull($type, 'should return null if namespace is not found to be chainable');
194+
}
159195
}

0 commit comments

Comments
 (0)
Please sign in to comment.