PageRenderTime 82ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/test/Mocker.php

http://github.com/UnionOfRAD/lithium
PHP | 843 lines | 429 code | 39 blank | 375 comment | 21 complexity | cc73248a2f1c1b7892bdaee248420abc MD5 | raw file
  1. <?php
  2. /**
  3. * li₃: the most RAD framework for PHP (http://li3.me)
  4. *
  5. * Copyright 2012, Union of RAD. All rights reserved. This source
  6. * code is distributed under the terms of the BSD 3-Clause License.
  7. * The full license text can be found in the LICENSE.txt file.
  8. */
  9. namespace lithium\test;
  10. use lithium\aop\Filters;
  11. use lithium\util\Text;
  12. use ReflectionClass;
  13. use ReflectionMethod;
  14. use ReflectionFunction;
  15. use ReflectionFunctionAbstract;
  16. use Reflection;
  17. $message = 'lithium\test\Mocker has been deprecated, as alternatives ';
  18. $message .= 'exist (i.e. Mockery) which take the task of maintaining a ';
  19. $message .= 'mocking framework from us.';
  20. trigger_error($message, E_USER_DEPRECATED);
  21. /**
  22. * The Mocker class aids in the creation of Mocks on the fly, allowing you to
  23. * use Lithium filters on most methods in a class as close to the test as
  24. * possible.
  25. *
  26. * ## How to use it
  27. * To create a new Mock, you need to register `Mocker`, then call or instantiate
  28. * the same class but with '\Mock' appended to the end of the class name.
  29. *
  30. * ### Registering Mocker
  31. * To enable the autoloading of mocks you simply need to make a simple method
  32. * call.
  33. * ```
  34. * use lithium\core\Environment;
  35. * use lithium\test\Mocker;
  36. * if (!Environment::is('production')) {
  37. * Mocker::register();
  38. * }
  39. * ```
  40. *
  41. * You can also enable autoloading inside the setup of a unit test class. This
  42. * method can be called redundantly.
  43. * ```
  44. * use lithium\test\Mocker;
  45. * class MockerTest extends \lithium\test\Unit {
  46. * public function setUp() {
  47. * Mocker::register();
  48. * }
  49. * }
  50. * ```
  51. *
  52. * ### Usage and Examples
  53. * Using Mocker is the fun magical part, it's autoloaded so simply call the
  54. * class you want to mock with the '\Mock' at the end. The autoloader will
  55. * detect you want to autoload it, and create it for you. Now you can filter
  56. * any method.
  57. *
  58. * ```
  59. * use lithium\console\dispatcher\Mock as DispatcherMock;
  60. * $dispatcher = new DispatcherMock();
  61. * $dispatcher->applyFilter('config', function($params, $next) {
  62. * return [];
  63. * });
  64. * $results = $dispatcher->config();
  65. * ```
  66. * ```
  67. * use lithium\analysis\parser\Mock as ParserMock;
  68. * $code = 'echo "foobar";';
  69. * ParserMock::applyFilter('config', function($params, $next) {
  70. * return [];
  71. * });
  72. * $tokens = ParserMock::tokenize($code, ['wrap' => true]);
  73. * ```
  74. *
  75. * Mocker also gives the ability, if used correctly, to stub build in php
  76. * function calls. Consider the following example.
  77. * ```
  78. * namespace app\extensions;
  79. *
  80. * class AwesomeFileEditor {
  81. *
  82. * public static function updateJson($file) {
  83. * if (file_exists($file)) {
  84. * $time = microtime(true);
  85. * $packages = json_decode(file_get_contents($file), true);
  86. * foreach ($packages['users'] as &$package) {
  87. * $package['updated'] = $time;
  88. * }
  89. * return $packages;
  90. * }
  91. * return false;
  92. * }
  93. *
  94. * }
  95. * ```
  96. * ```
  97. * namespace app\tests\cases\extensions;
  98. *
  99. * use lithium\test\Mocker;
  100. * use app\extensions\AwesomeFileEditor;
  101. *
  102. * class AwesomeFileEditorTest extends \lithium\test\Unit {
  103. *
  104. * public function setUp() {
  105. * Mocker::overwriteFunction(false);
  106. * }
  107. *
  108. * public function testUpdateJson() {
  109. * Mocker::overwriteFunction('app\extensions\file_exists', function() {
  110. * return true;
  111. * });
  112. * Mocker::overwriteFunction('app\extensions\file_get_contents', function() {
  113. * return <<<EOD
  114. * {
  115. * "users": [
  116. * {
  117. * "name": "BlaineSch",
  118. * "updated": 0
  119. * }
  120. * ]
  121. * }
  122. * EOD;
  123. * });
  124. *
  125. * $results = AwesomeFileEditor::updateJson('idontexist.json');
  126. * $this->assertNotEqual(0, $results['users'][0]['updated']);
  127. * }
  128. *
  129. * }
  130. * ```
  131. *
  132. * ## How does Mocking classes work?
  133. * This section isn't necessary to read, but can help others better understand
  134. * it so that they can add new features, or debug current ones.
  135. *
  136. * ### TLDR
  137. * The `Mocker` class dynamically makes two classes, a `Delegate` and a `Mock`.
  138. * Both of these classes extend the target class. The `Delegate` is passed into
  139. * the `Mock` class for it to call within (anonymous functions) filters. This
  140. * allows public and protected methods to be filterable.
  141. *
  142. * ### Theory
  143. * I'll walk you through the steps I did in order to figure out how `Mocker`
  144. * should work. The goal here is to mock class `Person`.
  145. *
  146. * ```
  147. * class Person {
  148. * public function speak() {
  149. * $this->_openMouth();
  150. * return true;
  151. * }
  152. * protected function _openMouth() {
  153. * return $this->mouth = 'open';
  154. * }
  155. * }
  156. * ```
  157. *
  158. * In order to make the `speak()` method filterable we'll need to create a class
  159. * called `MockPerson` and we'll make its `speak()` method filterable, however
  160. * there is already an issue since a filter works inside of an anonymous
  161. * function you cannot call `parent`, so `MockPerson` will also need an instance
  162. * of `Person`.
  163. *
  164. * ```
  165. * class MockPerson extends Person {
  166. * public $person;
  167. * public function speak() {
  168. * $params = compact();
  169. * $person = $this->person;
  170. * return Filters::run($this, __FUNCTION__, [], function($params) use (&$person) {
  171. * return $person->speak();
  172. * };
  173. * }
  174. * }
  175. * ```
  176. *
  177. * You might stop here and call it a day, but what about filtering protected
  178. * methods? For example you might want to make sure `_openMouth()` does not
  179. * modify the class. However this isn't possible with the current implementation
  180. * since `_openMouth` is protected and we can't call protected methods within an
  181. * anonymous function. The trick is that when you are extending a class you can
  182. * make a method MORE visible than its parent, with the exception of private
  183. * methods. So let's make a class `DelegatePerson` that simply extends `Person`
  184. * and makes `_openMouth()` public.
  185. *
  186. * ```
  187. * class DelegatePerson extends Person {
  188. * public function _openMouth() {
  189. * parent::_openMouth();
  190. * }
  191. * }
  192. * ```
  193. *
  194. * Now we simply pass `DelegatePerson` to `MockPerson` and all methods are now
  195. * filterable.
  196. *
  197. * ## How does overwriting PHP functions work?
  198. * In short, this is a hack. When you are inside of a namespace `foo\bar\baz`
  199. * and you call a function `file_get_contents` it first searches the current
  200. * namespace for that function `foo\bar\baz\file_get_contents`. `Mocker` simply
  201. * creates that function dynamically, so when its called it delegates back to
  202. * `Mocker` which will determine if it should call a user-defined function or
  203. * if it should go back to the original PHP function.
  204. *
  205. * @deprecated Please use an alternative mocking framework, i.e. Mockery.
  206. */
  207. class Mocker {
  208. /**
  209. * Functions to be called instead of the original.
  210. *
  211. * The key is the fully namespaced function name, and the value is the closure to be called.
  212. *
  213. * @var array
  214. */
  215. protected static $_functionCallbacks = [];
  216. /**
  217. * Results of function calls for later assertion in `MockerChain`.
  218. *
  219. * @var array
  220. */
  221. protected static $_functionResults = [];
  222. /**
  223. * A list of code to be generated for the `Delegate`.
  224. *
  225. * The `Delegate` directly extends the class you wish to mock and makes all
  226. * methods publically available to other classes but should not be accessed
  227. * directly by any other classes other than `Mock`.
  228. *
  229. * @item variable `$parent` Instance of `Mock`. Allows `Delegate` to send
  230. * calls back to `Mock` if it was called directly
  231. * from a parent class.
  232. * @var array
  233. */
  234. protected static $_mockDelegateIngredients = [
  235. 'startClass' => [
  236. 'namespace {:namespace};',
  237. 'class MockDelegate extends \{:mocker} {',
  238. ' public $parent = null;',
  239. ],
  240. 'constructor' => [
  241. '{:modifiers} function __construct({:args}) {',
  242. ' $args = compact({:stringArgs});',
  243. ' $argCount = func_num_args();',
  244. ' $this->parent = $argCount === 0 ? false : func_get_arg($argCount - 1);',
  245. ' if (!is_a($this->parent, __NAMESPACE__ . "\Mock")) {',
  246. ' $class = new \ReflectionClass(\'{:namespace}\Mock\');',
  247. ' $this->parent = $class->newInstanceArgs($args);',
  248. ' }',
  249. ' $this->parent->mocker = $this;',
  250. ' if (method_exists(\'{:mocker}\', "__construct")) {',
  251. ' call_user_func_array("parent::__construct", $args);',
  252. ' }',
  253. '}',
  254. ],
  255. 'method' => [
  256. '{:modifiers} function {:method}({:args}) {',
  257. ' $args = compact({:stringArgs});',
  258. ' $token = spl_object_hash($this);',
  259. ' if (func_num_args() > 0 && func_get_arg(func_num_args() - 1) === $token) {',
  260. ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
  261. ' }',
  262. ' $method = [$this->parent, "{:method}"];',
  263. ' return call_user_func_array($method, $args);',
  264. '}',
  265. ],
  266. 'staticMethod' => [
  267. '{:modifiers} function {:method}({:args}) {',
  268. ' $args = compact({:stringArgs});',
  269. ' $token = "1f3870be274f6c49b3e31a0c6728957f";',
  270. ' if (func_num_args() > 0 && func_get_arg(func_num_args() - 1) === $token) {',
  271. ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
  272. ' }',
  273. ' $method = \'{:namespace}\Mock::{:method}\';',
  274. ' return call_user_func_array($method, $args);',
  275. '}',
  276. ],
  277. 'endClass' => [
  278. '}',
  279. ],
  280. ];
  281. /**
  282. * List of code to be generated for overwriting php functions.
  283. *
  284. * @var array
  285. */
  286. protected static $_mockFunctionIngredients = [
  287. 'function' => [
  288. 'namespace {:namespace};',
  289. 'use lithium\test\Mocker;',
  290. 'function {:function}({:args}) {',
  291. ' $params = [];',
  292. ' foreach ([{:stringArgs}] as $value) {',
  293. ' if (!empty($value)) {',
  294. ' $params[] =& ${$value};',
  295. ' }',
  296. ' }',
  297. ' return Mocker::callFunction(__FUNCTION__, $params);',
  298. '}',
  299. ],
  300. ];
  301. /**
  302. * A list of code to be generated for the `Mock`.
  303. *
  304. * The Mock class directly extends the class you wish to mock but only
  305. * interacts with the `Delegate` directly. This class is the public
  306. * interface for users.
  307. *
  308. * @item variable `$results` All method calls allowing you for you make your
  309. * own custom assertions.
  310. * @item variable `$staticResults` See `$results`.
  311. * @item variable `$mocker` Home of the `Delegate` defined above.
  312. * @item variable `$_safeVars` Variables that should not be deleted on
  313. * `Mock`. We delete them so they cannot be
  314. * accessed directly, but sent to `Delegate` via
  315. * PHP magic methods on `Mock`.
  316. * @var array
  317. */
  318. protected static $_mockIngredients = [
  319. 'startClass' => [
  320. 'namespace {:namespace};',
  321. 'use lithium\aop\Filters as _Filters;',
  322. 'class Mock extends \{:mocker} {',
  323. ' public $mocker;',
  324. ' public $results = [];',
  325. ' public static $staticResults = [];',
  326. ' protected $_safeVars = [',
  327. ' "_classes",',
  328. ' "mocker",',
  329. ' "_safeVars",',
  330. ' "results",',
  331. ' "staticResults",',
  332. ' "_methodFilters",',
  333. ' ];',
  334. ],
  335. 'get' => [
  336. 'public function {:reference}__get($name) {',
  337. ' $data ={:reference} $this->mocker->$name;',
  338. ' return $data;',
  339. '}',
  340. ],
  341. 'set' => [
  342. 'public function __set($name, $value = null) {',
  343. ' return $this->mocker->$name = $value;',
  344. '}',
  345. ],
  346. 'isset' => [
  347. 'public function __isset($name) {',
  348. ' return isset($this->mocker->$name);',
  349. '}',
  350. ],
  351. 'unset' => [
  352. 'public function __unset($name) {',
  353. ' unset($this->mocker->$name);',
  354. '}',
  355. ],
  356. 'constructor' => [
  357. '{:modifiers} function __construct({:args}) {',
  358. ' $args = compact({:stringArgs});',
  359. ' array_push($args, $this);',
  360. ' foreach (get_class_vars(get_class($this)) as $key => $value) {',
  361. ' if (isset($this->{$key}) && !in_array($key, $this->_safeVars)) {',
  362. ' unset($this->$key);',
  363. ' }',
  364. ' }',
  365. ' $class = new \ReflectionClass(\'{:namespace}\MockDelegate\');',
  366. ' $class->newInstanceArgs($args);',
  367. '}',
  368. ],
  369. 'destructor' => [
  370. 'public function __destruct() {}',
  371. ],
  372. 'staticMethod' => [
  373. '{:modifiers} function {:method}({:args}) {',
  374. ' $args = compact({:stringArgs});',
  375. ' $args["hash"] = "1f3870be274f6c49b3e31a0c6728957f";',
  376. ' $method = \'{:namespace}\MockDelegate::{:method}\';',
  377. ' $result = _Filters::run(__CLASS__, "{:method}", $args,',
  378. ' function($args) use(&$method) {',
  379. ' return call_user_func_array($method, $args);',
  380. ' }',
  381. ' );',
  382. ' if (!isset(static::$staticResults["{:method}"])) {',
  383. ' static::$staticResults["{:method}"] = [];',
  384. ' }',
  385. ' static::$staticResults["{:method}"][] = [',
  386. ' "args" => func_get_args(),',
  387. ' "result" => $result,',
  388. ' "time" => microtime(true),',
  389. ' ];',
  390. ' return $result;',
  391. '}',
  392. ],
  393. 'method' => [
  394. '{:modifiers} function {:method}({:args}) {',
  395. ' $args = compact({:stringArgs});',
  396. ' $args["hash"] = spl_object_hash($this->mocker);',
  397. ' $_method = [$this->mocker, "{:method}"];',
  398. ' $result = _Filters::run(__CLASS__, "{:method}", $args,',
  399. ' function($args) use(&$_method) {',
  400. ' return call_user_func_array($_method, $args);',
  401. ' }',
  402. ' );',
  403. ' if (!isset($this->results["{:method}"])) {',
  404. ' $this->results["{:method}"] = [];',
  405. ' }',
  406. ' $this->results["{:method}"][] = [',
  407. ' "args" => func_get_args(),',
  408. ' "result" => $result,',
  409. ' "time" => microtime(true),',
  410. ' ];',
  411. ' return $result;',
  412. '}',
  413. ],
  414. 'applyFilter' => [
  415. 'public {:static} function applyFilter($method, $filter = null) {',
  416. ' $message = "<mocked class>::applyFilter() is deprecated. ";',
  417. ' $message .= "Use Filters::applyFilter(" . __CLASS__ .", ...) instead.";',
  418. ' // trigger_error($message, E_USER_DEPRECATED);',
  419. ' foreach ((array) $method as $m) {',
  420. ' if ($filter === null) {',
  421. ' _Filters::clear(__CLASS__, $m);',
  422. ' } else {',
  423. ' _Filters::apply(__CLASS__, $m, $filter);',
  424. ' }',
  425. ' }',
  426. '}',
  427. ],
  428. 'endClass' => [
  429. '}',
  430. ],
  431. ];
  432. /**
  433. * A list of methods we should not overwrite in our mock class.
  434. *
  435. * Some of these methods are are too custom inside the `Mock` or `Delegate`,
  436. * while others should simply not be filtered.
  437. *
  438. * @var array
  439. */
  440. protected static $_blackList = [
  441. '__destruct', '_parents',
  442. '__get', '__set', '__isset', '__unset', '__sleep',
  443. '__wakeup', '__toString', '__clone', '__invoke',
  444. '_stop', '_init', 'invokeMethod', '__set_state',
  445. '_instance', '_object', '_initialize',
  446. '_filter', 'applyFilter',
  447. ];
  448. /**
  449. * Will register this class into the autoloader.
  450. *
  451. * @return void
  452. */
  453. public static function register() {
  454. spl_autoload_register([__CLASS__, 'create']);
  455. }
  456. /**
  457. * The main entrance to create a new Mock class.
  458. *
  459. * @param string $mockee The fully namespaced `\Mock` class
  460. * @return void
  461. */
  462. public static function create($mockee) {
  463. if (!static::_validateMockee($mockee)) {
  464. return;
  465. }
  466. $mocker = static::_mocker($mockee);
  467. $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObjectDeprecated');
  468. $tokens = [
  469. 'namespace' => static::_namespace($mockee),
  470. 'mocker' => $mocker,
  471. 'mockee' => 'MockDelegate',
  472. 'static' => $isStatic ? 'static' : '',
  473. ];
  474. $mockDelegate = static::_dynamicCode('mockDelegate', 'startClass', $tokens);
  475. $mock = static::_dynamicCode('mock', 'startClass', $tokens);
  476. $reflectedClass = new ReflectionClass($mocker);
  477. $reflecedMethods = $reflectedClass->getMethods();
  478. $getByReference = false;
  479. $staticApplyFilter = true;
  480. $constructor = false;
  481. foreach ($reflecedMethods as $methodId => $method) {
  482. if (!in_array($method->name, static::$_blackList)) {
  483. $key = $method->isStatic() ? 'staticMethod' : 'method';
  484. if ($method->name === '__construct') {
  485. $key = 'constructor';
  486. $constructor = true;
  487. }
  488. $docs = ReflectionMethod::export($mocker, $method->name, true);
  489. if (preg_match('/&' . $method->name . '/', $docs) === 1) {
  490. continue;
  491. }
  492. $tokens = [
  493. 'namespace' => static::_namespace($mockee),
  494. 'method' => $method->name,
  495. 'modifiers' => static::_methodModifiers($method),
  496. 'args' => static::_methodParams($method),
  497. 'stringArgs' => static::_stringMethodParams($method),
  498. 'mocker' => $mocker,
  499. ];
  500. $mockDelegate .= static::_dynamicCode('mockDelegate', $key, $tokens);
  501. $mock .= static::_dynamicCode('mock', $key, $tokens);
  502. } elseif ($method->name === '__get') {
  503. $docs = ReflectionMethod::export($mocker, '__get', true);
  504. $getByReference = preg_match('/&__get/', $docs) === 1;
  505. } elseif ($method->name === 'applyFilter') {
  506. $staticApplyFilter = $method->isStatic();
  507. }
  508. }
  509. if (!$constructor) {
  510. $tokens = [
  511. 'namespace' => static::_namespace($mockee),
  512. 'modifiers' => 'public',
  513. 'args' => null,
  514. 'stringArgs' => 'array()',
  515. 'mocker' => $mocker,
  516. ];
  517. $mock .= static::_dynamicCode('mock', 'constructor', $tokens);
  518. $mockDelegate .= static::_dynamicCode('mockDelegate', 'constructor', $tokens);
  519. }
  520. $mockDelegate .= static::_dynamicCode('mockDelegate', 'endClass');
  521. $mock .= static::_dynamicCode('mock', 'get', [
  522. 'reference' => $getByReference ? '&' : '',
  523. ]);
  524. $mock .= static::_dynamicCode('mock', 'set');
  525. $mock .= static::_dynamicCode('mock', 'isset');
  526. $mock .= static::_dynamicCode('mock', 'unset');
  527. $mock .= static::_dynamicCode('mock', 'applyFilter', [
  528. 'static' => $staticApplyFilter ? 'static' : '',
  529. ]);
  530. $mock .= static::_dynamicCode('mock', 'destructor');
  531. $mock .= static::_dynamicCode('mock', 'endClass');
  532. eval($mockDelegate . $mock);
  533. }
  534. /**
  535. * Will determine what method mofifiers of a method.
  536. *
  537. * For instance: 'public static' or 'private abstract'
  538. *
  539. * @param ReflectionMethod $method
  540. * @return string
  541. */
  542. protected static function _methodModifiers(ReflectionMethod $method) {
  543. $modifierKey = $method->getModifiers();
  544. $modifierArray = Reflection::getModifierNames($modifierKey);
  545. $modifiers = implode(' ', $modifierArray);
  546. return str_replace(['private', 'protected'], 'public', $modifiers);
  547. }
  548. /**
  549. * Will determine what parameter prototype of a method.
  550. *
  551. * For instance: 'ReflectionFunctionAbstract $method' or '$name, array $foo = null'
  552. *
  553. * @param ReflectionFunctionAbstract $method
  554. * @return string
  555. */
  556. protected static function _methodParams(ReflectionFunctionAbstract $method) {
  557. $pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/';
  558. $replace = [
  559. 'from' => [' Array', 'or NULL'],
  560. 'to' => [' array()', ''],
  561. ];
  562. preg_match_all($pattern, $method, $matches);
  563. $params = implode(', ', $matches[1]);
  564. return str_replace($replace['from'], $replace['to'], $params);
  565. }
  566. /**
  567. * Will return the params in a way that can be placed into `compact()`
  568. *
  569. * @param ReflectionFunctionAbstract $method
  570. * @return string
  571. */
  572. protected static function _stringMethodParams(ReflectionFunctionAbstract $method) {
  573. $pattern = '/Parameter [^$]+\$([^ ]+)/';
  574. preg_match_all($pattern, $method, $matches);
  575. $params = implode("', '", $matches[1]);
  576. return strlen($params) > 0 ? "'{$params}'" : 'array()';
  577. }
  578. /**
  579. * Will generate the code you are wanting.
  580. *
  581. * This pulls from $_mockDelegateIngredients and $_mockIngredients.
  582. *
  583. * @param string $type The name of the array of ingredients to use
  584. * @param string $key The key from the array of ingredients
  585. * @param array $tokens Tokens, if any, that should be inserted
  586. * @return string
  587. */
  588. protected static function _dynamicCode($type, $key, $tokens = []) {
  589. $defaults = [
  590. 'master' => '\lithium\test\Mocker',
  591. ];
  592. $tokens += $defaults;
  593. $name = '_' . $type . 'Ingredients';
  594. $code = implode("\n", static::${$name}[$key]);
  595. return Text::insert($code, $tokens) . "\n";
  596. }
  597. /**
  598. * Will generate the mocker from the current mockee.
  599. *
  600. * @param string $mockee The fully namespaced `\Mock` class
  601. * @return array
  602. */
  603. protected static function _mocker($mockee) {
  604. $sections = explode('\\', $mockee);
  605. array_pop($sections);
  606. $sections[] = ucfirst(array_pop($sections));
  607. return implode('\\', $sections);
  608. }
  609. /**
  610. * Will generate the namespace from the current mockee.
  611. *
  612. * @param string $mockee The fully namespaced `\Mock` class
  613. * @return string
  614. */
  615. protected static function _namespace($mockee) {
  616. $matches = [];
  617. preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches);
  618. return isset($matches[1][0]) ? $matches[1][0] : null;
  619. }
  620. /**
  621. * Will validate if mockee is a valid class we should mock.
  622. *
  623. * Will fail if the mock already exists, or it doesn't contain `\Mock` in
  624. * the namespace.
  625. *
  626. * @param string $mockee The fully namespaced `\Mock` class
  627. * @return bool
  628. */
  629. protected static function _validateMockee($mockee) {
  630. return preg_match('/\\\\Mock$/', $mockee) === 1;
  631. }
  632. /**
  633. * Generate a chain class with the current rules of the mock.
  634. *
  635. * @param mixed $mock Mock object, namespaced static mock, namespaced function name.
  636. * @return object MockerChain instance
  637. */
  638. public static function chain($mock) {
  639. $results = [];
  640. $string = is_string($mock);
  641. if (is_object($mock) && isset($mock->results)) {
  642. $results = static::mergeResults($mock->results, $mock::$staticResults);
  643. } elseif ($string && class_exists($mock) && isset($mock::$staticResults)) {
  644. $results = $mock::$staticResults;
  645. } elseif ($string && function_exists($mock) && isset(static::$_functionResults[$mock])) {
  646. $results = [$mock => static::$_functionResults[$mock]];
  647. }
  648. return new MockerChain($results);
  649. }
  650. /**
  651. * Will merge two sets of results into each other.
  652. *
  653. * @param array $results
  654. * @param array $secondary
  655. * @return array
  656. */
  657. public static function mergeResults($results, $secondary) {
  658. foreach ($results as $method => $calls) {
  659. if (isset($secondary[$method])) {
  660. $results['method1'] = array_merge($results['method1'], $secondary['method1']);
  661. usort($results['method1'], function($el1, $el2) {
  662. return strcmp($el1['time'], $el2['time']);
  663. });
  664. unset($secondary['method1']);
  665. }
  666. }
  667. return $results + $secondary;
  668. }
  669. /**
  670. * Calls a method on this object with the given parameters. Provides an OO wrapper for
  671. * `forward_static_call_array()`.
  672. *
  673. * @param string $method Name of the method to call.
  674. * @param array $params Parameter list to use when calling `$method`.
  675. * @return mixed Returns the result of the method call.
  676. */
  677. public static function invokeMethod($method, $params = []) {
  678. return forward_static_call_array([get_called_class(), $method], $params);
  679. }
  680. /**
  681. * Will overwrite namespaced functions.
  682. *
  683. * @param string|bool $name Fully namespaced function, or `false` to reset functions.
  684. * @param closure|bool $callback Callback to be called, or `false` to reset this function.
  685. * @return void
  686. */
  687. public static function overwriteFunction($name, $callback = null) {
  688. if ($name === false) {
  689. static::$_functionResults = [];
  690. return static::$_functionCallbacks = [];
  691. }
  692. if ($callback === false) {
  693. static::$_functionResults[$name] = [];
  694. return static::$_functionCallbacks[$name] = false;
  695. }
  696. static::$_functionCallbacks[$name] = $callback;
  697. if (function_exists($name)) {
  698. return;
  699. }
  700. $function = new ReflectionFunction($callback);
  701. $pos = strrpos($name, '\\');
  702. eval(static::_dynamicCode('mockFunction', 'function', [
  703. 'namespace' => substr($name, 0, $pos),
  704. 'function' => substr($name, $pos + 1),
  705. 'args' => static::_methodParams($function),
  706. 'stringArgs' => static::_stringMethodParams($function),
  707. ]));
  708. return;
  709. }
  710. /**
  711. * A method to call user defined functions.
  712. *
  713. * This method should only be accessed by functions created by `Mocker::overwriteFunction()`.
  714. *
  715. * If no matching stored function exists, the global function will be called instead.
  716. *
  717. * @param string $name Fully namespaced function name to call.
  718. * @param array $params Params to be passed to the function.
  719. * @return mixed
  720. */
  721. public static function callFunction($name, array &$params = []) {
  722. $function = substr($name, strrpos($name, '\\'));
  723. $exists = isset(static::$_functionCallbacks[$name]);
  724. if ($exists && is_callable(static::$_functionCallbacks[$name])) {
  725. $function = static::$_functionCallbacks[$name];
  726. }
  727. $result = call_user_func_array($function, $params);
  728. if (!isset(static::$_functionResults[$name])) {
  729. static::$_functionResults[$name] = [];
  730. }
  731. static::$_functionResults[$name][] = [
  732. 'args' => $params,
  733. 'result' => $result,
  734. 'time' => microtime(true),
  735. ];
  736. return $result;
  737. }
  738. /* Deprecated / BC */
  739. /**
  740. * Stores the closures that represent the method filters. They are indexed by called class.
  741. *
  742. * @deprecated
  743. * @var array Method filters, indexed by class.
  744. */
  745. protected static $_methodFilters = [];
  746. /**
  747. * Apply a closure to a method of the current static object.
  748. *
  749. * @deprecated
  750. * @see lithium\core\StaticObject::_filter()
  751. * @see lithium\util\collection\Filters
  752. * @param string $class Fully namespaced class to apply filters.
  753. * @param mixed $method The name of the method to apply the closure to. Can either be a single
  754. * method name as a string, or an array of method names. Can also be false to remove
  755. * all filters on the current object.
  756. * @param \Closure $filter The closure that is used to filter the method(s), can also be false
  757. * to remove all the current filters for the given method.
  758. * @return void
  759. */
  760. public static function applyFilter($class, $method = null, $filter = null) {
  761. $message = '`' . __METHOD__ . '()` has been deprecated in favor of ';
  762. $message .= '`\lithium\aop\Filters::apply()` and `::clear()`.';
  763. trigger_error($message, E_USER_DEPRECATED);
  764. $class = get_called_class();
  765. if ($method === false) {
  766. Filters::clear($class);
  767. return;
  768. }
  769. foreach ((array) $method as $m) {
  770. if ($filter === false) {
  771. Filters::clear($class, $m);
  772. } else {
  773. Filters::apply($class, $m, $filter);
  774. }
  775. }
  776. }
  777. /**
  778. * Executes a set of filters against a method by taking a method's main implementation as a
  779. * callback, and iteratively wrapping the filters around it.
  780. *
  781. * @deprecated
  782. * @see lithium\util\collection\Filters
  783. * @param string $class Fully namespaced class to apply filters.
  784. * @param string|array $method The name of the method being executed, or an array containing
  785. * the name of the class that defined the method, and the method name.
  786. * @param array $params An associative array containing all the parameters passed into
  787. * the method.
  788. * @param \Closure $callback The method's implementation, wrapped in a closure.
  789. * @param array $filters Additional filters to apply to the method for this call only.
  790. * @return mixed
  791. */
  792. protected static function _filter($class, $method, $params, $callback, $filters = []) {
  793. $message = '`' . __METHOD__ . '()` has been deprecated in favor of ';
  794. $message .= '`\lithium\aop\Filters::run()` and `::apply()`.';
  795. trigger_error($message, E_USER_DEPRECATED);
  796. $class = get_called_class();
  797. foreach ($filters as $filter) {
  798. Filters::apply($class, $method, $filter);
  799. }
  800. return Filters::run($class, $method, $params, $callback);
  801. }
  802. }
  803. ?>