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