Total Complexity | 92 |
Total Lines | 379 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like FieldsBuilder 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 FieldsBuilder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
39 | final class FieldsBuilder implements FieldsBuilderInterface |
||
40 | { |
||
41 | private $propertyNameCollectionFactory; |
||
42 | private $propertyMetadataFactory; |
||
43 | private $resourceMetadataFactory; |
||
44 | private $typesContainer; |
||
45 | private $typeBuilder; |
||
46 | private $typeConverter; |
||
47 | private $itemResolverFactory; |
||
48 | private $collectionResolverFactory; |
||
49 | private $itemMutationResolverFactory; |
||
50 | private $filterLocator; |
||
51 | private $paginationEnabled; |
||
52 | |||
53 | public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, TypesContainerInterface $typesContainer, TypeBuilderInterface $typeBuilder, TypeConverterInterface $typeConverter, ResolverFactoryInterface $itemResolverFactory, ResolverFactoryInterface $collectionResolverFactory, ResolverFactoryInterface $itemMutationResolverFactory, ContainerInterface $filterLocator, bool $paginationEnabled) |
||
66 | } |
||
67 | |||
68 | /** |
||
69 | * {@inheritdoc} |
||
70 | */ |
||
71 | public function getNodeQueryFields(): array |
||
79 | ]; |
||
80 | } |
||
81 | |||
82 | /** |
||
83 | * {@inheritdoc} |
||
84 | */ |
||
85 | public function getItemQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array |
||
86 | { |
||
87 | $shortName = $resourceMetadata->getShortName(); |
||
88 | $fieldName = lcfirst('item_query' === $queryName ? $shortName : $queryName.$shortName); |
||
89 | |||
90 | $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true); |
||
91 | |||
92 | if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $queryName, null)) { |
||
93 | $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); |
||
|
|||
94 | $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]]; |
||
95 | |||
96 | return [$fieldName => array_merge($fieldConfiguration, $configuration)]; |
||
97 | } |
||
98 | |||
99 | return []; |
||
100 | } |
||
101 | |||
102 | /** |
||
103 | * {@inheritdoc} |
||
104 | */ |
||
105 | public function getCollectionQueryFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $queryName, array $configuration): array |
||
106 | { |
||
107 | $shortName = $resourceMetadata->getShortName(); |
||
108 | $fieldName = lcfirst('collection_query' === $queryName ? $shortName : $queryName.$shortName); |
||
109 | |||
110 | $deprecationReason = (string) $resourceMetadata->getGraphqlAttribute($queryName, 'deprecation_reason', '', true); |
||
111 | |||
112 | if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, null, $deprecationReason, new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $queryName, null)) { |
||
113 | $args = $this->resolveResourceArgs($configuration['args'] ?? [], $queryName, $shortName); |
||
114 | $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args']; |
||
115 | |||
116 | return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)]; |
||
117 | } |
||
118 | |||
119 | return []; |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * {@inheritdoc} |
||
124 | */ |
||
125 | public function getMutationFields(string $resourceClass, ResourceMetadata $resourceMetadata, string $mutationName): array |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * {@inheritdoc} |
||
147 | */ |
||
148 | public function getResourceObjectTypeFields(?string $resourceClass, ResourceMetadata $resourceMetadata, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0, ?array $ioMetadata = null): array |
||
149 | { |
||
150 | $fields = []; |
||
151 | $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())]; |
||
152 | $clientMutationId = GraphQLType::string(); |
||
153 | |||
154 | if (null !== $ioMetadata && \array_key_exists('class', $ioMetadata) && null === $ioMetadata['class']) { |
||
155 | if ($input) { |
||
156 | return ['clientMutationId' => $clientMutationId]; |
||
157 | } |
||
158 | |||
159 | return []; |
||
160 | } |
||
161 | |||
162 | if ('delete' === $mutationName) { |
||
163 | $fields = [ |
||
164 | 'id' => $idField, |
||
165 | ]; |
||
166 | |||
167 | if ($input) { |
||
168 | $fields['clientMutationId'] = $clientMutationId; |
||
169 | } |
||
170 | |||
171 | return $fields; |
||
172 | } |
||
173 | |||
174 | if (!$input || 'create' !== $mutationName) { |
||
175 | $fields['id'] = $idField; |
||
176 | } |
||
177 | |||
178 | ++$depth; // increment the depth for the call to getResourceFieldConfiguration. |
||
179 | |||
180 | if (null !== $resourceClass) { |
||
181 | foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { |
||
182 | $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, ['graphql_operation_name' => $mutationName ?? $queryName]); |
||
183 | if ( |
||
184 | null === ($propertyType = $propertyMetadata->getType()) |
||
185 | || (!$input && false === $propertyMetadata->isReadable()) |
||
186 | || ($input && null !== $mutationName && false === $propertyMetadata->isWritable()) |
||
187 | ) { |
||
188 | continue; |
||
189 | } |
||
190 | |||
191 | if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getAttribute('deprecation_reason', ''), $propertyType, $resourceClass, $input, $queryName, $mutationName, $depth)) { |
||
192 | $fields['id' === $property ? '_id' : $property] = $fieldConfiguration; |
||
193 | } |
||
194 | } |
||
195 | } |
||
196 | |||
197 | if (null !== $mutationName && $input) { |
||
198 | $fields['clientMutationId'] = $clientMutationId; |
||
199 | } |
||
200 | |||
201 | return $fields; |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * {@inheritdoc} |
||
206 | */ |
||
207 | public function resolveResourceArgs(array $args, string $operationName, string $shortName): array |
||
208 | { |
||
209 | foreach ($args as $id => $arg) { |
||
210 | if (!isset($arg['type'])) { |
||
211 | throw new \InvalidArgumentException(sprintf('The argument "%s" of the custom operation "%s" in %s needs a "type" option.', $id, $operationName, $shortName)); |
||
212 | } |
||
213 | |||
214 | $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); |
||
215 | } |
||
216 | |||
217 | return $args; |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Get the field configuration of a resource. |
||
222 | * |
||
223 | * @see http://webonyx.github.io/graphql-php/type-system/object-types/ |
||
224 | */ |
||
225 | private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, string $deprecationReason, Type $type, string $rootResource, bool $input, ?string $queryName, ?string $mutationName, int $depth = 0): ?array |
||
226 | { |
||
227 | try { |
||
228 | $resourceClass = $this->typeBuilder->isCollection($type) && ($collectionValueType = $type->getCollectionValueType()) ? $collectionValueType->getClassName() : $type->getClassName(); |
||
229 | |||
230 | if (null === $graphqlType = $this->convertType($type, $input, $queryName, $mutationName, $resourceClass ?? '', $rootResource, $property, $depth)) { |
||
231 | return null; |
||
232 | } |
||
233 | |||
234 | $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType() : $graphqlType; |
||
235 | $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); |
||
236 | if ($isStandardGraphqlType) { |
||
237 | $resourceClass = ''; |
||
238 | } |
||
239 | |||
240 | $resourceMetadata = null; |
||
241 | if (!empty($resourceClass)) { |
||
242 | try { |
||
243 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||
244 | } catch (ResourceClassNotFoundException $e) { |
||
245 | } |
||
246 | } |
||
247 | |||
248 | $args = []; |
||
249 | if (!$input && null === $mutationName && !$isStandardGraphqlType && $this->typeBuilder->isCollection($type)) { |
||
250 | if ($this->paginationEnabled) { |
||
251 | $args = [ |
||
252 | 'first' => [ |
||
253 | 'type' => GraphQLType::int(), |
||
254 | 'description' => 'Returns the first n elements from the list.', |
||
255 | ], |
||
256 | 'last' => [ |
||
257 | 'type' => GraphQLType::int(), |
||
258 | 'description' => 'Returns the last n elements from the list.', |
||
259 | ], |
||
260 | 'before' => [ |
||
261 | 'type' => GraphQLType::string(), |
||
262 | 'description' => 'Returns the elements in the list that come before the specified cursor.', |
||
263 | ], |
||
264 | 'after' => [ |
||
265 | 'type' => GraphQLType::string(), |
||
266 | 'description' => 'Returns the elements in the list that come after the specified cursor.', |
||
267 | ], |
||
268 | ]; |
||
269 | } |
||
270 | |||
271 | $args = $this->getFilterArgs($args, $resourceClass, $resourceMetadata, $rootResource, $property, $queryName, $mutationName, $depth); |
||
272 | } |
||
273 | |||
274 | if ($isStandardGraphqlType || $input) { |
||
275 | $resolve = null; |
||
276 | } elseif ($this->typeBuilder->isCollection($type)) { |
||
277 | $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $queryName); |
||
278 | } else { |
||
279 | $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $queryName); |
||
280 | } |
||
281 | |||
282 | return [ |
||
283 | 'type' => $graphqlType, |
||
284 | 'description' => $fieldDescription, |
||
285 | 'args' => $args, |
||
286 | 'resolve' => $resolve, |
||
287 | 'deprecationReason' => $deprecationReason, |
||
288 | ]; |
||
289 | } catch (InvalidTypeException $e) { |
||
290 | // just ignore invalid types |
||
291 | } |
||
292 | |||
293 | return null; |
||
294 | } |
||
295 | |||
296 | private function getFilterArgs(array $args, ?string $resourceClass, ?ResourceMetadata $resourceMetadata, string $rootResource, ?string $property, ?string $queryName, ?string $mutationName, int $depth): array |
||
297 | { |
||
298 | if (null === $resourceMetadata || null === $resourceClass) { |
||
299 | return $args; |
||
300 | } |
||
301 | |||
302 | foreach ($resourceMetadata->getGraphqlAttribute($queryName, 'filters', [], true) as $filterId) { |
||
303 | if (null === $this->filterLocator || !$this->filterLocator->has($filterId)) { |
||
304 | continue; |
||
305 | } |
||
306 | |||
307 | foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { |
||
308 | $nullable = isset($value['required']) ? !$value['required'] : true; |
||
309 | $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); |
||
310 | $graphqlFilterType = $this->convertType($filterType, false, $queryName, $mutationName, $resourceClass, $rootResource, $property, $depth); |
||
311 | |||
312 | if ('[]' === substr($key, -2)) { |
||
313 | $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); |
||
314 | $key = substr($key, 0, -2).'_list'; |
||
315 | } |
||
316 | |||
317 | parse_str($key, $parsed); |
||
318 | if (\array_key_exists($key, $parsed) && \is_array($parsed[$key])) { |
||
319 | $parsed = [$key => '']; |
||
320 | } |
||
321 | array_walk_recursive($parsed, function (&$value) use ($graphqlFilterType) { |
||
322 | $value = $graphqlFilterType; |
||
323 | }); |
||
324 | $args = $this->mergeFilterArgs($args, $parsed, $resourceMetadata, $key); |
||
325 | } |
||
326 | } |
||
327 | |||
328 | return $this->convertFilterArgsToTypes($args); |
||
329 | } |
||
330 | |||
331 | private function mergeFilterArgs(array $args, array $parsed, ResourceMetadata $resourceMetadata = null, $original = ''): array |
||
351 | } |
||
352 | |||
353 | private function convertFilterArgsToTypes(array $args): array |
||
354 | { |
||
355 | foreach ($args as $key => $value) { |
||
356 | if (strpos($key, '.')) { |
||
357 | // Declare relations/nested fields in a GraphQL compatible syntax. |
||
358 | $args[str_replace('.', '_', $key)] = $value; |
||
359 | unset($args[$key]); |
||
360 | } |
||
361 | } |
||
362 | |||
363 | foreach ($args as $key => $value) { |
||
364 | if (!\is_array($value) || !isset($value['#name'])) { |
||
365 | continue; |
||
366 | } |
||
367 | |||
368 | $name = $value['#name']; |
||
369 | |||
370 | if ($this->typesContainer->has($name)) { |
||
371 | $args[$key] = $this->typesContainer->get($name); |
||
372 | continue; |
||
373 | } |
||
374 | |||
375 | unset($value['#name']); |
||
376 | |||
377 | $filterArgType = new InputObjectType([ |
||
378 | 'name' => $name, |
||
379 | 'fields' => $this->convertFilterArgsToTypes($value), |
||
380 | ]); |
||
381 | |||
382 | $this->typesContainer->set($name, $filterArgType); |
||
383 | |||
384 | $args[$key] = $filterArgType; |
||
385 | } |
||
386 | |||
387 | return $args; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Converts a built-in type to its GraphQL equivalent. |
||
392 | * |
||
393 | * @throws InvalidTypeException |
||
394 | */ |
||
395 | private function convertType(Type $type, bool $input, ?string $queryName, ?string $mutationName, string $resourceClass, string $rootResource, ?string $property, int $depth) |
||
418 | } |
||
419 | } |
||
420 |