Complex classes like AbstractFieldsConfigurationFactory 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 AbstractFieldsConfigurationFactory, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | /** |
||
27 | * A factory to create a configuration for all fields of an entity |
||
28 | */ |
||
29 | abstract class AbstractFieldsConfigurationFactory |
||
30 | { |
||
31 | /** |
||
32 | * @var Types |
||
33 | */ |
||
34 | private $types; |
||
35 | |||
36 | /** |
||
37 | * @var EntityManager |
||
38 | */ |
||
39 | private $entityManager; |
||
40 | |||
41 | /** |
||
42 | * Doctrine metadata for the entity |
||
43 | * @var ClassMetadata |
||
44 | */ |
||
45 | private $metadata; |
||
46 | |||
47 | /** |
||
48 | * The identity field name, eg: "id" |
||
49 | * @var string |
||
50 | */ |
||
51 | private $identityField; |
||
52 | |||
53 | public function __construct(Types $types, EntityManager $entityManager) |
||
54 | { |
||
55 | $this->types = $types; |
||
56 | $this->entityManager = $entityManager; |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * Returns the regexp pattern to filter method names |
||
61 | */ |
||
62 | abstract protected function getMethodPattern(): string; |
||
63 | |||
64 | /** |
||
65 | * Get the entire configuration for a method |
||
66 | * @param ReflectionMethod $method |
||
67 | * @return array |
||
68 | */ |
||
69 | abstract protected function methodToConfiguration(ReflectionMethod $method): ?array; |
||
70 | |||
71 | /** |
||
72 | * Create a configuration for all fields of Doctrine entity |
||
73 | * @param string $className |
||
74 | * @return array |
||
75 | */ |
||
76 | public function create(string $className): array |
||
77 | { |
||
78 | $this->findIdentityField($className); |
||
79 | |||
80 | $class = new ReflectionClass($className); |
||
81 | $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC); |
||
82 | $fieldConfigurations = []; |
||
83 | foreach ($methods as $method) { |
||
84 | // Skip non-callable, non-instance or non-getter methods |
||
85 | if ($method->isAbstract() || $method->isStatic()) { |
||
86 | continue; |
||
87 | } |
||
88 | |||
89 | // Skip non-getter methods |
||
90 | $name = $method->getName(); |
||
91 | if (!preg_match($this->getMethodPattern(), $name)) { |
||
92 | continue; |
||
93 | } |
||
94 | |||
95 | // Skip exclusion specified by user |
||
96 | if ($this->isExcluded($method)) { |
||
97 | continue; |
||
98 | } |
||
99 | |||
100 | $configuration = $this->methodToConfiguration($method); |
||
101 | if ($configuration) { |
||
102 | $fieldConfigurations[] = $configuration; |
||
103 | } |
||
104 | } |
||
105 | |||
106 | return $fieldConfigurations; |
||
107 | } |
||
108 | |||
109 | /** |
||
110 | * Returns whether the getter is excluded |
||
111 | * @param ReflectionMethod $method |
||
112 | * @return bool |
||
113 | */ |
||
114 | private function isExcluded(ReflectionMethod $method): bool |
||
115 | { |
||
116 | $exclude = $this->getAnnotationReader()->getMethodAnnotation($method, Exclude::class); |
||
117 | |||
118 | return $exclude !== null; |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * Get annotation reader |
||
123 | * @return Reader |
||
124 | */ |
||
125 | protected function getAnnotationReader(): Reader |
||
126 | { |
||
127 | $driver = $this->entityManager->getConfiguration()->getMetadataDriverImpl(); |
||
128 | if ($driver instanceof MappingDriverChain::class) { |
||
|
|||
129 | $drivers = $driver->getDrivers(); |
||
130 | foreach ($drivers as $driver) { |
||
131 | if ($driver instanceof AnnotationDriver::class) { |
||
132 | break; |
||
133 | } |
||
134 | } |
||
135 | } |
||
136 | |||
137 | if ($driver instanceof AnnotationDriver::class) { |
||
138 | return $driver->getReader(); |
||
139 | } |
||
140 | |||
141 | return new AnnotationReader(); |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * Get instance of GraphQL type from a PHP class name |
||
146 | * |
||
147 | * Supported syntaxes are the following: |
||
148 | * |
||
149 | * - `?MyType` |
||
150 | * - `null|MyType` |
||
151 | * - `MyType|null` |
||
152 | * - `MyType[]` |
||
153 | * - `?MyType[]` |
||
154 | * - `null|MyType[]` |
||
155 | * - `MyType[]|null` |
||
156 | * |
||
157 | * @param ReflectionMethod $method |
||
158 | * @param string|null $typeDeclaration |
||
159 | * @param bool $isEntityId |
||
160 | * @return Type|null |
||
161 | */ |
||
162 | protected function getTypeFromPhpDeclaration(ReflectionMethod $method, ?string $typeDeclaration, bool $isEntityId = false): ?Type |
||
163 | { |
||
164 | if (!$typeDeclaration) { |
||
165 | return null; |
||
166 | } |
||
167 | |||
168 | $isNullable = 0; |
||
169 | $name = preg_replace('~(^\?|^null\||\|null$)~', '', $typeDeclaration, -1, $isNullable); |
||
170 | |||
171 | $isList = 0; |
||
172 | $name = preg_replace('~^(.*)\[\]$~', '$1', $name, -1, $isList); |
||
173 | $name = $this->adjustNamespace($method, $name); |
||
174 | $type = $this->getTypeFromRegistry($name, $isEntityId); |
||
175 | |||
176 | if ($isList) { |
||
177 | $type = Type::listOf($type); |
||
178 | } |
||
179 | |||
180 | if (!$isNullable) { |
||
181 | $type = Type::nonNull($type); |
||
182 | } |
||
183 | |||
184 | return $type; |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Prepend namespace of the method if the class actually exists |
||
189 | * @param ReflectionMethod $method |
||
190 | * @param string $type |
||
191 | * @return string |
||
192 | */ |
||
193 | private function adjustNamespace(ReflectionMethod $method, string $type): string |
||
194 | { |
||
195 | $namespace = $method->getDeclaringClass()->getNamespaceName(); |
||
196 | if ($namespace) { |
||
197 | $namespace = $namespace . '\\'; |
||
198 | } |
||
199 | $namespacedType = $namespace . $type; |
||
200 | |||
201 | return class_exists($namespacedType) ? $namespacedType : $type; |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * Get a GraphQL type instance from PHP type hinted type, possibly looking up the content of collections |
||
206 | * @param ReflectionMethod $method |
||
207 | * @param string $fieldName |
||
208 | * @throws Exception |
||
209 | * @return Type|null |
||
210 | */ |
||
211 | protected function getTypeFromReturnTypeHint(ReflectionMethod $method, string $fieldName): ?Type |
||
212 | { |
||
213 | $returnType = $method->getReturnType(); |
||
214 | if (!$returnType) { |
||
215 | return null; |
||
216 | } |
||
217 | |||
218 | $returnTypeName = (string) $returnType; |
||
219 | if (is_a($returnTypeName, Collection::class, true) || $returnTypeName === 'array') { |
||
220 | $targetEntity = $this->getTargetEntity($fieldName); |
||
221 | if (!$targetEntity) { |
||
222 | throw new Exception('The method ' . $this->getMethodFullName($method) . ' is type hinted with a return type of `' . $returnTypeName . '`, but the entity contained in that collection could not be automatically detected. Either fix the type hint, fix the doctrine mapping, or specify the type with `@API\Field` annotation.'); |
||
223 | } |
||
224 | |||
225 | $type = Type::listOf($this->getTypeFromRegistry($targetEntity, false)); |
||
226 | if (!$returnType->allowsNull()) { |
||
227 | $type = Type::nonNull($type); |
||
228 | } |
||
229 | |||
230 | return $type; |
||
231 | } |
||
232 | |||
233 | return $this->refelectionTypeToType($returnType); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Convert a reflected type to GraphQL Type |
||
238 | * @param ReflectionType $reflectionType |
||
239 | * @param bool $isEntityId |
||
240 | * @return Type |
||
241 | */ |
||
242 | protected function refelectionTypeToType(ReflectionType $reflectionType, bool $isEntityId = false): Type |
||
243 | { |
||
244 | $type = $this->getTypeFromRegistry((string) $reflectionType, $isEntityId); |
||
245 | if (!$reflectionType->allowsNull()) { |
||
246 | $type = Type::nonNull($type); |
||
247 | } |
||
248 | |||
249 | return $type; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Look up which field is the ID |
||
254 | * @param string $className |
||
255 | */ |
||
256 | private function findIdentityField(string $className) |
||
257 | { |
||
258 | $this->metadata = $this->entityManager->getClassMetadata($className); |
||
259 | foreach ($this->metadata->fieldMappings as $meta) { |
||
260 | if ($meta['id'] ?? false) { |
||
261 | $this->identityField = $meta['fieldName']; |
||
262 | } |
||
263 | } |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * Returns the fully qualified method name |
||
268 | * @param ReflectionMethod $method |
||
269 | * @return string |
||
270 | */ |
||
271 | protected function getMethodFullName(ReflectionMethod $method): string |
||
272 | { |
||
273 | return '`' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()`'; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Throws exception if type is an array |
||
278 | * @param ReflectionParameter $param |
||
279 | * @param string|null $type |
||
280 | * @throws Exception |
||
281 | */ |
||
282 | protected function throwIfArray(ReflectionParameter $param, ?string $type) |
||
283 | { |
||
284 | if ($type === 'array') { |
||
285 | throw new Exception('The parameter `$' . $param->getName() . '` on method ' . $this->getMethodFullName($param->getDeclaringFunction()) . ' is type hinted as `array` and is not overriden via `@API\Argument` annotation. Either change the type hint or specify the type with `@API\Argument` annotation.'); |
||
286 | } |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Returns whether the given field name is the identity for the entity |
||
291 | * @param string $fieldName |
||
292 | * @return bool |
||
293 | */ |
||
294 | protected function isIdentityField(string $fieldName): bool |
||
295 | { |
||
296 | return $this->identityField === $fieldName; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * Finds the target entity in the given association |
||
301 | * @param string $fieldName |
||
302 | * @return string|null |
||
303 | */ |
||
304 | private function getTargetEntity(string $fieldName): ?string |
||
305 | { |
||
306 | return $this->metadata->associationMappings[$fieldName]['targetEntity'] ?? null; |
||
307 | } |
||
308 | |||
309 | /** |
||
310 | * Returns a type from our registry |
||
311 | * @param string $type |
||
312 | * @param bool $isEntityid |
||
313 | * @return Type |
||
314 | */ |
||
315 | private function getTypeFromRegistry(string $type, bool $isEntityid): Type |
||
316 | { |
||
317 | if (!$this->types->isEntity($type) || !$isEntityid) { |
||
318 | return $this->types->get($type); |
||
319 | } |
||
320 | |||
321 | return $this->types->getId($type); |
||
322 | } |
||
323 | |||
324 | /** |
||
325 | * Input with default values cannot be non-null |
||
326 | * @param ReflectionParameter $param |
||
327 | * @param Type $type |
||
328 | * @return Type |
||
329 | */ |
||
330 | protected function nonNullIfHasDefault(ReflectionParameter $param, ?Type $type): ?Type |
||
331 | { |
||
332 | if ($type instanceof NonNull && $param->isDefaultValueAvailable()) { |
||
333 | return $type->getWrappedType(); |
||
334 | } |
||
335 | |||
336 | return $type; |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * Throws exception if argument type is invalid |
||
341 | * @param ReflectionParameter $param |
||
342 | * @param Type $type |
||
343 | * @throws Exception |
||
360 |