99
1010use GraphQL \Type \Definition \ScalarType ;
1111use GraphQL \Utils \BuildSchema ;
12- use Magento \Framework \Component \ComponentRegistrar ;
1312use Magento \Framework \Config \FileResolverInterface ;
1413use Magento \Framework \Config \ReaderInterface ;
1514use Magento \Framework \GraphQl \Type \TypeManagement ;
@@ -48,11 +47,6 @@ class GraphQlReader implements ReaderInterface
4847 */
4948 private $ defaultScope ;
5049
51- /**
52- * @var ComponentRegistrar
53- */
54- private static $ componentRegistrar ;
55-
5650 /**
5751 * @param FileResolverInterface $fileResolver
5852 * @param TypeReaderComposite $typeReader
@@ -75,7 +69,7 @@ public function __construct(
7569
7670 /**
7771 * @inheritdoc
78- *
72+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
7973 * @param string|null $scope
8074 * @return array
8175 */
@@ -84,28 +78,103 @@ public function read($scope = null): array
8478 $ results = [];
8579 $ scope = $ scope ?: $ this ->defaultScope ;
8680 $ schemaFiles = $ this ->fileResolver ->get ($ this ->fileName , $ scope );
87-
8881 if (!count ($ schemaFiles )) {
8982 return $ results ;
9083 }
9184
9285 /**
86+ * Gather as many schema together to be parsed in one go for performance
87+ * Collect any duplicate types in an array to retry after the initial large parse
88+ *
9389 * Compatible with @see GraphQlReader::parseTypes
9490 */
91+ $ typesToRedo = [];
9592 $ knownTypes = [];
96- foreach ($ schemaFiles as $ filePath => $ partialSchemaContent ) {
97- $ partialSchemaTypes = $ this ->parseTypes ($ partialSchemaContent );
93+ foreach ($ schemaFiles as $ partialSchemaContent ) {
94+ $ partialSchemaTypes = $ this ->parseTypesWithUnionHandling ($ partialSchemaContent );
95+
96+ // Filter out duplicated ones and save them into a list to be retried
97+ $ tmpTypes = $ knownTypes ;
98+ foreach ($ partialSchemaTypes as $ intendedKey => $ partialSchemaType ) {
99+ if (isset ($ tmpTypes [$ intendedKey ])) {
100+ if (!isset ($ typesToRedo [$ intendedKey ])) {
101+ $ typesToRedo [$ intendedKey ] = [];
102+ }
103+ $ typesToRedo [$ intendedKey ][] = $ partialSchemaType ;
104+ continue ;
105+ }
106+ $ tmpTypes [$ intendedKey ] = $ partialSchemaType ;
107+ }
108+ $ knownTypes = $ tmpTypes ;
109+ }
110+
111+ /**
112+ * Read this large batch of data, this builds most of the $results array
113+ */
114+ $ schemaContent = implode ("\n" , $ knownTypes );
115+ $ results = $ this ->readPartialTypes ($ schemaContent );
116+
117+ /**
118+ * Go over the list of types to be retried and batch them up into as few batches as possible
119+ */
120+ $ typesToRedoBatches = [];
121+ foreach ($ typesToRedo as $ type => $ batches ) {
122+ foreach ($ batches as $ id => $ data ) {
123+ if (!isset ($ typesToRedoBatches [$ id ])) {
124+ $ typesToRedoBatches [$ id ] = [];
125+ }
126+ $ typesToRedoBatches [$ id ][$ type ] = $ data ;
127+ }
128+ }
98129
99- // Keep declarations from current partial schema, add missing declarations from all previously read schemas
100- $ knownTypes = $ partialSchemaTypes + $ knownTypes ;
101- $ schemaContent = implode ("\n" , $ knownTypes );
130+ /**
131+ * Process each remaining batch with the minimal amount of additional schema data for performance
132+ */
133+ foreach ($ typesToRedoBatches as $ typesToRedoBatch ) {
134+ $ typesToUse = $ this ->getTypesToUse ($ typesToRedoBatch , $ knownTypes );
135+ $ knownTypes = $ typesToUse + $ knownTypes ;
136+ $ schemaContent = implode ("\n" , $ typesToUse );
102137
103138 $ partialResults = $ this ->readPartialTypes ($ schemaContent );
104139 $ results = array_replace_recursive ($ results , $ partialResults );
105- $ results = $ this ->addModuleNameToTypes ($ results , $ filePath );
106140 }
107141
108- return $ this ->copyInterfaceFieldsToConcreteTypes ($ results );
142+ $ results = $ this ->copyInterfaceFieldsToConcreteTypes ($ results );
143+ return $ results ;
144+ }
145+
146+ /**
147+ * Get the minimum amount of additional types so that performance is improved
148+ *
149+ * The use of a strpos check here is a bit odd in the context of feeding data into an AST but for the performance
150+ * gains and to prevent downtime it is necessary
151+ *
152+ * @link https://github.com/webonyx/graphql-php/issues/244
153+ * @link https://github.com/webonyx/graphql-php/issues/244#issuecomment-383912418
154+ *
155+ * @param array $typesToRedoBatch
156+ * @param array $types
157+ * @return array
158+ */
159+ private function getTypesToUse ($ typesToRedoBatch , $ types ): array
160+ {
161+ $ totalKnownSymbolsCount = count ($ typesToRedoBatch ) + count ($ types );
162+
163+ $ typesToUse = $ typesToRedoBatch ;
164+ for ($ i =0 ; $ i < $ totalKnownSymbolsCount ; $ i ++) {
165+ $ changesMade = false ;
166+ $ schemaContent = implode ("\n" , $ typesToUse );
167+ foreach ($ types as $ type => $ schema ) {
168+ if ((!isset ($ typesToUse [$ type ]) && strpos ($ schemaContent , $ type ) !== false )) {
169+ $ typesToUse [$ type ] = $ schema ;
170+ $ changesMade = true ;
171+ }
172+ }
173+ if (!$ changesMade ) {
174+ break ;
175+ }
176+ }
177+ return $ typesToUse ;
109178 }
110179
111180 /**
@@ -137,6 +206,56 @@ private function readPartialTypes(string $graphQlSchemaContent): array
137206 return $ this ->removePlaceholderFromResults ($ partialResults );
138207 }
139208
209+ /**
210+ * Extract types as string from a larger string that represents the graphql schema using regular expressions
211+ *
212+ * The regex in parseTypes does not have the ability to split out the union data from the type below it for example
213+ *
214+ * > union X = Y | Z
215+ * >
216+ * > type foo {}
217+ *
218+ * This would produce only type key from parseTypes, X, which would contain also the type foo entry.
219+ *
220+ * This wrapper does some post processing as a workaround to split out the union data from the type data below it
221+ * which would give us two entries, X and foo
222+ *
223+ * @param string $graphQlSchemaContent
224+ * @return string[] [$typeName => $typeDeclaration, ...]
225+ */
226+ private function parseTypesWithUnionHandling (string $ graphQlSchemaContent ): array
227+ {
228+ $ types = $ this ->parseTypes ($ graphQlSchemaContent );
229+
230+ /*
231+ * A union schema contains also the data from the schema below it
232+ *
233+ * If there are two newlines in this union schema then it has data below its definition, meaning it contains
234+ * type information not relevant to its actual type
235+ */
236+ $ unionTypes = array_filter (
237+ $ types ,
238+ function ($ t ) {
239+ return (strpos ($ t , 'union ' ) !== false ) && (strpos ($ t , PHP_EOL . PHP_EOL ) !== false );
240+ }
241+ );
242+
243+ foreach ($ unionTypes as $ type => $ schema ) {
244+ $ splitSchema = explode (PHP_EOL . PHP_EOL , $ schema );
245+ // Get the type data at the bottom, this will be the additional type data not related to the union
246+ $ additionalTypeSchema = end ($ splitSchema );
247+ // Parse the additional type from the bottom so we can have its type key => schema pair
248+ $ additionalTypeData = $ this ->parseTypes ($ additionalTypeSchema );
249+ // Fix the union type schema so it does not contain the definition below it
250+ $ types [$ type ] = str_replace ($ additionalTypeSchema , '' , $ schema );
251+ // Append the additional data to types array
252+ $ additionalTypeKey = array_key_first ($ additionalTypeData );
253+ $ types [$ additionalTypeKey ] = $ additionalTypeData [$ additionalTypeKey ];
254+ }
255+
256+ return $ types ;
257+ }
258+
140259 /**
141260 * Extract types as string from a larger string that represents the graphql schema using regular expressions
142261 *
@@ -147,7 +266,7 @@ private function parseTypes(string $graphQlSchemaContent): array
147266 {
148267 $ typeKindsPattern = '(type|interface|union|enum|input) ' ;
149268 $ typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+) ' ;
150- $ typeDefinitionPattern = '([^\{]*)(\{[^\}]*\}) ' ;
269+ $ typeDefinitionPattern = '([^\{\} ]*)(\{[^\}]*\}) ' ;
151270 $ spacePattern = '[\s\t\n\r]+ ' ;
152271
153272 preg_match_all (
@@ -259,7 +378,7 @@ private function addPlaceHolderInSchema(string $graphQlSchemaContent): string
259378 $ typesKindsPattern = '(type|interface|input|union) ' ;
260379 $ enumKindsPattern = '(enum) ' ;
261380 $ typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+) ' ;
262- $ typeDefinitionPattern = '([^\{]*)(\{[\s\t\n\r^\}]*\}) ' ;
381+ $ typeDefinitionPattern = '([^\{\} ]*)(\{[\s\t\n\r^\}]*\}) ' ;
263382 $ spacePattern = '([\s\t\n\r]+) ' ;
264383
265384 //add placeholder in empty types
@@ -299,49 +418,4 @@ private function removePlaceholderFromResults(array $partialResults): array
299418 }
300419 return $ partialResults ;
301420 }
302-
303- /**
304- * Get a module name by file path
305- *
306- * @param string $file
307- * @return string
308- */
309- private static function getModuleNameForRelevantFile (string $ file ): string
310- {
311- if (!isset (self ::$ componentRegistrar )) {
312- self ::$ componentRegistrar = new ComponentRegistrar ();
313- }
314- $ foundModuleName = '' ;
315- foreach (self ::$ componentRegistrar ->getPaths (ComponentRegistrar::MODULE ) as $ moduleName => $ moduleDir ) {
316- if (strpos ($ file , $ moduleDir . '/ ' ) !== false ) {
317- $ foundModuleName = str_replace ('_ ' , '\\' , $ moduleName );
318- break ;
319- }
320- }
321-
322- return $ foundModuleName ;
323- }
324-
325- /**
326- * Add a module name to types
327- *
328- * @param array $source
329- * @param string $filePath
330- * @return array
331- */
332- private function addModuleNameToTypes (array $ source , string $ filePath ): array
333- {
334- foreach ($ source as $ typeName => $ typeDefinition ) {
335- if (!isset ($ typeDefinition ['module ' ])) {
336- $ hasTypeResolver = (bool )($ typeDefinition ['typeResolver ' ] ?? false );
337- $ hasImplements = (bool )($ typeDefinition ['implements ' ] ?? false );
338- $ typeDefinition = (bool )($ typeDefinition ['type ' ] ?? false );
339- if ((($ typeDefinition === InterfaceType::GRAPHQL_INTERFACE && $ hasTypeResolver ) || $ hasImplements )) {
340- $ source [$ typeName ]['module ' ] = self ::getModuleNameForRelevantFile ($ filePath );
341- }
342- }
343- }
344-
345- return $ source ;
346- }
347421}
0 commit comments