applyFilter('config', function($params, $next) { * return []; * }); * $results = $dispatcher->config(); * ``` * ``` * use lithium\analysis\parser\Mock as ParserMock; * $code = 'echo "foobar";'; * ParserMock::applyFilter('config', function($params, $next) { * return []; * }); * $tokens = ParserMock::tokenize($code, ['wrap' => true]); * ``` * * Mocker also gives the ability, if used correctly, to stub build in php * function calls. Consider the following example. * ``` * namespace app\extensions; * * class AwesomeFileEditor { * * public static function updateJson($file) { * if (file_exists($file)) { * $time = microtime(true); * $packages = json_decode(file_get_contents($file), true); * foreach ($packages['users'] as &$package) { * $package['updated'] = $time; * } * return $packages; * } * return false; * } * * } * ``` * ``` * namespace app\tests\cases\extensions; * * use lithium\test\Mocker; * use app\extensions\AwesomeFileEditor; * * class AwesomeFileEditorTest extends \lithium\test\Unit { * * public function setUp() { * Mocker::overwriteFunction(false); * } * * public function testUpdateJson() { * Mocker::overwriteFunction('app\extensions\file_exists', function() { * return true; * }); * Mocker::overwriteFunction('app\extensions\file_get_contents', function() { * return <<assertNotEqual(0, $results['users'][0]['updated']); * } * * } * ``` * * ## How does Mocking classes work? * This section isn't necessary to read, but can help others better understand * it so that they can add new features, or debug current ones. * * ### TLDR * The `Mocker` class dynamically makes two classes, a `Delegate` and a `Mock`. * Both of these classes extend the target class. The `Delegate` is passed into * the `Mock` class for it to call within (anonymous functions) filters. This * allows public and protected methods to be filterable. * * ### Theory * I'll walk you through the steps I did in order to figure out how `Mocker` * should work. The goal here is to mock class `Person`. * * ``` * class Person { * public function speak() { * $this->_openMouth(); * return true; * } * protected function _openMouth() { * return $this->mouth = 'open'; * } * } * ``` * * In order to make the `speak()` method filterable we'll need to create a class * called `MockPerson` and we'll make its `speak()` method filterable, however * there is already an issue since a filter works inside of an anonymous * function you cannot call `parent`, so `MockPerson` will also need an instance * of `Person`. * * ``` * class MockPerson extends Person { * public $person; * public function speak() { * $params = compact(); * $person = $this->person; * return Filters::run($this, __FUNCTION__, [], function($params) use (&$person) { * return $person->speak(); * }; * } * } * ``` * * You might stop here and call it a day, but what about filtering protected * methods? For example you might want to make sure `_openMouth()` does not * modify the class. However this isn't possible with the current implementation * since `_openMouth` is protected and we can't call protected methods within an * anonymous function. The trick is that when you are extending a class you can * make a method MORE visible than its parent, with the exception of private * methods. So let's make a class `DelegatePerson` that simply extends `Person` * and makes `_openMouth()` public. * * ``` * class DelegatePerson extends Person { * public function _openMouth() { * parent::_openMouth(); * } * } * ``` * * Now we simply pass `DelegatePerson` to `MockPerson` and all methods are now * filterable. * * ## How does overwriting PHP functions work? * In short, this is a hack. When you are inside of a namespace `foo\bar\baz` * and you call a function `file_get_contents` it first searches the current * namespace for that function `foo\bar\baz\file_get_contents`. `Mocker` simply * creates that function dynamically, so when its called it delegates back to * `Mocker` which will determine if it should call a user-defined function or * if it should go back to the original PHP function. * * @deprecated Please use an alternative mocking framework, i.e. Mockery. */ class Mocker { /** * Functions to be called instead of the original. * * The key is the fully namespaced function name, and the value is the closure to be called. * * @var array */ protected static $_functionCallbacks = []; /** * Results of function calls for later assertion in `MockerChain`. * * @var array */ protected static $_functionResults = []; /** * A list of code to be generated for the `Delegate`. * * The `Delegate` directly extends the class you wish to mock and makes all * methods publically available to other classes but should not be accessed * directly by any other classes other than `Mock`. * * @item variable `$parent` Instance of `Mock`. Allows `Delegate` to send * calls back to `Mock` if it was called directly * from a parent class. * @var array */ protected static $_mockDelegateIngredients = [ 'startClass' => [ 'namespace {:namespace};', 'class MockDelegate extends \{:mocker} {', ' public $parent = null;', ], 'constructor' => [ '{:modifiers} function __construct({:args}) {', ' $args = compact({:stringArgs});', ' $argCount = func_num_args();', ' $this->parent = $argCount === 0 ? false : func_get_arg($argCount - 1);', ' if (!is_a($this->parent, __NAMESPACE__ . "\Mock")) {', ' $class = new \ReflectionClass(\'{:namespace}\Mock\');', ' $this->parent = $class->newInstanceArgs($args);', ' }', ' $this->parent->mocker = $this;', ' if (method_exists(\'{:mocker}\', "__construct")) {', ' call_user_func_array("parent::__construct", $args);', ' }', '}', ], 'method' => [ '{:modifiers} function {:method}({:args}) {', ' $args = compact({:stringArgs});', ' $token = spl_object_hash($this);', ' if (func_num_args() > 0 && func_get_arg(func_num_args() - 1) === $token) {', ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));', ' }', ' $method = [$this->parent, "{:method}"];', ' return call_user_func_array($method, $args);', '}', ], 'staticMethod' => [ '{:modifiers} function {:method}({:args}) {', ' $args = compact({:stringArgs});', ' $token = "1f3870be274f6c49b3e31a0c6728957f";', ' if (func_num_args() > 0 && func_get_arg(func_num_args() - 1) === $token) {', ' return call_user_func_array("parent::{:method}", compact({:stringArgs}));', ' }', ' $method = \'{:namespace}\Mock::{:method}\';', ' return call_user_func_array($method, $args);', '}', ], 'endClass' => [ '}', ], ]; /** * List of code to be generated for overwriting php functions. * * @var array */ protected static $_mockFunctionIngredients = [ 'function' => [ 'namespace {:namespace};', 'use lithium\test\Mocker;', 'function {:function}({:args}) {', ' $params = [];', ' foreach ([{:stringArgs}] as $value) {', ' if (!empty($value)) {', ' $params[] =& ${$value};', ' }', ' }', ' return Mocker::callFunction(__FUNCTION__, $params);', '}', ], ]; /** * A list of code to be generated for the `Mock`. * * The Mock class directly extends the class you wish to mock but only * interacts with the `Delegate` directly. This class is the public * interface for users. * * @item variable `$results` All method calls allowing you for you make your * own custom assertions. * @item variable `$staticResults` See `$results`. * @item variable `$mocker` Home of the `Delegate` defined above. * @item variable `$_safeVars` Variables that should not be deleted on * `Mock`. We delete them so they cannot be * accessed directly, but sent to `Delegate` via * PHP magic methods on `Mock`. * @var array */ protected static $_mockIngredients = [ 'startClass' => [ 'namespace {:namespace};', 'use lithium\aop\Filters as _Filters;', 'class Mock extends \{:mocker} {', ' public $mocker;', ' public $results = [];', ' public static $staticResults = [];', ' protected $_safeVars = [', ' "_classes",', ' "mocker",', ' "_safeVars",', ' "results",', ' "staticResults",', ' "_methodFilters",', ' ];', ], 'get' => [ 'public function {:reference}__get($name) {', ' $data ={:reference} $this->mocker->$name;', ' return $data;', '}', ], 'set' => [ 'public function __set($name, $value = null) {', ' return $this->mocker->$name = $value;', '}', ], 'isset' => [ 'public function __isset($name) {', ' return isset($this->mocker->$name);', '}', ], 'unset' => [ 'public function __unset($name) {', ' unset($this->mocker->$name);', '}', ], 'constructor' => [ '{:modifiers} function __construct({:args}) {', ' $args = compact({:stringArgs});', ' array_push($args, $this);', ' foreach (get_class_vars(get_class($this)) as $key => $value) {', ' if (isset($this->{$key}) && !in_array($key, $this->_safeVars)) {', ' unset($this->$key);', ' }', ' }', ' $class = new \ReflectionClass(\'{:namespace}\MockDelegate\');', ' $class->newInstanceArgs($args);', '}', ], 'destructor' => [ 'public function __destruct() {}', ], 'staticMethod' => [ '{:modifiers} function {:method}({:args}) {', ' $args = compact({:stringArgs});', ' $args["hash"] = "1f3870be274f6c49b3e31a0c6728957f";', ' $method = \'{:namespace}\MockDelegate::{:method}\';', ' $result = _Filters::run(__CLASS__, "{:method}", $args,', ' function($args) use(&$method) {', ' return call_user_func_array($method, $args);', ' }', ' );', ' if (!isset(static::$staticResults["{:method}"])) {', ' static::$staticResults["{:method}"] = [];', ' }', ' static::$staticResults["{:method}"][] = [', ' "args" => func_get_args(),', ' "result" => $result,', ' "time" => microtime(true),', ' ];', ' return $result;', '}', ], 'method' => [ '{:modifiers} function {:method}({:args}) {', ' $args = compact({:stringArgs});', ' $args["hash"] = spl_object_hash($this->mocker);', ' $_method = [$this->mocker, "{:method}"];', ' $result = _Filters::run(__CLASS__, "{:method}", $args,', ' function($args) use(&$_method) {', ' return call_user_func_array($_method, $args);', ' }', ' );', ' if (!isset($this->results["{:method}"])) {', ' $this->results["{:method}"] = [];', ' }', ' $this->results["{:method}"][] = [', ' "args" => func_get_args(),', ' "result" => $result,', ' "time" => microtime(true),', ' ];', ' return $result;', '}', ], 'applyFilter' => [ 'public {:static} function applyFilter($method, $filter = null) {', ' $message = "::applyFilter() is deprecated. ";', ' $message .= "Use Filters::applyFilter(" . __CLASS__ .", ...) instead.";', ' // trigger_error($message, E_USER_DEPRECATED);', ' foreach ((array) $method as $m) {', ' if ($filter === null) {', ' _Filters::clear(__CLASS__, $m);', ' } else {', ' _Filters::apply(__CLASS__, $m, $filter);', ' }', ' }', '}', ], 'endClass' => [ '}', ], ]; /** * A list of methods we should not overwrite in our mock class. * * Some of these methods are are too custom inside the `Mock` or `Delegate`, * while others should simply not be filtered. * * @var array */ protected static $_blackList = [ '__destruct', '_parents', '__get', '__set', '__isset', '__unset', '__sleep', '__wakeup', '__toString', '__clone', '__invoke', '_stop', '_init', 'invokeMethod', '__set_state', '_instance', '_object', '_initialize', '_filter', 'applyFilter', ]; /** * Will register this class into the autoloader. * * @return void */ public static function register() { spl_autoload_register([__CLASS__, 'create']); } /** * The main entrance to create a new Mock class. * * @param string $mockee The fully namespaced `\Mock` class * @return void */ public static function create($mockee) { if (!static::_validateMockee($mockee)) { return; } $mocker = static::_mocker($mockee); $isStatic = is_subclass_of($mocker, 'lithium\core\StaticObjectDeprecated'); $tokens = [ 'namespace' => static::_namespace($mockee), 'mocker' => $mocker, 'mockee' => 'MockDelegate', 'static' => $isStatic ? 'static' : '', ]; $mockDelegate = static::_dynamicCode('mockDelegate', 'startClass', $tokens); $mock = static::_dynamicCode('mock', 'startClass', $tokens); $reflectedClass = new ReflectionClass($mocker); $reflecedMethods = $reflectedClass->getMethods(); $getByReference = false; $staticApplyFilter = true; $constructor = false; foreach ($reflecedMethods as $methodId => $method) { if (!in_array($method->name, static::$_blackList)) { $key = $method->isStatic() ? 'staticMethod' : 'method'; if ($method->name === '__construct') { $key = 'constructor'; $constructor = true; } $docs = ReflectionMethod::export($mocker, $method->name, true); if (preg_match('/&' . $method->name . '/', $docs) === 1) { continue; } $tokens = [ 'namespace' => static::_namespace($mockee), 'method' => $method->name, 'modifiers' => static::_methodModifiers($method), 'args' => static::_methodParams($method), 'stringArgs' => static::_stringMethodParams($method), 'mocker' => $mocker, ]; $mockDelegate .= static::_dynamicCode('mockDelegate', $key, $tokens); $mock .= static::_dynamicCode('mock', $key, $tokens); } elseif ($method->name === '__get') { $docs = ReflectionMethod::export($mocker, '__get', true); $getByReference = preg_match('/&__get/', $docs) === 1; } elseif ($method->name === 'applyFilter') { $staticApplyFilter = $method->isStatic(); } } if (!$constructor) { $tokens = [ 'namespace' => static::_namespace($mockee), 'modifiers' => 'public', 'args' => null, 'stringArgs' => 'array()', 'mocker' => $mocker, ]; $mock .= static::_dynamicCode('mock', 'constructor', $tokens); $mockDelegate .= static::_dynamicCode('mockDelegate', 'constructor', $tokens); } $mockDelegate .= static::_dynamicCode('mockDelegate', 'endClass'); $mock .= static::_dynamicCode('mock', 'get', [ 'reference' => $getByReference ? '&' : '', ]); $mock .= static::_dynamicCode('mock', 'set'); $mock .= static::_dynamicCode('mock', 'isset'); $mock .= static::_dynamicCode('mock', 'unset'); $mock .= static::_dynamicCode('mock', 'applyFilter', [ 'static' => $staticApplyFilter ? 'static' : '', ]); $mock .= static::_dynamicCode('mock', 'destructor'); $mock .= static::_dynamicCode('mock', 'endClass'); eval($mockDelegate . $mock); } /** * Will determine what method mofifiers of a method. * * For instance: 'public static' or 'private abstract' * * @param ReflectionMethod $method * @return string */ protected static function _methodModifiers(ReflectionMethod $method) { $modifierKey = $method->getModifiers(); $modifierArray = Reflection::getModifierNames($modifierKey); $modifiers = implode(' ', $modifierArray); return str_replace(['private', 'protected'], 'public', $modifiers); } /** * Will determine what parameter prototype of a method. * * For instance: 'ReflectionFunctionAbstract $method' or '$name, array $foo = null' * * @param ReflectionFunctionAbstract $method * @return string */ protected static function _methodParams(ReflectionFunctionAbstract $method) { $pattern = '/Parameter #[0-9]+ \[ [^\>]+>([^\]]+) \]/'; $replace = [ 'from' => [' Array', 'or NULL'], 'to' => [' array()', ''], ]; preg_match_all($pattern, $method, $matches); $params = implode(', ', $matches[1]); return str_replace($replace['from'], $replace['to'], $params); } /** * Will return the params in a way that can be placed into `compact()` * * @param ReflectionFunctionAbstract $method * @return string */ protected static function _stringMethodParams(ReflectionFunctionAbstract $method) { $pattern = '/Parameter [^$]+\$([^ ]+)/'; preg_match_all($pattern, $method, $matches); $params = implode("', '", $matches[1]); return strlen($params) > 0 ? "'{$params}'" : 'array()'; } /** * Will generate the code you are wanting. * * This pulls from $_mockDelegateIngredients and $_mockIngredients. * * @param string $type The name of the array of ingredients to use * @param string $key The key from the array of ingredients * @param array $tokens Tokens, if any, that should be inserted * @return string */ protected static function _dynamicCode($type, $key, $tokens = []) { $defaults = [ 'master' => '\lithium\test\Mocker', ]; $tokens += $defaults; $name = '_' . $type . 'Ingredients'; $code = implode("\n", static::${$name}[$key]); return Text::insert($code, $tokens) . "\n"; } /** * Will generate the mocker from the current mockee. * * @param string $mockee The fully namespaced `\Mock` class * @return array */ protected static function _mocker($mockee) { $sections = explode('\\', $mockee); array_pop($sections); $sections[] = ucfirst(array_pop($sections)); return implode('\\', $sections); } /** * Will generate the namespace from the current mockee. * * @param string $mockee The fully namespaced `\Mock` class * @return string */ protected static function _namespace($mockee) { $matches = []; preg_match_all('/^(.*)\\\\Mock$/', $mockee, $matches); return isset($matches[1][0]) ? $matches[1][0] : null; } /** * Will validate if mockee is a valid class we should mock. * * Will fail if the mock already exists, or it doesn't contain `\Mock` in * the namespace. * * @param string $mockee The fully namespaced `\Mock` class * @return bool */ protected static function _validateMockee($mockee) { return preg_match('/\\\\Mock$/', $mockee) === 1; } /** * Generate a chain class with the current rules of the mock. * * @param mixed $mock Mock object, namespaced static mock, namespaced function name. * @return object MockerChain instance */ public static function chain($mock) { $results = []; $string = is_string($mock); if (is_object($mock) && isset($mock->results)) { $results = static::mergeResults($mock->results, $mock::$staticResults); } elseif ($string && class_exists($mock) && isset($mock::$staticResults)) { $results = $mock::$staticResults; } elseif ($string && function_exists($mock) && isset(static::$_functionResults[$mock])) { $results = [$mock => static::$_functionResults[$mock]]; } return new MockerChain($results); } /** * Will merge two sets of results into each other. * * @param array $results * @param array $secondary * @return array */ public static function mergeResults($results, $secondary) { foreach ($results as $method => $calls) { if (isset($secondary[$method])) { $results['method1'] = array_merge($results['method1'], $secondary['method1']); usort($results['method1'], function($el1, $el2) { return strcmp($el1['time'], $el2['time']); }); unset($secondary['method1']); } } return $results + $secondary; } /** * Calls a method on this object with the given parameters. Provides an OO wrapper for * `forward_static_call_array()`. * * @param string $method Name of the method to call. * @param array $params Parameter list to use when calling `$method`. * @return mixed Returns the result of the method call. */ public static function invokeMethod($method, $params = []) { return forward_static_call_array([get_called_class(), $method], $params); } /** * Will overwrite namespaced functions. * * @param string|bool $name Fully namespaced function, or `false` to reset functions. * @param closure|bool $callback Callback to be called, or `false` to reset this function. * @return void */ public static function overwriteFunction($name, $callback = null) { if ($name === false) { static::$_functionResults = []; return static::$_functionCallbacks = []; } if ($callback === false) { static::$_functionResults[$name] = []; return static::$_functionCallbacks[$name] = false; } static::$_functionCallbacks[$name] = $callback; if (function_exists($name)) { return; } $function = new ReflectionFunction($callback); $pos = strrpos($name, '\\'); eval(static::_dynamicCode('mockFunction', 'function', [ 'namespace' => substr($name, 0, $pos), 'function' => substr($name, $pos + 1), 'args' => static::_methodParams($function), 'stringArgs' => static::_stringMethodParams($function), ])); return; } /** * A method to call user defined functions. * * This method should only be accessed by functions created by `Mocker::overwriteFunction()`. * * If no matching stored function exists, the global function will be called instead. * * @param string $name Fully namespaced function name to call. * @param array $params Params to be passed to the function. * @return mixed */ public static function callFunction($name, array &$params = []) { $function = substr($name, strrpos($name, '\\')); $exists = isset(static::$_functionCallbacks[$name]); if ($exists && is_callable(static::$_functionCallbacks[$name])) { $function = static::$_functionCallbacks[$name]; } $result = call_user_func_array($function, $params); if (!isset(static::$_functionResults[$name])) { static::$_functionResults[$name] = []; } static::$_functionResults[$name][] = [ 'args' => $params, 'result' => $result, 'time' => microtime(true), ]; return $result; } /* Deprecated / BC */ /** * Stores the closures that represent the method filters. They are indexed by called class. * * @deprecated * @var array Method filters, indexed by class. */ protected static $_methodFilters = []; /** * Apply a closure to a method of the current static object. * * @deprecated * @see lithium\core\StaticObject::_filter() * @see lithium\util\collection\Filters * @param string $class Fully namespaced class to apply filters. * @param mixed $method The name of the method to apply the closure to. Can either be a single * method name as a string, or an array of method names. Can also be false to remove * all filters on the current object. * @param \Closure $filter The closure that is used to filter the method(s), can also be false * to remove all the current filters for the given method. * @return void */ public static function applyFilter($class, $method = null, $filter = null) { $message = '`' . __METHOD__ . '()` has been deprecated in favor of '; $message .= '`\lithium\aop\Filters::apply()` and `::clear()`.'; trigger_error($message, E_USER_DEPRECATED); $class = get_called_class(); if ($method === false) { Filters::clear($class); return; } foreach ((array) $method as $m) { if ($filter === false) { Filters::clear($class, $m); } else { Filters::apply($class, $m, $filter); } } } /** * Executes a set of filters against a method by taking a method's main implementation as a * callback, and iteratively wrapping the filters around it. * * @deprecated * @see lithium\util\collection\Filters * @param string $class Fully namespaced class to apply filters. * @param string|array $method The name of the method being executed, or an array containing * the name of the class that defined the method, and the method name. * @param array $params An associative array containing all the parameters passed into * the method. * @param \Closure $callback The method's implementation, wrapped in a closure. * @param array $filters Additional filters to apply to the method for this call only. * @return mixed */ protected static function _filter($class, $method, $params, $callback, $filters = []) { $message = '`' . __METHOD__ . '()` has been deprecated in favor of '; $message .= '`\lithium\aop\Filters::run()` and `::apply()`.'; trigger_error($message, E_USER_DEPRECATED); $class = get_called_class(); foreach ($filters as $filter) { Filters::apply($class, $method, $filter); } return Filters::run($class, $method, $params, $callback); } } ?>