vendor/overblog/graphql-bundle/src/DependencyInjection/Compiler/ConfigParserPass.php line 236

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Overblog\GraphQLBundle\DependencyInjection\Compiler;
  4. use InvalidArgumentException;
  5. use Overblog\GraphQLBundle\Config\Parser\AnnotationParser;
  6. use Overblog\GraphQLBundle\Config\Parser\AttributeParser;
  7. use Overblog\GraphQLBundle\Config\Parser\GraphQLParser;
  8. use Overblog\GraphQLBundle\Config\Parser\PreParserInterface;
  9. use Overblog\GraphQLBundle\Config\Parser\YamlParser;
  10. use Overblog\GraphQLBundle\DependencyInjection\TypesConfiguration;
  11. use Overblog\GraphQLBundle\OverblogGraphQLBundle;
  12. use ReflectionClass;
  13. use ReflectionException;
  14. use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
  15. use Symfony\Component\Config\Definition\Processor;
  16. use Symfony\Component\Config\Resource\FileResource;
  17. use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
  18. use Symfony\Component\DependencyInjection\ContainerBuilder;
  19. use Symfony\Component\Finder\Finder;
  20. use Symfony\Component\Finder\SplFileInfo;
  21. use function array_count_values;
  22. use function array_filter;
  23. use function array_keys;
  24. use function array_map;
  25. use function array_merge;
  26. use function array_replace_recursive;
  27. use function dirname;
  28. use function implode;
  29. use function is_a;
  30. use function is_dir;
  31. use function sprintf;
  32. class ConfigParserPass implements CompilerPassInterface
  33. {
  34. public const SUPPORTED_TYPES_EXTENSIONS = [
  35. 'yaml' => '{yaml,yml}',
  36. 'graphql' => '{graphql,graphqls}',
  37. 'annotation' => 'php',
  38. 'attribute' => 'php',
  39. ];
  40. /**
  41. * @var array<string, class-string<PreParserInterface>>
  42. */
  43. public const PARSERS = [
  44. 'yaml' => YamlParser::class,
  45. 'graphql' => GraphQLParser::class,
  46. 'annotation' => AnnotationParser::class,
  47. 'attribute' => AttributeParser::class,
  48. ];
  49. private static array $defaultDefaultConfig = [
  50. 'definitions' => [
  51. 'mappings' => [
  52. 'auto_discover' => [
  53. 'root_dir' => true,
  54. 'bundles' => true,
  55. 'built_in' => true,
  56. ],
  57. 'types' => [],
  58. ],
  59. ],
  60. ];
  61. private array $treatedFiles = [];
  62. private array $preTreatedFiles = [];
  63. public const DEFAULT_TYPES_SUFFIX = '.types';
  64. public function process(ContainerBuilder $container): void
  65. {
  66. $config = $this->processConfiguration([$this->getConfigs($container)]);
  67. $container->setParameter($this->getAlias().'.config', $config);
  68. }
  69. public function processConfiguration(array $configs): array
  70. {
  71. return (new Processor())->processConfiguration(new TypesConfiguration(), $configs);
  72. }
  73. private function getConfigs(ContainerBuilder $container): array
  74. {
  75. $config = $container->getParameterBag()->resolveValue($container->getParameter('overblog_graphql.config'));
  76. $container->getParameterBag()->remove('overblog_graphql.config');
  77. $container->setParameter($this->getAlias().'.classes_map', []);
  78. $typesMappings = $this->mappingConfig($config, $container);
  79. // reset treated files
  80. $this->treatedFiles = [];
  81. $typesMappings = array_merge(...$typesMappings);
  82. $typeConfigs = [];
  83. // treats mappings
  84. // Pre-parse all files
  85. AnnotationParser::reset($config);
  86. AttributeParser::reset($config);
  87. $typesNeedPreParsing = $this->typesNeedPreParsing();
  88. foreach ($typesMappings as $params) {
  89. if ($typesNeedPreParsing[$params['type']]) {
  90. $this->parseTypeConfigFiles($params['type'], $params['files'], $container, $config, true);
  91. }
  92. }
  93. // Parse all files and get related config
  94. foreach ($typesMappings as $params) {
  95. $typeConfigs = array_merge($typeConfigs, $this->parseTypeConfigFiles($params['type'], $params['files'], $container, $config));
  96. }
  97. $this->checkTypesDuplication($typeConfigs);
  98. // flatten config is a requirement to support inheritance
  99. $flattenTypeConfig = array_merge(...$typeConfigs);
  100. return $flattenTypeConfig;
  101. }
  102. private function typesNeedPreParsing(): array
  103. {
  104. $needPreParsing = [];
  105. foreach (self::PARSERS as $type => $className) {
  106. $needPreParsing[$type] = is_a($className, PreParserInterface::class, true);
  107. }
  108. return $needPreParsing;
  109. }
  110. /**
  111. * @param SplFileInfo[] $files
  112. */
  113. private function parseTypeConfigFiles(string $type, iterable $files, ContainerBuilder $container, array $configs, bool $preParse = false): array
  114. {
  115. if ($preParse) {
  116. $method = 'preParse';
  117. $treatedFiles = &$this->preTreatedFiles;
  118. } else {
  119. $method = 'parse';
  120. $treatedFiles = &$this->treatedFiles;
  121. }
  122. $config = [];
  123. foreach ($files as $file) {
  124. $fileRealPath = $file->getRealPath();
  125. if (isset($treatedFiles[$fileRealPath])) {
  126. continue;
  127. }
  128. $parser = [self::PARSERS[$type], $method];
  129. if (is_callable($parser)) {
  130. $config[] = ($parser)($file, $container, $configs);
  131. }
  132. $treatedFiles[$file->getRealPath()] = true;
  133. }
  134. return $config;
  135. }
  136. private function checkTypesDuplication(array $typeConfigs): void
  137. {
  138. $types = array_merge(...array_map('array_keys', $typeConfigs));
  139. $duplications = array_keys(array_filter(array_count_values($types), fn ($count) => $count > 1));
  140. if (!empty($duplications)) {
  141. throw new ForbiddenOverwriteException(sprintf(
  142. 'Types (%s) cannot be overwritten. See inheritance doc section for more details.',
  143. implode(', ', array_map('json_encode', $duplications))
  144. ));
  145. }
  146. }
  147. private function mappingConfig(array $config, ContainerBuilder $container): array
  148. {
  149. // use default value if needed
  150. $config = array_replace_recursive(self::$defaultDefaultConfig, $config);
  151. $mappingConfig = $config['definitions']['mappings'];
  152. $typesMappings = $mappingConfig['types'];
  153. // app only config files (yml or xml or graphql)
  154. if ($mappingConfig['auto_discover']['root_dir'] && $container->hasParameter('kernel.root_dir')) {
  155. // @phpstan-ignore-next-line
  156. $typesMappings[] = ['dir' => $container->getParameter('kernel.root_dir').'/config/graphql', 'types' => null];
  157. }
  158. if ($mappingConfig['auto_discover']['bundles']) {
  159. $mappingFromBundles = $this->mappingFromBundles($container);
  160. $typesMappings = array_merge($typesMappings, $mappingFromBundles);
  161. }
  162. if ($mappingConfig['auto_discover']['built_in']) {
  163. $typesMappings[] = [
  164. 'dir' => $this->bundleDir(OverblogGraphQLBundle::class).'/Resources/config/graphql',
  165. 'types' => ['yaml'],
  166. ];
  167. }
  168. // from config
  169. $typesMappings = $this->detectFilesFromTypesMappings($typesMappings, $container);
  170. return $typesMappings;
  171. }
  172. private function detectFilesFromTypesMappings(array $typesMappings, ContainerBuilder $container): array
  173. {
  174. return array_filter(array_map(
  175. function (array $typeMapping) use ($container) {
  176. $suffix = $typeMapping['suffix'] ?? '';
  177. $types = $typeMapping['types'] ?? null;
  178. return $this->detectFilesByTypes($container, $typeMapping['dir'], $suffix, $types);
  179. },
  180. $typesMappings
  181. ));
  182. }
  183. private function mappingFromBundles(ContainerBuilder $container): array
  184. {
  185. $typesMappings = [];
  186. /** @var array<string, class-string> $bundles */
  187. $bundles = $container->getParameter('kernel.bundles');
  188. // auto detect from bundle
  189. foreach ($bundles as $class) {
  190. // skip this bundle
  191. if (OverblogGraphQLBundle::class === $class) {
  192. continue;
  193. }
  194. $bundleDir = $this->bundleDir($class);
  195. // only config files (yml)
  196. $typesMappings[] = ['dir' => $bundleDir.'/Resources/config/graphql', 'types' => null];
  197. }
  198. return $typesMappings;
  199. }
  200. private function detectFilesByTypes(ContainerBuilder $container, string $path, string $suffix, array $types = null): array
  201. {
  202. // add the closest existing directory as a resource
  203. $resource = $path;
  204. while (!is_dir($resource)) {
  205. $resource = dirname($resource);
  206. }
  207. $container->addResource(new FileResource($resource));
  208. $stopOnFirstTypeMatching = empty($types);
  209. $types = $stopOnFirstTypeMatching ? array_keys(self::SUPPORTED_TYPES_EXTENSIONS) : $types;
  210. $files = [];
  211. foreach ($types as $type) {
  212. $finder = Finder::create();
  213. try {
  214. $finder->files()->in($path)->name(sprintf('*%s.%s', $suffix, self::SUPPORTED_TYPES_EXTENSIONS[$type]));
  215. } catch (InvalidArgumentException $e) {
  216. continue;
  217. }
  218. if ($finder->count() > 0) {
  219. $files[] = [
  220. 'type' => $type,
  221. 'files' => $finder,
  222. ];
  223. if ($stopOnFirstTypeMatching) {
  224. break;
  225. }
  226. }
  227. }
  228. return $files;
  229. }
  230. /**
  231. * @throws ReflectionException
  232. */
  233. private function bundleDir(string $bundleClass): string
  234. {
  235. $bundle = new ReflectionClass($bundleClass); // @phpstan-ignore-line
  236. return dirname($bundle->getFileName());
  237. }
  238. private function getAliasPrefix(): string
  239. {
  240. return 'overblog_graphql';
  241. }
  242. private function getAlias(): string
  243. {
  244. return $this->getAliasPrefix().'_types';
  245. }
  246. }