Chris@14: Chris@14: * Chris@14: * For the full copyright and license information, please view the LICENSE Chris@14: * file that was distributed with this source code. Chris@14: */ Chris@14: namespace PHPUnit\Framework\MockObject; Chris@14: Chris@14: use Doctrine\Instantiator\Exception\ExceptionInterface as InstantiatorException; Chris@14: use Doctrine\Instantiator\Instantiator; Chris@14: use Iterator; Chris@14: use IteratorAggregate; Chris@14: use PHPUnit\Framework\Exception; Chris@14: use PHPUnit\Util\InvalidArgumentHelper; Chris@14: use ReflectionClass; Chris@14: use ReflectionException; Chris@14: use ReflectionMethod; Chris@14: use SoapClient; Chris@14: use Text_Template; Chris@14: use Traversable; Chris@14: Chris@14: /** Chris@14: * Mock Object Code Generator Chris@14: */ Chris@14: class Generator Chris@14: { Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private static $cache = []; Chris@14: Chris@14: /** Chris@14: * @var Text_Template[] Chris@14: */ Chris@14: private static $templates = []; Chris@14: Chris@14: /** Chris@14: * @var array Chris@14: */ Chris@14: private $blacklistedMethodNames = [ Chris@14: '__CLASS__' => true, Chris@14: '__DIR__' => true, Chris@14: '__FILE__' => true, Chris@14: '__FUNCTION__' => true, Chris@14: '__LINE__' => true, Chris@14: '__METHOD__' => true, Chris@14: '__NAMESPACE__' => true, Chris@14: '__TRAIT__' => true, Chris@14: '__clone' => true, Chris@14: '__halt_compiler' => true, Chris@14: ]; Chris@14: Chris@14: /** Chris@14: * Returns a mock object for the specified class. Chris@14: * Chris@14: * @param string|string[] $type Chris@14: * @param array $methods Chris@14: * @param array $arguments Chris@14: * @param string $mockClassName Chris@14: * @param bool $callOriginalConstructor Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * @param bool $cloneArguments Chris@14: * @param bool $callOriginalMethods Chris@14: * @param object $proxyTarget Chris@14: * @param bool $allowMockingUnknownTypes Chris@14: * Chris@14: * @return MockObject Chris@14: * Chris@14: * @throws Exception Chris@14: * @throws RuntimeException Chris@14: * @throws \PHPUnit\Framework\Exception Chris@14: * @throws \ReflectionException Chris@14: */ Chris@14: public function getMock($type, $methods = [], array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null, $allowMockingUnknownTypes = true) Chris@14: { Chris@14: if (!\is_array($type) && !\is_string($type)) { Chris@14: throw InvalidArgumentHelper::factory(1, 'array or string'); Chris@14: } Chris@14: Chris@14: if (!\is_string($mockClassName)) { Chris@14: throw InvalidArgumentHelper::factory(4, 'string'); Chris@14: } Chris@14: Chris@14: if (!\is_array($methods) && null !== $methods) { Chris@14: throw InvalidArgumentHelper::factory(2, 'array', $methods); Chris@14: } Chris@14: Chris@14: if ($type === 'Traversable' || $type === '\\Traversable') { Chris@14: $type = 'Iterator'; Chris@14: } Chris@14: Chris@14: if (\is_array($type)) { Chris@14: $type = \array_unique( Chris@14: \array_map( Chris@14: function ($type) { Chris@14: if ($type === 'Traversable' || Chris@14: $type === '\\Traversable' || Chris@14: $type === '\\Iterator') { Chris@14: return 'Iterator'; Chris@14: } Chris@14: Chris@14: return $type; Chris@14: }, Chris@14: $type Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: if (!$allowMockingUnknownTypes) { Chris@14: if (\is_array($type)) { Chris@14: foreach ($type as $_type) { Chris@14: if (!\class_exists($_type, $callAutoload) && Chris@14: !\interface_exists($_type, $callAutoload)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Cannot stub or mock class or interface "%s" which does not exist', Chris@14: $_type Chris@14: ) Chris@14: ); Chris@14: } Chris@14: } Chris@14: } else { Chris@14: if (!\class_exists($type, $callAutoload) && Chris@14: !\interface_exists($type, $callAutoload) Chris@14: ) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Cannot stub or mock class or interface "%s" which does not exist', Chris@14: $type Chris@14: ) Chris@14: ); Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: if (null !== $methods) { Chris@14: foreach ($methods as $method) { Chris@14: if (!\preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Cannot stub or mock method with invalid name "%s"', Chris@14: $method Chris@14: ) Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: if ($methods !== \array_unique($methods)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Cannot stub or mock using a method list that contains duplicates: "%s" (duplicate: "%s")', Chris@14: \implode(', ', $methods), Chris@14: \implode(', ', \array_unique(\array_diff_assoc($methods, \array_unique($methods)))) Chris@14: ) Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: if ($mockClassName !== '' && \class_exists($mockClassName, false)) { Chris@14: $reflect = new ReflectionClass($mockClassName); Chris@14: Chris@14: if (!$reflect->implementsInterface(MockObject::class)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Class "%s" already exists.', Chris@14: $mockClassName Chris@14: ) Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: if ($callOriginalConstructor === false && $callOriginalMethods === true) { Chris@14: throw new RuntimeException( Chris@14: 'Proxying to original methods requires invoking the original constructor' Chris@14: ); Chris@14: } Chris@14: Chris@14: $mock = $this->generate( Chris@14: $type, Chris@14: $methods, Chris@14: $mockClassName, Chris@14: $callOriginalClone, Chris@14: $callAutoload, Chris@14: $cloneArguments, Chris@14: $callOriginalMethods Chris@14: ); Chris@14: Chris@14: return $this->getObject( Chris@14: $mock['code'], Chris@14: $mock['mockClassName'], Chris@14: $type, Chris@14: $callOriginalConstructor, Chris@14: $callAutoload, Chris@14: $arguments, Chris@14: $callOriginalMethods, Chris@14: $proxyTarget Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $code Chris@14: * @param string $className Chris@14: * @param array|string $type Chris@14: * @param bool $callOriginalConstructor Chris@14: * @param bool $callAutoload Chris@14: * @param array $arguments Chris@14: * @param bool $callOriginalMethods Chris@14: * @param object $proxyTarget Chris@14: * Chris@14: * @return MockObject Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws RuntimeException Chris@14: */ Chris@14: private function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = [], $callOriginalMethods = false, $proxyTarget = null) Chris@14: { Chris@14: $this->evalClass($code, $className); Chris@14: Chris@14: if ($callOriginalConstructor && Chris@14: \is_string($type) && Chris@14: !\interface_exists($type, $callAutoload)) { Chris@14: if (\count($arguments) === 0) { Chris@14: $object = new $className; Chris@14: } else { Chris@14: $class = new ReflectionClass($className); Chris@14: $object = $class->newInstanceArgs($arguments); Chris@14: } Chris@14: } else { Chris@14: try { Chris@14: $instantiator = new Instantiator; Chris@14: $object = $instantiator->instantiate($className); Chris@14: } catch (InstantiatorException $exception) { Chris@14: throw new RuntimeException($exception->getMessage()); Chris@14: } Chris@14: } Chris@14: Chris@14: if ($callOriginalMethods) { Chris@14: if (!\is_object($proxyTarget)) { Chris@14: if (\count($arguments) === 0) { Chris@14: $proxyTarget = new $type; Chris@14: } else { Chris@14: $class = new ReflectionClass($type); Chris@14: $proxyTarget = $class->newInstanceArgs($arguments); Chris@14: } Chris@14: } Chris@14: Chris@14: $object->__phpunit_setOriginalObject($proxyTarget); Chris@14: } Chris@14: Chris@14: return $object; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $code Chris@14: * @param string $className Chris@14: */ Chris@14: private function evalClass($code, $className) Chris@14: { Chris@14: if (!\class_exists($className, false)) { Chris@14: eval($code); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns a mock object for the specified abstract class with all abstract Chris@14: * methods of the class mocked. Concrete methods to mock can be specified with Chris@14: * the last parameter Chris@14: * Chris@14: * @param string $originalClassName Chris@14: * @param array $arguments Chris@14: * @param string $mockClassName Chris@14: * @param bool $callOriginalConstructor Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * @param array $mockedMethods Chris@14: * @param bool $cloneArguments Chris@14: * Chris@14: * @return MockObject Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws RuntimeException Chris@14: * @throws Exception Chris@14: */ Chris@14: public function getMockForAbstractClass($originalClassName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) Chris@14: { Chris@14: if (!\is_string($originalClassName)) { Chris@14: throw InvalidArgumentHelper::factory(1, 'string'); Chris@14: } Chris@14: Chris@14: if (!\is_string($mockClassName)) { Chris@14: throw InvalidArgumentHelper::factory(3, 'string'); Chris@14: } Chris@14: Chris@14: if (\class_exists($originalClassName, $callAutoload) || Chris@14: \interface_exists($originalClassName, $callAutoload)) { Chris@14: $reflector = new ReflectionClass($originalClassName); Chris@14: $methods = $mockedMethods; Chris@14: Chris@14: foreach ($reflector->getMethods() as $method) { Chris@14: if ($method->isAbstract() && !\in_array($method->getName(), $methods)) { Chris@14: $methods[] = $method->getName(); Chris@14: } Chris@14: } Chris@14: Chris@14: if (empty($methods)) { Chris@14: $methods = null; Chris@14: } Chris@14: Chris@14: return $this->getMock( Chris@14: $originalClassName, Chris@14: $methods, Chris@14: $arguments, Chris@14: $mockClassName, Chris@14: $callOriginalConstructor, Chris@14: $callOriginalClone, Chris@14: $callAutoload, Chris@14: $cloneArguments Chris@14: ); Chris@14: } Chris@14: Chris@14: throw new RuntimeException( Chris@14: \sprintf('Class "%s" does not exist.', $originalClassName) Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns a mock object for the specified trait with all abstract methods Chris@14: * of the trait mocked. Concrete methods to mock can be specified with the Chris@14: * `$mockedMethods` parameter. Chris@14: * Chris@14: * @param string $traitName Chris@14: * @param array $arguments Chris@14: * @param string $mockClassName Chris@14: * @param bool $callOriginalConstructor Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * @param array $mockedMethods Chris@14: * @param bool $cloneArguments Chris@14: * Chris@14: * @return MockObject Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws RuntimeException Chris@14: * @throws Exception Chris@14: */ Chris@14: public function getMockForTrait($traitName, array $arguments = [], $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = [], $cloneArguments = true) Chris@14: { Chris@14: if (!\is_string($traitName)) { Chris@14: throw InvalidArgumentHelper::factory(1, 'string'); Chris@14: } Chris@14: Chris@14: if (!\is_string($mockClassName)) { Chris@14: throw InvalidArgumentHelper::factory(3, 'string'); Chris@14: } Chris@14: Chris@14: if (!\trait_exists($traitName, $callAutoload)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Trait "%s" does not exist.', Chris@14: $traitName Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: $className = $this->generateClassName( Chris@14: $traitName, Chris@14: '', Chris@14: 'Trait_' Chris@14: ); Chris@14: Chris@14: $classTemplate = $this->getTemplate('trait_class.tpl'); Chris@14: Chris@14: $classTemplate->setVar( Chris@14: [ Chris@14: 'prologue' => 'abstract ', Chris@14: 'class_name' => $className['className'], Chris@14: 'trait_name' => $traitName Chris@14: ] Chris@14: ); Chris@14: Chris@14: $this->evalClass( Chris@14: $classTemplate->render(), Chris@14: $className['className'] Chris@14: ); Chris@14: Chris@14: return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns an object for the specified trait. Chris@14: * Chris@14: * @param string $traitName Chris@14: * @param array $arguments Chris@14: * @param string $traitClassName Chris@14: * @param bool $callOriginalConstructor Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * Chris@14: * @return object Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws RuntimeException Chris@14: * @throws Exception Chris@14: */ Chris@14: public function getObjectForTrait($traitName, array $arguments = [], $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true) Chris@14: { Chris@14: if (!\is_string($traitName)) { Chris@14: throw InvalidArgumentHelper::factory(1, 'string'); Chris@14: } Chris@14: Chris@14: if (!\is_string($traitClassName)) { Chris@14: throw InvalidArgumentHelper::factory(3, 'string'); Chris@14: } Chris@14: Chris@14: if (!\trait_exists($traitName, $callAutoload)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Trait "%s" does not exist.', Chris@14: $traitName Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: $className = $this->generateClassName( Chris@14: $traitName, Chris@14: $traitClassName, Chris@14: 'Trait_' Chris@14: ); Chris@14: Chris@14: $classTemplate = $this->getTemplate('trait_class.tpl'); Chris@14: Chris@14: $classTemplate->setVar( Chris@14: [ Chris@14: 'prologue' => '', Chris@14: 'class_name' => $className['className'], Chris@14: 'trait_name' => $traitName Chris@14: ] Chris@14: ); Chris@14: Chris@14: return $this->getObject($classTemplate->render(), $className['className']); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array|string $type Chris@14: * @param array $methods Chris@14: * @param string $mockClassName Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * @param bool $cloneArguments Chris@14: * @param bool $callOriginalMethods Chris@14: * Chris@14: * @return array Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: * @throws \PHPUnit\Framework\MockObject\RuntimeException Chris@14: */ Chris@14: public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false) Chris@14: { Chris@14: if (\is_array($type)) { Chris@14: \sort($type); Chris@14: } Chris@14: Chris@14: if ($mockClassName === '') { Chris@14: $key = \md5( Chris@14: \is_array($type) ? \implode('_', $type) : $type . Chris@14: \serialize($methods) . Chris@14: \serialize($callOriginalClone) . Chris@14: \serialize($cloneArguments) . Chris@14: \serialize($callOriginalMethods) Chris@14: ); Chris@14: Chris@14: if (isset(self::$cache[$key])) { Chris@14: return self::$cache[$key]; Chris@14: } Chris@14: } Chris@14: Chris@14: $mock = $this->generateMock( Chris@14: $type, Chris@14: $methods, Chris@14: $mockClassName, Chris@14: $callOriginalClone, Chris@14: $callAutoload, Chris@14: $cloneArguments, Chris@14: $callOriginalMethods Chris@14: ); Chris@14: Chris@14: if (isset($key)) { Chris@14: self::$cache[$key] = $mock; Chris@14: } Chris@14: Chris@14: return $mock; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $wsdlFile Chris@14: * @param string $className Chris@14: * @param array $methods Chris@14: * @param array $options Chris@14: * Chris@14: * @return string Chris@14: * Chris@14: * @throws RuntimeException Chris@14: */ Chris@14: public function generateClassFromWsdl($wsdlFile, $className, array $methods = [], array $options = []) Chris@14: { Chris@14: if (!\extension_loaded('soap')) { Chris@14: throw new RuntimeException( Chris@14: 'The SOAP extension is required to generate a mock object from WSDL.' Chris@14: ); Chris@14: } Chris@14: Chris@14: $options = \array_merge($options, ['cache_wsdl' => WSDL_CACHE_NONE]); Chris@14: $client = new SoapClient($wsdlFile, $options); Chris@14: $_methods = \array_unique($client->__getFunctions()); Chris@14: unset($client); Chris@14: Chris@14: \sort($_methods); Chris@14: Chris@14: $methodTemplate = $this->getTemplate('wsdl_method.tpl'); Chris@14: $methodsBuffer = ''; Chris@14: Chris@14: foreach ($_methods as $method) { Chris@14: $nameStart = \strpos($method, ' ') + 1; Chris@14: $nameEnd = \strpos($method, '('); Chris@14: $name = \substr($method, $nameStart, $nameEnd - $nameStart); Chris@14: Chris@14: if (empty($methods) || \in_array($name, $methods)) { Chris@14: $args = \explode( Chris@14: ',', Chris@14: \substr( Chris@14: $method, Chris@14: $nameEnd + 1, Chris@14: \strpos($method, ')') - $nameEnd - 1 Chris@14: ) Chris@14: ); Chris@14: Chris@14: foreach (\range(0, \count($args) - 1) as $i) { Chris@14: $args[$i] = \substr($args[$i], \strpos($args[$i], '$')); Chris@14: } Chris@14: Chris@14: $methodTemplate->setVar( Chris@14: [ Chris@14: 'method_name' => $name, Chris@14: 'arguments' => \implode(', ', $args) Chris@14: ] Chris@14: ); Chris@14: Chris@14: $methodsBuffer .= $methodTemplate->render(); Chris@14: } Chris@14: } Chris@14: Chris@14: $optionsBuffer = 'array('; Chris@14: Chris@14: foreach ($options as $key => $value) { Chris@14: $optionsBuffer .= $key . ' => ' . $value; Chris@14: } Chris@14: Chris@14: $optionsBuffer .= ')'; Chris@14: Chris@14: $classTemplate = $this->getTemplate('wsdl_class.tpl'); Chris@14: $namespace = ''; Chris@14: Chris@14: if (\strpos($className, '\\') !== false) { Chris@14: $parts = \explode('\\', $className); Chris@14: $className = \array_pop($parts); Chris@14: $namespace = 'namespace ' . \implode('\\', $parts) . ';' . "\n\n"; Chris@14: } Chris@14: Chris@14: $classTemplate->setVar( Chris@14: [ Chris@14: 'namespace' => $namespace, Chris@14: 'class_name' => $className, Chris@14: 'wsdl' => $wsdlFile, Chris@14: 'options' => $optionsBuffer, Chris@14: 'methods' => $methodsBuffer Chris@14: ] Chris@14: ); Chris@14: Chris@14: return $classTemplate->render(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array|string $type Chris@14: * @param array|null $methods Chris@14: * @param string $mockClassName Chris@14: * @param bool $callOriginalClone Chris@14: * @param bool $callAutoload Chris@14: * @param bool $cloneArguments Chris@14: * @param bool $callOriginalMethods Chris@14: * Chris@14: * @return array Chris@14: * Chris@14: * @throws \InvalidArgumentException Chris@14: * @throws \ReflectionException Chris@14: * @throws RuntimeException Chris@14: */ Chris@14: private function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods) Chris@14: { Chris@14: $methodReflections = []; Chris@14: $classTemplate = $this->getTemplate('mocked_class.tpl'); Chris@14: Chris@14: $additionalInterfaces = []; Chris@14: $cloneTemplate = ''; Chris@14: $isClass = false; Chris@14: $isInterface = false; Chris@14: $isMultipleInterfaces = false; Chris@14: Chris@14: if (\is_array($type)) { Chris@14: foreach ($type as $_type) { Chris@14: if (!\interface_exists($_type, $callAutoload)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Interface "%s" does not exist.', Chris@14: $_type Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: $isMultipleInterfaces = true; Chris@14: Chris@14: $additionalInterfaces[] = $_type; Chris@14: $typeClass = new ReflectionClass($this->generateClassName( Chris@14: $_type, Chris@14: $mockClassName, Chris@14: 'Mock_' Chris@14: )['fullClassName'] Chris@14: ); Chris@14: Chris@14: foreach ($this->getClassMethods($_type) as $method) { Chris@14: if (\in_array($method, $methods)) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Duplicate method "%s" not allowed.', Chris@14: $method Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: $methodReflections[$method] = $typeClass->getMethod($method); Chris@14: $methods[] = $method; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: $mockClassName = $this->generateClassName( Chris@14: $type, Chris@14: $mockClassName, Chris@14: 'Mock_' Chris@14: ); Chris@14: Chris@14: if (\class_exists($mockClassName['fullClassName'], $callAutoload)) { Chris@14: $isClass = true; Chris@14: } elseif (\interface_exists($mockClassName['fullClassName'], $callAutoload)) { Chris@14: $isInterface = true; Chris@14: } Chris@14: Chris@14: if (!$isClass && !$isInterface) { Chris@14: $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n"; Chris@14: Chris@14: if (!empty($mockClassName['namespaceName'])) { Chris@14: $prologue = 'namespace ' . $mockClassName['namespaceName'] . Chris@14: " {\n\n" . $prologue . "}\n\n" . Chris@14: "namespace {\n\n"; Chris@14: Chris@14: $epilogue = "\n\n}"; Chris@14: } Chris@14: Chris@14: $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); Chris@14: } else { Chris@14: $class = new ReflectionClass($mockClassName['fullClassName']); Chris@14: Chris@14: if ($class->isFinal()) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Class "%s" is declared "final" and cannot be mocked.', Chris@14: $mockClassName['fullClassName'] Chris@14: ) Chris@14: ); Chris@14: } Chris@14: Chris@14: if ($class->hasMethod('__clone')) { Chris@14: $cloneMethod = $class->getMethod('__clone'); Chris@14: Chris@14: if (!$cloneMethod->isFinal()) { Chris@14: if ($callOriginalClone && !$isInterface) { Chris@14: $cloneTemplate = $this->getTemplate('unmocked_clone.tpl'); Chris@14: } else { Chris@14: $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); Chris@14: } Chris@14: } Chris@14: } else { Chris@14: $cloneTemplate = $this->getTemplate('mocked_clone.tpl'); Chris@14: } Chris@14: } Chris@14: Chris@14: if (\is_object($cloneTemplate)) { Chris@14: $cloneTemplate = $cloneTemplate->render(); Chris@14: } Chris@14: Chris@14: if (\is_array($methods) && empty($methods) && Chris@14: ($isClass || $isInterface)) { Chris@14: $methods = $this->getClassMethods($mockClassName['fullClassName']); Chris@14: } Chris@14: Chris@14: if (!\is_array($methods)) { Chris@14: $methods = []; Chris@14: } Chris@14: Chris@14: $mockedMethods = ''; Chris@14: $configurable = []; Chris@14: Chris@14: foreach ($methods as $methodName) { Chris@14: if ($methodName !== '__construct' && $methodName !== '__clone') { Chris@14: $configurable[] = \strtolower($methodName); Chris@14: } Chris@14: } Chris@14: Chris@14: if (isset($class)) { Chris@14: // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 Chris@14: if ($isInterface && $class->implementsInterface(Traversable::class) && Chris@14: !$class->implementsInterface(Iterator::class) && Chris@14: !$class->implementsInterface(IteratorAggregate::class)) { Chris@14: $additionalInterfaces[] = Iterator::class; Chris@14: $methods = \array_merge($methods, $this->getClassMethods(Iterator::class)); Chris@14: } Chris@14: Chris@14: foreach ($methods as $methodName) { Chris@14: try { Chris@14: $method = $class->getMethod($methodName); Chris@14: Chris@14: if ($this->canMockMethod($method)) { Chris@14: $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( Chris@14: $method, Chris@14: $cloneArguments, Chris@14: $callOriginalMethods Chris@14: ); Chris@14: } Chris@14: } catch (ReflectionException $e) { Chris@14: $mockedMethods .= $this->generateMockedMethodDefinition( Chris@14: $mockClassName['fullClassName'], Chris@14: $methodName, Chris@14: $cloneArguments Chris@14: ); Chris@14: } Chris@14: } Chris@14: } elseif ($isMultipleInterfaces) { Chris@14: foreach ($methods as $methodName) { Chris@14: if ($this->canMockMethod($methodReflections[$methodName])) { Chris@14: $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( Chris@14: $methodReflections[$methodName], Chris@14: $cloneArguments, Chris@14: $callOriginalMethods Chris@14: ); Chris@14: } Chris@14: } Chris@14: } else { Chris@14: foreach ($methods as $methodName) { Chris@14: $mockedMethods .= $this->generateMockedMethodDefinition( Chris@14: $mockClassName['fullClassName'], Chris@14: $methodName, Chris@14: $cloneArguments Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: $method = ''; Chris@14: Chris@14: if (!\in_array('method', $methods) && (!isset($class) || !$class->hasMethod('method'))) { Chris@14: $methodTemplate = $this->getTemplate('mocked_class_method.tpl'); Chris@14: Chris@14: $method = $methodTemplate->render(); Chris@14: } Chris@14: Chris@14: $classTemplate->setVar( Chris@14: [ Chris@14: 'prologue' => $prologue ?? '', Chris@14: 'epilogue' => $epilogue ?? '', Chris@14: 'class_declaration' => $this->generateMockClassDeclaration( Chris@14: $mockClassName, Chris@14: $isInterface, Chris@14: $additionalInterfaces Chris@14: ), Chris@14: 'clone' => $cloneTemplate, Chris@14: 'mock_class_name' => $mockClassName['className'], Chris@14: 'mocked_methods' => $mockedMethods, Chris@14: 'method' => $method, Chris@14: 'configurable' => '[' . \implode(', ', \array_map(function ($m) { Chris@14: return '\'' . $m . '\''; Chris@14: }, $configurable)) . ']' Chris@14: ] Chris@14: ); Chris@14: Chris@14: return [ Chris@14: 'code' => $classTemplate->render(), Chris@14: 'mockClassName' => $mockClassName['className'] Chris@14: ]; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array|string $type Chris@14: * @param string $className Chris@14: * @param string $prefix Chris@14: * Chris@14: * @return array Chris@14: */ Chris@14: private function generateClassName($type, $className, $prefix) Chris@14: { Chris@14: if (\is_array($type)) { Chris@14: $type = \implode('_', $type); Chris@14: } Chris@14: Chris@14: if ($type[0] === '\\') { Chris@14: $type = \substr($type, 1); Chris@14: } Chris@14: Chris@14: $classNameParts = \explode('\\', $type); Chris@14: Chris@14: if (\count($classNameParts) > 1) { Chris@14: $type = \array_pop($classNameParts); Chris@14: $namespaceName = \implode('\\', $classNameParts); Chris@14: $fullClassName = $namespaceName . '\\' . $type; Chris@14: } else { Chris@14: $namespaceName = ''; Chris@14: $fullClassName = $type; Chris@14: } Chris@14: Chris@14: if ($className === '') { Chris@14: do { Chris@14: $className = $prefix . $type . '_' . Chris@14: \substr(\md5(\mt_rand()), 0, 8); Chris@14: } while (\class_exists($className, false)); Chris@14: } Chris@14: Chris@14: return [ Chris@14: 'className' => $className, Chris@14: 'originalClassName' => $type, Chris@14: 'fullClassName' => $fullClassName, Chris@14: 'namespaceName' => $namespaceName Chris@14: ]; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param array $mockClassName Chris@14: * @param bool $isInterface Chris@14: * @param array $additionalInterfaces Chris@14: * Chris@14: * @return string Chris@14: */ Chris@14: private function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = []) Chris@14: { Chris@14: $buffer = 'class '; Chris@14: Chris@14: $additionalInterfaces[] = MockObject::class; Chris@14: $interfaces = \implode(', ', $additionalInterfaces); Chris@14: Chris@14: if ($isInterface) { Chris@14: $buffer .= \sprintf( Chris@14: '%s implements %s', Chris@14: $mockClassName['className'], Chris@14: $interfaces Chris@14: ); Chris@14: Chris@14: if (!\in_array($mockClassName['originalClassName'], $additionalInterfaces)) { Chris@14: $buffer .= ', '; Chris@14: Chris@14: if (!empty($mockClassName['namespaceName'])) { Chris@14: $buffer .= $mockClassName['namespaceName'] . '\\'; Chris@14: } Chris@14: Chris@14: $buffer .= $mockClassName['originalClassName']; Chris@14: } Chris@14: } else { Chris@14: $buffer .= \sprintf( Chris@14: '%s extends %s%s implements %s', Chris@14: $mockClassName['className'], Chris@14: !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '', Chris@14: $mockClassName['originalClassName'], Chris@14: $interfaces Chris@14: ); Chris@14: } Chris@14: Chris@14: return $buffer; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param ReflectionMethod $method Chris@14: * @param bool $cloneArguments Chris@14: * @param bool $callOriginalMethods Chris@14: * Chris@14: * @return string Chris@14: * Chris@14: * @throws \PHPUnit\Framework\MockObject\RuntimeException Chris@14: */ Chris@14: private function generateMockedMethodDefinitionFromExisting(ReflectionMethod $method, $cloneArguments, $callOriginalMethods) Chris@14: { Chris@14: if ($method->isPrivate()) { Chris@14: $modifier = 'private'; Chris@14: } elseif ($method->isProtected()) { Chris@14: $modifier = 'protected'; Chris@14: } else { Chris@14: $modifier = 'public'; Chris@14: } Chris@14: Chris@14: if ($method->isStatic()) { Chris@14: $modifier .= ' static'; Chris@14: } Chris@14: Chris@14: if ($method->returnsReference()) { Chris@14: $reference = '&'; Chris@14: } else { Chris@14: $reference = ''; Chris@14: } Chris@14: Chris@14: if ($method->hasReturnType()) { Chris@14: $returnType = (string) $method->getReturnType(); Chris@14: } else { Chris@14: $returnType = ''; Chris@14: } Chris@14: Chris@14: if (\preg_match('#\*[ \t]*+@deprecated[ \t]*+(.*?)\r?+\n[ \t]*+\*(?:[ \t]*+@|/$)#s', $method->getDocComment(), $deprecation)) { Chris@14: $deprecation = \trim(\preg_replace('#[ \t]*\r?\n[ \t]*+\*[ \t]*+#', ' ', $deprecation[1])); Chris@14: } else { Chris@14: $deprecation = false; Chris@14: } Chris@14: Chris@14: return $this->generateMockedMethodDefinition( Chris@14: $method->getDeclaringClass()->getName(), Chris@14: $method->getName(), Chris@14: $cloneArguments, Chris@14: $modifier, Chris@14: $this->getMethodParameters($method), Chris@14: $this->getMethodParameters($method, true), Chris@14: $returnType, Chris@14: $reference, Chris@14: $callOriginalMethods, Chris@14: $method->isStatic(), Chris@14: $deprecation, Chris@14: $method->hasReturnType() && PHP_VERSION_ID >= 70100 && $method->getReturnType()->allowsNull() Chris@14: ); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $className Chris@14: * @param string $methodName Chris@14: * @param bool $cloneArguments Chris@14: * @param string $modifier Chris@14: * @param string $argumentsForDeclaration Chris@14: * @param string $argumentsForCall Chris@14: * @param string $returnType Chris@14: * @param string $reference Chris@14: * @param bool $callOriginalMethods Chris@14: * @param bool $static Chris@14: * @param bool|string $deprecation Chris@14: * @param bool $allowsReturnNull Chris@14: * Chris@14: * @return string Chris@14: * Chris@14: * @throws \InvalidArgumentException Chris@14: */ Chris@14: private function generateMockedMethodDefinition($className, $methodName, $cloneArguments = true, $modifier = 'public', $argumentsForDeclaration = '', $argumentsForCall = '', $returnType = '', $reference = '', $callOriginalMethods = false, $static = false, $deprecation = false, $allowsReturnNull = false) Chris@14: { Chris@14: if ($static) { Chris@14: $templateFile = 'mocked_static_method.tpl'; Chris@14: } else { Chris@14: if ($returnType === 'void') { Chris@14: $templateFile = \sprintf( Chris@14: '%s_method_void.tpl', Chris@14: $callOriginalMethods ? 'proxied' : 'mocked' Chris@14: ); Chris@14: } else { Chris@14: $templateFile = \sprintf( Chris@14: '%s_method.tpl', Chris@14: $callOriginalMethods ? 'proxied' : 'mocked' Chris@14: ); Chris@14: } Chris@14: } Chris@14: Chris@14: // Mocked interfaces returning 'self' must explicitly declare the Chris@14: // interface name as the return type. See Chris@14: // https://bugs.php.net/bug.php?id=70722 Chris@14: if ($returnType === 'self') { Chris@14: $returnType = $className; Chris@14: } Chris@14: Chris@14: if (false !== $deprecation) { Chris@14: $deprecation = "The $className::$methodName method is deprecated ($deprecation)."; Chris@14: $deprecationTemplate = $this->getTemplate('deprecation.tpl'); Chris@14: Chris@14: $deprecationTemplate->setVar( Chris@14: [ Chris@14: 'deprecation' => \var_export($deprecation, true), Chris@14: ] Chris@14: ); Chris@14: Chris@14: $deprecation = $deprecationTemplate->render(); Chris@14: } Chris@14: Chris@14: $template = $this->getTemplate($templateFile); Chris@14: Chris@14: $template->setVar( Chris@14: [ Chris@14: 'arguments_decl' => $argumentsForDeclaration, Chris@14: 'arguments_call' => $argumentsForCall, Chris@14: 'return_delim' => $returnType ? ': ' : '', Chris@14: 'return_type' => $allowsReturnNull ? '?' . $returnType : $returnType, Chris@14: 'arguments_count' => !empty($argumentsForCall) ? \substr_count($argumentsForCall, ',') + 1 : 0, Chris@14: 'class_name' => $className, Chris@14: 'method_name' => $methodName, Chris@14: 'modifier' => $modifier, Chris@14: 'reference' => $reference, Chris@14: 'clone_arguments' => $cloneArguments ? 'true' : 'false', Chris@14: 'deprecation' => $deprecation Chris@14: ] Chris@14: ); Chris@14: Chris@14: return $template->render(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param ReflectionMethod $method Chris@14: * Chris@14: * @return bool Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: */ Chris@14: private function canMockMethod(ReflectionMethod $method) Chris@14: { Chris@14: return !($method->isConstructor() || $method->isFinal() || $method->isPrivate() || $this->isMethodNameBlacklisted($method->getName())); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns whether a method name is blacklisted Chris@14: * Chris@14: * @param string $name Chris@14: * Chris@14: * @return bool Chris@14: */ Chris@14: private function isMethodNameBlacklisted($name) Chris@14: { Chris@14: return isset($this->blacklistedMethodNames[$name]); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Returns the parameters of a function or method. Chris@14: * Chris@14: * @param ReflectionMethod $method Chris@14: * @param bool $forCall Chris@14: * Chris@14: * @return string Chris@14: * Chris@14: * @throws RuntimeException Chris@14: */ Chris@14: private function getMethodParameters(ReflectionMethod $method, $forCall = false) Chris@14: { Chris@14: $parameters = []; Chris@14: Chris@14: foreach ($method->getParameters() as $i => $parameter) { Chris@14: $name = '$' . $parameter->getName(); Chris@14: Chris@14: /* Note: PHP extensions may use empty names for reference arguments Chris@14: * or "..." for methods taking a variable number of arguments. Chris@14: */ Chris@14: if ($name === '$' || $name === '$...') { Chris@14: $name = '$arg' . $i; Chris@14: } Chris@14: Chris@14: if ($parameter->isVariadic()) { Chris@14: if ($forCall) { Chris@14: continue; Chris@14: } Chris@14: Chris@14: $name = '...' . $name; Chris@14: } Chris@14: Chris@14: $nullable = ''; Chris@14: $default = ''; Chris@14: $reference = ''; Chris@14: $typeDeclaration = ''; Chris@14: Chris@14: if (!$forCall) { Chris@14: if (PHP_VERSION_ID >= 70100 && $parameter->hasType() && $parameter->allowsNull()) { Chris@14: $nullable = '?'; Chris@14: } Chris@14: Chris@14: if ($parameter->hasType() && (string) $parameter->getType() !== 'self') { Chris@14: $typeDeclaration = (string) $parameter->getType() . ' '; Chris@14: } elseif ($parameter->isArray()) { Chris@14: $typeDeclaration = 'array '; Chris@14: } elseif ($parameter->isCallable()) { Chris@14: $typeDeclaration = 'callable '; Chris@14: } else { Chris@14: try { Chris@14: $class = $parameter->getClass(); Chris@14: } catch (ReflectionException $e) { Chris@14: throw new RuntimeException( Chris@14: \sprintf( Chris@14: 'Cannot mock %s::%s() because a class or ' . Chris@14: 'interface used in the signature is not loaded', Chris@14: $method->getDeclaringClass()->getName(), Chris@14: $method->getName() Chris@14: ), Chris@14: 0, Chris@14: $e Chris@14: ); Chris@14: } Chris@14: Chris@14: if ($class !== null) { Chris@14: $typeDeclaration = $class->getName() . ' '; Chris@14: } Chris@14: } Chris@14: Chris@14: if (!$parameter->isVariadic()) { Chris@14: if ($parameter->isDefaultValueAvailable()) { Chris@16: $value = $parameter->getDefaultValueConstantName(); Chris@16: Chris@16: if ($value === null) { Chris@16: $value = \var_export($parameter->getDefaultValue(), true); Chris@17: } elseif (!\defined($value)) { Chris@17: $rootValue = \preg_replace('/^.*\\\\/', '', $value); Chris@17: $value = \defined($rootValue) ? $rootValue : $value; Chris@16: } Chris@16: Chris@16: $default = ' = ' . $value; Chris@14: } elseif ($parameter->isOptional()) { Chris@14: $default = ' = null'; Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: if ($parameter->isPassedByReference()) { Chris@14: $reference = '&'; Chris@14: } Chris@14: Chris@14: $parameters[] = $nullable . $typeDeclaration . $reference . $name . $default; Chris@14: } Chris@14: Chris@14: return \implode(', ', $parameters); Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $className Chris@14: * Chris@14: * @return array Chris@14: * Chris@14: * @throws \ReflectionException Chris@14: */ Chris@14: public function getClassMethods($className) Chris@14: { Chris@14: $class = new ReflectionClass($className); Chris@14: $methods = []; Chris@14: Chris@14: foreach ($class->getMethods() as $method) { Chris@14: if ($method->isPublic() || $method->isAbstract()) { Chris@14: $methods[] = $method->getName(); Chris@14: } Chris@14: } Chris@14: Chris@14: return $methods; Chris@14: } Chris@14: Chris@14: /** Chris@14: * @param string $template Chris@14: * Chris@14: * @return Text_Template Chris@14: * Chris@14: * @throws \InvalidArgumentException Chris@14: */ Chris@14: private function getTemplate($template) Chris@14: { Chris@14: $filename = __DIR__ . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR . $template; Chris@14: Chris@14: if (!isset(self::$templates[$filename])) { Chris@14: self::$templates[$filename] = new Text_Template($filename); Chris@14: } Chris@14: Chris@14: return self::$templates[$filename]; Chris@14: } Chris@14: }