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",