3 * This file is part of the PHPUnit_MockObject package.
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 use Doctrine\Instantiator\Instantiator;
12 use Doctrine\Instantiator\Exception\InvalidArgumentException as InstantiatorInvalidArgumentException;
13 use Doctrine\Instantiator\Exception\UnexpectedValueException as InstantiatorUnexpectedValueException;
15 if (!function_exists('trait_exists')) {
16 function trait_exists($traitname, $autoload = true)
23 * Mock Object Code Generator
25 * @since Class available since Release 1.0.0
27 class PHPUnit_Framework_MockObject_Generator
32 private static $cache = array();
37 protected $blacklistedMethodNames = array(
41 '__FUNCTION__' => true,
44 '__NAMESPACE__' => true,
47 '__halt_compiler' => true,
87 'include_once' => true,
101 'require_once' => true,
116 * Returns a mock object for the specified class.
118 * @param array|string $type
119 * @param array $methods
120 * @param array $arguments
121 * @param string $mockClassName
122 * @param bool $callOriginalConstructor
123 * @param bool $callOriginalClone
124 * @param bool $callAutoload
125 * @param bool $cloneArguments
126 * @param bool $callOriginalMethods
127 * @param object $proxyTarget
129 * @throws InvalidArgumentException
130 * @throws PHPUnit_Framework_Exception
131 * @throws PHPUnit_Framework_MockObject_RuntimeException
132 * @since Method available since Release 1.0.0
134 public function getMock($type, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null)
136 if (!is_array($type) && !is_string($type)) {
137 throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'array or string');
140 if (!is_string($mockClassName)) {
141 throw PHPUnit_Util_InvalidArgumentHelper::factory(4, 'string');
144 if (!is_array($methods) && !is_null($methods)) {
145 throw new InvalidArgumentException;
148 if ($type === 'Traversable' || $type === '\\Traversable') {
152 if (is_array($type)) {
153 $type = array_unique(array_map(
155 if ($type === 'Traversable' ||
156 $type === '\\Traversable' ||
157 $type === '\\Iterator') {
167 if (null !== $methods) {
168 foreach ($methods as $method) {
169 if (!preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) {
170 throw new PHPUnit_Framework_Exception(
172 'Cannot stub or mock method with invalid name "%s"',
179 if ($methods != array_unique($methods)) {
180 throw new PHPUnit_Framework_MockObject_RuntimeException(
182 'Cannot stub or mock using a method list that contains duplicates: "%s"',
183 implode(', ', $methods)
189 if ($mockClassName != '' && class_exists($mockClassName, false)) {
190 $reflect = new ReflectionClass($mockClassName);
192 if (!$reflect->implementsInterface('PHPUnit_Framework_MockObject_MockObject')) {
193 throw new PHPUnit_Framework_MockObject_RuntimeException(
195 'Class "%s" already exists.',
202 $mock = $this->generate(
212 return $this->getObject(
214 $mock['mockClassName'],
216 $callOriginalConstructor,
219 $callOriginalMethods,
225 * @param string $code
226 * @param string $className
227 * @param array|string $type
228 * @param bool $callOriginalConstructor
229 * @param bool $callAutoload
230 * @param array $arguments
231 * @param bool $callOriginalMethods
232 * @param object $proxyTarget
235 protected function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = array(), $callOriginalMethods = false, $proxyTarget = null)
237 $this->evalClass($code, $className);
239 if ($callOriginalConstructor &&
241 !interface_exists($type, $callAutoload)) {
242 if (count($arguments) == 0) {
243 $object = new $className;
245 $class = new ReflectionClass($className);
246 $object = $class->newInstanceArgs($arguments);
250 $instantiator = new Instantiator;
251 $object = $instantiator->instantiate($className);
252 } catch (InstantiatorUnexpectedValueException $exception) {
253 if ($exception->getPrevious()) {
254 $exception = $exception->getPrevious();
257 throw new PHPUnit_Framework_MockObject_RuntimeException(
258 $exception->getMessage()
260 } catch (InstantiatorInvalidArgumentException $exception) {
261 throw new PHPUnit_Framework_MockObject_RuntimeException(
262 $exception->getMessage()
267 if ($callOriginalMethods) {
268 if (!is_object($proxyTarget)) {
269 if (count($arguments) == 0) {
270 $proxyTarget = new $type;
272 $class = new ReflectionClass($type);
273 $proxyTarget = $class->newInstanceArgs($arguments);
277 $object->__phpunit_setOriginalObject($proxyTarget);
284 * @param string $code
285 * @param string $className
287 protected function evalClass($code, $className)
289 if (!class_exists($className, false)) {
295 * Returns a mock object for the specified abstract class with all abstract
296 * methods of the class mocked. Concrete methods to mock can be specified with
299 * @param string $originalClassName
300 * @param array $arguments
301 * @param string $mockClassName
302 * @param bool $callOriginalConstructor
303 * @param bool $callOriginalClone
304 * @param bool $callAutoload
305 * @param array $mockedMethods
306 * @param bool $cloneArguments
308 * @since Method available since Release 1.0.0
309 * @throws PHPUnit_Framework_MockObject_RuntimeException
310 * @throws PHPUnit_Framework_Exception
312 public function getMockForAbstractClass($originalClassName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = true)
314 if (!is_string($originalClassName)) {
315 throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string');
318 if (!is_string($mockClassName)) {
319 throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string');
322 if (class_exists($originalClassName, $callAutoload) ||
323 interface_exists($originalClassName, $callAutoload)) {
324 $reflector = new ReflectionClass($originalClassName);
325 $methods = $mockedMethods;
327 foreach ($reflector->getMethods() as $method) {
328 if ($method->isAbstract() && !in_array($method->getName(), $methods)) {
329 $methods[] = $method->getName();
333 if (empty($methods)) {
337 return $this->getMock(
342 $callOriginalConstructor,
348 throw new PHPUnit_Framework_MockObject_RuntimeException(
349 sprintf('Class "%s" does not exist.', $originalClassName)
355 * Returns a mock object for the specified trait with all abstract methods
356 * of the trait mocked. Concrete methods to mock can be specified with the
357 * `$mockedMethods` parameter.
359 * @param string $traitName
360 * @param array $arguments
361 * @param string $mockClassName
362 * @param bool $callOriginalConstructor
363 * @param bool $callOriginalClone
364 * @param bool $callAutoload
365 * @param array $mockedMethods
366 * @param bool $cloneArguments
368 * @since Method available since Release 1.2.3
369 * @throws PHPUnit_Framework_MockObject_RuntimeException
370 * @throws PHPUnit_Framework_Exception
372 public function getMockForTrait($traitName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = true)
374 if (!is_string($traitName)) {
375 throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string');
378 if (!is_string($mockClassName)) {
379 throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string');
382 if (!trait_exists($traitName, $callAutoload)) {
383 throw new PHPUnit_Framework_MockObject_RuntimeException(
385 'Trait "%s" does not exist.',
391 $className = $this->generateClassName(
397 $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' .
399 $classTemplate = new Text_Template(
400 $templateDir . 'trait_class.tpl'
403 $classTemplate->setVar(
405 'prologue' => 'abstract ',
406 'class_name' => $className['className'],
407 'trait_name' => $traitName
412 $classTemplate->render(),
413 $className['className']
416 return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments);
420 * Returns an object for the specified trait.
422 * @param string $traitName
423 * @param array $arguments
424 * @param string $traitClassName
425 * @param bool $callOriginalConstructor
426 * @param bool $callOriginalClone
427 * @param bool $callAutoload
429 * @since Method available since Release 1.1.0
430 * @throws PHPUnit_Framework_MockObject_RuntimeException
431 * @throws PHPUnit_Framework_Exception
433 public function getObjectForTrait($traitName, array $arguments = array(), $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true)
435 if (!is_string($traitName)) {
436 throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string');
439 if (!is_string($traitClassName)) {
440 throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string');
443 if (!trait_exists($traitName, $callAutoload)) {
444 throw new PHPUnit_Framework_MockObject_RuntimeException(
446 'Trait "%s" does not exist.',
452 $className = $this->generateClassName(
458 $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' .
460 $classTemplate = new Text_Template(
461 $templateDir . 'trait_class.tpl'
464 $classTemplate->setVar(
467 'class_name' => $className['className'],
468 'trait_name' => $traitName
472 return $this->getObject(
473 $classTemplate->render(),
474 $className['className']
479 * @param array|string $type
480 * @param array $methods
481 * @param string $mockClassName
482 * @param bool $callOriginalClone
483 * @param bool $callAutoload
484 * @param bool $cloneArguments
485 * @param bool $callOriginalMethods
488 public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false)
490 if (is_array($type)) {
494 if ($mockClassName == '') {
496 is_array($type) ? implode('_', $type) : $type .
497 serialize($methods) .
498 serialize($callOriginalClone) .
499 serialize($cloneArguments) .
500 serialize($callOriginalMethods)
503 if (isset(self::$cache[$key])) {
504 return self::$cache[$key];
508 $mock = $this->generateMock(
519 self::$cache[$key] = $mock;
526 * @param string $wsdlFile
527 * @param string $className
528 * @param array $methods
529 * @param array $options
531 * @throws PHPUnit_Framework_MockObject_RuntimeException
533 public function generateClassFromWsdl($wsdlFile, $className, array $methods = array(), array $options = array())
535 if (!extension_loaded('soap')) {
536 throw new PHPUnit_Framework_MockObject_RuntimeException(
537 'The SOAP extension is required to generate a mock object from WSDL.'
541 $options = array_merge($options, array('cache_wsdl' => WSDL_CACHE_NONE));
542 $client = new SoapClient($wsdlFile, $options);
543 $_methods = array_unique($client->__getFunctions());
548 $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR;
549 $methodTemplate = new Text_Template($templateDir . 'wsdl_method.tpl');
552 foreach ($_methods as $method) {
553 $nameStart = strpos($method, ' ') + 1;
554 $nameEnd = strpos($method, '(');
555 $name = substr($method, $nameStart, $nameEnd - $nameStart);
557 if (empty($methods) || in_array($name, $methods)) {
563 strpos($method, ')') - $nameEnd - 1
566 $numArgs = count($args);
568 for ($i = 0; $i < $numArgs; $i++) {
569 $args[$i] = substr($args[$i], strpos($args[$i], '$'));
572 $methodTemplate->setVar(
574 'method_name' => $name,
575 'arguments' => implode(', ', $args)
579 $methodsBuffer .= $methodTemplate->render();
583 $optionsBuffer = 'array(';
585 foreach ($options as $key => $value) {
586 $optionsBuffer .= $key . ' => ' . $value;
589 $optionsBuffer .= ')';
591 $classTemplate = new Text_Template($templateDir . 'wsdl_class.tpl');
594 if (strpos($className, '\\') !== false) {
595 $parts = explode('\\', $className);
596 $className = array_pop($parts);
597 $namespace = 'namespace ' . implode('\\', $parts) . ';' . "\n\n";
600 $classTemplate->setVar(
602 'namespace' => $namespace,
603 'class_name' => $className,
605 'options' => $optionsBuffer,
606 'methods' => $methodsBuffer
610 return $classTemplate->render();
614 * @param array|string $type
615 * @param array|null $methods
616 * @param string $mockClassName
617 * @param bool $callOriginalClone
618 * @param bool $callAutoload
619 * @param bool $cloneArguments
620 * @param bool $callOriginalMethods
622 * @throws PHPUnit_Framework_Exception
624 protected function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods)
626 $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' .
628 $classTemplate = new Text_Template(
629 $templateDir . 'mocked_class.tpl'
632 $additionalInterfaces = array();
635 $isInterface = false;
637 $mockClassName = $this->generateClassName(
643 if (is_array($type)) {
644 foreach ($type as $_type) {
645 if (!interface_exists($_type, $callAutoload)) {
646 throw new PHPUnit_Framework_Exception(
648 'Interface "%s" does not exist.',
654 $additionalInterfaces[] = $_type;
656 foreach ($this->getClassMethods($_type) as $method) {
657 if (in_array($method, $methods)) {
658 throw new PHPUnit_Framework_Exception(
660 'Duplicate method "%s" not allowed.',
666 $methods[] = $method;
671 if (class_exists($mockClassName['fullClassName'], $callAutoload)) {
674 if (interface_exists($mockClassName['fullClassName'], $callAutoload)) {
679 if (!class_exists($mockClassName['fullClassName'], $callAutoload) &&
680 !interface_exists($mockClassName['fullClassName'], $callAutoload)) {
681 $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
683 if (!empty($mockClassName['namespaceName'])) {
684 $prologue = 'namespace ' . $mockClassName['namespaceName'] .
685 " {\n\n" . $prologue . "}\n\n" .
691 $cloneTemplate = new Text_Template(
692 $templateDir . 'mocked_clone.tpl'
695 $class = new ReflectionClass($mockClassName['fullClassName']);
697 if ($class->isFinal()) {
698 throw new PHPUnit_Framework_Exception(
700 'Class "%s" is declared "final" and cannot be mocked.',
701 $mockClassName['fullClassName']
706 if ($class->hasMethod('__clone')) {
707 $cloneMethod = $class->getMethod('__clone');
709 if (!$cloneMethod->isFinal()) {
710 if ($callOriginalClone && !$isInterface) {
711 $cloneTemplate = new Text_Template(
712 $templateDir . 'unmocked_clone.tpl'
715 $cloneTemplate = new Text_Template(
716 $templateDir . 'mocked_clone.tpl'
721 $cloneTemplate = new Text_Template(
722 $templateDir . 'mocked_clone.tpl'
727 if (is_object($cloneTemplate)) {
728 $cloneTemplate = $cloneTemplate->render();
731 if (is_array($methods) && empty($methods) &&
732 ($isClass || $isInterface)) {
733 $methods = $this->getClassMethods($mockClassName['fullClassName']);
736 if (!is_array($methods)) {
743 // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
744 if ($isInterface && $class->implementsInterface('Traversable') &&
745 !$class->implementsInterface('Iterator') &&
746 !$class->implementsInterface('IteratorAggregate')) {
747 $additionalInterfaces[] = 'Iterator';
748 $methods = array_merge($methods, $this->getClassMethods('Iterator'));
751 foreach ($methods as $methodName) {
753 $method = $class->getMethod($methodName);
755 if ($this->canMockMethod($method)) {
756 $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting(
763 } catch (ReflectionException $e) {
764 $mockedMethods .= $this->generateMockedMethodDefinition(
766 $mockClassName['fullClassName'],
773 foreach ($methods as $methodName) {
774 $mockedMethods .= $this->generateMockedMethodDefinition(
776 $mockClassName['fullClassName'],
785 if (!in_array('method', $methods)) {
786 $methodTemplate = new Text_Template(
787 $templateDir . 'mocked_class_method.tpl'
790 $method = $methodTemplate->render();
793 $classTemplate->setVar(
795 'prologue' => isset($prologue) ? $prologue : '',
796 'epilogue' => isset($epilogue) ? $epilogue : '',
797 'class_declaration' => $this->generateMockClassDeclaration(
800 $additionalInterfaces
802 'clone' => $cloneTemplate,
803 'mock_class_name' => $mockClassName['className'],
804 'mocked_methods' => $mockedMethods,
810 'code' => $classTemplate->render(),
811 'mockClassName' => $mockClassName['className']
816 * @param array|string $type
817 * @param string $className
818 * @param string $prefix
821 protected function generateClassName($type, $className, $prefix)
823 if (is_array($type)) {
824 $type = implode('_', $type);
827 if ($type[0] == '\\') {
828 $type = substr($type, 1);
831 $classNameParts = explode('\\', $type);
833 if (count($classNameParts) > 1) {
834 $type = array_pop($classNameParts);
835 $namespaceName = implode('\\', $classNameParts);
836 $fullClassName = $namespaceName . '\\' . $type;
839 $fullClassName = $type;
842 if ($className == '') {
844 $className = $prefix . $type . '_' .
845 substr(md5(microtime()), 0, 8);
846 } while (class_exists($className, false));
850 'className' => $className,
851 'originalClassName' => $type,
852 'fullClassName' => $fullClassName,
853 'namespaceName' => $namespaceName
858 * @param array $mockClassName
859 * @param bool $isInterface
860 * @param array $additionalInterfaces
863 protected function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = array())
867 $additionalInterfaces[] = 'PHPUnit_Framework_MockObject_MockObject';
868 $interfaces = implode(', ', $additionalInterfaces);
873 $mockClassName['className'],
877 if (!in_array($mockClassName['originalClassName'], $additionalInterfaces)) {
880 if (!empty($mockClassName['namespaceName'])) {
881 $buffer .= $mockClassName['namespaceName'] . '\\';
884 $buffer .= $mockClassName['originalClassName'];
888 '%s extends %s%s implements %s',
889 $mockClassName['className'],
890 !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '',
891 $mockClassName['originalClassName'],
900 * @param string $templateDir
901 * @param ReflectionMethod $method
902 * @param bool $cloneArguments
903 * @param bool $callOriginalMethods
906 protected function generateMockedMethodDefinitionFromExisting($templateDir, ReflectionMethod $method, $cloneArguments, $callOriginalMethods)
908 if ($method->isPrivate()) {
909 $modifier = 'private';
910 } elseif ($method->isProtected()) {
911 $modifier = 'protected';
913 $modifier = 'public';
916 if ($method->isStatic()) {
917 $modifier .= ' static';
920 if ($method->returnsReference()) {
926 return $this->generateMockedMethodDefinition(
928 $method->getDeclaringClass()->getName(),
932 $this->getMethodParameters($method),
933 $this->getMethodParameters($method, true),
935 $callOriginalMethods,
941 * @param string $templateDir
942 * @param string $className
943 * @param string $methodName
944 * @param bool $cloneArguments
945 * @param string $modifier
946 * @param string $arguments_decl
947 * @param string $arguments_call
948 * @param string $reference
949 * @param bool $callOriginalMethods
950 * @param bool $static
953 protected function generateMockedMethodDefinition($templateDir, $className, $methodName, $cloneArguments = true, $modifier = 'public', $arguments_decl = '', $arguments_call = '', $reference = '', $callOriginalMethods = false, $static = false)
956 $templateFile = 'mocked_static_method.tpl';
958 $templateFile = sprintf(
960 $callOriginalMethods ? 'proxied' : 'mocked'
964 $template = new Text_Template($templateDir . $templateFile);
968 'arguments_decl' => $arguments_decl,
969 'arguments_call' => $arguments_call,
970 'arguments_count' => !empty($arguments_call) ? count(explode(',', $arguments_call)) : 0,
971 'class_name' => $className,
972 'method_name' => $methodName,
973 'modifier' => $modifier,
974 'reference' => $reference,
975 'clone_arguments' => $cloneArguments ? 'TRUE' : 'FALSE'
979 return $template->render();
983 * @param ReflectionMethod $method
986 protected function canMockMethod(ReflectionMethod $method)
988 if ($method->isConstructor() ||
989 $method->isFinal() ||
990 $method->isPrivate() ||
991 isset($this->blacklistedMethodNames[$method->getName()])) {
999 * Returns the parameters of a function or method.
1001 * @param ReflectionMethod $method
1002 * @param bool $forCall
1004 * @throws PHPUnit_Framework_MockObject_RuntimeException
1005 * @since Method available since Release 2.0.0
1007 protected function getMethodParameters(ReflectionMethod $method, $forCall = false)
1009 $parameters = array();
1011 foreach ($method->getParameters() as $i => $parameter) {
1012 $name = '$' . $parameter->getName();
1014 /* Note: PHP extensions may use empty names for reference arguments
1015 * or "..." for methods taking a variable number of arguments.
1017 if ($name === '$' || $name === '$...') {
1018 $name = '$arg' . $i;
1021 if ($this->isVariadic($parameter)) {
1025 $name = '...' . $name;
1031 $typeDeclaration = '';
1034 if ($this->hasType($parameter)) {
1035 $typeDeclaration = (string) $parameter->getType() . ' ';
1036 } elseif ($parameter->isArray()) {
1037 $typeDeclaration = 'array ';
1038 } elseif ((defined('HHVM_VERSION') || version_compare(PHP_VERSION, '5.4.0', '>='))
1039 && $parameter->isCallable()) {
1040 $typeDeclaration = 'callable ';
1043 $class = $parameter->getClass();
1044 } catch (ReflectionException $e) {
1045 throw new PHPUnit_Framework_MockObject_RuntimeException(
1047 'Cannot mock %s::%s() because a class or ' .
1048 'interface used in the signature is not loaded',
1049 $method->getDeclaringClass()->getName(),
1057 if ($class !== null) {
1058 $typeDeclaration = $class->getName() . ' ';
1062 if (!$this->isVariadic($parameter)) {
1063 if ($parameter->isDefaultValueAvailable()) {
1064 $value = $parameter->getDefaultValue();
1065 $default = ' = ' . var_export($value, true);
1066 } elseif ($parameter->isOptional()) {
1067 $default = ' = null';
1072 if ($parameter->isPassedByReference()) {
1076 $parameters[] = $typeDeclaration . $reference . $name . $default;
1079 return implode(', ', $parameters);
1083 * @param ReflectionParameter $parameter
1085 * @since Method available since Release 2.2.1
1087 private function isVariadic(ReflectionParameter $parameter)
1089 return method_exists('ReflectionParameter', 'isVariadic') && $parameter->isVariadic();
1093 * @param ReflectionParameter $parameter
1095 * @since Method available since Release 2.3.4
1097 private function hasType(ReflectionParameter $parameter)
1099 return method_exists('ReflectionParameter', 'hasType') && $parameter->hasType();
1103 * @param string $className
1105 * @since Method available since Release 2.3.2
1107 private function getClassMethods($className)
1109 $class = new ReflectionClass($className);
1112 foreach ($class->getMethods() as $method) {
1113 if (($method->isPublic() || $method->isAbstract()) && !in_array($method->getName(), $methods)) {
1114 $methods[] = $method->getName();