Mocker.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\test;
  9. use lithium\util\String;
  10. use ReflectionClass;
  11. use ReflectionMethod;
  12. use Reflection;
  13. /**
  14. * The Mocker class aids in the creation of Mocks on the fly, allowing you to
  15. * use Lithium filters on most methods in the class.
  16. *
  17. * To enable the autoloading of mocks you simply need to make a simple method
  18. * call.
  19. * {{{
  20. * use lithium\core\Environment;
  21. * use lithium\test\Mocker;
  22. * if (!Environment::is('production')) {
  23. * Mocker::register();
  24. * }
  25. * }}}
  26. *
  27. * You can also enable autoloading inside the setup of a unit test class. This
  28. * method can be called redundantly.
  29. * {{{
  30. * use lithium\test\Mocker;
  31. * class MockerTest extends \lithium\test\Unit {
  32. * public function setUp() {
  33. * Mocker::register();
  34. * }
  35. * }
  36. * }}}
  37. *
  38. * Using Mocker is the fun magical part, it's autoloaded so simply call the
  39. * class you want to mock with the '\Mock' at the end. The autoloader will
  40. * detect you want to autoload it, and create it for you. Now you can filter
  41. * any method.
  42. * {{{
  43. * use lithium\console\dispatcher\Mock as DispatcherMock;
  44. * $dispatcher = new DispatcherMock();
  45. * $dispatcher->applyFilter('config', function($self, $params, $chain) {
  46. * return array();
  47. * });
  48. * $results = $dispatcher->config();
  49. * }}}
  50. * {{{
  51. * use lithium\analysis\parser\Mock as ParserMock;
  52. * $code = 'echo "foobar";';
  53. * ParserMock::applyFilter('config', function($self, $params, $chain) {
  54. * return array();
  55. * });
  56. * $tokens = ParserMock::tokenize($code, array('wrap' => true));
  57. * }}}
  58. */
  59. class Mocker {
  60. /**
  61. * A list of code to be generated for the delegator.
  62. *
  63. * The MockDelgate directly extends the mocker and makes all methods
  64. * publically available to other classes but should not be accessed directly
  65. * by any other application. This should be called only by the mocker and
  66. * the mockee and never by the consumer.
  67. *
  68. * @var array
  69. */
  70. protected static $_mockDelegateIngredients = array(
  71. 'startClass' => array(
  72. 'namespace {:namespace};',
  73. 'class MockDelegate extends \{:mocker} {'
  74. ),
  75. 'constructor' => array(
  76. '{:modifiers} function __construct({:args}) {',
  77. ' $args = func_get_args();',
  78. ' $this->parent = array_pop($args);',
  79. ' $this->parent->mocker = $this;',
  80. ' call_user_func_array("parent::__construct", $args);',
  81. '}',
  82. ),
  83. 'method' => array(
  84. '{:modifiers} function {:method}({:args}) {',
  85. ' $args = func_get_args();',
  86. ' $token = spl_object_hash($this);',
  87. ' $id = count($args) - 1;',
  88. ' if (!isset($args[$id]) || $args[$id] !== $token) {',
  89. ' $method = array($this->parent, "{:method}");',
  90. ' return call_user_func_array($method, $args);',
  91. ' }',
  92. ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
  93. '}',
  94. ),
  95. 'staticMethod' => array(
  96. '{:modifiers} function {:method}({:args}) {',
  97. ' $args = func_get_args();',
  98. ' $token = "1f3870be274f6c49b3e31a0c6728957f";',
  99. ' $id = count($args) - 1;',
  100. ' if (!isset($args[$id]) || $args[$id] !== $token) {',
  101. ' $method = \'{:namespace}\Mock::{:method}\';',
  102. ' return call_user_func_array($method, $args);',
  103. ' }',
  104. ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));',
  105. '}',
  106. ),
  107. 'endClass' => array(
  108. '}',
  109. ),
  110. );
  111. /**
  112. * A list of code to be generated for the mocker.
  113. *
  114. * The Mock class directly extends the mock class but only directly
  115. * interacts with the MockDelegate directly. This class is the actual
  116. * interface for consumers, instantiation or static method calls, and can
  117. * have most of its methods filtered.
  118. *
  119. * The `$results` variable holds all method calls allowing you for you
  120. * make your own custom assertions on them.
  121. *
  122. * @var array
  123. */
  124. protected static $_mockIngredients = array(
  125. 'startClass' => array(
  126. 'namespace {:namespace};',
  127. 'class Mock extends \{:mocker} {',
  128. ' public $mocker;',
  129. ' public {:static} $results = array();',
  130. ' protected $_safeVars = array(',
  131. ' "_classes",',
  132. ' "_methodFilters",',
  133. ' "mocker",',
  134. ' "_safeVars",',
  135. ' "results",',
  136. ' );',
  137. ),
  138. 'get' => array(
  139. 'public function {:reference}__get($name) {',
  140. ' $data ={:reference} $this->mocker->$name;',
  141. ' return $data;',
  142. '}',
  143. ),
  144. 'set' => array(
  145. 'public function __set($name, $value = null) {',
  146. ' return $this->mocker->$name = $value;',
  147. '}',
  148. ),
  149. 'constructor' => array(
  150. '{:modifiers} function __construct({:args}) {',
  151. ' $args = array_values(get_defined_vars());',
  152. ' array_push($args, $this);',
  153. ' foreach ($this as $key => $value) {',
  154. ' if (!in_array($key, $this->_safeVars)) {',
  155. ' unset($this->$key);',
  156. ' }',
  157. ' }',
  158. ' $class = new \ReflectionClass(\'{:namespace}\MockDelegate\');',
  159. ' $class->newInstanceArgs($args);',
  160. '}',
  161. ),
  162. 'destructor' => array(
  163. 'public function __destruct() {}',
  164. ),
  165. 'staticMethod' => array(
  166. '{:modifiers} function {:method}({:args}) {',
  167. ' $args = compact({:stringArgs});',
  168. ' $args["hash"] = "1f3870be274f6c49b3e31a0c6728957f";',
  169. ' $method = \'{:namespace}\MockDelegate::{:method}\';',
  170. ' $result = self::_filter("{:method}", $args, function($self, $args) use(&$method) {',
  171. ' return call_user_func_array($method, $args);',
  172. ' });',
  173. ' if (!isset(self::$results["{:method}"])) {',
  174. ' self::$results["{:method}"] = array();',
  175. ' }',
  176. ' self::$results["{:method}"][] = array(',
  177. ' "args" => func_get_args(),',
  178. ' "result" => $result,',
  179. ' "time" => microtime(true),',
  180. ' );',
  181. ' return $result;',
  182. '}',
  183. ),
  184. 'method' => array(
  185. '{:modifiers} function {:method}({:args}) {',
  186. ' $args = compact({:stringArgs});',
  187. ' $args["hash"] = spl_object_hash($this->mocker);',
  188. ' $method = array($this->mocker, "{:method}");',
  189. ' $result = $this->_filter(__METHOD__, $args, function($self, $args) use(&$method) {',
  190. ' return call_user_func_array($method, $args);',
  191. ' });',
  192. ' if (!isset($this->results["{:method}"])) {',
  193. ' $this->results["{:method}"] = array();',
  194. ' }',
  195. ' $this->results["{:method}"][] = array(',
  196. ' "args" => func_get_args(),',
  197. ' "result" => $result,',
  198. ' "time" => microtime(true),',
  199. ' );',
  200. ' return $result;',
  201. '}',
  202. ),
  203. 'endClass' => array(
  204. '}',
  205. ),
  206. );
  207. /**
  208. * A list of methods we should not overwrite in our mock class.
  209. *
  210. * @var array
  211. */
  212. protected static $_blackList = array(
  213. '__destruct', '__call', '__callStatic', '_parents',
  214. '__get', '__set', '__isset', '__unset', '__sleep',
  215. '__wakeup', '__toString', '__clone', '__invoke',
  216. '_stop', '_init', 'invokeMethod', '__set_state',
  217. '_instance', '_filter', '_object', '_initialize',
  218. 'applyFilter',
  219. );
  220. /**
  221. * Will register this class into the autoloader.
  222. *
  223. * @return void
  224. */
  225. public static function register() {
  226. spl_autoload_register(array(__CLASS__, 'create'));
  227. }
  228. /**
  229. * The main entrance to create a new Mock class.
  230. *
  231. * @param string $mockee The fully namespaced `\Mock` class
  232. * @return void
  233. */
  234. public static function create($mockee) {
  235. if (!self::_validateMockee($mockee)) {
  236. return;
  237. }
  238. $mocker = self::_mocker($mockee);
  239. $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
  240. $tokens = array(
  241. 'namespace' => self::_namespace($mockee),
  242. 'mocker' => $mocker,
  243. 'mockee' => 'MockDelegate',
  244. 'static' => $isStatic ? 'static' : '',
  245. );
  246. $mockDelegate = self::_dynamicCode('mockDelegate', 'startClass', $tokens);
  247. $mock = self::_dynamicCode('mock', 'startClass', $tokens);
  248. $reflectedClass = new ReflectionClass($mocker);
  249. $reflecedMethods = $reflectedClass->getMethods();
  250. $getByReference = false;
  251. foreach ($reflecedMethods as $methodId => $method) {
  252. if (!in_array($method->name, self::$_blackList)) {
  253. $key = $method->isStatic() ? 'staticMethod' : 'method';
  254. $key = $method->name === '__construct' ? 'constructor' : $key;
  255. $docs = ReflectionMethod::export($mocker, $method->name, true);
  256. if (preg_match('/&' . $method->name . '/', $docs) === 1) {
  257. continue;
  258. }
  259. $tokens = array(
  260. 'namespace' => self::_namespace($mockee),
  261. 'method' => $method->name,
  262. 'modifiers' => self::_methodModifiers($method),
  263. 'args' => self::_methodParams($method),
  264. 'stringArgs' => self::_stringMethodParams($method),
  265. 'mocker' => $mocker,
  266. );
  267. $mockDelegate .= self::_dynamicCode('mockDelegate', $key, $tokens);
  268. $mock .= self::_dynamicCode('mock', $key, $tokens);
  269. } elseif ($method->name === '__get') {
  270. $docs = ReflectionMethod::export($mocker, '__get', true);
  271. $getByReference = preg_match('/&__get/', $docs) === 1;
  272. }
  273. }
  274. $mockDelegate .= self::_dynamicCode('mockDelegate', 'endClass');
  275. $mock .= self::_dynamicCode('mock', 'get', array(
  276. 'reference' => $getByReference ? '&' : '',
  277. ));
  278. $mock .= self::_dynamicCode('mock', 'set');
  279. $mock .= self::_dynamicCode('mock', 'destructor');
  280. $mock .= self::_dynamicCode('mock', 'endClass');
  281. eval($mockDelegate . $mock);
  282. }
  283. /**
  284. * Will determine what method mofifiers of a method.
  285. *
  286. * For instance: 'public static' or 'private abstract'
  287. *
  288. * @param ReflectionMethod $method
  289. * @return string
  290. */
  291. protected static function _methodModifiers(ReflectionMethod $method) {
  292. $modifierKey = $method->getModifiers();
  293. $modifierArray = Reflection::getModifierNames($modifierKey);
  294. $modifiers = implode(' ', $modifierArray);
  295. return str_replace(array('private', 'protected'), 'public', $modifiers);
  296. }
  297. /**
  298. * Will determine what parameter prototype of a method.
  299. *
  300. * For instance: 'ReflectionMethod $method' or '$name, array $foo = null'
  301. *
  302. * @param ReflectionMethod $method
  303. * @return string
  304. */
  305. protected static function _methodParams(ReflectionMethod $method) {
  306. $pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/';
  307. $replace = array(
  308. 'from' => array('Array', 'or NULL'),
  309. 'to' => array('array()', ''),
  310. );
  311. preg_match_all($pattern, $method, $matches);
  312. $params = implode(', ', $matches[1]);
  313. return str_replace($replace['from'], $replace['to'], $params);
  314. }
  315. /**
  316. * Will return the params in a way that can be placed into `compact()`
  317. *
  318. * @param ReflectionMethod $method
  319. * @return string
  320. */
  321. protected static function _stringMethodParams(ReflectionMethod $method) {
  322. $pattern = '/Parameter [^$]+\$([^ ]+)/';
  323. preg_match_all($pattern, $method, $matches);
  324. $params = implode("', '", $matches[1]);
  325. return strlen($params) > 0 ? "'{$params}'" : 'array()';
  326. }
  327. /**
  328. * Will generate the code you are wanting.
  329. *
  330. * This pulls from $_mockDelegateIngredients and $_mockIngredients.
  331. *
  332. * @param string $type The name of the array of ingredients to use
  333. * @param string $key The key from the array of ingredients
  334. * @param array $tokens Tokens, if any, that should be inserted
  335. * @return string
  336. */
  337. protected static function _dynamicCode($type, $key, $tokens = array()) {
  338. $name = '_' . $type . 'Ingredients';
  339. $code = implode("\n", self::${$name}[$key]);
  340. return String::insert($code, $tokens) . "\n";
  341. }
  342. /**
  343. * Will generate the mocker from the current mockee.
  344. *
  345. * @param string $mockee The fully namespaced `\Mock` class
  346. * @return array
  347. */
  348. protected static function _mocker($mockee) {
  349. $matches = array();
  350. preg_match_all('/^(.*)\\\\([^\\\\]+)\\\\Mock$/', $mockee, $matches);
  351. if (!isset($matches[1][0])) {
  352. return;
  353. }
  354. return $matches[1][0] . '\\' . ucfirst($matches[2][0]);
  355. }
  356. /**
  357. * Will generate the namespace from the current mockee.
  358. *
  359. * @param string $mockee The fully namespaced `\Mock` class
  360. * @return string
  361. */
  362. protected static function _namespace($mockee) {
  363. $matches = array();
  364. preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches);
  365. return isset($matches[1][0]) ? $matches[1][0] : null;
  366. }
  367. /**
  368. * Will validate if mockee is a valid class we should mock.
  369. *
  370. * @param string $mockee The fully namespaced `\Mock` class
  371. * @return bool
  372. */
  373. protected static function _validateMockee($mockee) {
  374. if (class_exists($mockee) || preg_match('/\\\\Mock$/', $mockee) !== 1) {
  375. return false;
  376. }
  377. $mocker = self::_mocker($mockee);
  378. $isObject = is_subclass_of($mocker, 'lithium\core\Object');
  379. $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObject');
  380. if (!$isObject && !$isStatic) {
  381. return false;
  382. }
  383. return true;
  384. }
  385. /**
  386. * Generate a chain class with the current rules of the mock.
  387. *
  388. * @param object $mock Mock to chain
  389. * @return object MockerChain instance
  390. */
  391. public static function chain($mock) {
  392. $results = array();
  393. if (is_object($mock) && isset($mock->results)) {
  394. $results = $mock->results;
  395. } elseif (is_string($mock) && class_exists($mock) && isset($mock::$results)) {
  396. $results = $mock::$results;
  397. }
  398. return new MockerChain($results);
  399. }
  400. }
  401. ?>