1<?php 2 3/* 4 * This file is part of the Prophecy. 5 * (c) Konstantin Kudryashov <ever.zet@gmail.com> 6 * Marcello Duarte <marcello.duarte@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Prophecy\Call; 13 14use Prophecy\Exception\Prophecy\MethodProphecyException; 15use Prophecy\Prophecy\MethodProphecy; 16use Prophecy\Prophecy\ObjectProphecy; 17use Prophecy\Argument\ArgumentsWildcard; 18use Prophecy\Util\StringUtil; 19use Prophecy\Exception\Call\UnexpectedCallException; 20 21/** 22 * Calls receiver & manager. 23 * 24 * @author Konstantin Kudryashov <ever.zet@gmail.com> 25 */ 26class CallCenter 27{ 28 private $util; 29 30 /** 31 * @var Call[] 32 */ 33 private $recordedCalls = array(); 34 35 /** 36 * Initializes call center. 37 * 38 * @param StringUtil $util 39 */ 40 public function __construct(StringUtil $util = null) 41 { 42 $this->util = $util ?: new StringUtil; 43 } 44 45 /** 46 * Makes and records specific method call for object prophecy. 47 * 48 * @param ObjectProphecy $prophecy 49 * @param string $methodName 50 * @param array $arguments 51 * 52 * @return mixed Returns null if no promise for prophecy found or promise return value. 53 * 54 * @throws \Prophecy\Exception\Call\UnexpectedCallException If no appropriate method prophecy found 55 */ 56 public function makeCall(ObjectProphecy $prophecy, $methodName, array $arguments) 57 { 58 // For efficiency exclude 'args' from the generated backtrace 59 if (PHP_VERSION_ID >= 50400) { 60 // Limit backtrace to last 3 calls as we don't use the rest 61 // Limit argument was introduced in PHP 5.4.0 62 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); 63 } elseif (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) { 64 // DEBUG_BACKTRACE_IGNORE_ARGS was introduced in PHP 5.3.6 65 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 66 } else { 67 $backtrace = debug_backtrace(); 68 } 69 70 $file = $line = null; 71 if (isset($backtrace[2]) && isset($backtrace[2]['file'])) { 72 $file = $backtrace[2]['file']; 73 $line = $backtrace[2]['line']; 74 } 75 76 // If no method prophecies defined, then it's a dummy, so we'll just return null 77 if ('__destruct' === $methodName || 0 == count($prophecy->getMethodProphecies())) { 78 $this->recordedCalls[] = new Call($methodName, $arguments, null, null, $file, $line); 79 80 return null; 81 } 82 83 // There are method prophecies, so it's a fake/stub. Searching prophecy for this call 84 $matches = array(); 85 foreach ($prophecy->getMethodProphecies($methodName) as $methodProphecy) { 86 if (0 < $score = $methodProphecy->getArgumentsWildcard()->scoreArguments($arguments)) { 87 $matches[] = array($score, $methodProphecy); 88 } 89 } 90 91 // If fake/stub doesn't have method prophecy for this call - throw exception 92 if (!count($matches)) { 93 throw $this->createUnexpectedCallException($prophecy, $methodName, $arguments); 94 } 95 96 // Sort matches by their score value 97 @usort($matches, function ($match1, $match2) { return $match2[0] - $match1[0]; }); 98 99 $score = $matches[0][0]; 100 // If Highest rated method prophecy has a promise - execute it or return null instead 101 $methodProphecy = $matches[0][1]; 102 $returnValue = null; 103 $exception = null; 104 if ($promise = $methodProphecy->getPromise()) { 105 try { 106 $returnValue = $promise->execute($arguments, $prophecy, $methodProphecy); 107 } catch (\Exception $e) { 108 $exception = $e; 109 } 110 } 111 112 if ($methodProphecy->hasReturnVoid() && $returnValue !== null) { 113 throw new MethodProphecyException( 114 "The method \"$methodName\" has a void return type, but the promise returned a value", 115 $methodProphecy 116 ); 117 } 118 119 $this->recordedCalls[] = $call = new Call( 120 $methodName, $arguments, $returnValue, $exception, $file, $line 121 ); 122 $call->addScore($methodProphecy->getArgumentsWildcard(), $score); 123 124 if (null !== $exception) { 125 throw $exception; 126 } 127 128 return $returnValue; 129 } 130 131 /** 132 * Searches for calls by method name & arguments wildcard. 133 * 134 * @param string $methodName 135 * @param ArgumentsWildcard $wildcard 136 * 137 * @return Call[] 138 */ 139 public function findCalls($methodName, ArgumentsWildcard $wildcard) 140 { 141 return array_values( 142 array_filter($this->recordedCalls, function (Call $call) use ($methodName, $wildcard) { 143 return $methodName === $call->getMethodName() 144 && 0 < $call->getScore($wildcard) 145 ; 146 }) 147 ); 148 } 149 150 private function createUnexpectedCallException(ObjectProphecy $prophecy, $methodName, 151 array $arguments) 152 { 153 $classname = get_class($prophecy->reveal()); 154 $indentationLength = 8; // looks good 155 $argstring = implode( 156 ",\n", 157 $this->indentArguments( 158 array_map(array($this->util, 'stringify'), $arguments), 159 $indentationLength 160 ) 161 ); 162 163 $expected = array(); 164 165 foreach (call_user_func_array('array_merge', $prophecy->getMethodProphecies()) as $methodProphecy) { 166 $expected[] = sprintf( 167 " - %s(\n" . 168 "%s\n" . 169 " )", 170 $methodProphecy->getMethodName(), 171 implode( 172 ",\n", 173 $this->indentArguments( 174 array_map('strval', $methodProphecy->getArgumentsWildcard()->getTokens()), 175 $indentationLength 176 ) 177 ) 178 ); 179 } 180 181 return new UnexpectedCallException( 182 sprintf( 183 "Unexpected method call on %s:\n". 184 " - %s(\n". 185 "%s\n". 186 " )\n". 187 "expected calls were:\n". 188 "%s", 189 190 $classname, $methodName, $argstring, implode("\n", $expected) 191 ), 192 $prophecy, $methodName, $arguments 193 194 ); 195 } 196 197 private function formatExceptionMessage(MethodProphecy $methodProphecy) 198 { 199 return sprintf( 200 " - %s(\n". 201 "%s\n". 202 " )", 203 $methodProphecy->getMethodName(), 204 implode( 205 ",\n", 206 $this->indentArguments( 207 array_map( 208 function ($token) { 209 return (string) $token; 210 }, 211 $methodProphecy->getArgumentsWildcard()->getTokens() 212 ), 213 $indentationLength 214 ) 215 ) 216 ); 217 } 218 219 private function indentArguments(array $arguments, $indentationLength) 220 { 221 return preg_replace_callback( 222 '/^/m', 223 function () use ($indentationLength) { 224 return str_repeat(' ', $indentationLength); 225 }, 226 $arguments 227 ); 228 } 229} 230