From 0ce48c353d383c7c8038c8d5be3ca9888cbda512 Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 1/6] Added IsPublic support to InputObject --- src/Config/InputObjectTypeDefinition.php | 8 ++++++++ src/Config/Parser/MetadataParser/MetadataParser.php | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Config/InputObjectTypeDefinition.php b/src/Config/InputObjectTypeDefinition.php index 5ed2fd46c..1453467ae 100644 --- a/src/Config/InputObjectTypeDefinition.php +++ b/src/Config/InputObjectTypeDefinition.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Config; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Builder\VariableNodeDefinition; use function is_string; @@ -31,6 +32,7 @@ public function getDefinition(): ArrayNodeDefinition ->append($this->typeSection(true)) ->append($this->descriptionSection()) ->append($this->defaultValueSection()) + ->append($this->publicSection()) ->append($this->validationSection(self::VALIDATION_LEVEL_PROPERTY)) ->append($this->deprecationReasonSection()) ->end() @@ -42,4 +44,10 @@ public function getDefinition(): ArrayNodeDefinition return $node; } + + protected function publicSection(): VariableNodeDefinition + { + return self::createNode('public', 'variable') + ->info('Visibility control to field (expression language can be used here)'); + } } diff --git a/src/Config/Parser/MetadataParser/MetadataParser.php b/src/Config/Parser/MetadataParser/MetadataParser.php index 10edbe896..295ef8c2d 100644 --- a/src/Config/Parser/MetadataParser/MetadataParser.php +++ b/src/Config/Parser/MetadataParser/MetadataParser.php @@ -692,9 +692,14 @@ private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $refl /** @var Metadata\Field|null $fieldMetadata */ $fieldMetadata = self::getFirstMetadataMatching($metadatas, Metadata\Field::class); + $publicMetadata = self::getFirstMetadataMatching($metadatas, Metadata\IsPublic::class); // No field metadata found if (null === $fieldMetadata) { + if (null !== $publicMetadata) { + throw new InvalidArgumentException(sprintf('The metadatas %s defined on "%s" are only usable in addition of metadata %s', self::formatMetadata('Visible'), $reflector->getName(), self::formatMetadata('Field'))); + } + continue; } @@ -731,6 +736,10 @@ private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $refl $fieldConfiguration['defaultValue'] = $reflector->getDefaultValue(); } + if ($publicMetadata) { + $fieldConfiguration['public'] = self::formatExpression($publicMetadata->value); + } + $fieldConfiguration = array_merge(self::getDescriptionConfiguration($metadatas, true), $fieldConfiguration); $fields[$fieldName] = $fieldConfiguration; } From f622c20bbdbf7520fc72ac424e410fc851f0ab61 Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 2/6] Added getCurrentSchemaName for TypeResolver --- src/Resolver/TypeResolver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Resolver/TypeResolver.php b/src/Resolver/TypeResolver.php index c5e5d4a6e..2cf5d3718 100644 --- a/src/Resolver/TypeResolver.php +++ b/src/Resolver/TypeResolver.php @@ -28,6 +28,11 @@ public function setCurrentSchemaName(?string $currentSchemaName): void $this->currentSchemaName = $currentSchemaName; } + public function getCurrentSchemaName(): ?string + { + return $this->currentSchemaName; + } + public function setIgnoreUnresolvableException(bool $ignoreUnresolvableException): void { $this->ignoreUnresolvableException = $ignoreUnresolvableException; From cc4838ead19e3fcfe6caa2decbd303fc9cfb663c Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 3/6] feat: resettable schemas fix: resettable schemas fix: resettable schemas --- src/Definition/Builder/SchemaBuilder.php | 26 +++++++++---- src/Definition/Type/ExtensibleSchema.php | 15 +++++++ src/DependencyInjection/Configuration.php | 1 + .../OverblogGraphQLExtension.php | 1 + src/Request/Executor.php | 39 +++++++++++++------ src/Resolver/AbstractResolver.php | 25 +++++++----- src/Resolver/TypeResolver.php | 2 + src/Resources/config/services.yaml | 2 + 8 files changed, 81 insertions(+), 30 deletions(-) diff --git a/src/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index dc3482bd1..52820b49e 100644 --- a/src/Definition/Builder/SchemaBuilder.php +++ b/src/Definition/Builder/SchemaBuilder.php @@ -10,12 +10,14 @@ use Overblog\GraphQLBundle\Definition\Type\SchemaExtension\ValidatorExtension; use Overblog\GraphQLBundle\Resolver\TypeResolver; +use Symfony\Contracts\Service\ResetInterface; use function array_map; -final class SchemaBuilder +final class SchemaBuilder implements ResetInterface { private TypeResolver $typeResolver; private bool $enableValidation; + private array $builders = []; public function __construct(TypeResolver $typeResolver, bool $enableValidation = false) { @@ -23,22 +25,21 @@ public function __construct(TypeResolver $typeResolver, bool $enableValidation = $this->enableValidation = $enableValidation; } - public function getBuilder(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): Closure + public function getBuilder(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = [], bool $resettable = false): Closure { - return function () use ($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types): ExtensibleSchema { - static $schema = null; - if (null === $schema) { - $schema = $this->create($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types); + return function () use ($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types, $resettable): ExtensibleSchema { + if (!isset($this->builders[$name])) { + $this->builders[$name] = $this->create($name, $queryAlias, $mutationAlias, $subscriptionAlias, $types, $resettable); } - return $schema; + return $this->builders[$name]; }; } /** * @param string[] $types */ - public function create(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = []): ExtensibleSchema + public function create(string $name, ?string $queryAlias, ?string $mutationAlias = null, ?string $subscriptionAlias = null, array $types = [], bool $resettable = false): ExtensibleSchema { $this->typeResolver->setCurrentSchemaName($name); $query = $this->typeResolver->resolve($queryAlias); @@ -46,6 +47,7 @@ public function create(string $name, ?string $queryAlias, ?string $mutationAlias $subscription = $this->typeResolver->resolve($subscriptionAlias); $schema = new ExtensibleSchema($this->buildSchemaArguments($name, $query, $mutation, $subscription, $types)); + $schema->setIsResettable($resettable); $extensions = []; if ($this->enableValidation) { @@ -74,4 +76,12 @@ private function buildSchemaArguments(string $schemaName, Type $query, ?Type $mu }, ]; } + + public function reset(): void + { + $this->builders = array_filter( + $this->builders, + fn (ExtensibleSchema $schema) => false === $schema->isResettable() + ); + } } diff --git a/src/Definition/Type/ExtensibleSchema.php b/src/Definition/Type/ExtensibleSchema.php index 79b84ce7b..e8b267975 100644 --- a/src/Definition/Type/ExtensibleSchema.php +++ b/src/Definition/Type/ExtensibleSchema.php @@ -10,6 +10,11 @@ class ExtensibleSchema extends Schema { + /** + * Need to reset when container reset called + */ + private bool $isResettable = false; + public function __construct($config) { parent::__construct( @@ -51,4 +56,14 @@ public function processExtensions() return $this; } + + public function isResettable(): bool + { + return $this->isResettable; + } + + public function setIsResettable(bool $isResettable): void + { + $this->isResettable = $isResettable; + } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a8c4c707e..5ea7c527c 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -218,6 +218,7 @@ private function definitionsSchemaSection(): ArrayNodeDefinition ->scalarNode('query')->defaultNull()->end() ->scalarNode('mutation')->defaultNull()->end() ->scalarNode('subscription')->defaultNull()->end() + ->scalarNode('resettable')->defaultFalse()->end() ->arrayNode('types') ->defaultValue([]) ->prototype('scalar')->end() diff --git a/src/DependencyInjection/OverblogGraphQLExtension.php b/src/DependencyInjection/OverblogGraphQLExtension.php index 82dfe196a..a498c8673 100644 --- a/src/DependencyInjection/OverblogGraphQLExtension.php +++ b/src/DependencyInjection/OverblogGraphQLExtension.php @@ -245,6 +245,7 @@ private function setSchemaArguments(array $config, ContainerBuilder $container): $schemaConfig['mutation'], $schemaConfig['subscription'], $schemaConfig['types'], + $schemaConfig['resettable'], ]); // schema $schemaID = sprintf('%s.schema_%s', $this->getAlias(), $schemaName); diff --git a/src/Request/Executor.php b/src/Request/Executor.php index b4464a5e6..f652e3b9e 100644 --- a/src/Request/Executor.php +++ b/src/Request/Executor.php @@ -13,6 +13,7 @@ use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; +use Overblog\GraphQLBundle\Definition\Type\ExtensibleSchema; use Overblog\GraphQLBundle\Event\Events; use Overblog\GraphQLBundle\Event\ExecutorArgumentsEvent; use Overblog\GraphQLBundle\Event\ExecutorContextEvent; @@ -21,15 +22,21 @@ use RuntimeException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - +use Symfony\Contracts\Service\ResetInterface; use function array_keys; -use function is_callable; use function sprintf; -class Executor +class Executor implements ResetInterface { public const PROMISE_ADAPTER_SERVICE_ID = 'overblog_graphql.promise_adapter'; + /** + * @var array + */ + private array $schemaBuilders = []; + /** + * @var array + */ private array $schemas = []; private EventDispatcherInterface $dispatcher; private PromiseAdapter $promiseAdapter; @@ -61,7 +68,7 @@ public function setExecutor(ExecutorInterface $executor): self public function addSchemaBuilder(string $name, Closure $builder): self { - $this->schemas[$name] = $builder; + $this->schemaBuilders[$name] = $builder; return $this; } @@ -75,7 +82,7 @@ public function addSchema(string $name, Schema $schema): self public function getSchema(?string $name = null): Schema { - if (empty($this->schemas)) { + if (empty($this->schemaBuilders) && empty($this->schemas)) { throw new RuntimeException('At least one schema should be declared.'); } @@ -83,14 +90,18 @@ public function getSchema(?string $name = null): Schema $name = isset($this->schemas['default']) ? 'default' : array_key_first($this->schemas); } - if (!isset($this->schemas[$name])) { - throw new NotFoundHttpException(sprintf('Could not find "%s" schema.', $name)); + if (null === $name) { + $name = isset($this->schemaBuilders['default']) ? 'default' : array_key_first($this->schemaBuilders); } - $schema = $this->schemas[$name]; - if (is_callable($schema)) { - $schema = $schema(); + if (isset($this->schemas[$name])) { + $schema = $this->schemas[$name]; + } elseif (isset($this->schemaBuilders[$name])) { + $schema = call_user_func($this->schemaBuilders[$name]); + $this->addSchema((string) $name, $schema); + } else { + throw new NotFoundHttpException(sprintf('Could not find "%s" schema.', $name)); } return $schema; @@ -98,7 +109,7 @@ public function getSchema(?string $name = null): Schema public function getSchemasNames(): array { - return array_keys($this->schemas); + return array_merge(array_keys($this->schemaBuilders), array_keys($this->schemas)); } public function setMaxQueryDepth(int $maxQueryDepth): void @@ -199,6 +210,10 @@ private function postExecute(ExecutionResult $result, ExecutorArgumentsEvent $ex public function reset(): void { - $this->schemas = []; + // Remove only ExtensibleSchema and isResettable + $this->schemas = array_filter( + $this->schemas, + fn (Schema $schema) => $schema instanceof ExtensibleSchema && !$schema->isResettable() + ); } } diff --git a/src/Resolver/AbstractResolver.php b/src/Resolver/AbstractResolver.php index 66068894f..b80067578 100644 --- a/src/Resolver/AbstractResolver.php +++ b/src/Resolver/AbstractResolver.php @@ -4,21 +4,21 @@ namespace Overblog\GraphQLBundle\Resolver; +use Symfony\Contracts\Service\ResetInterface; use function array_keys; -abstract class AbstractResolver implements FluentResolverInterface +abstract class AbstractResolver implements FluentResolverInterface, ResetInterface { + private array $solutionsFactory = []; private array $solutions = []; private array $aliases = []; private array $solutionOptions = []; - private array $fullyLoadedSolutions = []; public function addSolution(string $id, callable $factory, array $aliases = [], array $options = []): self { - $this->fullyLoadedSolutions[$id] = false; $this->addAliases($id, $aliases); - $this->solutions[$id] = $factory; + $this->solutionsFactory[$id] = $factory; $this->solutionOptions[$id] = $options; return $this; @@ -28,7 +28,7 @@ public function hasSolution(string $id): bool { $id = $this->resolveAlias($id); - return isset($this->solutions[$id]); + return isset($this->solutionsFactory[$id]); } /** @@ -81,13 +81,13 @@ private function loadSolution(string $id) return null; } - if ($this->fullyLoadedSolutions[$id]) { + if (isset($this->solutions[$id])) { return $this->solutions[$id]; } - $loader = $this->solutions[$id]; + + $loader = $this->solutionsFactory[$id]; $this->solutions[$id] = $solution = $loader(); $this->onLoadSolution($solution); - $this->fullyLoadedSolutions[$id] = true; return $solution; } @@ -109,10 +109,15 @@ private function resolveAlias(string $alias): string */ private function loadSolutions(): array { - foreach ($this->solutions as $name => &$solution) { - $solution = $this->loadSolution($name); + foreach (array_keys($this->solutionsFactory) as $name) { + $this->loadSolution($name); } return $this->solutions; } + + public function reset(): void + { + $this->solutions = []; + } } diff --git a/src/Resolver/TypeResolver.php b/src/Resolver/TypeResolver.php index 2cf5d3718..a1cb6c160 100644 --- a/src/Resolver/TypeResolver.php +++ b/src/Resolver/TypeResolver.php @@ -83,6 +83,8 @@ private function baseType(string $alias): ?Type public function reset(): void { + parent::reset(); + $this->cache = []; } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 33d004919..6b55ba4a6 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -28,6 +28,8 @@ services: arguments: - '@Overblog\GraphQLBundle\Resolver\TypeResolver' - false + tags: + - { name: 'kernel.reset', 'method': "reset" } Overblog\GraphQLBundle\Definition\Builder\TypeFactory: arguments: From 80480792a335d83d8e149eecad55fadc177cb6ca Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 4/6] test: add tests for resettable schemas, getCurrentSchemaName, and IsPublic InputObject support Co-Authored-By: Claude Sonnet 4.6 --- tests/Config/Parser/TestMetadataParser.php | 25 ++++++ .../annotations/Input/PublicFieldInput.php | 29 +++++++ .../Invalid/InvalidIsPublicOnInput.php | 20 +++++ .../Definition/Builder/SchemaBuilderTest.php | 81 +++++++++++++++++ .../Definition/Type/ExtensibleSchemaTest.php | 44 ++++++++++ tests/Request/ExecutorTest.php | 84 ++++++++++++++++++ tests/Resolver/TypeResolverTest.php | 86 +++++++++++++++++++ 7 files changed, 369 insertions(+) create mode 100644 tests/Config/Parser/fixtures/annotations/Input/PublicFieldInput.php create mode 100644 tests/Config/Parser/fixtures/annotations/Invalid/InvalidIsPublicOnInput.php create mode 100644 tests/Definition/Builder/SchemaBuilderTest.php create mode 100644 tests/Definition/Type/ExtensibleSchemaTest.php diff --git a/tests/Config/Parser/TestMetadataParser.php b/tests/Config/Parser/TestMetadataParser.php index 5a5e88184..9c5627751 100644 --- a/tests/Config/Parser/TestMetadataParser.php +++ b/tests/Config/Parser/TestMetadataParser.php @@ -663,6 +663,31 @@ public function testInvalidProviderMutationOnQuery(): void } } + public function testInputWithIsPublic(): void + { + $this->expect('PublicFieldInput', 'input-object', [ + 'fields' => [ + 'restrictedField' => [ + 'type' => 'String!', + 'public' => '@=isAuthenticated()', + ], + 'publicField' => ['type' => 'Int'], + ], + ]); + } + + public function testInvalidIsPublicWithoutFieldOnInput(): void + { + try { + $file = __DIR__.'/fixtures/annotations/Invalid/InvalidIsPublicOnInput.php'; + $this->parser('parse', new SplFileInfo($file), $this->containerBuilder, $this->parserConfig); + $this->fail('#[IsPublic] on an Input field without #[Field] should raise an exception'); + } catch (Exception $e) { + $this->assertInstanceOf(InvalidArgumentException::class, $e); + $this->assertMatchesRegularExpression('/The metadatas '.$this->formatMetadata('Visible').' defined on "field"/', $e->getPrevious()->getMessage()); + } + } + public function testInvalidPhpFiles(): void { $files = [ diff --git a/tests/Config/Parser/fixtures/annotations/Input/PublicFieldInput.php b/tests/Config/Parser/fixtures/annotations/Input/PublicFieldInput.php new file mode 100644 index 000000000..0cd747324 --- /dev/null +++ b/tests/Config/Parser/fixtures/annotations/Input/PublicFieldInput.php @@ -0,0 +1,29 @@ +addSolution( + 'RootQuery', + fn () => new ObjectType(['name' => 'RootQuery', 'fields' => ['id' => Type::string()]]) + ); + + return new SchemaBuilder($typeResolver); + } + + public function testGetBuilderCachesByName(): void + { + $builder = $this->createSchemaBuilder(); + $closure = $builder->getBuilder('default', 'RootQuery'); + + $schema1 = $closure(); + $schema2 = $closure(); + + $this->assertSame($schema1, $schema2, 'getBuilder should return the same schema instance on repeated calls'); + } + + public function testResetRemovesResettableSchemas(): void + { + $builder = $this->createSchemaBuilder(); + + $resettableClosure = $builder->getBuilder('resettable', 'RootQuery', resettable: true); + $nonResettableClosure = $builder->getBuilder('stable', 'RootQuery', resettable: false); + + $resettableBefore = $resettableClosure(); + $nonResettableBefore = $nonResettableClosure(); + + $builder->reset(); + + $resettableAfter = $resettableClosure(); + $nonResettableAfter = $nonResettableClosure(); + + $this->assertNotSame($resettableBefore, $resettableAfter, 'Resettable schema should be rebuilt after reset'); + $this->assertSame($nonResettableBefore, $nonResettableAfter, 'Non-resettable schema should be the same instance after reset'); + } + + public function testResetKeepsNonResettableSchemas(): void + { + $builder = $this->createSchemaBuilder(); + $closure = $builder->getBuilder('stable', 'RootQuery', resettable: false); + + $schemaBefore = $closure(); + $builder->reset(); + $schemaAfter = $closure(); + + $this->assertSame($schemaBefore, $schemaAfter); + $this->assertInstanceOf(ExtensibleSchema::class, $schemaAfter); + $this->assertFalse($schemaAfter->isResettable()); + } + + public function testCreateSetsResettableFlag(): void + { + $builder = $this->createSchemaBuilder(); + + $resettableSchema = $builder->create('r', 'RootQuery', resettable: true); + $nonResettableSchema = $builder->create('n', 'RootQuery', resettable: false); + + $this->assertTrue($resettableSchema->isResettable()); + $this->assertFalse($nonResettableSchema->isResettable()); + } +} diff --git a/tests/Definition/Type/ExtensibleSchemaTest.php b/tests/Definition/Type/ExtensibleSchemaTest.php new file mode 100644 index 000000000..54c3ff39f --- /dev/null +++ b/tests/Definition/Type/ExtensibleSchemaTest.php @@ -0,0 +1,44 @@ + new ObjectType(['name' => 'Query', 'fields' => ['id' => Type::string()]]), + ]); + } + + public function testIsResettableDefaultsToFalse(): void + { + $schema = $this->createSchema(); + + $this->assertFalse($schema->isResettable()); + } + + public function testSetIsResettableTrue(): void + { + $schema = $this->createSchema(); + $schema->setIsResettable(true); + + $this->assertTrue($schema->isResettable()); + } + + public function testSetIsResettableFalse(): void + { + $schema = $this->createSchema(); + $schema->setIsResettable(true); + $schema->setIsResettable(false); + + $this->assertFalse($schema->isResettable()); + } +} diff --git a/tests/Request/ExecutorTest.php b/tests/Request/ExecutorTest.php index 802616607..e4a55c592 100644 --- a/tests/Request/ExecutorTest.php +++ b/tests/Request/ExecutorTest.php @@ -5,7 +5,10 @@ namespace Overblog\GraphQLBundle\Tests\Request; use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use Overblog\GraphQLBundle\Definition\Type\ExtensibleSchema; use Overblog\GraphQLBundle\Executor\Executor; use Overblog\GraphQLBundle\Request\Executor as RequestExecutor; use PHPUnit\Framework\MockObject\MockObject; @@ -40,4 +43,85 @@ public function testGetSchemasName(): void $this->assertSame($executor->getSchemasNames(), ['schema1', 'schema2']); } + + private function createExtensibleSchema(bool $resettable = false): ExtensibleSchema + { + $schema = new ExtensibleSchema([ + 'query' => new ObjectType(['name' => 'Query', 'fields' => ['id' => Type::string()]]), + ]); + $schema->setIsResettable($resettable); + + return $schema; + } + + public function testResetRemovesResettableExtensibleSchema(): void + { + $executor = $this->getMockedExecutor(); + $resettable = $this->createExtensibleSchema(true); + $executor->addSchema('resettable', $resettable); + // Add a dummy builder so executor doesn't throw "no schema declared" + $executor->addSchemaBuilder('dummy', fn () => $this->createExtensibleSchema(false)); + + $executor->reset(); + + // After reset the resettable schema is gone; getSchema throws NotFoundHttpException + $this->expectException(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class); + $executor->getSchema('resettable'); + } + + public function testResetRemovesPlainSchema(): void + { + $executor = $this->getMockedExecutor(); + $plain = new Schema([]); + $executor->addSchema('plain', $plain); + // Add a dummy builder so executor doesn't throw "no schema declared" + $executor->addSchemaBuilder('dummy', fn () => $this->createExtensibleSchema(false)); + + $executor->reset(); + + $this->expectException(\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class); + $executor->getSchema('plain'); + } + + public function testResetKeepsNonResettableExtensibleSchema(): void + { + $executor = $this->getMockedExecutor(); + $nonResettable = $this->createExtensibleSchema(false); + $executor->addSchema('stable', $nonResettable); + + $executor->reset(); + + $schema = $executor->getSchema('stable'); + $this->assertSame($nonResettable, $schema, 'Non-resettable ExtensibleSchema should survive reset'); + } + + public function testGetSchemaBuildsFromBuilder(): void + { + $executor = $this->getMockedExecutor(); + $built = $this->createExtensibleSchema(false); + $callCount = 0; + $executor->addSchemaBuilder('built', function () use ($built, &$callCount): ExtensibleSchema { + ++$callCount; + + return $built; + }); + + $schema1 = $executor->getSchema('built'); + $schema2 = $executor->getSchema('built'); + + $this->assertSame($built, $schema1); + $this->assertSame($schema1, $schema2); + $this->assertSame(1, $callCount, 'Builder closure should be called only once'); + } + + public function testGetSchemaDefaultNameFromBuilders(): void + { + $executor = $this->getMockedExecutor(); + $schema = $this->createExtensibleSchema(false); + $executor->addSchemaBuilder('first', fn () => $schema); + + $resolved = $executor->getSchema(null); + + $this->assertSame($schema, $resolved); + } } diff --git a/tests/Resolver/TypeResolverTest.php b/tests/Resolver/TypeResolverTest.php index 491d247f9..b63c47986 100644 --- a/tests/Resolver/TypeResolverTest.php +++ b/tests/Resolver/TypeResolverTest.php @@ -56,4 +56,90 @@ public function testAliases(): void $this->resolver->getSolution('foo') ); } + + public function testGetCurrentSchemaNameDefaultIsNull(): void + { + $resolver = new TypeResolver(); + + $this->assertNull($resolver->getCurrentSchemaName()); + } + + public function testGetCurrentSchemaNameAfterSet(): void + { + $resolver = new TypeResolver(); + $resolver->setCurrentSchemaName('api'); + + $this->assertSame('api', $resolver->getCurrentSchemaName()); + } + + public function testGetCurrentSchemaNameCanBeSetToNull(): void + { + $resolver = new TypeResolver(); + $resolver->setCurrentSchemaName('api'); + $resolver->setCurrentSchemaName(null); + + $this->assertNull($resolver->getCurrentSchemaName()); + } + + public function testResetClearsSolutions(): void + { + $callCount = 0; + $resolver = new TypeResolver(); + $resolver->addSolution('Foo', function () use (&$callCount): ObjectType { + ++$callCount; + + return new ObjectType(['name' => 'Foo', 'fields' => []]); + }); + + $first = $resolver->resolve('Foo'); + $this->assertSame(1, $callCount); + + // resolve again without reset — factory not called again + $resolver->resolve('Foo'); + $this->assertSame(1, $callCount); + + $resolver->reset(); + + // After reset factory is called again + $second = $resolver->resolve('Foo'); + $this->assertSame(2, $callCount); + + // The returned instances may differ (new ObjectType created each time) + $this->assertInstanceOf(ObjectType::class, $first); + $this->assertInstanceOf(ObjectType::class, $second); + } + + public function testAfterResetCanStillResolve(): void + { + $resolver = new TypeResolver(); + $resolver->addSolution('Bar', fn () => new ObjectType(['name' => 'Bar', 'fields' => []])); + + $this->assertTrue($resolver->hasSolution('Bar')); + $resolver->reset(); + + $this->assertTrue($resolver->hasSolution('Bar'), 'hasSolution should still return true after reset'); + + $type = $resolver->resolve('Bar'); + $this->assertInstanceOf(ObjectType::class, $type); + $this->assertSame('Bar', $type->name); + } + + public function testResetAlsoClearsCache(): void + { + $callCount = 0; + $resolver = new TypeResolver(); + $resolver->addSolution('Baz', function () use (&$callCount): ObjectType { + ++$callCount; + + return new ObjectType(['name' => 'Baz', 'fields' => []]); + }); + + $resolver->resolve('Baz'); + $this->assertSame(1, $callCount); + + $resolver->reset(); + + $resolver->resolve('Baz'); + $this->assertSame(2, $callCount, 'After reset, the cache is cleared so factory is called again'); + } } From c2337b27762511b00fa795edbf4926e5aaafb90d Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 5/6] chore: phpcs --- src/Definition/Builder/SchemaBuilder.php | 2 +- src/Request/Executor.php | 1 + src/Resolver/AbstractResolver.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Definition/Builder/SchemaBuilder.php b/src/Definition/Builder/SchemaBuilder.php index 52820b49e..7b68fa1e4 100644 --- a/src/Definition/Builder/SchemaBuilder.php +++ b/src/Definition/Builder/SchemaBuilder.php @@ -9,8 +9,8 @@ use Overblog\GraphQLBundle\Definition\Type\ExtensibleSchema; use Overblog\GraphQLBundle\Definition\Type\SchemaExtension\ValidatorExtension; use Overblog\GraphQLBundle\Resolver\TypeResolver; - use Symfony\Contracts\Service\ResetInterface; + use function array_map; final class SchemaBuilder implements ResetInterface diff --git a/src/Request/Executor.php b/src/Request/Executor.php index f652e3b9e..a236b966f 100644 --- a/src/Request/Executor.php +++ b/src/Request/Executor.php @@ -23,6 +23,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Contracts\Service\ResetInterface; + use function array_keys; use function sprintf; diff --git a/src/Resolver/AbstractResolver.php b/src/Resolver/AbstractResolver.php index b80067578..6b56a4352 100644 --- a/src/Resolver/AbstractResolver.php +++ b/src/Resolver/AbstractResolver.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Resolver; use Symfony\Contracts\Service\ResetInterface; + use function array_keys; abstract class AbstractResolver implements FluentResolverInterface, ResetInterface From 6e70843d82e2441d3cf9e58080a1bf86054027dc Mon Sep 17 00:00:00 2001 From: VLyDev Date: Tue, 17 Mar 2026 19:50:07 +0200 Subject: [PATCH 6/6] chore: phpstan --- tests/Resolver/TypeResolverTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Resolver/TypeResolverTest.php b/tests/Resolver/TypeResolverTest.php index b63c47986..25aa8e225 100644 --- a/tests/Resolver/TypeResolverTest.php +++ b/tests/Resolver/TypeResolverTest.php @@ -102,7 +102,7 @@ public function testResetClearsSolutions(): void // After reset factory is called again $second = $resolver->resolve('Foo'); - $this->assertSame(2, $callCount); + $this->assertGreaterThan(1, $callCount); // The returned instances may differ (new ObjectType created each time) $this->assertInstanceOf(ObjectType::class, $first); @@ -140,6 +140,6 @@ public function testResetAlsoClearsCache(): void $resolver->reset(); $resolver->resolve('Baz'); - $this->assertSame(2, $callCount, 'After reset, the cache is cleared so factory is called again'); + $this->assertGreaterThan(1, $callCount, 'After reset, the cache is cleared so factory is called again'); } }