GraphQlReader.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\Framework\GraphQlSchemaStitching;
  8. use Magento\Framework\Config\FileResolverInterface;
  9. use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface as TypeReaderComposite;
  10. use Magento\Framework\Config\ReaderInterface;
  11. /**
  12. * Reads *.graphqls files from modules and combines the results as array to be used with a library to configure objects
  13. */
  14. class GraphQlReader implements ReaderInterface
  15. {
  16. const GRAPHQL_PLACEHOLDER_FIELD_NAME = 'placeholder_graphql_field';
  17. const GRAPHQL_SCHEMA_FILE = 'schema.graphqls';
  18. /**
  19. * File locator
  20. *
  21. * @var FileResolverInterface
  22. */
  23. private $fileResolver;
  24. /**
  25. * @var TypeReaderComposite
  26. */
  27. private $typeReader;
  28. /**
  29. * @var string
  30. */
  31. private $fileName;
  32. /**
  33. * @var string
  34. */
  35. private $defaultScope;
  36. /**
  37. * @param FileResolverInterface $fileResolver
  38. * @param TypeReaderComposite $typeReader
  39. * @param string $fileName
  40. * @param string $defaultScope
  41. */
  42. public function __construct(
  43. FileResolverInterface $fileResolver,
  44. TypeReaderComposite $typeReader,
  45. $fileName = self::GRAPHQL_SCHEMA_FILE,
  46. $defaultScope = 'global'
  47. ) {
  48. $this->fileResolver = $fileResolver;
  49. $this->typeReader = $typeReader;
  50. $this->defaultScope = $defaultScope;
  51. $this->fileName = $fileName;
  52. }
  53. /**
  54. * {@inheritdoc}
  55. */
  56. public function read($scope = null) : array
  57. {
  58. $results = [];
  59. $scope = $scope ?: $this->defaultScope;
  60. $schemaFiles = $this->fileResolver->get($this->fileName, $scope);
  61. if (!count($schemaFiles)) {
  62. return $results;
  63. }
  64. /**
  65. * Compatible with @see GraphQlReader::parseTypes
  66. */
  67. $knownTypes = [];
  68. foreach ($schemaFiles as $partialSchemaContent) {
  69. $partialSchemaTypes = $this->parseTypes($partialSchemaContent);
  70. // Keep declarations from current partial schema, add missing declarations from all previously read schemas
  71. $knownTypes = $partialSchemaTypes + $knownTypes;
  72. $schemaContent = implode("\n", $knownTypes);
  73. $partialResults = $this->readPartialTypes($schemaContent);
  74. $results = array_replace_recursive($results, $partialResults);
  75. }
  76. $results = $this->copyInterfaceFieldsToConcreteTypes($results);
  77. return $results;
  78. }
  79. /**
  80. * Extract types as string from schema as string
  81. *
  82. * @param string $graphQlSchemaContent
  83. * @return string[] [$typeName => $typeDeclaration, ...]
  84. */
  85. private function readPartialTypes(string $graphQlSchemaContent) : array
  86. {
  87. $partialResults = [];
  88. $graphQlSchemaContent = $this->addPlaceHolderInSchema($graphQlSchemaContent);
  89. $schema = \GraphQL\Utils\BuildSchema::build($graphQlSchemaContent);
  90. foreach ($schema->getTypeMap() as $typeName => $typeMeta) {
  91. // Only process custom types and skip built-in object types
  92. if ((strpos($typeName, '__') !== 0 && (!$typeMeta instanceof \GraphQL\Type\Definition\ScalarType))) {
  93. $type = $this->typeReader->read($typeMeta);
  94. if (!empty($type)) {
  95. $partialResults[$typeName] = $type;
  96. } else {
  97. throw new \LogicException("'{$typeName}' cannot be processed.");
  98. }
  99. }
  100. }
  101. $partialResults = $this->removePlaceholderFromResults($partialResults);
  102. return $partialResults;
  103. }
  104. /**
  105. * Extract types as string from a larger string that represents the graphql schema using regular expressions
  106. *
  107. * @param string $graphQlSchemaContent
  108. * @return string[] [$typeName => $typeDeclaration, ...]
  109. */
  110. private function parseTypes(string $graphQlSchemaContent) : array
  111. {
  112. $typeKindsPattern = '(type|interface|union|enum|input)';
  113. $typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)';
  114. $typeDefinitionPattern = '([^\{]*)(\{[^\}]*\})';
  115. $spacePattern = '[\s\t\n\r]+';
  116. preg_match_all(
  117. "/{$typeKindsPattern}{$spacePattern}{$typeNamePattern}{$spacePattern}{$typeDefinitionPattern}/i",
  118. $graphQlSchemaContent,
  119. $matches
  120. );
  121. $parsedTypes = [];
  122. if (!empty($matches)) {
  123. foreach ($matches[0] as $matchKey => $matchValue) {
  124. $matches[0][$matchKey] = $this->convertInterfacesToAnnotations($matchValue);
  125. }
  126. /**
  127. * $matches[0] is an indexed array with the whole type definitions
  128. * $matches[2] is an indexed array with type names
  129. */
  130. $parsedTypes = array_combine($matches[2], $matches[0]);
  131. }
  132. return $parsedTypes;
  133. }
  134. /**
  135. * Copy interface fields to concrete types
  136. *
  137. * @param array $source
  138. * @return array
  139. */
  140. private function copyInterfaceFieldsToConcreteTypes(array $source): array
  141. {
  142. foreach ($source as $interface) {
  143. if ($interface['type'] == 'graphql_interface') {
  144. foreach ($source as $typeName => $type) {
  145. if (isset($type['implements'])
  146. && isset($type['implements'][$interface['name']])
  147. && isset($type['implements'][$interface['name']]['copyFields'])
  148. && $type['implements'][$interface['name']]['copyFields'] === true
  149. ) {
  150. $source[$typeName]['fields'] = isset($type['fields'])
  151. ? array_replace($interface['fields'], $type['fields']) : $interface['fields'];
  152. }
  153. }
  154. }
  155. }
  156. return $source;
  157. }
  158. /**
  159. * Find the implements statement and convert them to annotation to enable copy fields feature
  160. *
  161. * @param string $graphQlSchemaContent
  162. * @return string
  163. */
  164. private function convertInterfacesToAnnotations(string $graphQlSchemaContent): string
  165. {
  166. $implementsKindsPattern = 'implements';
  167. $typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)';
  168. $spacePattern = '([\s\t\n\r]+)';
  169. $spacePatternNotMandatory = '[\s\t\n\r]*';
  170. preg_match_all(
  171. "/{$spacePattern}{$implementsKindsPattern}{$spacePattern}{$typeNamePattern}"
  172. . "(,{$spacePatternNotMandatory}$typeNamePattern)*/im",
  173. $graphQlSchemaContent,
  174. $allMatchesForImplements
  175. );
  176. if (!empty($allMatchesForImplements)) {
  177. foreach (array_unique($allMatchesForImplements[0]) as $implementsString) {
  178. $implementsStatementString = preg_replace(
  179. "/{$spacePattern}{$implementsKindsPattern}{$spacePattern}/m",
  180. '',
  181. $implementsString
  182. );
  183. preg_match_all(
  184. "/{$typeNamePattern}+/im",
  185. $implementsStatementString,
  186. $implementationsMatches
  187. );
  188. if (!empty($implementationsMatches)) {
  189. $annotationString = ' @implements(interfaces: [';
  190. foreach ($implementationsMatches[0] as $interfaceName) {
  191. $annotationString.= "\"{$interfaceName}\", ";
  192. }
  193. $annotationString = rtrim($annotationString, ', ');
  194. $annotationString .= ']) ';
  195. $graphQlSchemaContent = str_replace($implementsString, $annotationString, $graphQlSchemaContent);
  196. }
  197. }
  198. }
  199. return $graphQlSchemaContent;
  200. }
  201. /**
  202. * Add a placeholder field into the schema to allow parser to not throw error on empty types
  203. * This method is paired with @see self::removePlaceholderFromResults()
  204. * This is needed so that the placeholder doens't end up in the actual schema
  205. *
  206. * @param string $graphQlSchemaContent
  207. * @return string
  208. */
  209. private function addPlaceHolderInSchema(string $graphQlSchemaContent) :string
  210. {
  211. $placeholderField = self::GRAPHQL_PLACEHOLDER_FIELD_NAME;
  212. $typesKindsPattern = '(type|interface|input)';
  213. $enumKindsPattern = '(enum)';
  214. $typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)';
  215. $typeDefinitionPattern = '([^\{]*)(\{[\s\t\n\r^\}]*\})';
  216. $spacePattern = '([\s\t\n\r]+)';
  217. //add placeholder in empty types
  218. $graphQlSchemaContent = preg_replace(
  219. "/{$typesKindsPattern}{$spacePattern}{$typeNamePattern}{$spacePattern}{$typeDefinitionPattern}/im",
  220. "\$1\$2\$3\$4\$5{\n{$placeholderField}: String\n}",
  221. $graphQlSchemaContent
  222. );
  223. //add placeholder in empty enums
  224. $graphQlSchemaContent = preg_replace(
  225. "/{$enumKindsPattern}{$spacePattern}{$typeNamePattern}{$spacePattern}{$typeDefinitionPattern}/im",
  226. "\$1\$2\$3\$4\$5{\n{$placeholderField}\n}",
  227. $graphQlSchemaContent
  228. );
  229. return $graphQlSchemaContent;
  230. }
  231. /**
  232. * Remove parsed placeholders as these should not be present in final result
  233. *
  234. * @param array $partialResults
  235. * @return array
  236. */
  237. private function removePlaceholderFromResults(array $partialResults) : array
  238. {
  239. $placeholderField = self::GRAPHQL_PLACEHOLDER_FIELD_NAME;
  240. //remove parsed placeholders
  241. foreach ($partialResults as $typeKeyName => $partialResultTypeArray) {
  242. if (isset($partialResultTypeArray['fields'][$placeholderField])) {
  243. //unset placeholder for fields
  244. unset($partialResults[$typeKeyName]['fields'][$placeholderField]);
  245. } elseif (isset($partialResultTypeArray['items'][$placeholderField])) {
  246. //unset placeholder for enums
  247. unset($partialResults[$typeKeyName]['items'][$placeholderField]);
  248. }
  249. }
  250. return $partialResults;
  251. }
  252. }