Mercurial > hg > isophonics-drupal-site
comparison vendor/phpunit/phpunit-mock-objects/src/Generator.php @ 14:1fec387a4317
Update Drupal core to 8.5.2 via Composer
author | Chris Cannam |
---|---|
date | Mon, 23 Apr 2018 09:46:53 +0100 |
parents | |
children | c2387f117808 |
comparison
equal
deleted
inserted
replaced
13:5fb285c0d0e3 | 14:1fec387a4317 |
---|---|
1 <?php | |
2 /* | |
3 * This file is part of the phpunit-mock-objects package. | |
4 * | |
5 * (c) Sebastian Bergmann <sebastian@phpunit.de> | |
6 * | |
7 * For the full copyright and license information, please view the LICENSE | |
8 * file that was distributed with this source code. | |
9 */ | |
10 namespace PHPUnit\Framework\MockObject; | |
11 | |
12 use Doctrine\Instantiator\Exception\ExceptionInterface as InstantiatorException; | |
13 use Doctrine\Instantiator\Instantiator; | |
14 use Iterator; | |
15 use IteratorAggregate; | |
16 use PHPUnit\Framework\Exception; | |
17 use PHPUnit\Util\InvalidArgumentHelper; | |
18 use ReflectionClass; | |
19 use ReflectionException; | |
20 use ReflectionMethod; | |
21 use SoapClient; | |
22 use Text_Template; | |
23 use Traversable; | |
24 | |
25 /** | |
26 * Mock Object Code Generator | |
27 */ | |
28 class Generator | |
29 { | |
30 /** | |
31 * @var array | |
32 */ | |
33 private static $cache = []; | |
34 | |
35 /** | |
36 * @var Text_Template[] | |
37 */ | |
38 private static $templates = []; | |
39 | |
40 /** | |
41 * @var array | |
42 */ | |
43 private $blacklistedMethodNames = [ | |
44 '__CLASS__' => true, | |
45 '__DIR__' => true, | |
46 '__FILE__' => true, | |
47 '__FUNCTION__' => true, | |
48 '__LINE__' => true, | |
49 '__METHOD__' => true, | |
50 '__NAMESPACE__' => true, | |
51 '__TRAIT__' => true, | |
52 '__clone' => true, | |
53 '__halt_compiler' => true, | |
54 ]; | |
55 | |
56 /** | |
57 * Returns a mock object for the specified class. | |
58 * | |
59 * @param string|string[] $type | |
60 * @param array $methods | |
61 * @param array $arguments | |
62 * @param string $mockClassName | |
63 * @param bool $callOriginalConstructor | |
64 * @param bool $callOriginalClone | |
65 * @param bool $callAutoload | |
66 * @param bool $cloneArguments | |
67 * @param bool $callOriginalMethods | |
68 * @param object $proxyTarget | |
69 * @param bool $allowMockingUnknownTypes | |
70 * | |
71 * @return MockObject | |
72 * | |
73 * @throws Exception | |
74 * @throws RuntimeException | |
75 * @throws \PHPUnit\Framework\Exception | |
76 * @throws \ReflectionException | |
77 */ | |
78 public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true) | |
79 { | |
80 if (!\is_array($type) && !\is_string($type)) { | |
81 throw InvalidArgumentHelper::factory(1, 'array or string'); | |
82 } | |
83 | |
84 if (!\is_string($mockClassName)) { | |
85 throw InvalidArgumentHelper::factory(4, 'string'); | |
86 } | |
87 | |
88 if (!\is_array($methods) && null !== $methods) { | |
89 throw InvalidArgumentHelper::factory(2, 'array', $methods); | |
90 } | |
91 | |
92 if ($type === 'Traversable' || $type === '\\Traversable') { | |
93 $type = 'Iterator'; | |
94 } | |
95 | |
96 if (\is_array($type)) { | |
97 $type = \array_unique( | |
98 \array_map( | |
99 function ($type) { | |
100 if ($type === 'Traversable' || | |
101 $type === '\\Traversable' || | |
102 $type === '\\Iterator') { | |
103 return 'Iterator'; | |
104 } | |
105 | |
106 return $type; | |
107 }, | |
108 $type | |
109 ) | |
110 ); | |
111 } | |
112 | |
113 if (!$allowMockingUnknownTypes) { | |
114 if (\is_array($type)) { | |
115 foreach ($type as $_type) { | |
116 if (!\class_exists($_type, $callAutoload) && | |
117 !\interface_exists($_type, $callAutoload)) { | |
118 throw new RuntimeException( | |
119 \sprintf( | |
120 'Cannot stub or mock class or interface "%s" which does not exist', | |
121 $_type | |
122 ) | |
123 ); | |
124 } | |
125 } | |
126 } else { | |
127 if (!\class_exists($type, $callAutoload) && | |
128 !\interface_exists($type, $callAutoload) | |
129 ) { | |
130 throw new RuntimeException( | |
131 \sprintf( | |
132 'Cannot stub or mock class or interface "%s" which does not exist', | |
133 $type | |
134 ) | |
135 ); | |
136 } | |
137 } | |
138 } | |
139 | |
140 if (null !== $methods) { | |
141 foreach ($methods as $method) { | |
142 if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) { | |
143 throw new RuntimeException( | |
144 \sprintf( | |
145 'Cannot stub or mock method with invalid name "%s"', | |
146 $method | |
147 ) | |
148 ); | |
149 } | |
150 } | |
151 | |
152 if ($methods !== \array_unique($methods)) { | |
153 throw new RuntimeException( | |
154 \sprintf( | |
155 'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")', | |
156 \implode(', ', $methods), | |
157 \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods)))) | |
158 ) | |
159 ); | |
160 } | |
161 } | |
162 | |
163 if ($mockClassName !== '' && \class_exists($mockClassName, false)) { | |
164 $reflect = new ReflectionClass($mockClassName); | |
165 | |
166 if (!$reflect->implementsInterface(MockObject::class)) { | |
167 throw new RuntimeException( | |
168 \sprintf( | |
169 'Class "%s" already exists.', | |
170 $mockClassName | |
171 ) | |
172 ); | |
173 } | |
174 } | |
175 | |
176 if ($callOriginalConstructor === false && $callOriginalMethods === true) { | |
177 throw new RuntimeException( | |
178 'Proxying to original methods requires invoking the original constructor' | |
179 ); | |
180 } | |
181 | |
182 $mock = $this->generate( | |
183 $type, | |
184 $methods, | |
185 $mockClassName, | |
186 $callOriginalClone, | |
187 $callAutoload, | |
188 $cloneArguments, | |
189 $callOriginalMethods | |
190 ); | |
191 | |
192 return $this->getObject( | |
193 $mock['code'], | |
194 $mock['mockClassName'], | |
195 $type, | |
196 $callOriginalConstructor, | |
197 $callAutoload, | |
198 $arguments, | |
199 $callOriginalMethods, | |
200 $proxyTarget | |
201 ); | |
202 } | |
203 | |
204 /** | |
205 * @param string $code | |
206 * @param string $className | |
207 * @param array|string $type | |
208 * @param bool $callOriginalConstructor | |
209 * @param bool $callAutoload | |
210 * @param array $arguments | |
211 * @param bool $callOriginalMethods | |
212 * @param object $proxyTarget | |
213 * | |
214 * @return MockObject | |
215 * | |
216 * @throws \ReflectionException | |
217 * @throws RuntimeException | |
218 */ | |
219 private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null) | |
220 { | |
221 $this->evalClass($code, $className); | |
222 | |
223 if ($callOriginalConstructor && | |
224 \is_string($type) && | |
225 !\interface_exists($type, $callAutoload)) { | |
226 if (\count($arguments) === 0) { | |
227 $object = new $className; | |
228 } else { | |
229 $class = new ReflectionClass($className); | |
230 $object = $class->newInstanceArgs($arguments); | |
231 } | |
232 } else { | |
233 try { | |
234 $instantiator = new Instantiator; | |
235 $object = $instantiator->instantiate($className); | |
236 } catch (InstantiatorException $exception) { | |
237 throw new RuntimeException($exception->getMessage()); | |
238 } | |
239 } | |
240 | |
241 if ($callOriginalMethods) { | |
242 if (!\is_object($proxyTarget)) { | |
243 if (\count($arguments) === 0) { | |
244 $proxyTarget = new $type; | |
245 } else { | |
246 $class = new ReflectionClass($type); | |
247 $proxyTarget = $class->newInstanceArgs($arguments); | |
248 } | |
249 } | |
250 | |
251 $object->__phpunit_setOriginalObject($proxyTarget); | |
252 } | |
253 | |
254 return $object; | |
255 } | |
256 | |
257 /** | |
258 * @param string $code | |
259 * @param string $className | |
260 */ | |
261 private function evalClass($code, $className) | |
262 { | |
263 if (!\class_exists($className, false)) { | |
264 eval($code); | |
265 } | |
266 } | |
267 | |
268 /** | |
269 * Returns a mock object for the specified abstract class with all abstract | |
270 * methods of the class mocked. Concrete methods to mock can be specified with | |
271 * the last parameter | |
272 * | |
273 * @param string $originalClassName | |
274 * @param array $arguments | |
275 * @param string $mockClassName | |
276 * @param bool $callOriginalConstructor | |
277 * @param bool $callOriginalClone | |
278 * @param bool $callAutoload | |
279 * @param array $mockedMethods | |
280 * @param bool $cloneArguments | |
281 * | |
282 * @return MockObject | |
283 * | |
284 * @throws \ReflectionException | |
285 * @throws RuntimeException | |
286 * @throws Exception | |
287 */ | |
288 public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) | |
289 { | |
290 if (!\is_string($originalClassName)) { | |
291 throw InvalidArgumentHelper::factory(1, 'string'); | |
292 } | |
293 | |
294 if (!\is_string($mockClassName)) { | |
295 throw InvalidArgumentHelper::factory(3, 'string'); | |
296 } | |
297 | |
298 if (\class_exists($originalClassName, $callAutoload) || | |
299 \interface_exists($originalClassName, $callAutoload)) { | |
300 $reflector = new ReflectionClass($originalClassName); | |
301 $methods = $mockedMethods; | |
302 | |
303 foreach ($reflector->getMethods() as $method) { | |
304 if ($method->isAbstract() && !\in_array($method->getName(), $methods)) { | |
305 $methods[] = $method->getName(); | |
306 } | |
307 } | |
308 | |
309 if (empty($methods)) { | |
310 $methods = null; | |
311 } | |
312 | |
313 return $this->getMock( | |
314 $originalClassName, | |
315 $methods, | |
316 $arguments, | |
317 $mockClassName, | |
318 $callOriginalConstructor, | |
319 $callOriginalClone, | |
320 $callAutoload, | |
321 $cloneArguments | |
322 ); | |
323 } | |
324 | |
325 throw new RuntimeException( | |
326 \sprintf('Class "%s" does not exist.', $originalClassName) | |
327 ); | |
328 } | |
329 | |
330 /** | |
331 * Returns a mock object for the specified trait with all abstract methods | |
332 * of the trait mocked. Concrete methods to mock can be specified with the | |
333 * `$mockedMethods` parameter. | |
334 * | |
335 * @param string $traitName | |
336 * @param array $arguments | |
337 * @param string $mockClassName | |
338 * @param bool $callOriginalConstructor | |
339 * @param bool $callOriginalClone | |
340 * @param bool $callAutoload | |
341 * @param array $mockedMethods | |
342 * @param bool $cloneArguments | |
343 * | |
344 * @return MockObject | |
345 * | |
346 * @throws \ReflectionException | |
347 * @throws RuntimeException | |
348 * @throws Exception | |
349 */ | |
350 public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) | |
351 { | |
352 if (!\is_string($traitName)) { | |
353 throw InvalidArgumentHelper::factory(1, 'string'); | |
354 } | |
355 | |
356 if (!\is_string($mockClassName)) { | |
357 throw InvalidArgumentHelper::factory(3, 'string'); | |
358 } | |
359 | |
360 if (!\trait_exists($traitName, $callAutoload)) { | |
361 throw new RuntimeException( | |
362 \sprintf( | |
363 'Trait "%s" does not exist.', | |
364 $traitName | |
365 ) | |
366 ); | |
367 } | |
368 | |
369 $className = $this->generateClassName( | |
370 $traitName, | |
371 '', | |
372 'Trait_' | |
373 ); | |
374 | |
375 $classTemplate = $this->getTemplate('trait_class.tpl'); | |
376 | |
377 $classTemplate->setVar( | |
378 [ | |
379 'prologue' => 'abstract ', | |
380 'class_name' => $className['className'], | |
381 'trait_name' => $traitName | |
382 ] | |
383 ); | |
384 | |
385 $this->evalClass( | |
386 $classTemplate->render(), | |
387 $className['className'] | |
388 ); | |
389 | |
390 return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments); | |
391 } | |
392 | |
393 /** | |
394 * Returns an object for the specified trait. | |
395 * | |
396 * @param string $traitName | |
397 * @param array $arguments | |
398 * @param string $traitClassName | |
399 * @param bool $callOriginalConstructor | |
400 * @param bool $callOriginalClone | |
401 * @param bool $callAutoload | |
402 * | |
403 * @return object | |
404 * | |
405 * @throws \ReflectionException | |
406 * @throws RuntimeException | |
407 * @throws Exception | |
408 */ | |
409 public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true) | |
410 { | |
411 if (!\is_string($traitName)) { | |
412 throw InvalidArgumentHelper::factory(1, 'string'); | |
413 } | |
414 | |
415 if (!\is_string($traitClassName)) { | |
416 throw InvalidArgumentHelper::factory(3, 'string'); | |
417 } | |
418 | |
419 if (!\trait_exists($traitName, $callAutoload)) { | |
420 throw new RuntimeException( | |
421 \sprintf( | |
422 'Trait "%s" does not exist.', | |
423 $traitName | |
424 ) | |
425 ); | |
426 } | |
427 | |
428 $className = $this->generateClassName( | |
429 $traitName, | |
430 $traitClassName, | |
431 'Trait_' | |
432 ); | |
433 | |
434 $classTemplate = $this->getTemplate('trait_class.tpl'); | |
435 | |
436 $classTemplate->setVar( | |
437 [ | |
438 'prologue' => '', | |
439 'class_name' => $className['className'], | |
440 'trait_name' => $traitName | |
441 ] | |
442 ); | |
443 | |
444 return $this->getObject($classTemplate->render(), $className['className']); | |
445 } | |
446 | |
447 /** | |
448 * @param array|string $type | |
449 * @param array $methods | |
450 * @param string $mockClassName | |
451 * @param bool $callOriginalClone | |
452 * @param bool $callAutoload | |
453 * @param bool $cloneArguments | |
454 * @param bool $callOriginalMethods | |
455 * | |
456 * @return array | |
457 * | |
458 * @throws \ReflectionException | |
459 * @throws \PHPUnit\Framework\MockObject\RuntimeException | |
460 */ | |
461 public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false) | |
462 { | |
463 if (\is_array($type)) { | |
464 \sort($type); | |
465 } | |
466 | |
467 if ($mockClassName === '') { | |
468 $key = \md5( | |
469 \is_array($type) ? \implode('_', $type) : $type . | |
470 \serialize($methods) . | |
471 \serialize($callOriginalClone) . | |
472 \serialize($cloneArguments) . | |
473 \serialize($callOriginalMethods) | |
474 ); | |
475 | |
476 if (isset(self::$cache[$key])) { | |
477 return self::$cache[$key]; | |
478 } | |
479 } | |
480 | |
481 $mock = $this->generateMock( | |
482 $type, | |
483 $methods, | |
484 $mockClassName, | |
485 $callOriginalClone, | |
486 $callAutoload, | |
487 $cloneArguments, | |
488 $callOriginalMethods | |
489 ); | |
490 | |
491 if (isset($key)) { | |
492 self::$cache[$key] = $mock; | |
493 } | |
494 | |
495 return $mock; | |
496 } | |
497 | |
498 /** | |
499 * @param string $wsdlFile | |
500 * @param string $className | |
501 * @param array $methods | |
502 * @param array $options | |
503 * | |
504 * @return string | |
505 * | |
506 * @throws RuntimeException | |
507 */ | |
508 public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = []) | |
509 { | |
510 if (!\extension_loaded('soap')) { | |
511 throw new RuntimeException( | |
512 'The SOAP extension is required to generate a mock object from WSDL.' | |
513 ); | |
514 } | |
515 | |
516 $options = \array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]); | |
517 $client = new SoapClient($wsdlFile, $options); | |
518 $_methods = \array_unique($client->__getFunctions()); | |
519 unset($client); | |
520 | |
521 \sort($_methods); | |
522 | |
523 $methodTemplate = $this->getTemplate('wsdl_method.tpl'); | |
524 $methodsBuffer = ''; | |
525 | |
526 foreach ($_methods as $method) { | |
527 $nameStart = \strpos($method, ' ') + 1; | |
528 $nameEnd = \strpos($method, '('); | |
529 $name = \substr($method, $nameStart, $nameEnd - $nameStart); | |
530 | |
531 if (empty($methods) || \in_array($name, $methods)) { | |
532 $args = \explode( | |
533 ',', | |
534 \substr( | |
535 $method, | |
536 $nameEnd + 1, | |
537 \strpos($method, ')') - $nameEnd - 1 | |
538 ) | |
539 ); | |
540 | |
541 foreach (\range(0, \count($args) - 1) as $i) { | |
542 $args[$i] = \substr($args[$i], \strpos($args[$i], '$')); | |
543 } | |
544 | |
545 $methodTemplate->setVar( | |
546 [ | |
547 'method_name' => $name, | |
548 'arguments' => \implode(', ', $args) | |
549 ] | |
550 ); | |
551 | |
552 $methodsBuffer .= $methodTemplate->render(); | |
553 } | |
554 } | |
555 | |
556 $optionsBuffer = 'array('; | |
557 | |
558 foreach ($options as $key => $value) { | |
559 $optionsBuffer .= $key . ' => ' . $value; | |
560 } | |
561 | |
562 $optionsBuffer .= ')'; | |
563 | |
564 $classTemplate = $this->getTemplate('wsdl_class.tpl'); | |
565 $namespace = ''; | |
566 | |
567 if (\strpos($className, '\\') !== false) { | |
568 $parts = \explode('\\', $className); | |
569 $className = \array_pop($parts); | |
570 $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n"; | |
571 } | |
572 | |
573 $classTemplate->setVar( | |
574 [ | |
575 'namespace' => $namespace, | |
576 'class_name' => $className, | |
577 'wsdl' => $wsdlFile, | |
578 'options' => $optionsBuffer, | |
579 'methods' => $methodsBuffer | |
580 ] | |
581 ); | |
582 | |
583 return $classTemplate->render(); | |
584 } | |
585 | |
586 /** | |
587 * @param array|string $type | |
588 * @param array|null $methods | |
589 * @param string $mockClassName | |
590 * @param bool $callOriginalClone | |
591 * @param bool $callAutoload | |
592 * @param bool $cloneArguments | |
593 * @param bool $callOriginalMethods | |
594 * | |
595 * @return array | |
596 * | |
597 * @throws \InvalidArgumentException | |
598 * @throws \ReflectionException | |
599 * @throws RuntimeException | |
600 */ | |
601 private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods) | |
602 { | |
603 $methodReflections = []; | |
604 $classTemplate = $this->getTemplate('mocked_class.tpl'); | |
605 | |
606 $additionalInterfaces = []; | |
607 $cloneTemplate = ''; | |
608 $isClass = false; | |
609 $isInterface = false; | |
610 $isMultipleInterfaces = false; | |
611 | |
612 if (\is_array($type)) { | |
613 foreach ($type as $_type) { | |
614 if (!\interface_exists($_type, $callAutoload)) { | |
615 throw new RuntimeException( | |
616 \sprintf( | |
617 'Interface "%s" does not exist.', | |
618 $_type | |
619 ) | |
620 ); | |
621 } | |
622 | |
623 $isMultipleInterfaces = true; | |
624 | |
625 $additionalInterfaces[] = $_type; | |
626 $typeClass = new ReflectionClass($this->generateClassName( | |
627 $_type, | |
628 $mockClassName, | |
629 'Mock_' | |
630 )['fullClassName'] | |
631 ); | |
632 | |
633 foreach ($this->getClassMethods($_type) as $method) { | |
634 if (\in_array($method, $methods)) { | |
635 throw new RuntimeException( | |
636 \sprintf( | |
637 'Duplicate method "%s" not allowed.', | |
638 $method | |
639 ) | |
640 ); | |
641 } | |
642 | |
643 $methodReflections[$method] = $typeClass->getMethod($method); | |
644 $methods[] = $method; | |
645 } | |
646 } | |
647 } | |
648 | |
649 $mockClassName = $this->generateClassName( | |
650 $type, | |
651 $mockClassName, | |
652 'Mock_' | |
653 ); | |
654 | |
655 if (\class_exists($mockClassName['fullClassName'], $callAutoload)) { | |
656 $isClass = true; | |
657 } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) { | |
658 $isInterface = true; | |
659 } | |
660 | |
661 if (!$isClass && !$isInterface) { | |
662 $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n"; | |
663 | |
664 if (!empty($mockClassName['namespaceName'])) { | |
665 $prologue = 'namespace ' . $mockClassName['namespaceName'] . | |
666 " {\n\n" . $prologue . "}\n\n" . | |
667 "namespace {\n\n"; | |
668 | |
669 $epilogue = "\n\n}"; | |
670 } | |
671 | |
672 $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); | |
673 } else { | |
674 $class = new ReflectionClass($mockClassName['fullClassName']); | |
675 | |
676 if ($class->isFinal()) { | |
677 throw new RuntimeException( | |
678 \sprintf( | |
679 'Class "%s" is declared "final" and cannot be mocked.', | |
680 $mockClassName['fullClassName'] | |
681 ) | |
682 ); | |
683 } | |
684 | |
685 if ($class->hasMethod('__clone')) { | |
686 $cloneMethod = $class->getMethod('__clone'); | |
687 | |
688 if (!$cloneMethod->isFinal()) { | |
689 if ($callOriginalClone && !$isInterface) { | |
690 $cloneTemplate = $this->getTemplate('unmocked_clone.tpl'); | |
691 } else { | |
692 $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); | |
693 } | |
694 } | |
695 } else { | |
696 $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); | |
697 } | |
698 } | |
699 | |
700 if (\is_object($cloneTemplate)) { | |
701 $cloneTemplate = $cloneTemplate->render(); | |
702 } | |
703 | |
704 if (\is_array($methods) && empty($methods) && | |
705 ($isClass || $isInterface)) { | |
706 $methods = $this->getClassMethods($mockClassName['fullClassName']); | |
707 } | |
708 | |
709 if (!\is_array($methods)) { | |
710 $methods = []; | |
711 } | |
712 | |
713 $mockedMethods = ''; | |
714 $configurable = []; | |
715 | |
716 foreach ($methods as $methodName) { | |
717 if ($methodName !== '__construct' && $methodName !== '__clone') { | |
718 $configurable[] = \strtolower($methodName); | |
719 } | |
720 } | |
721 | |
722 if (isset($class)) { | |
723 // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 | |
724 if ($isInterface && $class->implementsInterface(Traversable::class) && | |
725 !$class->implementsInterface(Iterator::class) && | |
726 !$class->implementsInterface(IteratorAggregate::class)) { | |
727 $additionalInterfaces[] = Iterator::class; | |
728 $methods = \array_merge($methods, $this->getClassMethods(Iterator::class)); | |
729 } | |
730 | |
731 foreach ($methods as $methodName) { | |
732 try { | |
733 $method = $class->getMethod($methodName); | |
734 | |
735 if ($this->canMockMethod($method)) { | |
736 $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( | |
737 $method, | |
738 $cloneArguments, | |
739 $callOriginalMethods | |
740 ); | |
741 } | |
742 } catch (ReflectionException $e) { | |
743 $mockedMethods .= $this->generateMockedMethodDefinition( | |
744 $mockClassName['fullClassName'], | |
745 $methodName, | |
746 $cloneArguments | |
747 ); | |
748 } | |
749 } | |
750 } elseif ($isMultipleInterfaces) { | |
751 foreach ($methods as $methodName) { | |
752 if ($this->canMockMethod($methodReflections[$methodName])) { | |
753 $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( | |
754 $methodReflections[$methodName], | |
755 $cloneArguments, | |
756 $callOriginalMethods | |
757 ); | |
758 } | |
759 } | |
760 } else { | |
761 foreach ($methods as $methodName) { | |
762 $mockedMethods .= $this->generateMockedMethodDefinition( | |
763 $mockClassName['fullClassName'], | |
764 $methodName, | |
765 $cloneArguments | |
766 ); | |
767 } | |
768 } | |
769 | |
770 $method = ''; | |
771 | |
772 if (!\in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) { | |
773 $methodTemplate = $this->getTemplate('mocked_class_method.tpl'); | |
774 | |
775 $method = $methodTemplate->render(); | |
776 } | |
777 | |
778 $classTemplate->setVar( | |
779 [ | |
780 'prologue' => $prologue ?? '', | |
781 'epilogue' => $epilogue ?? '', | |
782 'class_declaration' => $this->generateMockClassDeclaration( | |
783 $mockClassName, | |
784 $isInterface, | |
785 $additionalInterfaces | |
786 ), | |
787 'clone' => $cloneTemplate, | |
788 'mock_class_name' => $mockClassName['className'], | |
789 'mocked_methods' => $mockedMethods, | |
790 'method' => $method, | |
791 'configurable' => '[' . \implode(', ', \array_map(function ($m) { | |
792 return '\'' . $m . '\''; | |
793 }, $configurable)) . ']' | |
794 ] | |
795 ); | |
796 | |
797 return [ | |
798 'code' => $classTemplate->render(), | |
799 'mockClassName' => $mockClassName['className'] | |
800 ]; | |
801 } | |
802 | |
803 /** | |
804 * @param array|string $type | |
805 * @param string $className | |
806 * @param string $prefix | |
807 * | |
808 * @return array | |
809 */ | |
810 private function generateClassName($type, $className, $prefix) | |
811 { | |
812 if (\is_array($type)) { | |
813 $type = \implode('_', $type); | |
814 } | |
815 | |
816 if ($type[0] === '\\') { | |
817 $type = \substr($type, 1); | |
818 } | |
819 | |
820 $classNameParts = \explode('\\', $type); | |
821 | |
822 if (\count($classNameParts) > 1) { | |
823 $type = \array_pop($classNameParts); | |
824 $namespaceName = \implode('\\', $classNameParts); | |
825 $fullClassName = $namespaceName . '\\' . $type; | |
826 } else { | |
827 $namespaceName = ''; | |
828 $fullClassName = $type; | |
829 } | |
830 | |
831 if ($className === '') { | |
832 do { | |
833 $className = $prefix . $type . '_' . | |
834 \substr(\md5(\mt_rand()), 0, 8); | |
835 } while (\class_exists($className, false)); | |
836 } | |
837 | |
838 return [ | |
839 'className' => $className, | |
840 'originalClassName' => $type, | |
841 'fullClassName' => $fullClassName, | |
842 'namespaceName' => $namespaceName | |
843 ]; | |
844 } | |
845 | |
846 /** | |
847 * @param array $mockClassName | |
848 * @param bool $isInterface | |
849 * @param array $additionalInterfaces | |
850 * | |
851 * @return string | |
852 */ | |
853 private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = []) | |
854 { | |
855 $buffer = 'class '; | |
856 | |
857 $additionalInterfaces[] = MockObject::class; | |
858 $interfaces = \implode(', ', $additionalInterfaces); | |
859 | |
860 if ($isInterface) { | |
861 $buffer .= \sprintf( | |
862 '%s implements %s', | |
863 $mockClassName['className'], | |
864 $interfaces | |
865 ); | |
866 | |
867 if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) { | |
868 $buffer .= ', '; | |
869 | |
870 if (!empty($mockClassName['namespaceName'])) { | |
871 $buffer .= $mockClassName['namespaceName'] . '\\'; | |
872 } | |
873 | |
874 $buffer .= $mockClassName['originalClassName']; | |
875 } | |
876 } else { | |
877 $buffer .= \sprintf( | |
878 '%s extends %s%s implements %s', | |
879 $mockClassName['className'], | |
880 !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '', | |
881 $mockClassName['originalClassName'], | |
882 $interfaces | |
883 ); | |
884 } | |
885 | |
886 return $buffer; | |
887 } | |
888 | |
889 /** | |
890 * @param ReflectionMethod $method | |
891 * @param bool $cloneArguments | |
892 * @param bool $callOriginalMethods | |
893 * | |
894 * @return string | |
895 * | |
896 * @throws \PHPUnit\Framework\MockObject\RuntimeException | |
897 */ | |
898 private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods) | |
899 { | |
900 if ($method->isPrivate()) { | |
901 $modifier = 'private'; | |
902 } elseif ($method->isProtected()) { | |
903 $modifier = 'protected'; | |
904 } else { | |
905 $modifier = 'public'; | |
906 } | |
907 | |
908 if ($method->isStatic()) { | |
909 $modifier .= ' static'; | |
910 } | |
911 | |
912 if ($method->returnsReference()) { | |
913 $reference = '&'; | |
914 } else { | |
915 $reference = ''; | |
916 } | |
917 | |
918 if ($method->hasReturnType()) { | |
919 $returnType = (string) $method->getReturnType(); | |
920 } else { | |
921 $returnType = ''; | |
922 } | |
923 | |
924 if (\preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) { | |
925 $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1])); | |
926 } else { | |
927 $deprecation = false; | |
928 } | |
929 | |
930 return $this->generateMockedMethodDefinition( | |
931 $method->getDeclaringClass()->getName(), | |
932 $method->getName(), | |
933 $cloneArguments, | |
934 $modifier, | |
935 $this->getMethodParameters($method), | |
936 $this->getMethodParameters($method, true), | |
937 $returnType, | |
938 $reference, | |
939 $callOriginalMethods, | |
940 $method->isStatic(), | |
941 $deprecation, | |
942 $method->hasReturnType() && PHP_VERSION_ID >= 70100 && $method->getReturnType()->allowsNull() | |
943 ); | |
944 } | |
945 | |
946 /** | |
947 * @param string $className | |
948 * @param string $methodName | |
949 * @param bool $cloneArguments | |
950 * @param string $modifier | |
951 * @param string $argumentsForDeclaration | |
952 * @param string $argumentsForCall | |
953 * @param string $returnType | |
954 * @param string $reference | |
955 * @param bool $callOriginalMethods | |
956 * @param bool $static | |
957 * @param bool|string $deprecation | |
958 * @param bool $allowsReturnNull | |
959 * | |
960 * @return string | |
961 * | |
962 * @throws \InvalidArgumentException | |
963 */ | |
964 private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false) | |
965 { | |
966 if ($static) { | |
967 $templateFile = 'mocked_static_method.tpl'; | |
968 } else { | |
969 if ($returnType === 'void') { | |
970 $templateFile = \sprintf( | |
971 '%s_method_void.tpl', | |
972 $callOriginalMethods ? 'proxied' : 'mocked' | |
973 ); | |
974 } else { | |
975 $templateFile = \sprintf( | |
976 '%s_method.tpl', | |
977 $callOriginalMethods ? 'proxied' : 'mocked' | |
978 ); | |
979 } | |
980 } | |
981 | |
982 // Mocked interfaces returning 'self' must explicitly declare the | |
983 // interface name as the return type. See | |
984 // https://bugs.php.net/bug.php?id=70722 | |
985 if ($returnType === 'self') { | |
986 $returnType = $className; | |
987 } | |
988 | |
989 if (false !== $deprecation) { | |
990 $deprecation = "The $className::$methodName method is deprecated ($deprecation)."; | |
991 $deprecationTemplate = $this->getTemplate('deprecation.tpl'); | |
992 | |
993 $deprecationTemplate->setVar( | |
994 [ | |
995 'deprecation' => \var_export($deprecation, true), | |
996 ] | |
997 ); | |
998 | |
999 $deprecation = $deprecationTemplate->render(); | |
1000 } | |
1001 | |
1002 $template = $this->getTemplate($templateFile); | |
1003 | |
1004 $template->setVar( | |
1005 [ | |
1006 'arguments_decl' => $argumentsForDeclaration, | |
1007 'arguments_call' => $argumentsForCall, | |
1008 'return_delim' => $returnType ? ': ' : '', | |
1009 'return_type' => $allowsReturnNull ? '?' . $returnType : $returnType, | |
1010 'arguments_count' => !empty($argumentsForCall) ? \substr_count($argumentsForCall, ',') + 1 : 0, | |
1011 'class_name' => $className, | |
1012 'method_name' => $methodName, | |
1013 'modifier' => $modifier, | |
1014 'reference' => $reference, | |
1015 'clone_arguments' => $cloneArguments ? 'true' : 'false', | |
1016 'deprecation' => $deprecation | |
1017 ] | |
1018 ); | |
1019 | |
1020 return $template->render(); | |
1021 } | |
1022 | |
1023 /** | |
1024 * @param ReflectionMethod $method | |
1025 * | |
1026 * @return bool | |
1027 * | |
1028 * @throws \ReflectionException | |
1029 */ | |
1030 private function canMockMethod(ReflectionMethod $method) | |
1031 { | |
1032 return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName())); | |
1033 } | |
1034 | |
1035 /** | |
1036 * Returns whether a method name is blacklisted | |
1037 * | |
1038 * @param string $name | |
1039 * | |
1040 * @return bool | |
1041 */ | |
1042 private function isMethodNameBlacklisted($name) | |
1043 { | |
1044 return isset($this->blacklistedMethodNames[$name]); | |
1045 } | |
1046 | |
1047 /** | |
1048 * Returns the parameters of a function or method. | |
1049 * | |
1050 * @param ReflectionMethod $method | |
1051 * @param bool $forCall | |
1052 * | |
1053 * @return string | |
1054 * | |
1055 * @throws RuntimeException | |
1056 */ | |
1057 private function getMethodParameters(ReflectionMethod $method, $forCall = false) | |
1058 { | |
1059 $parameters = []; | |
1060 | |
1061 foreach ($method->getParameters() as $i => $parameter) { | |
1062 $name = '$' . $parameter->getName(); | |
1063 | |
1064 /* Note: PHP extensions may use empty names for reference arguments | |
1065 * or "..." for methods taking a variable number of arguments. | |
1066 */ | |
1067 if ($name === '$' || $name === '$...') { | |
1068 $name = '$arg' . $i; | |
1069 } | |
1070 | |
1071 if ($parameter->isVariadic()) { | |
1072 if ($forCall) { | |
1073 continue; | |
1074 } | |
1075 | |
1076 $name = '...' . $name; | |
1077 } | |
1078 | |
1079 $nullable = ''; | |
1080 $default = ''; | |
1081 $reference = ''; | |
1082 $typeDeclaration = ''; | |
1083 | |
1084 if (!$forCall) { | |
1085 if (PHP_VERSION_ID >= 70100 && $parameter->hasType() && $parameter->allowsNull()) { | |
1086 $nullable = '?'; | |
1087 } | |
1088 | |
1089 if ($parameter->hasType() && (string) $parameter->getType() !== 'self') { | |
1090 $typeDeclaration = (string) $parameter->getType() . ' '; | |
1091 } elseif ($parameter->isArray()) { | |
1092 $typeDeclaration = 'array '; | |
1093 } elseif ($parameter->isCallable()) { | |
1094 $typeDeclaration = 'callable '; | |
1095 } else { | |
1096 try { | |
1097 $class = $parameter->getClass(); | |
1098 } catch (ReflectionException $e) { | |
1099 throw new RuntimeException( | |
1100 \sprintf( | |
1101 'Cannot mock %s::%s() because a class or ' . | |
1102 'interface used in the signature is not loaded', | |
1103 $method->getDeclaringClass()->getName(), | |
1104 $method->getName() | |
1105 ), | |
1106 0, | |
1107 $e | |
1108 ); | |
1109 } | |
1110 | |
1111 if ($class !== null) { | |
1112 $typeDeclaration = $class->getName() . ' '; | |
1113 } | |
1114 } | |
1115 | |
1116 if (!$parameter->isVariadic()) { | |
1117 if ($parameter->isDefaultValueAvailable()) { | |
1118 $value = $parameter->getDefaultValue(); | |
1119 $default = ' = ' . \var_export($value, true); | |
1120 } elseif ($parameter->isOptional()) { | |
1121 $default = ' = null'; | |
1122 } | |
1123 } | |
1124 } | |
1125 | |
1126 if ($parameter->isPassedByReference()) { | |
1127 $reference = '&'; | |
1128 } | |
1129 | |
1130 $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default; | |
1131 } | |
1132 | |
1133 return \implode(', ', $parameters); | |
1134 } | |
1135 | |
1136 /** | |
1137 * @param string $className | |
1138 * | |
1139 * @return array | |
1140 * | |
1141 * @throws \ReflectionException | |
1142 */ | |
1143 public function getClassMethods($className) | |
1144 { | |
1145 $class = new ReflectionClass($className); | |
1146 $methods = []; | |
1147 | |
1148 foreach ($class->getMethods() as $method) { | |
1149 if ($method->isPublic() || $method->isAbstract()) { | |
1150 $methods[] = $method->getName(); | |
1151 } | |
1152 } | |
1153 | |
1154 return $methods; | |
1155 } | |
1156 | |
1157 /** | |
1158 * @param string $template | |
1159 * | |
1160 * @return Text_Template | |
1161 * | |
1162 * @throws \InvalidArgumentException | |
1163 */ | |
1164 private function getTemplate($template) | |
1165 { | |
1166 $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template; | |
1167 | |
1168 if (!isset(self::$templates[$filename])) { | |
1169 self::$templates[$filename] = new Text_Template($filename); | |
1170 } | |
1171 | |
1172 return self::$templates[$filename]; | |
1173 } | |
1174 } |