Total Complexity | 55 |
Total Lines | 444 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like BuildClientSchema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use BuildClientSchema, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
33 | class BuildClientSchema |
||
34 | { |
||
35 | /** @var array<string, mixed[]> */ |
||
36 | private $introspection; |
||
37 | |||
38 | /** @var array<string, bool> */ |
||
39 | private $options; |
||
40 | |||
41 | /** @var array<string, NamedType&Type> */ |
||
42 | private $typeMap; |
||
43 | |||
44 | /** |
||
45 | * @param array<string, mixed[]> $introspectionQuery |
||
46 | * @param array<string, bool> $options |
||
47 | */ |
||
48 | public function __construct(array $introspectionQuery, array $options = []) |
||
49 | { |
||
50 | $this->introspection = $introspectionQuery; |
||
51 | $this->options = $options; |
||
52 | } |
||
53 | |||
54 | /** |
||
55 | * Build a schema for use by client tools. |
||
56 | * |
||
57 | * Given the result of a client running the introspection query, creates and |
||
58 | * returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php |
||
59 | * tools, but cannot be used to execute a query, as introspection does not |
||
60 | * represent the "resolver", "parse" or "serialize" functions or any other |
||
61 | * server-internal mechanisms. |
||
62 | * |
||
63 | * This function expects a complete introspection result. Don't forget to check |
||
64 | * the "errors" field of a server response before calling this function. |
||
65 | * |
||
66 | * Accepts options as a third argument: |
||
67 | * |
||
68 | * - assumeValid: |
||
69 | * When building a schema from a GraphQL service's introspection result, it |
||
70 | * might be safe to assume the schema is valid. Set to true to assume the |
||
71 | * produced schema is valid. |
||
72 | * |
||
73 | * Default: false |
||
74 | * |
||
75 | * @param array<string, mixed[]> $introspectionQuery |
||
76 | * @param array<string, bool> $options |
||
77 | * |
||
78 | * @api |
||
79 | */ |
||
80 | public static function build(array $introspectionQuery, array $options = []) : Schema |
||
81 | { |
||
82 | $builder = new self($introspectionQuery, $options); |
||
83 | |||
84 | return $builder->buildSchema(); |
||
85 | } |
||
86 | |||
87 | public function buildSchema() : Schema |
||
88 | { |
||
89 | if (! array_key_exists('__schema', $this->introspection)) { |
||
90 | throw new InvariantViolation('Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ' . json_encode($this->introspection) . '.'); |
||
91 | } |
||
92 | |||
93 | $schemaIntrospection = $this->introspection['__schema']; |
||
94 | |||
95 | $this->typeMap = Utils::keyValMap( |
||
96 | $schemaIntrospection['types'], |
||
97 | static function (array $typeIntrospection) { |
||
98 | return $typeIntrospection['name']; |
||
99 | }, |
||
100 | function (array $typeIntrospection) { |
||
101 | return $this->buildType($typeIntrospection); |
||
102 | } |
||
103 | ); |
||
104 | |||
105 | $builtInTypes = array_merge( |
||
106 | Type::getStandardTypes(), |
||
107 | Introspection::getTypes() |
||
108 | ); |
||
109 | foreach ($builtInTypes as $name => $type) { |
||
110 | if (! isset($this->typeMap[$name])) { |
||
111 | continue; |
||
112 | } |
||
113 | |||
114 | $this->typeMap[$name] = $type; |
||
115 | } |
||
116 | |||
117 | $queryType = isset($schemaIntrospection['queryType']) |
||
118 | ? $this->getObjectType($schemaIntrospection['queryType']) |
||
119 | : null; |
||
120 | |||
121 | $mutationType = isset($schemaIntrospection['mutationType']) |
||
122 | ? $this->getObjectType($schemaIntrospection['mutationType']) |
||
123 | : null; |
||
124 | |||
125 | $subscriptionType = isset($schemaIntrospection['subscriptionType']) |
||
126 | ? $this->getObjectType($schemaIntrospection['subscriptionType']) |
||
127 | : null; |
||
128 | |||
129 | $directives = isset($schemaIntrospection['directives']) |
||
130 | ? array_map( |
||
131 | [$this, 'buildDirective'], |
||
132 | $schemaIntrospection['directives'] |
||
133 | ) |
||
134 | : []; |
||
135 | |||
136 | $schemaConfig = new SchemaConfig(); |
||
137 | $schemaConfig->setQuery($queryType) |
||
138 | ->setMutation($mutationType) |
||
139 | ->setSubscription($subscriptionType) |
||
140 | ->setTypes($this->typeMap) |
||
141 | ->setDirectives($directives) |
||
142 | ->setAssumeValid( |
||
143 | isset($this->options) |
||
144 | && isset($this->options['assumeValid']) |
||
145 | && $this->options['assumeValid'] |
||
146 | ); |
||
147 | |||
148 | return new Schema($schemaConfig); |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * @param array<string, mixed> $typeRef |
||
153 | */ |
||
154 | private function getType(array $typeRef) : Type |
||
155 | { |
||
156 | if (isset($typeRef['kind'])) { |
||
157 | if ($typeRef['kind'] === TypeKind::LIST) { |
||
158 | if (! isset($typeRef['ofType'])) { |
||
159 | throw new InvariantViolation('Decorated type deeper than introspection query.'); |
||
160 | } |
||
161 | |||
162 | return new ListOfType($this->getType($typeRef['ofType'])); |
||
163 | } |
||
164 | |||
165 | if ($typeRef['kind'] === TypeKind::NON_NULL) { |
||
166 | if (! isset($typeRef['ofType'])) { |
||
167 | throw new InvariantViolation('Decorated type deeper than introspection query.'); |
||
168 | } |
||
169 | /** @var NullableType $nullableType */ |
||
170 | $nullableType = $this->getType($typeRef['ofType']); |
||
171 | |||
172 | return new NonNull($nullableType); |
||
173 | } |
||
174 | } |
||
175 | |||
176 | if (! isset($typeRef['name'])) { |
||
177 | throw new InvariantViolation('Unknown type reference: ' . json_encode($typeRef) . '.'); |
||
178 | } |
||
179 | |||
180 | return $this->getNamedType($typeRef['name']); |
||
|
|||
181 | } |
||
182 | |||
183 | /** |
||
184 | * @return NamedType&Type |
||
185 | */ |
||
186 | private function getNamedType(string $typeName) : NamedType |
||
187 | { |
||
188 | if (! isset($this->typeMap[$typeName])) { |
||
189 | throw new InvariantViolation( |
||
190 | "Invalid or incomplete schema, unknown type: ${typeName}. Ensure that a full introspection query is used in order to build a client schema." |
||
191 | ); |
||
192 | } |
||
193 | |||
194 | return $this->typeMap[$typeName]; |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * @param array<string, mixed> $typeRef |
||
199 | */ |
||
200 | private function getInputType(array $typeRef) : InputType |
||
201 | { |
||
202 | $type = $this->getType($typeRef); |
||
203 | |||
204 | if ($type instanceof InputType) { |
||
205 | return $type; |
||
206 | } |
||
207 | |||
208 | throw new InvariantViolation('Introspection must provide input type for arguments, but received: ' . json_encode($type) . '.'); |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * @param array<string, mixed> $typeRef |
||
213 | */ |
||
214 | private function getOutputType(array $typeRef) : OutputType |
||
215 | { |
||
216 | $type = $this->getType($typeRef); |
||
217 | |||
218 | if ($type instanceof OutputType) { |
||
219 | return $type; |
||
220 | } |
||
221 | |||
222 | throw new InvariantViolation('Introspection must provide output type for fields, but received: ' . json_encode($type) . '.'); |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * @param array<string, mixed> $typeRef |
||
227 | */ |
||
228 | private function getObjectType(array $typeRef) : ObjectType |
||
229 | { |
||
230 | $type = $this->getType($typeRef); |
||
231 | |||
232 | return ObjectType::assertObjectType($type); |
||
233 | } |
||
234 | |||
235 | /** |
||
236 | * @param array<string, mixed> $typeRef |
||
237 | */ |
||
238 | public function getInterfaceType(array $typeRef) : InterfaceType |
||
239 | { |
||
240 | $type = $this->getType($typeRef); |
||
241 | |||
242 | return InterfaceType::assertInterfaceType($type); |
||
243 | } |
||
244 | |||
245 | /** |
||
246 | * @param array<string, mixed> $type |
||
247 | */ |
||
248 | private function buildType(array $type) : NamedType |
||
249 | { |
||
250 | if (array_key_exists('name', $type) && array_key_exists('kind', $type)) { |
||
251 | switch ($type['kind']) { |
||
252 | case TypeKind::SCALAR: |
||
253 | return $this->buildScalarDef($type); |
||
254 | case TypeKind::OBJECT: |
||
255 | return $this->buildObjectDef($type); |
||
256 | case TypeKind::INTERFACE: |
||
257 | return $this->buildInterfaceDef($type); |
||
258 | case TypeKind::UNION: |
||
259 | return $this->buildUnionDef($type); |
||
260 | case TypeKind::ENUM: |
||
261 | return $this->buildEnumDef($type); |
||
262 | case TypeKind::INPUT_OBJECT: |
||
263 | return $this->buildInputObjectDef($type); |
||
264 | } |
||
265 | } |
||
266 | |||
267 | throw new InvariantViolation( |
||
268 | 'Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ' . json_encode($type) . '.' |
||
269 | ); |
||
270 | } |
||
271 | |||
272 | /** |
||
273 | * @param array<string, string> $scalar |
||
274 | */ |
||
275 | private function buildScalarDef(array $scalar) : ScalarType |
||
276 | { |
||
277 | return new CustomScalarType([ |
||
278 | 'name' => $scalar['name'], |
||
279 | 'description' => $scalar['description'], |
||
280 | 'serialize' => static function ($value) : string { |
||
281 | return (string) $value; |
||
282 | }, |
||
283 | ]); |
||
284 | } |
||
285 | |||
286 | /** |
||
287 | * @param array<string, mixed> $object |
||
288 | */ |
||
289 | private function buildObjectDef(array $object) : ObjectType |
||
290 | { |
||
291 | if (! array_key_exists('interfaces', $object)) { |
||
292 | throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($object) . '.'); |
||
293 | } |
||
294 | |||
295 | return new ObjectType([ |
||
296 | 'name' => $object['name'], |
||
297 | 'description' => $object['description'], |
||
298 | 'interfaces' => function () use ($object) { |
||
299 | return array_map( |
||
300 | [$this, 'getInterfaceType'], |
||
301 | // Legacy support for interfaces with null as interfaces field |
||
302 | $object['interfaces'] ?? [] |
||
303 | ); |
||
304 | }, |
||
305 | 'fields' => function () use ($object) { |
||
306 | return $this->buildFieldDefMap($object); |
||
307 | }, |
||
308 | ]); |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * @param array<string, mixed> $interface |
||
313 | */ |
||
314 | private function buildInterfaceDef(array $interface) : InterfaceType |
||
315 | { |
||
316 | return new InterfaceType([ |
||
317 | 'name' => $interface['name'], |
||
318 | 'description' => $interface['description'], |
||
319 | 'fields' => function () use ($interface) { |
||
320 | return $this->buildFieldDefMap($interface); |
||
321 | }, |
||
322 | ]); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * @param array<string, string|array<string>> $union |
||
327 | */ |
||
328 | private function buildUnionDef(array $union) : UnionType |
||
329 | { |
||
330 | if (! array_key_exists('possibleTypes', $union)) { |
||
331 | throw new InvariantViolation('Introspection result missing possibleTypes: ' . json_encode($union) . '.'); |
||
332 | } |
||
333 | |||
334 | return new UnionType([ |
||
335 | 'name' => $union['name'], |
||
336 | 'description' => $union['description'], |
||
337 | 'types' => function () use ($union) { |
||
338 | return array_map( |
||
339 | [$this, 'getObjectType'], |
||
340 | $union['possibleTypes'] |
||
341 | ); |
||
342 | }, |
||
343 | ]); |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * @param array<string, string|array<string, string>> $enum |
||
348 | */ |
||
349 | private function buildEnumDef(array $enum) : EnumType |
||
350 | { |
||
351 | if (! array_key_exists('enumValues', $enum)) { |
||
352 | throw new InvariantViolation('Introspection result missing enumValues: ' . json_encode($enum) . '.'); |
||
353 | } |
||
354 | |||
355 | return new EnumType([ |
||
356 | 'name' => $enum['name'], |
||
357 | 'description' => $enum['description'], |
||
358 | 'values' => Utils::keyValMap( |
||
359 | $enum['enumValues'], |
||
360 | static function (array $enumValue) : string { |
||
361 | return $enumValue['name']; |
||
362 | }, |
||
363 | static function (array $enumValue) { |
||
364 | return [ |
||
365 | 'description' => $enumValue['description'], |
||
366 | 'deprecationReason' => $enumValue['deprecationReason'], |
||
367 | ]; |
||
368 | } |
||
369 | ), |
||
370 | ]); |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * @param array<string, mixed> $inputObject |
||
375 | */ |
||
376 | private function buildInputObjectDef(array $inputObject) : InputObjectType |
||
377 | { |
||
378 | if (! array_key_exists('inputFields', $inputObject)) { |
||
379 | throw new InvariantViolation('Introspection result missing inputFields: ' . json_encode($inputObject) . '.'); |
||
380 | } |
||
381 | |||
382 | return new InputObjectType([ |
||
383 | 'name' => $inputObject['name'], |
||
384 | 'description' => $inputObject['description'], |
||
385 | 'fields' => function () use ($inputObject) { |
||
386 | return $this->buildInputValueDefMap($inputObject['inputFields']); |
||
387 | }, |
||
388 | ]); |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * @param array<string, mixed> $typeIntrospection |
||
393 | */ |
||
394 | private function buildFieldDefMap(array $typeIntrospection) |
||
395 | { |
||
396 | if (! array_key_exists('fields', $typeIntrospection)) { |
||
397 | throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.'); |
||
398 | } |
||
399 | |||
400 | return Utils::keyValMap( |
||
401 | $typeIntrospection['fields'], |
||
402 | static function (array $fieldIntrospection) : string { |
||
403 | return $fieldIntrospection['name']; |
||
404 | }, |
||
405 | function (array $fieldIntrospection) { |
||
406 | if (! array_key_exists('args', $fieldIntrospection)) { |
||
407 | throw new InvariantViolation('Introspection result missing field args: ' . json_encode($fieldIntrospection) . '.'); |
||
408 | } |
||
409 | |||
410 | return [ |
||
411 | 'description' => $fieldIntrospection['description'], |
||
412 | 'deprecationReason' => $fieldIntrospection['deprecationReason'], |
||
413 | 'type' => $this->getOutputType($fieldIntrospection['type']), |
||
414 | 'args' => $this->buildInputValueDefMap($fieldIntrospection['args']), |
||
415 | ]; |
||
416 | } |
||
417 | ); |
||
418 | } |
||
419 | |||
420 | /** |
||
421 | * @param array<int, array<string, mixed>> $inputValueIntrospections |
||
422 | * |
||
423 | * @return array<string, array<string, mixed>> |
||
424 | */ |
||
425 | private function buildInputValueDefMap(array $inputValueIntrospections) : array |
||
426 | { |
||
427 | return Utils::keyValMap( |
||
428 | $inputValueIntrospections, |
||
429 | static function (array $inputValue) : string { |
||
430 | return $inputValue['name']; |
||
431 | }, |
||
432 | [$this, 'buildInputValue'] |
||
433 | ); |
||
434 | } |
||
435 | |||
436 | /** |
||
437 | * @param array<string, mixed> $inputValueIntrospection |
||
438 | * |
||
439 | * @return array<string, mixed> |
||
440 | */ |
||
441 | public function buildInputValue(array $inputValueIntrospection) : array |
||
442 | { |
||
443 | $type = $this->getInputType($inputValueIntrospection['type']); |
||
444 | |||
445 | $inputValue = [ |
||
446 | 'description' => $inputValueIntrospection['description'], |
||
447 | 'type' => $type, |
||
448 | ]; |
||
449 | |||
450 | if (isset($inputValueIntrospection['defaultValue'])) { |
||
451 | $inputValue['defaultValue'] = AST::valueFromAST( |
||
452 | Parser::parseValue($inputValueIntrospection['defaultValue']), |
||
453 | $type |
||
454 | ); |
||
455 | } |
||
456 | |||
457 | return $inputValue; |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * @param array<string, mixed> $directive |
||
462 | */ |
||
463 | public function buildDirective(array $directive) : Directive |
||
477 | ]); |
||
478 | } |
||
479 | } |
||
480 |