Complex classes like MockConfiguration 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 MockConfiguration, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class MockConfiguration |
||
28 | { |
||
29 | protected static $mockCounter = 0; |
||
30 | |||
31 | /** |
||
32 | * A class that we'd like to mock |
||
33 | */ |
||
34 | protected $targetClass; |
||
35 | protected $targetClassName; |
||
36 | |||
37 | /** |
||
38 | * A number of interfaces we'd like to mock, keyed by name to attempt to |
||
39 | * keep unique |
||
40 | */ |
||
41 | protected $targetInterfaces = array(); |
||
42 | protected $targetInterfaceNames = array(); |
||
43 | |||
44 | /** |
||
45 | * A number of traits we'd like to mock, keyed by name to attempt to |
||
46 | * keep unique |
||
47 | */ |
||
48 | protected $targetTraits = array(); |
||
49 | protected $targetTraitNames = array(); |
||
50 | |||
51 | /** |
||
52 | * An object we'd like our mock to proxy to |
||
53 | */ |
||
54 | protected $targetObject; |
||
55 | |||
56 | /** |
||
57 | * The class name we'd like to use for a generated mock |
||
58 | */ |
||
59 | protected $name; |
||
60 | |||
61 | /** |
||
62 | * Methods that should specifically not be mocked |
||
63 | * |
||
64 | * This is currently populated with stuff we don't know how to deal with, |
||
65 | * should really be somewhere else |
||
66 | */ |
||
67 | protected $blackListedMethods = array(); |
||
68 | |||
69 | /** |
||
70 | * If not empty, only these methods will be mocked |
||
71 | */ |
||
72 | protected $whiteListedMethods = array(); |
||
73 | |||
74 | /** |
||
75 | * An instance mock is where we override the original class before it's |
||
76 | * autoloaded |
||
77 | */ |
||
78 | protected $instanceMock = false; |
||
79 | |||
80 | /** |
||
81 | * Param overrides |
||
82 | */ |
||
83 | protected $parameterOverrides = array(); |
||
84 | |||
85 | /** |
||
86 | * Instance cache of all methods |
||
87 | */ |
||
88 | protected $allMethods; |
||
89 | |||
90 | /** |
||
91 | * If true, overrides original class destructor |
||
92 | */ |
||
93 | protected $mockOriginalDestructor = false; |
||
94 | |||
95 | 444 | public function __construct( |
|
112 | |||
113 | /** |
||
114 | * Attempt to create a hash of the configuration, in order to allow caching |
||
115 | * |
||
116 | * @TODO workout if this will work |
||
117 | * |
||
118 | * @return string |
||
119 | */ |
||
120 | 424 | public function getHash() |
|
121 | { |
||
122 | $vars = array( |
||
123 | 424 | 'targetClassName' => $this->targetClassName, |
|
124 | 424 | 'targetInterfaceNames' => $this->targetInterfaceNames, |
|
125 | 424 | 'targetTraitNames' => $this->targetTraitNames, |
|
126 | 424 | 'name' => $this->name, |
|
127 | 424 | 'blackListedMethods' => $this->blackListedMethods, |
|
128 | 424 | 'whiteListedMethod' => $this->whiteListedMethods, |
|
129 | 424 | 'instanceMock' => $this->instanceMock, |
|
130 | 424 | 'parameterOverrides' => $this->parameterOverrides, |
|
131 | 424 | 'mockOriginalDestructor' => $this->mockOriginalDestructor |
|
132 | 424 | ); |
|
133 | |||
134 | 424 | return md5(serialize($vars)); |
|
135 | } |
||
136 | |||
137 | /** |
||
138 | * Gets a list of methods from the classes, interfaces and objects and |
||
139 | * filters them appropriately. Lot's of filtering going on, perhaps we could |
||
140 | * have filter classes to iterate through |
||
141 | */ |
||
142 | 405 | public function getMethodsToMock() |
|
143 | { |
||
144 | 405 | $methods = $this->getAllMethods(); |
|
145 | |||
146 | 405 | foreach ($methods as $key => $method) { |
|
147 | 131 | if ($method->isFinal()) { |
|
148 | 12 | unset($methods[$key]); |
|
149 | 12 | } |
|
150 | 405 | } |
|
151 | |||
152 | /** |
||
153 | * Whitelist trumps everything else |
||
154 | */ |
||
155 | 405 | if (count($this->getWhiteListedMethods())) { |
|
156 | 19 | $whitelist = array_map('strtolower', $this->getWhiteListedMethods()); |
|
157 | $methods = array_filter($methods, function ($method) use ($whitelist) { |
||
158 | 19 | return $method->isAbstract() || in_array(strtolower($method->getName()), $whitelist); |
|
159 | 19 | }); |
|
160 | |||
161 | 19 | return $methods; |
|
162 | } |
||
163 | |||
164 | /** |
||
165 | * Remove blacklisted methods |
||
166 | */ |
||
167 | 387 | if (count($this->getBlackListedMethods())) { |
|
168 | 385 | $blacklist = array_map('strtolower', $this->getBlackListedMethods()); |
|
169 | $methods = array_filter($methods, function ($method) use ($blacklist) { |
||
170 | 110 | return !in_array(strtolower($method->getName()), $blacklist); |
|
171 | 385 | }); |
|
172 | 385 | } |
|
173 | |||
174 | /** |
||
175 | * Internal objects can not be instantiated with newInstanceArgs and if |
||
176 | * they implement Serializable, unserialize will have to be called. As |
||
177 | * such, we can't mock it and will need a pass to add a dummy |
||
178 | * implementation |
||
179 | */ |
||
180 | 387 | if ($this->getTargetClass() |
|
181 | 387 | && $this->getTargetClass()->implementsInterface("Serializable") |
|
182 | 387 | && $this->getTargetClass()->hasInternalAncestor()) { |
|
183 | $methods = array_filter($methods, function ($method) { |
||
184 | 1 | return $method->getName() !== "unserialize"; |
|
185 | 1 | }); |
|
186 | 1 | } |
|
187 | |||
188 | 387 | return array_values($methods); |
|
189 | } |
||
190 | |||
191 | /** |
||
192 | * We declare the __call method to handle undefined stuff, if the class |
||
193 | * we're mocking has also defined it, we need to comply with their interface |
||
194 | */ |
||
195 | 398 | public function requiresCallTypeHintRemoval() |
|
196 | { |
||
197 | 398 | foreach ($this->getAllMethods() as $method) { |
|
198 | 123 | if ("__call" === $method->getName()) { |
|
199 | 6 | $params = $method->getParameters(); |
|
200 | 6 | return !$params[1]->isArray(); |
|
201 | } |
||
202 | 392 | } |
|
203 | |||
204 | 392 | return false; |
|
205 | } |
||
206 | |||
207 | /** |
||
208 | * We declare the __callStatic method to handle undefined stuff, if the class |
||
209 | * we're mocking has also defined it, we need to comply with their interface |
||
210 | */ |
||
211 | 397 | public function requiresCallStaticTypeHintRemoval() |
|
212 | { |
||
213 | 397 | foreach ($this->getAllMethods() as $method) { |
|
214 | 123 | if ("__callStatic" === $method->getName()) { |
|
215 | 1 | $params = $method->getParameters(); |
|
216 | 1 | return !$params[1]->isArray(); |
|
217 | } |
||
218 | 396 | } |
|
219 | |||
220 | 396 | return false; |
|
221 | } |
||
222 | |||
223 | 397 | public function rename($className) |
|
224 | { |
||
225 | 397 | $targets = array(); |
|
226 | |||
227 | 397 | if ($this->targetClassName) { |
|
228 | 373 | $targets[] = $this->targetClassName; |
|
229 | 373 | } |
|
230 | |||
231 | 397 | if ($this->targetInterfaceNames) { |
|
232 | 19 | $targets = array_merge($targets, $this->targetInterfaceNames); |
|
233 | 19 | } |
|
234 | |||
235 | 397 | if ($this->targetTraitNames) { |
|
|
|||
236 | 3 | $targets = array_merge($targets, $this->targetTraitNames); |
|
237 | 3 | } |
|
238 | |||
239 | 397 | if ($this->targetObject) { |
|
240 | 14 | $targets[] = $this->targetObject; |
|
241 | 14 | } |
|
242 | |||
243 | 397 | return new self( |
|
244 | 397 | $targets, |
|
245 | 397 | $this->blackListedMethods, |
|
246 | 397 | $this->whiteListedMethods, |
|
247 | 397 | $className, |
|
248 | 397 | $this->instanceMock, |
|
249 | 397 | $this->parameterOverrides, |
|
250 | 397 | $this->mockOriginalDestructor |
|
251 | 397 | ); |
|
252 | } |
||
253 | |||
254 | 411 | protected function addTarget($target) |
|
255 | { |
||
256 | 411 | if (is_object($target)) { |
|
257 | 14 | $this->setTargetObject($target); |
|
258 | 14 | $this->setTargetClassName(get_class($target)); |
|
259 | 14 | return $this; |
|
260 | } |
||
261 | |||
262 | 411 | if ($target[0] !== "\\") { |
|
263 | 408 | $target = "\\" . $target; |
|
264 | 408 | } |
|
265 | |||
266 | 411 | if (class_exists($target)) { |
|
267 | 382 | $this->setTargetClassName($target); |
|
268 | 382 | return $this; |
|
269 | } |
||
270 | |||
271 | 43 | if (interface_exists($target)) { |
|
272 | 24 | $this->addTargetInterfaceName($target); |
|
273 | 24 | return $this; |
|
274 | } |
||
275 | |||
276 | 20 | if (trait_exists($target)) { |
|
277 | 3 | $this->addTargetTraitName($target); |
|
278 | 3 | return $this; |
|
279 | } |
||
280 | |||
281 | /** |
||
282 | * Default is to set as class, or interface if class already set |
||
283 | * |
||
284 | * Don't like this condition, can't remember what the default |
||
285 | * targetClass is for |
||
286 | */ |
||
287 | 17 | if ($this->getTargetClassName()) { |
|
288 | 1 | $this->addTargetInterfaceName($target); |
|
289 | 1 | return $this; |
|
290 | } |
||
291 | |||
292 | 17 | $this->setTargetClassName($target); |
|
293 | 17 | } |
|
294 | |||
295 | 444 | protected function addTargets($interfaces) |
|
296 | { |
||
297 | 444 | foreach ($interfaces as $interface) { |
|
298 | 411 | $this->addTarget($interface); |
|
299 | 444 | } |
|
300 | 444 | } |
|
301 | |||
302 | 17 | public function getTargetClassName() |
|
306 | |||
307 | 408 | public function getTargetClass() |
|
308 | { |
||
309 | 408 | if ($this->targetClass) { |
|
310 | 380 | return $this->targetClass; |
|
311 | } |
||
312 | |||
313 | 408 | if (!$this->targetClassName) { |
|
314 | 31 | return null; |
|
315 | } |
||
316 | |||
317 | 382 | if (class_exists($this->targetClassName)) { |
|
318 | 369 | $dtc = DefinedTargetClass::factory($this->targetClassName); |
|
319 | |||
320 | 369 | if ($this->getTargetObject() == false && $dtc->isFinal()) { |
|
321 | 2 | throw new \Mockery\Exception( |
|
322 | 2 | 'The class ' . $this->targetClassName . ' is marked final and its methods' |
|
323 | 2 | . ' cannot be replaced. Classes marked final can be passed in' |
|
324 | 2 | . ' to \Mockery::mock() as instantiated objects to create a' |
|
325 | 2 | . ' partial mock, but only if the mock is not subject to type' |
|
326 | 2 | . ' hinting checks.' |
|
327 | 2 | ); |
|
328 | } |
||
329 | |||
330 | 367 | $this->targetClass = $dtc; |
|
331 | 367 | } else { |
|
332 | 17 | $this->targetClass = UndefinedTargetClass::factory($this->targetClassName); |
|
333 | } |
||
334 | |||
335 | 380 | return $this->targetClass; |
|
336 | } |
||
337 | |||
338 | 406 | public function getTargetTraits() |
|
339 | { |
||
340 | 406 | if (!empty($this->targetTraits)) { |
|
341 | 3 | return $this->targetTraits; |
|
342 | } |
||
343 | |||
344 | 406 | foreach ($this->targetTraitNames as $targetTrait) { |
|
345 | 3 | $this->targetTraits[] = DefinedTargetClass::factory($targetTrait); |
|
346 | 406 | } |
|
347 | |||
348 | 406 | $this->targetTraits = array_unique($this->targetTraits); // just in case |
|
349 | 406 | return $this->targetTraits; |
|
350 | } |
||
351 | |||
352 | 410 | public function getTargetInterfaces() |
|
353 | { |
||
354 | 410 | if (!empty($this->targetInterfaces)) { |
|
355 | 19 | return $this->targetInterfaces; |
|
356 | } |
||
357 | |||
358 | 410 | foreach ($this->targetInterfaceNames as $targetInterface) { |
|
359 | 24 | if (!interface_exists($targetInterface)) { |
|
360 | 1 | $this->targetInterfaces[] = UndefinedTargetClass::factory($targetInterface); |
|
361 | 1 | return; |
|
362 | } |
||
363 | |||
364 | 24 | $dtc = DefinedTargetClass::factory($targetInterface); |
|
365 | 24 | $extendedInterfaces = array_keys($dtc->getInterfaces()); |
|
366 | 24 | $extendedInterfaces[] = $targetInterface; |
|
367 | |||
368 | 24 | $traversableFound = false; |
|
369 | 24 | $iteratorShiftedToFront = false; |
|
370 | 24 | foreach ($extendedInterfaces as $interface) { |
|
371 | 24 | if (!$traversableFound && preg_match("/^\\?Iterator(|Aggregate)$/i", $interface)) { |
|
372 | break; |
||
373 | } |
||
374 | |||
375 | 24 | if (preg_match("/^\\\\?IteratorAggregate$/i", $interface)) { |
|
376 | 3 | $this->targetInterfaces[] = DefinedTargetClass::factory("\\IteratorAggregate"); |
|
377 | 3 | $iteratorShiftedToFront = true; |
|
378 | 24 | } elseif (preg_match("/^\\\\?Iterator$/i", $interface)) { |
|
379 | 3 | $this->targetInterfaces[] = DefinedTargetClass::factory("\\Iterator"); |
|
380 | 3 | $iteratorShiftedToFront = true; |
|
381 | 24 | } elseif (preg_match("/^\\\\?Traversable$/i", $interface)) { |
|
382 | 11 | $traversableFound = true; |
|
383 | 11 | } |
|
384 | 24 | } |
|
385 | |||
386 | 24 | if ($traversableFound && !$iteratorShiftedToFront) { |
|
387 | 5 | $this->targetInterfaces[] = DefinedTargetClass::factory("\\IteratorAggregate"); |
|
388 | 5 | } |
|
389 | |||
390 | /** |
||
391 | * We never straight up implement Traversable |
||
392 | */ |
||
393 | 24 | if (!preg_match("/^\\\\?Traversable$/i", $targetInterface)) { |
|
394 | 23 | $this->targetInterfaces[] = $dtc; |
|
395 | 23 | } |
|
396 | 410 | } |
|
397 | 409 | $this->targetInterfaces = array_unique($this->targetInterfaces); // just in case |
|
398 | 409 | return $this->targetInterfaces; |
|
399 | } |
||
400 | |||
401 | 431 | public function getTargetObject() |
|
405 | |||
406 | 430 | public function getName() |
|
410 | |||
411 | /** |
||
412 | * Generate a suitable name based on the config |
||
413 | */ |
||
414 | 377 | public function generateName() |
|
415 | { |
||
416 | 377 | $name = 'Mockery_' . static::$mockCounter++; |
|
417 | |||
418 | 377 | if ($this->getTargetObject()) { |
|
419 | 14 | $name .= "_" . str_replace("\\", "_", get_class($this->getTargetObject())); |
|
420 | 14 | } |
|
421 | |||
422 | 377 | if ($this->getTargetClass()) { |
|
423 | 356 | $name .= "_" . str_replace("\\", "_", $this->getTargetClass()->getName()); |
|
424 | 356 | } |
|
425 | |||
426 | 376 | if ($this->getTargetInterfaces()) { |
|
427 | $name .= array_reduce($this->getTargetInterfaces(), function ($tmpname, $i) { |
||
428 | 18 | $tmpname .= '_' . str_replace("\\", "_", $i->getName()); |
|
429 | 18 | return $tmpname; |
|
430 | 18 | }, ''); |
|
431 | 18 | } |
|
432 | |||
433 | 376 | return $name; |
|
434 | } |
||
435 | |||
436 | 401 | public function getShortName() |
|
441 | |||
442 | 401 | public function getNamespaceName() |
|
453 | |||
454 | 387 | public function getBlackListedMethods() |
|
458 | |||
459 | 405 | public function getWhiteListedMethods() |
|
463 | |||
464 | 398 | public function isInstanceMock() |
|
468 | |||
469 | 109 | public function getParameterOverrides() |
|
473 | |||
474 | 373 | public function isMockOriginalDestructor() |
|
478 | |||
479 | 392 | protected function setTargetClassName($targetClassName) |
|
483 | |||
484 | 406 | protected function getAllMethods() |
|
485 | { |
||
486 | 406 | if ($this->allMethods) { |
|
487 | 123 | return $this->allMethods; |
|
488 | } |
||
489 | |||
490 | 406 | $classes = $this->getTargetInterfaces(); |
|
491 | |||
492 | 406 | if ($this->getTargetClass()) { |
|
493 | 380 | $classes[] = $this->getTargetClass(); |
|
494 | 380 | } |
|
495 | |||
496 | 406 | $methods = array(); |
|
497 | 406 | foreach ($classes as $class) { |
|
498 | 392 | $methods = array_merge($methods, $class->getMethods()); |
|
499 | 406 | } |
|
500 | |||
501 | 406 | foreach ($this->getTargetTraits() AS $trait) { |
|
502 | 3 | foreach ($trait->getMethods() as $method) { |
|
503 | 3 | if ($method->isAbstract()) { |
|
504 | 2 | $methods[] = $method; |
|
505 | 2 | } |
|
506 | 3 | } |
|
507 | 406 | } |
|
508 | |||
509 | 406 | $names = array(); |
|
510 | 406 | $methods = array_filter($methods, function ($method) use (&$names) { |
|
511 | 131 | if (in_array($method->getName(), $names)) { |
|
512 | 3 | return false; |
|
513 | } |
||
514 | |||
515 | 131 | $names[] = $method->getName(); |
|
516 | 131 | return true; |
|
517 | 406 | }); |
|
518 | |||
519 | 406 | return $this->allMethods = $methods; |
|
520 | } |
||
521 | |||
522 | /** |
||
523 | * If we attempt to implement Traversable, we must ensure we are also |
||
524 | * implementing either Iterator or IteratorAggregate, and that whichever one |
||
525 | * it is comes before Traversable in the list of implements. |
||
526 | */ |
||
527 | 24 | protected function addTargetInterfaceName($targetInterface) |
|
531 | |||
532 | 3 | protected function addTargetTraitName($targetTraitName) |
|
533 | { |
||
534 | 3 | $this->targetTraitNames[] = $targetTraitName; |
|
535 | 3 | } |
|
536 | |||
537 | 14 | protected function setTargetObject($object) |
|
541 | } |
||
542 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.