PageRenderTime 44ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/Spy.php

https://gitlab.com/oytunistrator/phpspy
PHP | 399 lines | 262 code | 56 blank | 81 comment | 38 complexity | c9fd78f969f6f7860ea1242702c7b5a3 MD5 | raw file
  1. <?php
  2. /**
  3. * A spy for tracking calls to methods.
  4. *
  5. * @author Christopher Aue <mail@christopheraue.net>
  6. * @license MIT
  7. * @link https://github.com/christopheraue/phpspy
  8. */
  9. namespace christopheraue\phpspy;
  10. class Spy
  11. {
  12. protected static $_spies = array();
  13. protected $_name = null;
  14. protected $_context = null;
  15. protected $_functionName = null;
  16. /** @var callable _substitute */
  17. protected $_substitute = null;
  18. protected $_origFuncSuffix = '_original';
  19. protected $_spyFuncSuffix = '_spy';
  20. protected $_calls = array();
  21. /**
  22. * Create a spy to spy on a function or a method
  23. *
  24. * @param string $context A classname or a functioname to spy on
  25. * @param string $methodName If $context is a classname, the name of the method to spy on
  26. */
  27. public function __construct($context, $methodName = null)
  28. {
  29. if (func_num_args() == 1) {
  30. $this->_initFunctionSpy($context);
  31. } else {
  32. $this->_initMethodSpy($context, $methodName);
  33. }
  34. }
  35. protected function _initFunctionSpy($functionName)
  36. {
  37. $this->_functionName = strtolower($functionName);
  38. $this->_name = $this->_functionName;
  39. $this->_storeInRegister();
  40. $this->_redirectCallToFunction();
  41. }
  42. protected function _initMethodSpy($classname, $methodname)
  43. {
  44. if (!class_exists($classname)) {
  45. //try to load it via autoloading, or fail
  46. new $classname();
  47. }
  48. //Sometimes $classname and $methodname are transferred to this constructor in all lower case
  49. //although they were given in camel case. Strange behavior. Doesn't matter, since PHP
  50. //class and function names are case insensitive. Convert the names to all lower case.
  51. $this->_context = strtolower($classname);
  52. $this->_functionName = strtolower($methodname);
  53. $this->_name = $this->_context.'::'.$this->_functionName;
  54. $this->_storeInRegister();
  55. $this->_redirectCallToMethod();
  56. }
  57. protected function _storeInRegister()
  58. {
  59. if (array_key_exists($this->_name, self::$_spies)) {
  60. /** @var \christopheraue\phpspy\Spy $spy */
  61. $spy = self::$_spies[$this->_name];
  62. $spy->kill();
  63. }
  64. self::$_spies[$this->_name] = $this;
  65. }
  66. protected function _redirectCallToMethod()
  67. {
  68. $newOrigFuncName = $this->_functionName.$this->_origFuncSuffix;
  69. $spyFuncName = $this->_functionName.$this->_spyFuncSuffix;
  70. $isStatic = (new \ReflectionMethod($this->_context, $this->_functionName))->isStatic();
  71. $isInherited = !method_exists($this->_context, $this->_functionName)
  72. && method_exists(get_parent_class($this->_context), $this->_functionName);
  73. runkit_method_add(
  74. $this->_context,
  75. $spyFuncName,
  76. '',
  77. $this->_getSpyFunctionBody(),
  78. RUNKIT_ACC_PRIVATE | ($isStatic ? RUNKIT_ACC_STATIC : 0)
  79. );
  80. if (!method_exists($this->_context, $newOrigFuncName)) {
  81. runkit_method_copy($this->_context, $newOrigFuncName, $this->_context, $this->_functionName);
  82. }
  83. //keep memory address of function to prevent seg fault
  84. $runkit_method_modifier = $isInherited ? 'runkit_method_add' : 'runkit_method_redefine';
  85. $runkit_method_modifier(
  86. $this->_context,
  87. $this->_functionName,
  88. '',
  89. $this->_getReplaceFunctionBody(),
  90. RUNKIT_ACC_PUBLIC | ($isStatic ? RUNKIT_ACC_STATIC : 0)
  91. );
  92. }
  93. protected function _redirectCallToFunction()
  94. {
  95. $newOrigFuncName = $this->_functionName.$this->_origFuncSuffix;
  96. $spyFuncName = $this->_functionName.$this->_spyFuncSuffix;
  97. runkit_function_add($spyFuncName, '', $this->_getSpyFunctionBody());
  98. if (!function_exists($newOrigFuncName)) {
  99. runkit_function_copy($this->_functionName, $newOrigFuncName);
  100. }
  101. //keep memory address of function to prevent seg fault
  102. runkit_function_redefine($this->_functionName, '', $this->_getReplaceFunctionBody());
  103. }
  104. protected function _getSpyFunctionBody()
  105. {
  106. $isSpyingOnMethod = !!$this->_context;
  107. if ($isSpyingOnMethod) {
  108. $isStatic = (new \ReflectionMethod($this->_context, $this->_functionName))->isStatic();
  109. if ($isStatic) {
  110. $context = 'get_called_class()';
  111. $getSpyParams = "'$this->_context', '$this->_functionName'";
  112. } else {
  113. $context = '$this';
  114. $getSpyParams = '$this, "'.$this->_functionName.'"';
  115. }
  116. } else {
  117. $context = 'null';
  118. $getSpyParams = "'$this->_functionName'";
  119. }
  120. return '$args = func_get_args();
  121. $context = '.$context.';
  122. $spy = '.__CLASS__.'::getSpy('.$getSpyParams.');
  123. return $spy->recordCall($context, $args);';
  124. }
  125. protected function _getReplaceFunctionBody()
  126. {
  127. $spyFuncName = $this->_functionName.$this->_spyFuncSuffix;
  128. $isSpyingOnMethod = !!$this->_context;
  129. if ($isSpyingOnMethod) {
  130. $isStatic = (new \ReflectionMethod($this->_context, $this->_functionName))->isStatic();
  131. if ($isStatic) {
  132. $spyCallback = 'array("'.$this->_context.'", "'.$spyFuncName.'")';
  133. } else {
  134. $spyCallback = 'array($this, "'.$spyFuncName.'")';
  135. }
  136. } else {
  137. $spyCallback = "\"$spyFuncName\"";
  138. }
  139. return '$args = func_get_args();
  140. return call_user_func_array('.$spyCallback.', $args);';
  141. }
  142. /**
  143. * Get a spy belonging to a function or method of a class or object
  144. * (for internal use only)
  145. *
  146. * @param object $context Context the function was called in
  147. * @param string $methodName Name of the function
  148. *
  149. * @return Spy
  150. */
  151. public static function getSpy($context, $methodName = null)
  152. {
  153. if (func_num_args() == 1) {
  154. return self::_getFunctionSpy($context);
  155. }
  156. return self::_getMethodSpy($context, $methodName);
  157. }
  158. protected static function _getFunctionSpy($functionName)
  159. {
  160. $spyName = $functionName;
  161. return self::$_spies[$spyName];
  162. }
  163. protected static function _getMethodSpy($context, $methodName)
  164. {
  165. $className = $context;
  166. if (is_object($className)) {
  167. $className = strtolower(get_class($className));
  168. }
  169. $spyName = $className.'::'.$methodName;
  170. return self::$_spies[$spyName];
  171. }
  172. /**
  173. * Save a call to the method (for internal use only)
  174. *
  175. * @param object|null $context For methods their instance, for function 'null'
  176. * @param array $args Arguments the function was called with
  177. *
  178. * @return mixed
  179. */
  180. public function recordCall($context, array $args)
  181. {
  182. $originalFuncName = $this->_functionName.$this->_origFuncSuffix;
  183. if ($this->isActingAs()) {
  184. $isSpyingOnMethod = !!$context;
  185. if ($this->_substitute instanceof \Closure) {
  186. if ($isSpyingOnMethod) {
  187. //call closure in context of instance or class
  188. if (is_string($context)) {
  189. $callable = $this->_substitute->bindTo(null, $context);
  190. } else {
  191. $callable = $this->_substitute->bindTo($context, get_class($context));
  192. }
  193. } else {
  194. $callable = $this->_substitute->bindTo(null, null);
  195. }
  196. } else {
  197. $callable = $this->_substitute;
  198. }
  199. } else {
  200. $callable = $context ? array($context, $originalFuncName) : "$originalFuncName";
  201. }
  202. $result = call_user_func_array($callable, $args);
  203. $call = new Spy\Call($context, $args, $result);
  204. array_push($this->_calls, $call);
  205. return $result;
  206. }
  207. /**
  208. * Reset tracked calls of a class' method
  209. *
  210. * @return void
  211. */
  212. public function reset()
  213. {
  214. $this->_calls = array();
  215. }
  216. /**
  217. * Return number of tracked calls
  218. *
  219. * @return int
  220. */
  221. public function getCallCount()
  222. {
  223. return count($this->_calls);
  224. }
  225. /**
  226. * Return a specific call
  227. *
  228. * @param int $idx Index indicating the nth call,
  229. * negative indices get call from the back of the list
  230. *
  231. * @return Spy\Call
  232. * @throws \Exception
  233. */
  234. public function getCall($idx)
  235. {
  236. if (!is_numeric($idx)) {
  237. throw new \Exception('$idx must be an integer.');
  238. }
  239. if ($idx < 0) {
  240. return $this->_calls[count($this->_calls)+$idx];
  241. }
  242. return $this->_calls[$idx];
  243. }
  244. /**
  245. * Intercept calls to the actual implementation and call a substitute instead
  246. *
  247. * @param callable $callable
  248. */
  249. public function actAs(callable $callable)
  250. {
  251. $this->_substitute = $callable;
  252. }
  253. /**
  254. * @return bool
  255. */
  256. public function isActingAs()
  257. {
  258. return !is_null($this->_substitute);
  259. }
  260. /**
  261. * Use the actual implementation (again) if the spied on function is called
  262. */
  263. public function actNaturally()
  264. {
  265. $this->_substitute = null;
  266. }
  267. /**
  268. * No longer spy on the function and restore everything at it was before
  269. * the spying started
  270. */
  271. public function kill()
  272. {
  273. if ($this->_context) {
  274. $this->_reverseMethodCallRedirection();
  275. } else {
  276. $this->_reverseFunctionCallRedirection();
  277. }
  278. $this->_deleteFromRegister();
  279. }
  280. protected function _reverseMethodCallRedirection()
  281. {
  282. $newOrigFuncName = $this->_functionName.$this->_origFuncSuffix;
  283. $spyFuncName = $this->_functionName.$this->_spyFuncSuffix;
  284. $isStatic = (new \ReflectionMethod($this->_context, $this->_functionName))->isStatic();
  285. if ($isStatic) {
  286. $origCallback = 'array("'.$this->_context.'", "'.$newOrigFuncName.'")';
  287. } else {
  288. $origCallback = 'array($this, "'.$newOrigFuncName.'")';
  289. }
  290. runkit_method_remove($this->_context, $spyFuncName);
  291. //keep memory address of function to prevent seg fault
  292. runkit_method_redefine(
  293. $this->_context,
  294. $this->_functionName,
  295. '',
  296. '$args = func_get_args();
  297. return call_user_func_array('.$origCallback.', $args);',
  298. RUNKIT_ACC_PUBLIC | ($isStatic ? RUNKIT_ACC_STATIC : 0)
  299. );
  300. }
  301. protected function _reverseFunctionCallRedirection()
  302. {
  303. $newOrigFuncName = $this->_functionName.$this->_origFuncSuffix;
  304. $spyFuncName = $this->_functionName.$this->_spyFuncSuffix;
  305. runkit_function_remove($spyFuncName);
  306. //keep memory address of function to prevent seg fault
  307. runkit_function_redefine(
  308. $this->_functionName,
  309. '',
  310. '$args = func_get_args();
  311. return call_user_func_array("'.$newOrigFuncName.'", $args);'
  312. );
  313. }
  314. protected function _deleteFromRegister()
  315. {
  316. unset(self::$_spies[$this->_name]);
  317. }
  318. /**
  319. * @param array $args Arguments to call the function with
  320. * @param null|object $instance Instance in which context to call a non-static method
  321. * @return mixed
  322. * @throws \Exception
  323. */
  324. public function callOriginal(array $args = array(), $instance = null)
  325. {
  326. $origFuncName = $this->_functionName.$this->_origFuncSuffix;
  327. if ($this->_context) {
  328. $isStatic = (new \ReflectionMethod($this->_context, $origFuncName))->isStatic();
  329. if ($isStatic) {
  330. $origFunc = array($this->_context, $origFuncName);
  331. } else {
  332. if (!$instance) {
  333. throw new \Exception('Instance argument is missing to call a non-static method.');
  334. }
  335. $origFunc = array($instance, $origFuncName);
  336. }
  337. } else {
  338. $origFunc = $origFuncName;
  339. }
  340. return call_user_func_array($origFunc, $args);
  341. }
  342. }