Viewing file: MockConfiguration.php (18.86 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * Mockery (https://docs.mockery.io/) * * @copyright https://github.com/mockery/mockery/blob/HEAD/COPYRIGHT.md * @license https://github.com/mockery/mockery/blob/HEAD/LICENSE BSD 3-Clause License * @link https://github.com/mockery/mockery for the canonical source repository */
namespace Mockery\Generator;
use Mockery\Exception; use Serializable; use function array_filter; use function array_keys; use function array_map; use function array_merge; use function array_pop; use function array_unique; use function array_values; use function class_alias; use function class_exists; use function explode; use function get_class; use function implode; use function in_array; use function interface_exists; use function is_object; use function md5; use function preg_match; use function serialize; use function strpos; use function strtolower; use function trait_exists;
/** * This class describes the configuration of mocks and hides away some of the * reflection implementation */ class MockConfiguration { /** * Instance cache of all methods * * @var list<Method> */ protected $allMethods = [];
/** * Methods that should specifically not be mocked * * This is currently populated with stuff we don't know how to deal with, should really be somewhere else */ protected $blackListedMethods = [];
protected $constantsMap = [];
/** * An instance mock is where we override the original class before it's autoloaded * * @var bool */ protected $instanceMock = false;
/** * If true, overrides original class destructor * * @var bool */ protected $mockOriginalDestructor = false;
/** * The class name we'd like to use for a generated mock * * @var string|null */ protected $name;
/** * Param overrides * * @var array<string,mixed> */ protected $parameterOverrides = [];
/** * A class that we'd like to mock * @var TargetClassInterface|null */ protected $targetClass;
/** * @var class-string|null */ protected $targetClassName;
/** * @var array<class-string> */ protected $targetInterfaceNames = [];
/** * A number of interfaces we'd like to mock, keyed by name to attempt to keep unique * * @var array<TargetClassInterface> */ protected $targetInterfaces = [];
/** * An object we'd like our mock to proxy to * * @var object|null */ protected $targetObject;
/** * @var array<string> */ protected $targetTraitNames = [];
/** * A number of traits we'd like to mock, keyed by name to attempt to keep unique * * @var array<string,DefinedTargetClass> */ protected $targetTraits = [];
/** * If not empty, only these methods will be mocked * * @var array<string> */ protected $whiteListedMethods = [];
/** * @param array<class-string|object> $targets * @param array<string> $blackListedMethods * @param array<string> $whiteListedMethods * @param string|null $name * @param bool $instanceMock * @param array<string,mixed> $parameterOverrides * @param bool $mockOriginalDestructor * @param array<string,array<scalar>|scalar> $constantsMap */ public function __construct( array $targets = [], array $blackListedMethods = [], array $whiteListedMethods = [], $name = null, $instanceMock = false, array $parameterOverrides = [], $mockOriginalDestructor = false, array $constantsMap = [] ) { $this->addTargets($targets); $this->blackListedMethods = $blackListedMethods; $this->whiteListedMethods = $whiteListedMethods; $this->name = $name; $this->instanceMock = $instanceMock; $this->parameterOverrides = $parameterOverrides; $this->mockOriginalDestructor = $mockOriginalDestructor; $this->constantsMap = $constantsMap; }
/** * Generate a suitable name based on the config * * @return string */ public function generateName() { $nameBuilder = new MockNameBuilder();
$targetObject = $this->getTargetObject(); if ($targetObject !== null) { $className = get_class($targetObject);
$nameBuilder->addPart(strpos($className, '@') !== false ? md5($className) : $className); }
$targetClass = $this->getTargetClass(); if ($targetClass instanceof TargetClassInterface) { $className = $targetClass->getName();
$nameBuilder->addPart(strpos($className, '@') !== false ? md5($className) : $className); }
foreach ($this->getTargetInterfaces() as $targetInterface) { $nameBuilder->addPart($targetInterface->getName()); }
return $nameBuilder->build(); }
/** * @return array<string> */ public function getBlackListedMethods() { return $this->blackListedMethods; }
/** * @return array<string,scalar|array<scalar>> */ public function getConstantsMap() { return $this->constantsMap; }
/** * Attempt to create a hash of the configuration, in order to allow caching * * @TODO workout if this will work * * @return string */ public function getHash() { $vars = [ 'targetClassName' => $this->targetClassName, 'targetInterfaceNames' => $this->targetInterfaceNames, 'targetTraitNames' => $this->targetTraitNames, 'name' => $this->name, 'blackListedMethods' => $this->blackListedMethods, 'whiteListedMethod' => $this->whiteListedMethods, 'instanceMock' => $this->instanceMock, 'parameterOverrides' => $this->parameterOverrides, 'mockOriginalDestructor' => $this->mockOriginalDestructor, ];
return md5(serialize($vars)); }
/** * Gets a list of methods from the classes, interfaces and objects and filters them appropriately. * Lot's of filtering going on, perhaps we could have filter classes to iterate through * * @return list<Method> */ public function getMethodsToMock() { $methods = $this->getAllMethods();
foreach ($methods as $key => $method) { if ($method->isFinal()) { unset($methods[$key]); } }
/** * Whitelist trumps everything else */ $whiteListedMethods = $this->getWhiteListedMethods(); if ($whiteListedMethods !== []) { $whitelist = array_map('strtolower', $whiteListedMethods);
return array_filter($methods, static function ($method) use ($whitelist) { if ($method->isAbstract()) { return true; }
return in_array(strtolower($method->getName()), $whitelist, true); }); }
/** * Remove blacklisted methods */ $blackListedMethods = $this->getBlackListedMethods(); if ($blackListedMethods !== []) { $blacklist = array_map('strtolower', $blackListedMethods);
$methods = array_filter($methods, static function ($method) use ($blacklist) { return ! in_array(strtolower($method->getName()), $blacklist, true); }); }
/** * Internal objects can not be instantiated with newInstanceArgs and if * they implement Serializable, unserialize will have to be called. As * such, we can't mock it and will need a pass to add a dummy * implementation */ $targetClass = $this->getTargetClass();
if ( $targetClass !== null && $targetClass->implementsInterface(Serializable::class) && $targetClass->hasInternalAncestor() ) { $methods = array_filter($methods, static function ($method) { return $method->getName() !== 'unserialize'; }); }
return array_values($methods); }
/** * @return string|null */ public function getName() { return $this->name; }
/** * @return string */ public function getNamespaceName() { $parts = explode('\\', $this->getName()); array_pop($parts);
if ($parts !== []) { return implode('\\', $parts); }
return ''; }
/** * @return array<string,mixed> */ public function getParameterOverrides() { return $this->parameterOverrides; }
/** * @return string */ public function getShortName() { $parts = explode('\\', $this->getName()); return array_pop($parts); }
/** * @return null|TargetClassInterface */ public function getTargetClass() { if ($this->targetClass) { return $this->targetClass; }
if (! $this->targetClassName) { return null; }
if (class_exists($this->targetClassName)) { $alias = null; if (strpos($this->targetClassName, '@') !== false) { $alias = (new MockNameBuilder()) ->addPart('anonymous_class') ->addPart(md5($this->targetClassName)) ->build(); class_alias($this->targetClassName, $alias); }
$dtc = DefinedTargetClass::factory($this->targetClassName, $alias);
if ($this->getTargetObject() === null && $dtc->isFinal()) { throw new Exception( 'The class ' . $this->targetClassName . ' is marked final and its methods' . ' cannot be replaced. Classes marked final can be passed in' . ' to \Mockery::mock() as instantiated objects to create a' . ' partial mock, but only if the mock is not subject to type' . ' hinting checks.' ); }
$this->targetClass = $dtc; } else { $this->targetClass = UndefinedTargetClass::factory($this->targetClassName); }
return $this->targetClass; }
/** * @return class-string|null */ public function getTargetClassName() { return $this->targetClassName; }
/** * @return list<TargetClassInterface> */ public function getTargetInterfaces() { if ($this->targetInterfaces !== []) { return $this->targetInterfaces; }
foreach ($this->targetInterfaceNames as $targetInterface) { if (! interface_exists($targetInterface)) { $this->targetInterfaces[] = UndefinedTargetClass::factory($targetInterface); continue; }
$dtc = DefinedTargetClass::factory($targetInterface); $extendedInterfaces = array_keys($dtc->getInterfaces()); $extendedInterfaces[] = $targetInterface;
$traversableFound = false; $iteratorShiftedToFront = false; foreach ($extendedInterfaces as $interface) { if (! $traversableFound && preg_match('/^\\?Iterator(|Aggregate)$/i', $interface)) { break; }
if (preg_match('/^\\\\?IteratorAggregate$/i', $interface)) { $this->targetInterfaces[] = DefinedTargetClass::factory('\\IteratorAggregate'); $iteratorShiftedToFront = true;
continue; }
if (preg_match('/^\\\\?Iterator$/i', $interface)) { $this->targetInterfaces[] = DefinedTargetClass::factory('\\Iterator'); $iteratorShiftedToFront = true;
continue; }
if (preg_match('/^\\\\?Traversable$/i', $interface)) { $traversableFound = true; } }
if ($traversableFound && ! $iteratorShiftedToFront) { $this->targetInterfaces[] = DefinedTargetClass::factory('\\IteratorAggregate'); }
/** * We never straight up implement Traversable */ $isTraversable = preg_match('/^\\\\?Traversable$/i', $targetInterface); if ($isTraversable === 0 || $isTraversable === false) { $this->targetInterfaces[] = $dtc; } }
return $this->targetInterfaces = array_unique($this->targetInterfaces); }
/** * @return object|null */ public function getTargetObject() { return $this->targetObject; }
/** * @return list<TargetClassInterface> */ public function getTargetTraits() { if ($this->targetTraits !== []) { return $this->targetTraits; }
foreach ($this->targetTraitNames as $targetTrait) { $this->targetTraits[] = DefinedTargetClass::factory($targetTrait); }
$this->targetTraits = array_unique($this->targetTraits); // just in case return $this->targetTraits; }
/** * @return array<string> */ public function getWhiteListedMethods() { return $this->whiteListedMethods; }
/** * @return bool */ public function isInstanceMock() { return $this->instanceMock; }
/** * @return bool */ public function isMockOriginalDestructor() { return $this->mockOriginalDestructor; }
/** * @param class-string $className * @return self */ public function rename($className) { $targets = [];
if ($this->targetClassName) { $targets[] = $this->targetClassName; }
if ($this->targetInterfaceNames) { $targets = array_merge($targets, $this->targetInterfaceNames); }
if ($this->targetTraitNames) { $targets = array_merge($targets, $this->targetTraitNames); }
if ($this->targetObject) { $targets[] = $this->targetObject; }
return new self( $targets, $this->blackListedMethods, $this->whiteListedMethods, $className, $this->instanceMock, $this->parameterOverrides, $this->mockOriginalDestructor, $this->constantsMap ); }
/** * We declare the __callStatic method to handle undefined stuff, if the class * we're mocking has also defined it, we need to comply with their interface * * @return bool */ public function requiresCallStaticTypeHintRemoval() { foreach ($this->getAllMethods() as $method) { if ($method->getName() === '__callStatic') { $params = $method->getParameters();
if (! array_key_exists(1, $params)) { return false; }
return ! $params[1]->isArray(); } }
return false; }
/** * We declare the __call method to handle undefined stuff, if the class * we're mocking has also defined it, we need to comply with their interface * * @return bool */ public function requiresCallTypeHintRemoval() { foreach ($this->getAllMethods() as $method) { if ($method->getName() === '__call') { $params = $method->getParameters(); return ! $params[1]->isArray(); } }
return false; }
/** * @param class-string|object $target */ protected function addTarget($target) { if (is_object($target)) { $this->setTargetObject($target); $this->setTargetClassName(get_class($target)); return; }
if ($target[0] !== '\\') { $target = '\\' . $target; }
if (class_exists($target)) { $this->setTargetClassName($target); return; }
if (interface_exists($target)) { $this->addTargetInterfaceName($target); return; }
if (trait_exists($target)) { $this->addTargetTraitName($target); return; }
/** * Default is to set as class, or interface if class already set * * Don't like this condition, can't remember what the default * targetClass is for */ if ($this->getTargetClassName()) { $this->addTargetInterfaceName($target); return; }
$this->setTargetClassName($target); }
/** * If we attempt to implement Traversable, * we must ensure we are also implementing either Iterator or IteratorAggregate, * and that whichever one it is comes before Traversable in the list of implements. * * @param class-string $targetInterface */ protected function addTargetInterfaceName($targetInterface) { $this->targetInterfaceNames[] = $targetInterface; }
/** * @param array<class-string> $interfaces */ protected function addTargets($interfaces) { foreach ($interfaces as $interface) { $this->addTarget($interface); } }
/** * @param class-string $targetTraitName */ protected function addTargetTraitName($targetTraitName) { $this->targetTraitNames[] = $targetTraitName; }
/** * @return list<Method> */ protected function getAllMethods() { if ($this->allMethods) { return $this->allMethods; }
$classes = $this->getTargetInterfaces();
if ($this->getTargetClass()) { $classes[] = $this->getTargetClass(); }
$methods = []; foreach ($classes as $class) { $methods = array_merge($methods, $class->getMethods()); }
foreach ($this->getTargetTraits() as $trait) { foreach ($trait->getMethods() as $method) { if ($method->isAbstract()) { $methods[] = $method; } } }
$names = []; $methods = array_filter($methods, static function ($method) use (&$names) { if (in_array($method->getName(), $names, true)) { return false; }
$names[] = $method->getName(); return true; });
return $this->allMethods = $methods; }
/** * @param class-string $targetClassName */ protected function setTargetClassName($targetClassName) { $this->targetClassName = $targetClassName; }
/** * @param object $object */ protected function setTargetObject($object) { $this->targetObject = $object; } }
|