Skip to content

Commit 58a765d

Browse files
jeroworkspawnia
authored andcommitted
Fix "Cannot traverse an already closed generator" in Schema::getTypeMap()
When `SchemaConfig::$types` is configured with a 'bare' `Generator` (an iterable that can only be traversed once, which is allowed according to the private property type), both `getScalarOverrides()` and `getTypeMap()` need to iterate over it. Previously, each method independently resolved `config->types` — calling the callable if needed and then iterating the result. This caused a problem: if the generator was consumed by `getScalarOverrides()` (either called from `getType()` beforehand, or from within `getTypeMap()` itself), the subsequent `foreach` in `getTypeMap()` would fail with "Cannot traverse an already closed generator". This fix extracts a `materializeTypes()` method that both `getScalarOverrides()` and `getTypeMap()` now call. This method resolves `config->types` by invoking the callable (if applicable) and converting any non-array iterable (such as a `Generator`) into an array via `iterator_to_array()`, storing the result back in `config->types`. This ensures the generator is consumed exactly once and all subsequent access operates on the materialized array.
1 parent e7ff849 commit 58a765d

2 files changed

Lines changed: 49 additions & 20 deletions

File tree

src/Type/Schema.php

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,12 @@ public function __construct($config)
122122
public function getTypeMap(): array
123123
{
124124
if (! $this->fullyLoaded) {
125-
$types = $this->config->types;
126-
if (is_callable($types)) {
127-
$types = $types();
128-
}
129-
130125
// Reset order of user provided types, since calls to getType() may have loaded them
131126
$this->resolvedTypes = [];
132127

133128
$scalarOverrides = $this->getScalarOverrides();
134129

135-
foreach ($types as $typeOrLazyType) {
130+
foreach ($this->materializeTypes() as $typeOrLazyType) {
136131
/** @var Type|callable(): Type $typeOrLazyType */
137132
$type = self::resolveType($typeOrLazyType);
138133
assert($type instanceof NamedType);
@@ -380,20 +375,8 @@ private function getScalarOverrides(): array
380375
if ($this->scalarOverrides === null) {
381376
$this->scalarOverrides = [];
382377

383-
$types = $this->config->types;
384-
if (is_callable($types)) {
385-
$types = $types();
386-
}
387-
388-
// Materialize the iterable in case it is a generator, so that
389-
// getTypeMap() can still iterate config->types later.
390-
if (! is_array($types)) {
391-
$types = iterator_to_array($types);
392-
$this->config->types = $types;
393-
}
394-
395378
$builtInScalars = Type::builtInScalars();
396-
foreach ($types as $typeOrLazyType) {
379+
foreach ($this->materializeTypes() as $typeOrLazyType) {
397380
/** @var Type|callable(): Type $typeOrLazyType */
398381
$type = self::resolveType($typeOrLazyType);
399382
if ($type instanceof ScalarType
@@ -408,6 +391,26 @@ private function getScalarOverrides(): array
408391
return $this->scalarOverrides;
409392
}
410393

394+
/**
395+
* Resolve config->types to an array, materializing callables and generators.
396+
*
397+
* @return array<Type|callable(): Type>
398+
*/
399+
private function materializeTypes(): array
400+
{
401+
$types = $this->config->types;
402+
if (is_callable($types)) {
403+
$types = $types();
404+
}
405+
406+
if (! is_array($types)) {
407+
$types = iterator_to_array($types);
408+
$this->config->types = $types;
409+
}
410+
411+
return $types;
412+
}
413+
411414
/**
412415
* @template T of Type
413416
*

tests/Type/ScalarOverridesTest.php

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ public function testTypesOverrideWorksWithCallableTypesConfig(): void
487487
self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray());
488488
}
489489

490-
public function testTypesOverrideWorksWithGeneratorTypesConfig(): void
490+
public function testTypesOverrideWorksWithCallableGeneratorTypesConfig(): void
491491
{
492492
$uppercaseString = self::createUppercaseString();
493493
$queryType = self::createQueryType();
@@ -507,6 +507,26 @@ public function testTypesOverrideWorksWithGeneratorTypesConfig(): void
507507
$schema->assertValid();
508508
}
509509

510+
public function testTypesOverrideWorksWithGeneratorTypesConfig(): void
511+
{
512+
$uppercaseString = self::createUppercaseString();
513+
$queryType = self::createQueryType();
514+
515+
/** @var \Generator<int, CustomScalarType> $types */
516+
$types = self::generateTypes($uppercaseString);
517+
518+
$schema = new Schema([
519+
'query' => $queryType,
520+
'types' => $types,
521+
]);
522+
523+
$result = GraphQL::executeQuery($schema, '{ greeting }');
524+
525+
self::assertSame(['data' => ['greeting' => 'HELLO WORLD']], $result->toArray());
526+
527+
$schema->assertValid();
528+
}
529+
510530
public function testGetTypeThenAssertValidBothWorkWithTypeLoader(): void
511531
{
512532
$uppercaseString = self::createUppercaseString();
@@ -610,6 +630,12 @@ private static function createUppercaseString(): CustomScalarType
610630
]);
611631
}
612632

633+
/** @return \Generator<int|string, ScalarType> */
634+
private static function generateTypes(ScalarType ...$types): \Generator
635+
{
636+
yield from $types;
637+
}
638+
613639
/** @throws InvariantViolation */
614640
private static function createQueryType(): ObjectType
615641
{

0 commit comments

Comments
 (0)