From be2e6db99aded511ec7978ddcbaf27d71f897a4b Mon Sep 17 00:00:00 2001 From: Mir-Zairan Date: Wed, 16 Sep 2026 18:01:36 +0530 Subject: [PATCH 1/2] :zap: Cache the schema on every environment that runs without debug The compiler pass only put the schema factory in prod mode for the environment literally named "prod". Any other name - staging, preprod, uat, qa - fell through to the factory default, dev mode, which stats every controller and type file and re-globs the configured namespaces on each request. That is what kernel.debug already means in Symfony, and it is what Twig's auto_reload and Doctrine's proxy autogeneration key on, so use it here too. --- .../GraphQLiteCompilerPass.php | 11 +++------ tests/FunctionalTest.php | 17 +++++++++++++ tests/GraphQLiteTestingKernel.php | 24 ++++++++++++++++--- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/DependencyInjection/GraphQLiteCompilerPass.php b/src/DependencyInjection/GraphQLiteCompilerPass.php index c254004..7e37d0c 100644 --- a/src/DependencyInjection/GraphQLiteCompilerPass.php +++ b/src/DependencyInjection/GraphQLiteCompilerPass.php @@ -85,16 +85,11 @@ public function process(ContainerBuilder $container): void assert(is_string($firewallName)); $firewallConfigServiceName = 'security.firewall.map.config.'.$firewallName; - // 2 seconds of TTL in environment mode. Otherwise, let's cache forever! - $schemaFactory = $container->getDefinition(SchemaFactory::class); - $env = $container->getParameter('kernel.environment'); - if ($env === 'prod') { - $schemaFactory->addMethodCall('prodMode'); - } elseif ($env === 'dev') { - $schemaFactory->addMethodCall('devMode'); - } + $debug = $container->getParameter('kernel.debug'); + assert(is_bool($debug)); + $schemaFactory->addMethodCall($debug ? 'devMode' : 'prodMode'); $disableLogin = false; if ($container->getParameter('graphqlite.security.enable_login') === 'auto' diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index cdfe673..a1b40e8 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -99,6 +99,23 @@ public function testServiceAutowiring(): void ], $result); } + /** + * @dataProvider schemaCachingModeProvider + */ + public function testSchemaCachingFollowsDebug(bool $debug, string $expectedMode): void + { + $kernel = new GraphQLiteTestingKernel(debug: $debug); + $kernel->boot(); + + $this->assertContains($expectedMode, $kernel->getContainer()->getParameter('graphqlite.tests.schema_factory_calls')); + } + + public function schemaCachingModeProvider(): iterable + { + yield 'debug rebuilds the schema' => [true, 'devMode']; + yield 'no debug caches the schema' => [false, 'prodMode']; + } + public function testErrors(): void { $kernel = new GraphQLiteTestingKernel(); diff --git a/tests/GraphQLiteTestingKernel.php b/tests/GraphQLiteTestingKernel.php index def5347..e55534d 100644 --- a/tests/GraphQLiteTestingKernel.php +++ b/tests/GraphQLiteTestingKernel.php @@ -15,9 +15,13 @@ use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use TheCodingMachine\GraphQLite\Bundle\GraphQLiteBundle; +use TheCodingMachine\GraphQLite\SchemaFactory; use Symfony\Component\Security\Core\User\InMemoryUser; +use function array_column; use function class_exists; use function serialize; +use function sys_get_temp_dir; +use function uniqid; class GraphQLiteTestingKernel extends Kernel implements CompilerPassInterface { @@ -61,6 +65,11 @@ class GraphQLiteTestingKernel extends Kernel implements CompilerPassInterface */ private $typesNamespace; + /** + * @var string + */ + private $cacheBaseDir; + /** * @param string[] $controllersNamespace * @param string[] $typesNamespace @@ -73,9 +82,13 @@ public function __construct(bool $enableSession = true, ?int $maximumQueryComplexity = null, ?int $maximumQueryDepth = null, array $controllersNamespace = ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Controller\\'], - array $typesNamespace = ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Types\\', 'TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Entities\\']) + array $typesNamespace = ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Types\\', 'TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Entities\\'], + bool $debug = true) { - parent::__construct('test', true); + parent::__construct('test', $debug); + // Without debug the kernel reuses a dumped container without checking whether its sources + // changed, which would hide an edit to the compiler pass from the tests. + $this->cacheBaseDir = $debug ? __DIR__.'/../cache/' : sys_get_temp_dir().'/graphqlite-bundle-'.uniqid().'/'; $this->enableSession = $enableSession; $this->enableLogin = $enableLogin; $this->enableSecurity = $enableSecurity; @@ -219,7 +232,7 @@ public function getCacheDir(): string .'_' .($this->introspection?'withIntrospection':'withoutIntrospection'); - return __DIR__.'/../cache/'.$prefix.'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); + return $this->cacheBaseDir.$prefix.'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); } public function process(ContainerBuilder $container): void @@ -227,5 +240,10 @@ public function process(ContainerBuilder $container): void if ($container->hasDefinition('security.untracked_token_storage')) { $container->getDefinition('security.untracked_token_storage')->setPublic(true); } + + $container->setParameter('graphqlite.tests.schema_factory_calls', array_column( + $container->getDefinition(SchemaFactory::class)->getMethodCalls(), + 0 + )); } } From bb2375c8e34a01ab407e77b26a276989373fd6c0 Mon Sep 17 00:00:00 2001 From: Mir-Zairan Date: Thu, 17 Sep 2026 18:11:54 +0530 Subject: [PATCH 2/2] :sparkles: Add a schema.auto_reload option Following kernel.debug is right for almost everyone, but not for a staging server that runs with debug on and still wants a cached schema, or for a development setup that runs with debug off and still wants its edits picked up. The option defaults to %kernel.debug%, so nothing changes unless it is set, and the README explains what each setting does and when the cache has to be cleared. --- README.md | 23 +++++++++++++++++++ src/DependencyInjection/Configuration.php | 5 ++++ .../GraphQLiteCompilerPass.php | 6 ++--- .../GraphQLiteExtension.php | 7 ++++++ tests/FunctionalTest.php | 15 ++++++++---- tests/GraphQLiteTestingKernel.php | 20 +++++++++++++--- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 42a3397..558adef 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,29 @@ final class HelloController - Uses Symfony cache (APCu or PHP files) for schema caching - Includes a `graphqlite:dump-schema` console command to export GraphQL SDL +## Schema caching + +Building the schema means reading every controller and type, so the result is cached. The +`schema.auto_reload` option decides whether that cache is checked against your files on each request. +It defaults to `'%kernel.debug%'`, so you see your changes while you work and deployed environments do +not pay for the check. + +Set it yourself when an environment does not fit that rule, such as a staging server that runs with +debug on but should still serve a cached schema: + +```yaml +# config/packages/graphqlite.yaml +graphqlite: + schema: + auto_reload: false +``` + +With `auto_reload: true` an edited controller or type shows up on the next request. With +`auto_reload: false` the cached schema is trusted until the cache is cleared, so a deploy that changes +a controller, a type or one of their attributes has to clear it. `cache:clear` does that. Note that +with the APCu adapter the console and the web server each keep their own copy, so the PHP processes +serving requests need restarting as well. + ## GraphiQL (playground) The bundle wires Overblog’s GraphiQL bundle if it is installed. See https://github.com/overblog/GraphiQLBundle for diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index e331543..33d0e37 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -39,6 +39,11 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('RETHROW_UNSAFE_EXCEPTIONS')->defaultTrue()->info('Exceptions that do not implement ClientAware interface are not caught by the engine and propagated to Symfony.')->end() ->end() ->end() + ->arrayNode('schema') + ->children() + ->booleanNode('auto_reload')->defaultValue('%kernel.debug%')->info('Check controller and type files for changes on every request, so that an edit shows up without clearing the cache. Defaults to kernel.debug.')->end() + ->end() + ->end() ->arrayNode('security') ->children() ->enumNode('enable_login')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a login/logout mutation. "on": enable, "auto": enable if security bundle is available.')->end() diff --git a/src/DependencyInjection/GraphQLiteCompilerPass.php b/src/DependencyInjection/GraphQLiteCompilerPass.php index 7e37d0c..76d60ad 100644 --- a/src/DependencyInjection/GraphQLiteCompilerPass.php +++ b/src/DependencyInjection/GraphQLiteCompilerPass.php @@ -87,9 +87,9 @@ public function process(ContainerBuilder $container): void $schemaFactory = $container->getDefinition(SchemaFactory::class); - $debug = $container->getParameter('kernel.debug'); - assert(is_bool($debug)); - $schemaFactory->addMethodCall($debug ? 'devMode' : 'prodMode'); + $autoReload = $container->getParameter('graphqlite.schema.auto_reload'); + assert(is_bool($autoReload)); + $schemaFactory->addMethodCall($autoReload ? 'devMode' : 'prodMode'); $disableLogin = false; if ($container->getParameter('graphqlite.security.enable_login') === 'auto' diff --git a/src/DependencyInjection/GraphQLiteExtension.php b/src/DependencyInjection/GraphQLiteExtension.php index 82dd202..25ea56b 100644 --- a/src/DependencyInjection/GraphQLiteExtension.php +++ b/src/DependencyInjection/GraphQLiteExtension.php @@ -76,6 +76,12 @@ function($namespace): string { $namespaceType = []; } + if (!isset($config['schema'])) { + $config['schema'] = []; + } + \assert(\is_array($config['schema'])); + $autoReload = $container->getParameterBag()->resolveValue($config['schema']['auto_reload'] ?? '%kernel.debug%'); + if (!isset($config['security'])) { $config['security'] = []; } @@ -85,6 +91,7 @@ function($namespace): string { $container->setParameter('graphqlite.namespace.controllers', $namespaceController); $container->setParameter('graphqlite.namespace.types', $namespaceType); + $container->setParameter('graphqlite.schema.auto_reload', (bool) $autoReload); $container->setParameter('graphqlite.security.enable_login', $enableLogin); $container->setParameter('graphqlite.security.enable_me', $enableMe); $container->setParameter('graphqlite.security.disableIntrospection', !($config['security']['introspection'] ?? true)); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index a1b40e8..6505063 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -102,18 +102,23 @@ public function testServiceAutowiring(): void /** * @dataProvider schemaCachingModeProvider */ - public function testSchemaCachingFollowsDebug(bool $debug, string $expectedMode): void + public function testSchemaCachingFollowsDebugUnlessOverridden(string $environment, bool $debug, ?bool $autoReload, string $expectedMode): void { - $kernel = new GraphQLiteTestingKernel(debug: $debug); + $kernel = new GraphQLiteTestingKernel(debug: $debug, environment: $environment, schemaAutoReload: $autoReload); $kernel->boot(); - $this->assertContains($expectedMode, $kernel->getContainer()->getParameter('graphqlite.tests.schema_factory_calls')); + $calls = $kernel->getContainer()->getParameter('graphqlite.tests.schema_factory_calls'); + + $this->assertContains($expectedMode, $calls); + $this->assertNotContains($expectedMode === 'devMode' ? 'prodMode' : 'devMode', $calls); } public function schemaCachingModeProvider(): iterable { - yield 'debug rebuilds the schema' => [true, 'devMode']; - yield 'no debug caches the schema' => [false, 'prodMode']; + yield 'staging without debug caches the schema' => ['staging', false, null, 'prodMode']; + yield 'prod with debug reloads the schema' => ['prod', true, null, 'devMode']; + yield 'staging can ask to keep reloading' => ['staging', false, true, 'devMode']; + yield 'dev can ask to keep caching' => ['dev', true, false, 'prodMode']; } public function testErrors(): void diff --git a/tests/GraphQLiteTestingKernel.php b/tests/GraphQLiteTestingKernel.php index e55534d..f732921 100644 --- a/tests/GraphQLiteTestingKernel.php +++ b/tests/GraphQLiteTestingKernel.php @@ -70,6 +70,11 @@ class GraphQLiteTestingKernel extends Kernel implements CompilerPassInterface */ private $cacheBaseDir; + /** + * @var bool|null + */ + private $schemaAutoReload; + /** * @param string[] $controllersNamespace * @param string[] $typesNamespace @@ -83,9 +88,12 @@ public function __construct(bool $enableSession = true, ?int $maximumQueryDepth = null, array $controllersNamespace = ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Controller\\'], array $typesNamespace = ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Types\\', 'TheCodingMachine\\GraphQLite\\Bundle\\Tests\\Fixtures\\Entities\\'], - bool $debug = true) + bool $debug = true, + string $environment = 'test', + ?bool $schemaAutoReload = null) { - parent::__construct('test', $debug); + parent::__construct($environment, $debug); + $this->schemaAutoReload = $schemaAutoReload; // Without debug the kernel reuses a dumped container without checking whether its sources // changed, which would hide an edit to the compiler pass from the tests. $this->cacheBaseDir = $debug ? __DIR__.'/../cache/' : sys_get_temp_dir().'/graphqlite-bundle-'.uniqid().'/'; @@ -208,6 +216,10 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) $graphqliteConf['security']['maximum_query_depth'] = $this->maximumQueryDepth; } + if ($this->schemaAutoReload !== null) { + $graphqliteConf['schema']['auto_reload'] = $this->schemaAutoReload; + } + $container->loadFromExtension('graphqlite', $graphqliteConf); }); $confDir = $this->getProjectDir().'/tests/Fixtures/config'; @@ -232,7 +244,9 @@ public function getCacheDir(): string .'_' .($this->introspection?'withIntrospection':'withoutIntrospection'); - return $this->cacheBaseDir.$prefix.'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); + $schemaCaching = $this->schemaAutoReload === null ? 'defaultReload' : ($this->schemaAutoReload ? 'autoReload' : 'noAutoReload'); + + return $this->cacheBaseDir.$this->environment.'_'.$schemaCaching.'_'.$prefix.'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); } public function process(ContainerBuilder $container): void