Blame | Letzte Änderung | Log anzeigen | RSS feed
<?php/*** Mockery** LICENSE** This source file is subject to the new BSD license that is bundled* with this package in the file LICENSE.txt.* It is also available through the world-wide-web at this URL:* http://github.com/padraic/mockery/blob/master/LICENSE* If you did not receive a copy of the license and are unable to* obtain it through the world-wide-web, please send an email* to padraic@php.net so we can send you a copy immediately.** @category Mockery* @package Mockery* @copyright Copyright (c) 2010 Pádraic Brady (http://blog.astrumfutura.com)* @license http://github.com/padraic/mockery/blob/master/LICENSE New BSD License*/namespace Mockery;use Closure;use Mockery\Matcher\NoArgs;use Mockery\Matcher\AnyArgs;use Mockery\Matcher\AndAnyOtherArgs;use Mockery\Matcher\ArgumentListMatcher;use Mockery\Matcher\MultiArgumentClosure;class Expectation implements ExpectationInterface{/*** Mock object to which this expectation belongs** @var \Mockery\LegacyMockInterface*/protected $_mock = null;/*** Method name** @var string*/protected $_name = null;/*** Exception message** @var string|null*/protected $_because = null;/*** Arguments expected by this expectation** @var array*/protected $_expectedArgs = array();/*** Count validator store** @var array*/protected $_countValidators = array();/*** The count validator class to use** @var string*/protected $_countValidatorClass = 'Mockery\CountValidator\Exact';/*** Actual count of calls to this expectation** @var int*/protected $_actualCount = 0;/*** Value to return from this expectation** @var mixed*/protected $_returnValue = null;/*** Array of return values as a queue for multiple return sequence** @var array*/protected $_returnQueue = array();/*** Array of closures executed with given arguments to generate a result* to be returned** @var array*/protected $_closureQueue = array();/*** Array of values to be set when this expectation matches** @var array*/protected $_setQueue = array();/*** Integer representing the call order of this expectation** @var int*/protected $_orderNumber = null;/*** Integer representing the call order of this expectation on a global basis** @var int*/protected $_globalOrderNumber = null;/*** Flag indicating that an exception is expected to be throw (not returned)** @var bool*/protected $_throw = false;/*** Flag indicating whether the order of calling is determined locally or* globally** @var bool*/protected $_globally = false;/*** Flag indicating if the return value should be obtained from the original* class method instead of returning predefined values from the return queue** @var bool*/protected $_passthru = false;/*** Constructor** @param \Mockery\LegacyMockInterface $mock* @param string $name*/public function __construct(\Mockery\LegacyMockInterface $mock, $name){$this->_mock = $mock;$this->_name = $name;$this->withAnyArgs();}/*** Return a string with the method name and arguments formatted** @param string $name Name of the expected method* @param array $args List of arguments to the method* @return string*/public function __toString(){return \Mockery::formatArgs($this->_name, $this->_expectedArgs);}/*** Verify the current call, i.e. that the given arguments match those* of this expectation** @param array $args* @return mixed*/public function verifyCall(array $args){$this->validateOrder();$this->_actualCount++;if (true === $this->_passthru) {return $this->_mock->mockery_callSubjectMethod($this->_name, $args);}$return = $this->_getReturnValue($args);$this->throwAsNecessary($return);$this->_setValues();return $return;}/*** Throws an exception if the expectation has been configured to do so** @throws \Throwable* @return void*/private function throwAsNecessary($return){if (!$this->_throw) {return;}if ($return instanceof \Throwable) {throw $return;}return;}/*** Sets public properties with queued values to the mock object** @param array $args* @return mixed*/protected function _setValues(){$mockClass = get_class($this->_mock);$container = $this->_mock->mockery_getContainer();/** @var Mock[] $mocks */$mocks = $container->getMocks();foreach ($this->_setQueue as $name => &$values) {if (count($values) > 0) {$value = array_shift($values);$this->_mock->{$name} = $value;foreach ($mocks as $mock) {if (is_a($mock, $mockClass) && $mock->mockery_isInstance()) {$mock->{$name} = $value;}}}}}/*** Fetch the return value for the matching args** @param array $args* @return mixed*/protected function _getReturnValue(array $args){if (count($this->_closureQueue) > 1) {return call_user_func_array(array_shift($this->_closureQueue), $args);} elseif (count($this->_closureQueue) > 0) {return call_user_func_array(current($this->_closureQueue), $args);} elseif (count($this->_returnQueue) > 1) {return array_shift($this->_returnQueue);} elseif (count($this->_returnQueue) > 0) {return current($this->_returnQueue);}return $this->_mock->mockery_returnValueForMethod($this->_name);}/*** Checks if this expectation is eligible for additional calls** @return bool*/public function isEligible(){foreach ($this->_countValidators as $validator) {if (!$validator->isEligible($this->_actualCount)) {return false;}}return true;}/*** Check if there is a constraint on call count** @return bool*/public function isCallCountConstrained(){return (count($this->_countValidators) > 0);}/*** Verify call order** @return void*/public function validateOrder(){if ($this->_orderNumber) {$this->_mock->mockery_validateOrder((string) $this, $this->_orderNumber, $this->_mock);}if ($this->_globalOrderNumber) {$this->_mock->mockery_getContainer()->mockery_validateOrder((string) $this, $this->_globalOrderNumber, $this->_mock);}}/*** Verify this expectation** @return void*/public function verify(){foreach ($this->_countValidators as $validator) {$validator->validate($this->_actualCount);}}/*** Check if the registered expectation is an ArgumentListMatcher* @return bool*/private function isArgumentListMatcher(){return (count($this->_expectedArgs) === 1 && ($this->_expectedArgs[0] instanceof ArgumentListMatcher));}private function isAndAnyOtherArgumentsMatcher($expectedArg){return $expectedArg instanceof AndAnyOtherArgs;}/*** Check if passed arguments match an argument expectation** @param array $args* @return bool*/public function matchArgs(array $args){if ($this->isArgumentListMatcher()) {return $this->_matchArg($this->_expectedArgs[0], $args);}$argCount = count($args);if ($argCount !== count((array) $this->_expectedArgs)) {$lastExpectedArgument = end($this->_expectedArgs);reset($this->_expectedArgs);if ($this->isAndAnyOtherArgumentsMatcher($lastExpectedArgument)) {$args = array_slice($args, 0, array_search($lastExpectedArgument, $this->_expectedArgs, true));return $this->_matchArgs($args);}return false;}return $this->_matchArgs($args);}/*** Check if the passed arguments match the expectations, one by one.** @param array $args* @return bool*/protected function _matchArgs($args){$argCount = count($args);for ($i=0; $i<$argCount; $i++) {$param =& $args[$i];if (!$this->_matchArg($this->_expectedArgs[$i], $param)) {return false;}}return true;}/*** Check if passed argument matches an argument expectation** @param mixed $expected* @param mixed $actual* @return bool*/protected function _matchArg($expected, &$actual){if ($expected === $actual) {return true;}if (!is_object($expected) && !is_object($actual) && $expected == $actual) {return true;}if (is_string($expected) && is_object($actual)) {$result = $actual instanceof $expected;if ($result) {return true;}}if (is_object($expected)) {$matcher = \Mockery::getConfiguration()->getDefaultMatcher(get_class($expected));if ($matcher !== null) {$expected = new $matcher($expected);}}if ($expected instanceof \Mockery\Matcher\MatcherAbstract) {return $expected->match($actual);}if ($expected instanceof \Hamcrest\Matcher || $expected instanceof \Hamcrest_Matcher) {return $expected->matches($actual);}return false;}/*** Expected argument setter for the expectation** @param mixed ...$args** @return self*/public function with(...$args){return $this->withArgs($args);}/*** Expected arguments for the expectation passed as an array** @param array $arguments* @return self*/private function withArgsInArray(array $arguments){if (empty($arguments)) {return $this->withNoArgs();}$this->_expectedArgs = $arguments;return $this;}/*** Expected arguments have to be matched by the given closure.** @param Closure $closure* @return self*/private function withArgsMatchedByClosure(Closure $closure){$this->_expectedArgs = [new MultiArgumentClosure($closure)];return $this;}/*** Expected arguments for the expectation passed as an array or a closure that matches each passed argument on* each function call.** @param array|Closure $argsOrClosure* @return self*/public function withArgs($argsOrClosure){if (is_array($argsOrClosure)) {$this->withArgsInArray($argsOrClosure);} elseif ($argsOrClosure instanceof Closure) {$this->withArgsMatchedByClosure($argsOrClosure);} else {throw new \InvalidArgumentException(sprintf('Call to %s with an invalid argument (%s), only array and ' .'closure are allowed', __METHOD__, $argsOrClosure));}return $this;}/*** Set with() as no arguments expected** @return self*/public function withNoArgs(){$this->_expectedArgs = [new NoArgs()];return $this;}/*** Set expectation that any arguments are acceptable** @return self*/public function withAnyArgs(){$this->_expectedArgs = [new AnyArgs()];return $this;}/*** Expected arguments should partially match the real arguments** @param mixed ...$expectedArgs* @return self*/public function withSomeOfArgs(...$expectedArgs){return $this->withArgs(function (...$args) use ($expectedArgs) {foreach ($expectedArgs as $expectedArg) {if (!in_array($expectedArg, $args, true)) {return false;}}return true;});}/*** Set a return value, or sequential queue of return values** @param mixed ...$args* @return self*/public function andReturn(...$args){$this->_returnQueue = $args;return $this;}/*** Set a return value, or sequential queue of return values** @param mixed ...$args* @return self*/public function andReturns(...$args){return call_user_func_array([$this, 'andReturn'], $args);}/*** Return this mock, like a fluent interface** @return self*/public function andReturnSelf(){return $this->andReturn($this->_mock);}/*** Set a sequential queue of return values with an array** @param array $values* @return self*/public function andReturnValues(array $values){call_user_func_array(array($this, 'andReturn'), $values);return $this;}/*** Set a closure or sequence of closures with which to generate return* values. The arguments passed to the expected method are passed to the* closures as parameters.** @param callable ...$args* @return self*/public function andReturnUsing(...$args){$this->_closureQueue = $args;return $this;}/*** Sets up a closure to return the nth argument from the expected method call** @param int $index* @return self*/public function andReturnArg($index){if (!is_int($index) || $index < 0) {throw new \InvalidArgumentException("Invalid argument index supplied. Index must be a non-negative integer.");}$closure = function (...$args) use ($index) {if (array_key_exists($index, $args)) {return $args[$index];}throw new \OutOfBoundsException("Cannot return an argument value. No argument exists for the index $index");};$this->_closureQueue = [$closure];return $this;}/*** Return a self-returning black hole object.** @return self*/public function andReturnUndefined(){$this->andReturn(new \Mockery\Undefined());return $this;}/*** Return null. This is merely a language construct for Mock describing.** @return self*/public function andReturnNull(){return $this->andReturn(null);}public function andReturnFalse(){return $this->andReturn(false);}public function andReturnTrue(){return $this->andReturn(true);}/*** Set Exception class and arguments to that class to be thrown** @param string|\Exception $exception* @param string $message* @param int $code* @param \Exception $previous* @return self*/public function andThrow($exception, $message = '', $code = 0, \Exception $previous = null){$this->_throw = true;if (is_object($exception)) {$this->andReturn($exception);} else {$this->andReturn(new $exception($message, $code, $previous));}return $this;}public function andThrows($exception, $message = '', $code = 0, \Exception $previous = null){return $this->andThrow($exception, $message, $code, $previous);}/*** Set Exception classes to be thrown** @param array $exceptions* @return self*/public function andThrowExceptions(array $exceptions){$this->_throw = true;foreach ($exceptions as $exception) {if (!is_object($exception)) {throw new Exception('You must pass an array of exception objects to andThrowExceptions');}}return $this->andReturnValues($exceptions);}/*** Register values to be set to a public property each time this expectation occurs** @param string $name* @param array ...$values* @return self*/public function andSet($name, ...$values){$this->_setQueue[$name] = $values;return $this;}/*** Sets up a closure that will yield each of the provided args** @param mixed ...$args* @return self*/public function andYield(...$args){$this->_closureQueue = [static function () use ($args) {foreach ($args as $arg) {yield $arg;}},];return $this;}/*** Alias to andSet(). Allows the natural English construct* - set('foo', 'bar')->andReturn('bar')** @param string $name* @param mixed $value* @return self*/public function set($name, $value){return call_user_func_array(array($this, 'andSet'), func_get_args());}/*** Indicates this expectation should occur zero or more times** @return self*/public function zeroOrMoreTimes(){$this->atLeast()->never();}/*** Indicates the number of times this expectation should occur** @param int $limit* @throws \InvalidArgumentException* @return self*/public function times($limit = null){if (is_null($limit)) {return $this;}if (!is_int($limit)) {throw new \InvalidArgumentException('The passed Times limit should be an integer value');}$this->_countValidators[$this->_countValidatorClass] = new $this->_countValidatorClass($this, $limit);if ('Mockery\CountValidator\Exact' !== $this->_countValidatorClass) {$this->_countValidatorClass = 'Mockery\CountValidator\Exact';unset($this->_countValidators[$this->_countValidatorClass]);}return $this;}/*** Indicates that this expectation is never expected to be called** @return self*/public function never(){return $this->times(0);}/*** Indicates that this expectation is expected exactly once** @return self*/public function once(){return $this->times(1);}/*** Indicates that this expectation is expected exactly twice** @return self*/public function twice(){return $this->times(2);}/*** Sets next count validator to the AtLeast instance** @return self*/public function atLeast(){$this->_countValidatorClass = 'Mockery\CountValidator\AtLeast';return $this;}/*** Sets next count validator to the AtMost instance** @return self*/public function atMost(){$this->_countValidatorClass = 'Mockery\CountValidator\AtMost';return $this;}/*** Shorthand for setting minimum and maximum constraints on call counts** @param int $minimum* @param int $maximum*/public function between($minimum, $maximum){return $this->atLeast()->times($minimum)->atMost()->times($maximum);}/*** Set the exception message** @param string $message* @return $this*/public function because($message){$this->_because = $message;return $this;}/*** Indicates that this expectation must be called in a specific given order** @param string $group Name of the ordered group* @return self*/public function ordered($group = null){if ($this->_globally) {$this->_globalOrderNumber = $this->_defineOrdered($group, $this->_mock->mockery_getContainer());} else {$this->_orderNumber = $this->_defineOrdered($group, $this->_mock);}$this->_globally = false;return $this;}/*** Indicates call order should apply globally** @return self*/public function globally(){$this->_globally = true;return $this;}/*** Setup the ordering tracking on the mock or mock container** @param string $group* @param object $ordering* @return int*/protected function _defineOrdered($group, $ordering){$groups = $ordering->mockery_getGroups();if (is_null($group)) {$result = $ordering->mockery_allocateOrder();} elseif (isset($groups[$group])) {$result = $groups[$group];} else {$result = $ordering->mockery_allocateOrder();$ordering->mockery_setGroup($group, $result);}return $result;}/*** Return order number** @return int*/public function getOrderNumber(){return $this->_orderNumber;}/*** Mark this expectation as being a default** @return self*/public function byDefault(){$director = $this->_mock->mockery_getExpectationsFor($this->_name);if (!empty($director)) {$director->makeExpectationDefault($this);}return $this;}/*** Return the parent mock of the expectation** @return \Mockery\LegacyMockInterface|\Mockery\MockInterface*/public function getMock(){return $this->_mock;}/*** Flag this expectation as calling the original class method with the* any provided arguments instead of using a return value queue.** @return self*/public function passthru(){if ($this->_mock instanceof Mock) {throw new Exception('Mock Objects not created from a loaded/existing class are '. 'incapable of passing method calls through to a parent class');}$this->_passthru = true;return $this;}/*** Cloning logic**/public function __clone(){$newValidators = array();$countValidators = $this->_countValidators;foreach ($countValidators as $validator) {$newValidators[] = clone $validator;}$this->_countValidators = $newValidators;}public function getName(){return $this->_name;}public function getExceptionMessage(){return $this->_because;}}