diff --git a/.gitignore b/.gitignore index 638ad6a42..9730d573c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea phpunit.xml /build /vendor diff --git a/README.md b/README.md index 241484d34..dbe8cd3ad 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Documentation - [Errors handling](docs/error-handling/index.md) - [Events](docs/events/index.md) - [Profiler](docs/profiler/index.md) +- [Tune configuration](docs/tune_configuration.md) Talks and slides to help you start ---------------------------------- diff --git a/composer.json b/composer.json index 68fb15164..28a3ece10 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ } }, "config": { + "allow-plugins": { + "phpstan/extension-installer": true + }, "bin-dir": "bin", "sort-packages": true }, diff --git a/docs/tune_configuration.md b/docs/tune_configuration.md new file mode 100644 index 000000000..954ffad9b --- /dev/null +++ b/docs/tune_configuration.md @@ -0,0 +1,22 @@ +Tune configuration +================== + +Custom GraphQl configuration parsers +------------------------------------ + +You can configure custom GraphQl configuration parsers. +Your parsers MUST implement at least `\Overblog\GraphQLBundle\Config\Parser\ParserInterface` +and optionally `\Overblog\GraphQLBundle\Config\Parser\PreParserInterface` when required. + +Default values will be applied when omitted. + +```yaml +overblog_graphql: + # ... + parsers: + yaml: 'Overblog\GraphQLBundle\Config\Parser\YamlParser' + graphql: 'Overblog\GraphQLBundle\Config\Parser\GraphQLParser' + annotation: 'Overblog\GraphQLBundle\Config\Parser\AnnotationParser' + attribute: 'Overblog\GraphQLBundle\Config\Parser\AttributeParser' + # ... +``` diff --git a/src/DependencyInjection/Compiler/ConfigParserPass.php b/src/DependencyInjection/Compiler/ConfigParserPass.php index 19178e0de..fb15d2aec 100644 --- a/src/DependencyInjection/Compiler/ConfigParserPass.php +++ b/src/DependencyInjection/Compiler/ConfigParserPass.php @@ -8,6 +8,7 @@ use Overblog\GraphQLBundle\Config\Parser\AnnotationParser; use Overblog\GraphQLBundle\Config\Parser\AttributeParser; use Overblog\GraphQLBundle\Config\Parser\GraphQLParser; +use Overblog\GraphQLBundle\Config\Parser\ParserInterface; use Overblog\GraphQLBundle\Config\Parser\PreParserInterface; use Overblog\GraphQLBundle\Config\Parser\YamlParser; use Overblog\GraphQLBundle\DependencyInjection\TypesConfiguration; @@ -35,24 +36,37 @@ class ConfigParserPass implements CompilerPassInterface { + public const TYPE_YAML = 'yaml'; + public const TYPE_GRAPHQL = 'graphql'; + public const TYPE_ANNOTATION = 'annotation'; + public const TYPE_ATTRIBUTE = 'attribute'; + + public const SUPPORTED_TYPES = [ + self::TYPE_YAML, + self::TYPE_GRAPHQL, + self::TYPE_ANNOTATION, + self::TYPE_ATTRIBUTE, + ]; + public const SUPPORTED_TYPES_EXTENSIONS = [ - 'yaml' => '{yaml,yml}', - 'graphql' => '{graphql,graphqls}', - 'annotation' => 'php', - 'attribute' => 'php', + self::TYPE_YAML => '{yaml,yml}', + self::TYPE_GRAPHQL => '{graphql,graphqls}', + self::TYPE_ANNOTATION => 'php', + self::TYPE_ATTRIBUTE => 'php', ]; /** - * @var array> + * @deprecated They are going to be configurable. + * @var array> */ public const PARSERS = [ - 'yaml' => YamlParser::class, - 'graphql' => GraphQLParser::class, - 'annotation' => AnnotationParser::class, - 'attribute' => AttributeParser::class, + self::TYPE_YAML => YamlParser::class, + self::TYPE_GRAPHQL => GraphQLParser::class, + self::TYPE_ANNOTATION => AnnotationParser::class, + self::TYPE_ATTRIBUTE => AttributeParser::class, ]; - private static array $defaultDefaultConfig = [ + private const DEFAULT_CONFIG = [ 'definitions' => [ 'mappings' => [ 'auto_discover' => [ @@ -63,8 +77,15 @@ class ConfigParserPass implements CompilerPassInterface 'types' => [], ], ], + 'parsers' => self::PARSERS, ]; + /** + * @deprecated Use {@see ConfigParserPass::PARSERS }. Added for the backward compatibility. + * @var array> + */ + private static array $defaultDefaultConfig = self::DEFAULT_CONFIG; + private array $treatedFiles = []; private array $preTreatedFiles = []; @@ -86,6 +107,10 @@ private function getConfigs(ContainerBuilder $container): array $config = $container->getParameterBag()->resolveValue($container->getParameter('overblog_graphql.config')); $container->getParameterBag()->remove('overblog_graphql.config'); $container->setParameter($this->getAlias().'.classes_map', []); + + // use default value if needed + $config = array_replace_recursive(self::DEFAULT_CONFIG, $config); + $typesMappings = $this->mappingConfig($config, $container); // reset treated files $this->treatedFiles = []; @@ -96,7 +121,7 @@ private function getConfigs(ContainerBuilder $container): array // Pre-parse all files AnnotationParser::reset($config); AttributeParser::reset($config); - $typesNeedPreParsing = $this->typesNeedPreParsing(); + $typesNeedPreParsing = $this->typesNeedPreParsing($config['parsers']); foreach ($typesMappings as $params) { if ($typesNeedPreParsing[$params['type']]) { $this->parseTypeConfigFiles($params['type'], $params['files'], $container, $config, true); @@ -115,10 +140,15 @@ private function getConfigs(ContainerBuilder $container): array return $flattenTypeConfig; } - private function typesNeedPreParsing(): array + /** + * @param array $parsers + * + * @return array + */ + private function typesNeedPreParsing(array $parsers): array { $needPreParsing = []; - foreach (self::PARSERS as $type => $className) { + foreach ($parsers as $type => $className) { $needPreParsing[$type] = is_a($className, PreParserInterface::class, true); } @@ -145,7 +175,7 @@ private function parseTypeConfigFiles(string $type, iterable $files, ContainerBu continue; } - $parser = [self::PARSERS[$type], $method]; + $parser = [$configs['parsers'][$type], $method]; if (is_callable($parser)) { $config[] = ($parser)($file, $container, $configs); } @@ -169,9 +199,6 @@ private function checkTypesDuplication(array $typeConfigs): void private function mappingConfig(array $config, ContainerBuilder $container): array { - // use default value if needed - $config = array_replace_recursive(self::$defaultDefaultConfig, $config); - $mappingConfig = $config['definitions']['mappings']; $typesMappings = $mappingConfig['types']; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 18a2b335c..7e04a9cef 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -7,6 +7,7 @@ use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; +use Overblog\GraphQLBundle\Config\Parser\ParserInterface; use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\DependencyInjection\Compiler\ConfigParserPass; use Overblog\GraphQLBundle\Error\ErrorHandler; @@ -57,6 +58,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->append($this->securitySection()) ->append($this->doctrineSection()) ->append($this->profilerSection()) + ->append($this->parsersSection()) ->end(); return $treeBuilder; @@ -318,6 +320,32 @@ private function doctrineSection(): ArrayNodeDefinition return $node; } + private function parsersSection(): ArrayNodeDefinition + { + /** @var ArrayNodeDefinition $node */ + $node = (new TreeBuilder('parsers'))->getRootNode(); + $node->useAttributeAsKey('name'); + + $parserPrototype = $node->scalarPrototype(); + $parserPrototype->cannotBeEmpty(); + $parserPrototype->validate() + ->ifTrue(static function (string $x): bool { + return !is_subclass_of($x, ParserInterface::class, true); + }) + ->thenInvalid(sprintf('Parser MUST implement "%s', ParserInterface::class)); + + $node->validate() + ->ifTrue(static function (array $x): bool { + return (bool) array_diff(array_keys($x), ConfigParserPass::SUPPORTED_TYPES); + }) + ->then(static function (array $x) { + $types = implode(', ', array_diff(array_keys($x), ConfigParserPass::SUPPORTED_TYPES)); + throw new \InvalidArgumentException(sprintf('Configured parsers for not supported types: %s', $types)); + }); + + return $node; + } + private function profilerSection(): ArrayNodeDefinition { $builder = new TreeBuilder('profiler');