9
9
10
10
use GraphQL \Type \Definition \ScalarType ;
11
11
use GraphQL \Utils \BuildSchema ;
12
- use Magento \Framework \Component \ComponentRegistrar ;
13
12
use Magento \Framework \Config \FileResolverInterface ;
14
13
use Magento \Framework \Config \ReaderInterface ;
15
14
use Magento \Framework \GraphQl \Type \TypeManagement ;
@@ -48,11 +47,6 @@ class GraphQlReader implements ReaderInterface
48
47
*/
49
48
private $ defaultScope ;
50
49
51
- /**
52
- * @var ComponentRegistrar
53
- */
54
- private static $ componentRegistrar ;
55
-
56
50
/**
57
51
* @param FileResolverInterface $fileResolver
58
52
* @param TypeReaderComposite $typeReader
@@ -75,7 +69,7 @@ public function __construct(
75
69
76
70
/**
77
71
* @inheritdoc
78
- *
72
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
79
73
* @param string|null $scope
80
74
* @return array
81
75
*/
@@ -84,28 +78,103 @@ public function read($scope = null): array
84
78
$ results = [];
85
79
$ scope = $ scope ?: $ this ->defaultScope ;
86
80
$ schemaFiles = $ this ->fileResolver ->get ($ this ->fileName , $ scope );
87
-
88
81
if (!count ($ schemaFiles )) {
89
82
return $ results ;
90
83
}
91
84
92
85
/**
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
+ *
93
89
* Compatible with @see GraphQlReader::parseTypes
94
90
*/
91
+ $ typesToRedo = [];
95
92
$ 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
+ }
98
129
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 );
102
137
103
138
$ partialResults = $ this ->readPartialTypes ($ schemaContent );
104
139
$ results = array_replace_recursive ($ results , $ partialResults );
105
- $ results = $ this ->addModuleNameToTypes ($ results , $ filePath );
106
140
}
107
141
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 ;
109
178
}
110
179
111
180
/**
@@ -137,6 +206,56 @@ private function readPartialTypes(string $graphQlSchemaContent): array
137
206
return $ this ->removePlaceholderFromResults ($ partialResults );
138
207
}
139
208
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
+
140
259
/**
141
260
* Extract types as string from a larger string that represents the graphql schema using regular expressions
142
261
*
@@ -147,7 +266,7 @@ private function parseTypes(string $graphQlSchemaContent): array
147
266
{
148
267
$ typeKindsPattern = '(type|interface|union|enum|input) ' ;
149
268
$ typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+) ' ;
150
- $ typeDefinitionPattern = '([^\{]*)(\{[^\}]*\}) ' ;
269
+ $ typeDefinitionPattern = '([^\{\} ]*)(\{[^\}]*\}) ' ;
151
270
$ spacePattern = '[\s\t\n\r]+ ' ;
152
271
153
272
preg_match_all (
@@ -259,7 +378,7 @@ private function addPlaceHolderInSchema(string $graphQlSchemaContent): string
259
378
$ typesKindsPattern = '(type|interface|input|union) ' ;
260
379
$ enumKindsPattern = '(enum) ' ;
261
380
$ typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+) ' ;
262
- $ typeDefinitionPattern = '([^\{]*)(\{[\s\t\n\r^\}]*\}) ' ;
381
+ $ typeDefinitionPattern = '([^\{\} ]*)(\{[\s\t\n\r^\}]*\}) ' ;
263
382
$ spacePattern = '([\s\t\n\r]+) ' ;
264
383
265
384
//add placeholder in empty types
@@ -299,49 +418,4 @@ private function removePlaceholderFromResults(array $partialResults): array
299
418
}
300
419
return $ partialResults ;
301
420
}
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
- }
347
421
}
0 commit comments