diff --git a/CacheWarmer/CompileCacheWarmer.php b/CacheWarmer/CompileCacheWarmer.php
new file mode 100644
index 000000000..87935a530
--- /dev/null
+++ b/CacheWarmer/CompileCacheWarmer.php
@@ -0,0 +1,33 @@
+typeGenerator = $typeGenerator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isOptional()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function warmUp($cacheDir)
+ {
+ $this->typeGenerator->compile(TypeGenerator::MODE_WRITE | TypeGenerator::MODE_OVERRIDE);
+ }
+}
diff --git a/Command/CompileCommand.php b/Command/CompileCommand.php
new file mode 100644
index 000000000..737e0be8b
--- /dev/null
+++ b/Command/CompileCommand.php
@@ -0,0 +1,45 @@
+typeGenerator = $typeGenerator;
+ }
+
+ protected function configure()
+ {
+ $this
+ ->setName('graphql:compile')
+ ->setDescription('Generate types manually.')
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Types compilation starts');
+ $classes = $this->typeGenerator->compile(TypeGenerator::MODE_WRITE | TypeGenerator::MODE_OVERRIDE);
+ $output->writeln('Types compilation ends successfully');
+ if ($output->getVerbosity() >= Output::VERBOSITY_VERBOSE) {
+ $io = new SymfonyStyle($input, $output);
+ $io->title('Summary');
+ $rows = [];
+ foreach ($classes as $class => $path) {
+ $rows[] = [$class, $path];
+ }
+ $io->table(['class', 'path'], $rows);
+ }
+ }
+}
diff --git a/DependencyInjection/Compiler/ConfigTypesPass.php b/DependencyInjection/Compiler/ConfigTypesPass.php
index 088f73461..60c04c8d0 100644
--- a/DependencyInjection/Compiler/ConfigTypesPass.php
+++ b/DependencyInjection/Compiler/ConfigTypesPass.php
@@ -2,57 +2,32 @@
namespace Overblog\GraphQLBundle\DependencyInjection\Compiler;
+use Overblog\GraphQLBundle\Generator\TypeGenerator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
-use Symfony\Component\ExpressionLanguage\Expression;
+use Symfony\Component\DependencyInjection\Reference;
class ConfigTypesPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
- $config = $container->getParameter('overblog_graphql_types.config');
- $generatedClasses = $container->get('overblog_graphql.cache_compiler')->compile(
- $this->processConfig($config),
- $container->getParameter('overblog_graphql.use_classloader_listener')
- );
+ $generatedClasses = $container->get('overblog_graphql.cache_compiler')
+ ->compile(TypeGenerator::MODE_MAPPING_ONLY);
foreach ($generatedClasses as $class => $file) {
- if (!class_exists($class)) {
- throw new \RuntimeException(sprintf(
- 'Type class %s not found. If you are using your own classLoader verify the path and the namespace please.',
- json_encode($class))
- );
- }
- $aliases = call_user_func($class.'::getAliases');
+ $aliases = [preg_replace('/Type$/', '', substr(strrchr($class, '\\'), 1))];
$this->setTypeServiceDefinition($container, $class, $aliases);
}
- $container->getParameterBag()->remove('overblog_graphql_types.config');
}
private function setTypeServiceDefinition(ContainerBuilder $container, $class, array $aliases)
{
$definition = $container->setDefinition($class, new Definition($class));
$definition->setPublic(false);
- $definition->setAutowired(true);
+ $definition->setArguments([new Reference('service_container')]);
foreach ($aliases as $alias) {
- $definition->addTag('overblog_graphql.type', ['alias' => $alias]);
+ $definition->addTag('overblog_graphql.type', ['alias' => $alias, 'generated' => true]);
}
}
-
- private function processConfig(array $configs)
- {
- return array_map(
- function ($v) {
- if (is_array($v)) {
- return call_user_func([$this, 'processConfig'], $v);
- } elseif (is_string($v) && 0 === strpos($v, '@=')) {
- return new Expression(substr($v, 2));
- }
-
- return $v;
- },
- $configs
- );
- }
}
diff --git a/DependencyInjection/Compiler/TaggedServiceMappingPass.php b/DependencyInjection/Compiler/TaggedServiceMappingPass.php
index a424a0ae4..93cb54693 100644
--- a/DependencyInjection/Compiler/TaggedServiceMappingPass.php
+++ b/DependencyInjection/Compiler/TaggedServiceMappingPass.php
@@ -15,10 +15,10 @@ private function getTaggedServiceMapping(ContainerBuilder $container, $tagName)
$serviceMapping = [];
$taggedServices = $container->findTaggedServiceIds($tagName);
+ $isType = TypeTaggedServiceMappingPass::TAG_NAME === $tagName;
foreach ($taggedServices as $id => $tags) {
$className = $container->findDefinition($id)->getClass();
- $isType = is_subclass_of($className, Type::class);
foreach ($tags as $tag) {
$this->checkRequirements($id, $tag);
$tag = array_merge($tag, ['id' => $id]);
@@ -62,7 +62,8 @@ function ($methodCall) {
$solutionDefinition->getMethodCalls()
);
if (
- is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class)
+ empty($options['generated']) // false is consider as empty
+ && is_subclass_of($solutionDefinition->getClass(), ContainerAwareInterface::class)
&& !in_array('setContainer', $methods)
) {
@trigger_error(
diff --git a/DependencyInjection/Compiler/TypeTaggedServiceMappingPass.php b/DependencyInjection/Compiler/TypeTaggedServiceMappingPass.php
index 2e73e8164..852966759 100644
--- a/DependencyInjection/Compiler/TypeTaggedServiceMappingPass.php
+++ b/DependencyInjection/Compiler/TypeTaggedServiceMappingPass.php
@@ -4,9 +4,11 @@
class TypeTaggedServiceMappingPass extends TaggedServiceMappingPass
{
+ const TAG_NAME = 'overblog_graphql.type';
+
protected function getTagName()
{
- return 'overblog_graphql.type';
+ return self::TAG_NAME;
}
protected function getResolverServiceID()
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 25c7b36fd..62cc6261c 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -48,6 +48,7 @@ public function getConfigTreeBuilder()
->scalarNode('class_namespace')->defaultValue('Overblog\\GraphQLBundle\\__DEFINITIONS__')->end()
->scalarNode('cache_dir')->defaultValue($this->cacheDir.'/overblog/graphql-bundle/__definitions__')->end()
->booleanNode('use_classloader_listener')->defaultTrue()->end()
+ ->booleanNode('auto_compile')->defaultTrue()->end()
->booleanNode('show_debug_info')->defaultFalse()->end()
->booleanNode('config_validation')->defaultValue($this->debug)->end()
->arrayNode('schema')
diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php
index 1a6694ac4..48fba5971 100644
--- a/DependencyInjection/OverblogGraphQLExtension.php
+++ b/DependencyInjection/OverblogGraphQLExtension.php
@@ -3,6 +3,7 @@
namespace Overblog\GraphQLBundle\DependencyInjection;
use GraphQL\Type\Schema;
+use Overblog\GraphQLBundle\CacheWarmer\CompileCacheWarmer;
use Overblog\GraphQLBundle\Config\TypeWithOutputFieldsDefinition;
use Overblog\GraphQLBundle\EventListener\ClassLoaderListener;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -39,6 +40,7 @@ public function load(array $configs, ContainerBuilder $container)
$this->setShowDebug($config, $container);
$this->setDefinitionParameters($config, $container);
$this->setClassLoaderListener($config, $container);
+ $this->setCompilerCacheWarmer($config, $container);
$container->setParameter($this->getAlias().'.resources_dir', realpath(__DIR__.'/../Resources'));
}
@@ -67,6 +69,18 @@ public function getConfiguration(array $config, ContainerBuilder $container)
);
}
+ private function setCompilerCacheWarmer(array $config, ContainerBuilder $container)
+ {
+ if ($config['definitions']['auto_compile']) {
+ $definition = $container->setDefinition(
+ CompileCacheWarmer::class,
+ new Definition(CompileCacheWarmer::class)
+ );
+ $definition->setArguments([new Reference($this->getAlias().'.cache_compiler')]);
+ $definition->addTag('kernel.cache_warmer', ['priority' => 50]);
+ }
+ }
+
private function setClassLoaderListener(array $config, ContainerBuilder $container)
{
$container->setParameter($this->getAlias().'.use_classloader_listener', $config['definitions']['use_classloader_listener']);
diff --git a/Generator/TypeGenerator.php b/Generator/TypeGenerator.php
index 7803780a6..00686d974 100644
--- a/Generator/TypeGenerator.php
+++ b/Generator/TypeGenerator.php
@@ -7,6 +7,7 @@
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Error\UserWarning;
use Overblog\GraphQLGenerator\Generator\TypeGenerator as BaseTypeGenerator;
+use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Filesystem\Filesystem;
class TypeGenerator extends BaseTypeGenerator
@@ -17,12 +18,18 @@ class TypeGenerator extends BaseTypeGenerator
private $defaultResolver;
+ private $configs;
+
+ private $useClassMap = true;
+
private static $classMapLoaded = false;
- public function __construct($classNamespace, array $skeletonDirs, $cacheDir, callable $defaultResolver)
+ public function __construct($classNamespace, array $skeletonDirs, $cacheDir, callable $defaultResolver, array $configs, $useClassMap = true)
{
$this->setCacheDir($cacheDir);
$this->defaultResolver = $defaultResolver;
+ $this->configs = $this->processConfigs($configs);
+ $this->useClassMap = $useClassMap;
parent::__construct($classNamespace, $skeletonDirs);
}
@@ -179,17 +186,17 @@ function ($childrenComplexity, $args = []) {
return $code;
}
- public function compile(array $configs, $loadClasses = true)
+ public function compile($mode)
{
$cacheDir = $this->getCacheDir();
- if (file_exists($cacheDir)) {
+ $writeMode = $mode & self::MODE_WRITE;
+ if ($writeMode && file_exists($cacheDir)) {
$fs = new Filesystem();
$fs->remove($cacheDir);
}
+ $classes = $this->generateClasses($this->configs, $cacheDir, $mode);
- $classes = $this->generateClasses($configs, $cacheDir, true);
-
- if ($loadClasses) {
+ if ($writeMode && $this->useClassMap) {
$content = " \''.$cacheDir, ' => __DIR__ . \'', $content);
@@ -204,8 +211,9 @@ public function compile(array $configs, $loadClasses = true)
public function loadClasses($forceReload = false)
{
- if (!self::$classMapLoaded || $forceReload) {
- $classes = require $this->getClassesMap();
+ if ($this->useClassMap && (!self::$classMapLoaded || $forceReload)) {
+ $classMapFile = $this->getClassesMap();
+ $classes = file_exists($classMapFile) ? require $classMapFile : [];
/** @var ClassLoader $mapClassLoader */
static $mapClassLoader = null;
if (null === $mapClassLoader) {
@@ -225,4 +233,20 @@ private function getClassesMap()
{
return $this->getCacheDir().'/__classes.map';
}
+
+ private function processConfigs(array $configs)
+ {
+ return array_map(
+ function ($v) {
+ if (is_array($v)) {
+ return call_user_func([$this, 'processConfigs'], $v);
+ } elseif (is_string($v) && 0 === strpos($v, '@=')) {
+ return new Expression(substr($v, 2));
+ }
+
+ return $v;
+ },
+ $configs
+ );
+ }
}
diff --git a/Resolver/TypeResolver.php b/Resolver/TypeResolver.php
index ab64757b4..179903ba6 100644
--- a/Resolver/TypeResolver.php
+++ b/Resolver/TypeResolver.php
@@ -48,7 +48,14 @@ private function string2Type($alias)
private function baseType($alias)
{
- $type = $this->getSolution($alias);
+ try {
+ $type = $this->getSolution($alias);
+ } catch (\Error $error) {
+ throw self::createTypeLoadingException($alias, $error);
+ } catch (\Exception $exception) {
+ throw self::createTypeLoadingException($alias, $exception);
+ }
+
if (null !== $type) {
return $type;
}
@@ -88,6 +95,24 @@ private function hasNeedListOfWrapper($alias)
return false;
}
+ /**
+ * @param string $alias
+ * @param \Throwable $errorOrException
+ *
+ * @return \RuntimeException
+ */
+ private static function createTypeLoadingException($alias, $errorOrException)
+ {
+ return new \RuntimeException(
+ sprintf(
+ 'Type class for alias %s could not be load. If you are using your own classLoader verify the path and the namespace please.',
+ json_encode($alias)
+ ),
+ 0,
+ $errorOrException
+ );
+ }
+
protected function postLoadSolution($solution)
{
// also add solution with real type name if needed for typeLoader when using autoMapping
diff --git a/Resources/config/services.yml b/Resources/config/services.yml
index b56243ce4..a3eb309f1 100644
--- a/Resources/config/services.yml
+++ b/Resources/config/services.yml
@@ -36,8 +36,6 @@ services:
class: Overblog\GraphQLBundle\Request\Parser
public: true
-
-
overblog_graphql.request_batch_parser:
class: Overblog\GraphQLBundle\Request\BatchParser
@@ -97,6 +95,8 @@ services:
- ["%overblog_graphql.resources_dir%/skeleton"]
- "%overblog_graphql.cache_dir%"
- "%overblog_graphql.default_resolver%"
+ - "%overblog_graphql_types.config%"
+ - "%overblog_graphql.use_classloader_listener%"
calls:
- ["addUseStatement", ["Symfony\\Component\\DependencyInjection\\ContainerInterface"]]
- ["addUseStatement", ["Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface"]]
@@ -151,3 +151,11 @@ services:
- "@overblog_graphql.resolver_resolver"
tags:
- { name: console.command }
+
+ overblog_graphql.command.compile:
+ class: Overblog\GraphQLBundle\Command\CompileCommand
+ public: true
+ arguments:
+ - "@overblog_graphql.cache_compiler"
+ tags:
+ - { name: console.command }
diff --git a/Resources/doc/index.md b/Resources/doc/index.md
index 1df4b48fa..b8af144e4 100644
--- a/Resources/doc/index.md
+++ b/Resources/doc/index.md
@@ -106,6 +106,9 @@ overblog_graphql:
definitions:
# disable listener the bundle out of box classLoader
use_classloader_listener: false
+ # change to "false" to disable auto compilation.
+ # To generate types manually, see "graphql:compile" command.
+ auto_compile: true
# change classes cache dir (recommends using a directory that will be committed)
cache_dir: "/my/path/to/my/generated/classes"
# Can also change the namespace
diff --git a/Resources/skeleton/TypeSystem.php.skeleton b/Resources/skeleton/TypeSystem.php.skeleton
index 7f3cd8e97..9c12d23a2 100644
--- a/Resources/skeleton/TypeSystem.php.skeleton
+++ b/Resources/skeleton/TypeSystem.php.skeleton
@@ -4,6 +4,7 @@
class extends
{
+
public function __construct(ContainerInterface $container)
{
$request = null;
@@ -36,9 +37,4 @@
}
return $filtered;
}
-
-public static function getAliases()
-{
-return [preg_replace('/Type$/', '', substr(strrchr(__CLASS__, '\\'), 1))];
-}
}
diff --git a/Tests/Functional/App/config/generatorCommand/config.yml b/Tests/Functional/App/config/generatorCommand/config.yml
new file mode 100644
index 000000000..274707510
--- /dev/null
+++ b/Tests/Functional/App/config/generatorCommand/config.yml
@@ -0,0 +1,6 @@
+imports:
+ - { resource: ../connection/config.yml }
+
+overblog_graphql:
+ definitions:
+ auto_compile: false
diff --git a/Tests/Functional/Command/CompileCommandTest.php b/Tests/Functional/Command/CompileCommandTest.php
new file mode 100644
index 000000000..05ed59570
--- /dev/null
+++ b/Tests/Functional/Command/CompileCommandTest.php
@@ -0,0 +1,97 @@
+ 'generatorCommand']);
+
+ $this->command = static::$kernel->getContainer()->get('overblog_graphql.command.compile');
+ $this->typesMapping = static::$kernel->getContainer()->get('overblog_graphql.cache_compiler')
+ ->compile(TypeGenerator::MODE_MAPPING_ONLY);
+ $this->cacheDir = static::$kernel->getContainer()->get('overblog_graphql.cache_compiler')->getCacheDir();
+ $this->commandTester = new CommandTester($this->command);
+ }
+
+ public function testFilesNotExistsBeforeGeneration()
+ {
+ foreach ($this->typesMapping as $class => $path) {
+ $this->assertFileNotExists($path);
+ }
+ }
+
+ public function testGeneration()
+ {
+ $this->commandTester->execute([]);
+ $this->assertEquals(0, $this->commandTester->getStatusCode());
+ $this->assertEquals($this->displayExpected(), $this->commandTester->getDisplay());
+ foreach ($this->typesMapping as $class => $path) {
+ $this->assertFileExists($path);
+ }
+ }
+
+ public function testVerboseGeneration()
+ {
+ $this->commandTester->execute([], ['verbosity' => Output::VERBOSITY_VERBOSE]);
+ $this->assertEquals(0, $this->commandTester->getStatusCode());
+ $this->assertRegExp(
+ '@'.$this->displayExpected(true).'@',
+ preg_replace('@\.php[^\n]*\n@', ".php\n", $this->commandTester->getDisplay())
+ );
+ }
+
+ private function displayExpected($isVerbose = false)
+ {
+ $display = <<<'OUTPUT'
+Types compilation starts
+Types compilation ends successfully
+
+OUTPUT;
+
+ if ($isVerbose) {
+ $display .= <<<'OUTPUT'
+
+Summary
+=======
+
+ \-[\-]+\s+\-[\-]+\s
+ class\s+path\s*
+ \-[\-]+\s+\-[\-]+\s
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\PageInfoType {{PATH}}/PageInfoType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\QueryType {{PATH}}/QueryType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\UserType {{PATH}}/UserType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\friendConnectionType {{PATH}}/friendConnectionType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\userConnectionType {{PATH}}/userConnectionType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\friendEdgeType {{PATH}}/friendEdgeType.php
+ Overblog\\GraphQLBundle\\Connection\\__DEFINITIONS__\\userEdgeType {{PATH}}/userEdgeType.php
+ \-[\-]+\s+\-[\-]+\s
+
+
+OUTPUT;
+ $display = str_replace('{{PATH}}', $this->cacheDir, $display);
+ }
+
+ return $display;
+ }
+}
diff --git a/Tests/Functional/Security/AccessTest.php b/Tests/Functional/Security/AccessTest.php
index f91063e3e..a7bd64d1d 100644
--- a/Tests/Functional/Security/AccessTest.php
+++ b/Tests/Functional/Security/AccessTest.php
@@ -2,14 +2,13 @@
namespace Overblog\GraphQLBundle\Tests\Functional\Security;
-use Composer\Autoload\ClassLoader;
use Overblog\GraphQLBundle\Tests\Functional\App\Mutation\SimpleMutationWithThunkFieldsMutation;
use Overblog\GraphQLBundle\Tests\Functional\TestCase;
use Symfony\Component\HttpKernel\Kernel;
class AccessTest extends TestCase
{
- /** @var ClassLoader */
+ /** @var \Closure */
private $loader;
private $userNameQuery = 'query { user { name } }';
@@ -45,23 +44,25 @@ public function setUp()
{
parent::setUp();
// load types
- /** @var ClassLoader $loader */
- $loader = new ClassLoader();
- $loader->addPsr4(
- 'Overblog\\GraphQLBundle\\Access\\__DEFINITIONS__\\',
- '/tmp/OverblogGraphQLBundle/'.Kernel::VERSION.'/access/cache/testaccess/overblog/graphql-bundle/__definitions__'
- );
- $loader->register();
- $this->loader = $loader;
+ $this->loader = function ($class) {
+ if (preg_match('@^'.preg_quote('Overblog\GraphQLBundle\Access\__DEFINITIONS__\\').'(.*)$@', $class, $matches)) {
+ $file = '/tmp/OverblogGraphQLBundle/'.Kernel::VERSION.'/access/cache/testaccess/overblog/graphql-bundle/__definitions__/'.$matches[1].'.php';
+ if (file_exists($file)) {
+ require $file;
+ }
+ }
+ };
+ spl_autoload_register($this->loader);
}
/**
* @expectedException \RuntimeException
- * @expectedExceptionMessage Type class "Overblog\\GraphQLBundle\\Access\\__DEFINITIONS__\\PageInfoType" not found. If you are using your own classLoader verify the path and the namespace please.
+ * @expectedExceptionMessage Type class for alias "RootQuery" could not be load. If you are using your own classLoader verify the path and the namespace please.
+ * @requires PHP 7
*/
public function testCustomClassLoaderNotRegister()
{
- $this->loader->unregister();
+ spl_autoload_unregister($this->loader);
$this->assertResponse($this->userNameQuery, [], static::ANONYMOUS_USER, 'access');
}
diff --git a/composer.json b/composer.json
index 670e7cb2c..a33dc0b04 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
"require": {
"php": ">=5.6",
"doctrine/doctrine-cache-bundle": "^1.2",
- "overblog/graphql-php-generator": "^0.5.0",
+ "overblog/graphql-php-generator": "^0.6.0",
"symfony/cache": "^3.1 || ^4.0",
"symfony/config": "^3.1 || ^4.0",
"symfony/dependency-injection": "^3.1 || ^4.0",