Your IP : 18.224.60.132
<?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;
}
}