Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 3 additions & 8 deletions src/DependencyInjection/GraphQLiteCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
$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'
Expand Down
7 changes: 7 additions & 0 deletions src/DependencyInjection/GraphQLiteExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = [];
}
Expand All @@ -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));
Expand Down
22 changes: 22 additions & 0 deletions tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ public function testServiceAutowiring(): void
], $result);
}

/**
* @dataProvider schemaCachingModeProvider
*/
public function testSchemaCachingFollowsDebugUnlessOverridden(string $environment, bool $debug, ?bool $autoReload, string $expectedMode): void
{
$kernel = new GraphQLiteTestingKernel(debug: $debug, environment: $environment, schemaAutoReload: $autoReload);
$kernel->boot();

$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 '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
{
$kernel = new GraphQLiteTestingKernel();
Expand Down
38 changes: 35 additions & 3 deletions tests/GraphQLiteTestingKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -61,6 +65,16 @@ class GraphQLiteTestingKernel extends Kernel implements CompilerPassInterface
*/
private $typesNamespace;

/**
* @var string
*/
private $cacheBaseDir;

/**
* @var bool|null
*/
private $schemaAutoReload;

/**
* @param string[] $controllersNamespace
* @param string[] $typesNamespace
Expand All @@ -73,9 +87,16 @@ 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,
string $environment = 'test',
?bool $schemaAutoReload = null)
{
parent::__construct('test', true);
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().'/';
$this->enableSession = $enableSession;
$this->enableLogin = $enableLogin;
$this->enableSecurity = $enableSecurity;
Expand Down Expand Up @@ -195,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';
Expand All @@ -219,13 +244,20 @@ public function getCacheDir(): string
.'_'
.($this->introspection?'withIntrospection':'withoutIntrospection');

return __DIR__.'/../cache/'.$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
{
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
));
}
}
Loading