From db4ec9233c522ce0869fbcbcb14dab48085a836c Mon Sep 17 00:00:00 2001 From: Giuseppe Mazzapica Date: Sun, 30 Apr 2017 15:29:48 +0200 Subject: [PATCH] Refactoring for version 2 --- .gitignore | 11 +- LICENSE | 2 +- README.md | 8 +- composer.json | 28 +- inc/api.php | 195 ++++++++ inc/wp-functions.php | 135 ------ inc/wp-helper-functions.php | 69 +++ inc/wp-hook-functions.php | 135 ++++++ phpunit.xml.dist | 6 +- src/Container.php | 107 +++++ src/Exception.php | 21 + src/Expectation/Exception/Exception.php | 37 ++ .../Exception/ExpectationArgsRequired.php | 51 +++ .../Exception/InvalidArgumentForStub.php | 21 + .../Exception/InvalidExpectationName.php | 39 ++ .../Exception/InvalidExpectationType.php | 35 ++ .../Exception/MissedPatchworkReplace.php | 32 ++ .../Exception/NotAllowedMethod.php | 54 +++ .../Exception/NotAllowedWhenHappen.php | 45 ++ src/Expectation/Expectation.php | 226 ++++++++++ src/Expectation/ExpectationFactory.php | 137 ++++++ src/Expectation/ExpectationTarget.php | 188 ++++++++ src/Expectation/FunctionStub.php | 243 ++++++++++ src/Expectation/FunctionStubFactory.php | 94 ++++ src/Hooks/Exception/Exception.php | 24 + .../Exception/InvalidAddedHookArgument.php | 87 ++++ src/Hooks/Exception/InvalidHookArgument.php | 79 ++++ src/Hooks/HookExpectationExecutor.php | 116 +++++ src/Hooks/HookRunningStack.php | 76 ++++ src/Hooks/HookStorage.php | 230 ++++++++++ src/Monkey.php | 162 ------- src/Monkey/Functions.php | 234 ---------- src/Monkey/MockeryBridge.php | 138 ------ src/Monkey/WP/Actions.php | 132 ------ src/Monkey/WP/Filters.php | 121 ----- src/Monkey/WP/Hooks.php | 423 ------------------ src/Monkey/WP/MockeryHookBridge.php | 55 --- src/Names/CallbackStringForm.php | 181 ++++++++ src/Names/ClassName.php | 72 +++ src/Names/ClosureStringForm.php | 115 +++++ src/Names/Exception/Exception.php | 24 + src/Names/Exception/InvalidCallable.php | 41 ++ src/Names/Exception/InvalidName.php | 83 ++++ .../NotInvokableObjectAsCallback.php | 29 ++ src/Names/FunctionName.php | 98 ++++ src/Names/MethodName.php | 62 +++ 46 files changed, 3076 insertions(+), 1425 deletions(-) create mode 100644 inc/api.php delete mode 100644 inc/wp-functions.php create mode 100644 inc/wp-helper-functions.php create mode 100644 inc/wp-hook-functions.php create mode 100644 src/Container.php create mode 100644 src/Exception.php create mode 100644 src/Expectation/Exception/Exception.php create mode 100644 src/Expectation/Exception/ExpectationArgsRequired.php create mode 100644 src/Expectation/Exception/InvalidArgumentForStub.php create mode 100644 src/Expectation/Exception/InvalidExpectationName.php create mode 100644 src/Expectation/Exception/InvalidExpectationType.php create mode 100644 src/Expectation/Exception/MissedPatchworkReplace.php create mode 100644 src/Expectation/Exception/NotAllowedMethod.php create mode 100644 src/Expectation/Exception/NotAllowedWhenHappen.php create mode 100644 src/Expectation/Expectation.php create mode 100644 src/Expectation/ExpectationFactory.php create mode 100644 src/Expectation/ExpectationTarget.php create mode 100644 src/Expectation/FunctionStub.php create mode 100644 src/Expectation/FunctionStubFactory.php create mode 100644 src/Hooks/Exception/Exception.php create mode 100644 src/Hooks/Exception/InvalidAddedHookArgument.php create mode 100644 src/Hooks/Exception/InvalidHookArgument.php create mode 100644 src/Hooks/HookExpectationExecutor.php create mode 100644 src/Hooks/HookRunningStack.php create mode 100644 src/Hooks/HookStorage.php delete mode 100644 src/Monkey.php delete mode 100644 src/Monkey/Functions.php delete mode 100644 src/Monkey/MockeryBridge.php delete mode 100644 src/Monkey/WP/Actions.php delete mode 100644 src/Monkey/WP/Filters.php delete mode 100644 src/Monkey/WP/Hooks.php delete mode 100644 src/Monkey/WP/MockeryHookBridge.php create mode 100644 src/Names/CallbackStringForm.php create mode 100644 src/Names/ClassName.php create mode 100644 src/Names/ClosureStringForm.php create mode 100644 src/Names/Exception/Exception.php create mode 100644 src/Names/Exception/InvalidCallable.php create mode 100644 src/Names/Exception/InvalidName.php create mode 100644 src/Names/Exception/NotInvokableObjectAsCallback.php create mode 100644 src/Names/FunctionName.php create mode 100644 src/Names/MethodName.php diff --git a/.gitignore b/.gitignore index 2264faf..f706e71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,6 @@ -Thumbs.db -ehthumbs.db -Desktop.ini -$RECYCLE.BIN/ -.DS_Store -.coverage -.tox -.idea/ vendor/ -composer.lock +/composer.lock +/phpunit.xml website/ couscous-theme/ couscous.* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 44344d1..48ba0d9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Giuseppe Mazzapica +Copyright (c) 2017 Giuseppe Mazzapica Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 90b4abc..349fe19 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Brain Monkey is a tests utility for PHP. It provides **two set of helpers**: - the first are framework-agnostic tools that allow to mock (or *monkey patch*) and to test behavior of any PHP function - - the second are specific to WordPress and make unit test WordPress extensions a no brainer. + - the second are specific to WordPress and make unit test WordPress extensions a no-brainer. # Requirements - - PHP 5.4+ + - PHP 5.6+ - [Composer](https://getcomposer.org/) to install # License @@ -28,4 +28,6 @@ Brain Monkey is hosted on GitHub. Feel free to open issues there for suggestions # Who's Behind -I'm Giuseppe, I deal with PHP since 2005. For questions, rants or chat ping me on Twitter ([@gmazzap](https://twitter.com/gmazzap)) or on ["The Loop"](http://chat.stackexchange.com/rooms/6/the-loop) (Stack Exchange) chat. Well, it's possible I'll ignore rants. +I'm Giuseppe, I deal with PHP since 2005. For questions, rants or chat ping me on Twitter ([@gmazzap](https://twitter.com/gmazzap)) +or on ["The Loop"](http://chat.stackexchange.com/rooms/6/the-loop) (Stack Exchange) chat. +Well, it's possible I'll ignore rants. diff --git a/composer.json b/composer.json index b41903e..40d76f2 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ { "name": "Giuseppe Mazzapica", "email": "giuseppe.mazzapica@gmail.com", - "homepage": "http://gm.zoomlab.it", + "homepage": "https://gmazzap.me", "role": "Developer" } ], @@ -27,21 +27,30 @@ }, "license": "MIT", "require": { - "php": ">=5.4.0", - "mockery/mockery": "*", + "php": ">=5.6.0", + "mockery/mockery": "1.*", "antecedent/patchwork": "2.0.*" }, "require-dev": { - "phpunit/phpunit": "~4.8", - "mockery/mockery": "0.9.3" + "phpunit/phpunit": "~5.7.9" }, "autoload": { "psr-4": { - "Brain\\": "src/" - } + "Brain\\Monkey\\": "src/" + }, + "files": [ + "vendor/antecedent/patchwork/Patchwork.php", + "inc/api.php" + ] }, "autoload-dev": { - "files": [ "inc/wp-functions.php" ] + "psr-4": { + "Brain\\Monkey\\Tests\\": "tests/src/" + }, + "files": [ + "inc/wp-helper-functions.php", + "inc/wp-hook-functions.php" + ] }, "minimum-stability": "dev", "prefer-stable": true, @@ -50,7 +59,8 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.0.x-dev", + "dev-version/2": "2.0.x-dev" } } } diff --git a/inc/api.php b/inc/api.php new file mode 100644 index 0000000..3f9a939 --- /dev/null +++ b/inc/api.php @@ -0,0 +1,195 @@ +reset(); + \Mockery::close(); + \Patchwork\restoreAll(); + } +} + +namespace Brain\Monkey\Functions { + + use Brain\Monkey\Container; + use Brain\Monkey\Expectation\FunctionStubFactory; + use Brain\Monkey\Names\FunctionName; + + /** + * Factory method: receives the name of the function to mock and returns an instance of + * FunctionStub. + * + * @param string $function_name the name of the function to mock + * @return \Brain\Monkey\Expectation\FunctionStub + */ + function when($function_name) + { + return Container::instance() + ->functionStubFactory() + ->create(new FunctionName($function_name), FunctionStubFactory::SCOPE_STUB); + } + + /** + * Returns a Mockery Expectation object, where is possible to set all the expectations, using + * Mockery methods. + * + * @param string $function_name + * @return \Brain\Monkey\Expectation\Expectation + */ + function expect($function_name) + { + $name = new FunctionName($function_name); + $expectation = Container::instance() + ->expectationFactory() + ->forFunctionExecuted($function_name); + + $factory = Container::instance()->functionStubFactory(); + if ( ! $factory->has($name)) { + $factory->create($name, FunctionStubFactory::SCOPE_EXPECTATION) + ->replaceUsingExpectation($expectation); + + } + + return $expectation; + } +} + +namespace Brain\Monkey\Actions { + + use Brain\Monkey\Container; + use Brain\Monkey\Hooks; + + /** + * @param string $action + * @return \Brain\Monkey\Expectation\Expectation + */ + function expectAdded($action) + { + return Container::instance() + ->expectationFactory() + ->forActionAdded($action); + } + + /** + * @param string $action + * @return \Brain\Monkey\Expectation\Expectation + */ + function expectDone($action) + { + return Container::instance() + ->expectationFactory() + ->forActionDone($action); + } + + /** + * @param string $action + * @param null $callback + * @return bool + */ + function has($action, $callback = null) + { + return Container::instance() + ->hookStorage() + ->isHookAdded(Hooks\HookStorage::ACTIONS, $action, $callback); + } + + /** + * @param string $action + * @return int + */ + function did($action) + { + return Container::instance() + ->hookStorage() + ->isHookDone(Hooks\HookStorage::ACTIONS, $action); + } + + /** + * @param string $action + * @return bool + */ + function doing($action) + { + return Container::instance() + ->hookRunningStack() + ->has($action); + } +} + +namespace Brain\Monkey\Filters { + + use Brain\Monkey\Container; + use Brain\Monkey\Hooks; + + /** + * @param string $filter + * @return \Brain\Monkey\Expectation\Expectation + */ + function expectAdded($filter) + { + return Container::instance() + ->expectationFactory() + ->forFilterAdded($filter); + } + + /** + * @param string $filter + * @return \Brain\Monkey\Expectation\Expectation + */ + function expectApplied($filter) + { + return Container::instance() + ->expectationFactory() + ->forFilterApplied($filter); + } + + /** + * @param string $filter + * @param null $callback + * @return bool + */ + function has($filter, $callback = null) + { + return Container::instance() + ->hookStorage() + ->isHookAdded(Hooks\HookStorage::FILTERS, $filter, $callback); + } + + /** + * @param string $filter + * @return int + */ + function applied($filter) + { + return Container::instance() + ->hookStorage() + ->isHookDone(Hooks\HookStorage::FILTERS, $filter); + } + + /** + * @param string $filter + * @return bool + */ + function doing($filter) + { + return Container::instance() + ->hookRunningStack() + ->has($filter); + } +} + diff --git a/inc/wp-functions.php b/inc/wp-functions.php deleted file mode 100644 index 6e682f1..0000000 --- a/inc/wp-functions.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - */ - -use Brain\Monkey\WP\Hooks; - -if (! function_exists('add_action')) { - function add_action() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'add'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('remove_action')) { - function remove_action() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'remove'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('do_action')) { - function do_action() - { - call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'run'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('do_action_ref_array')) { - function do_action_ref_array() - { - call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'runRef'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('did_action')) { - function did_action() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'did'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('has_action')) { - function has_action() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'has'.Hooks::ACTION], - func_get_args() - ); - } -} - -if (! function_exists('add_filter')) { - function add_filter() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'add'.Hooks::FILTER], - func_get_args() - ); - } -} - -if (! function_exists('remove_filter')) { - function remove_filter() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'remove'.Hooks::FILTER], - func_get_args() - ); - } -} - -if (! function_exists('apply_filters')) { - function apply_filters() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'run'.Hooks::FILTER], - func_get_args() - ); - } -} - -if (! function_exists('apply_filters_ref_array')) { - function apply_filters_ref_array() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'runRef'.Hooks::FILTER], - func_get_args() - ); - } -} - -if (! function_exists('has_filter')) { - function has_filter() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'has'.Hooks::FILTER], - func_get_args() - ); - } -} - -if (! function_exists('current_filter')) { - function current_filter() - { - return call_user_func_array( - ['Brain\Monkey\WP\Hooks', 'current'], - func_get_args() - ); - } -} diff --git a/inc/wp-helper-functions.php b/inc/wp-helper-functions.php new file mode 100644 index 0000000..d5383da --- /dev/null +++ b/inc/wp-helper-functions.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @author Giuseppe Mazzapica + * @license http://opensource.org/licenses/MIT MIT + * @package BrainMonkey + */ + +if ( ! function_exists('__return_true')) { + function __return_true() + { + return true; + } +} + +if ( ! function_exists('__return_false')) { + function __return_false() + { + return false; + } +} + +if ( ! function_exists('__return_null')) { + function __return_null() + { + return null; + } +} + +if ( ! function_exists('__return_zero')) { + function __return_zero() + { + return 0; + } +} + +if ( ! function_exists('__return_empty_array')) { + function __return_empty_array() + { + return []; + } +} + +if ( ! function_exists('__return_empty_string')) { + function __return_empty_string() + { + return ''; + } +} + +if ( ! function_exists('untrailingslashit')) { + function untrailingslashit($string) + { + return rtrim($string, '/\\'); + } +} + +if ( ! function_exists('trailingslashit')) { + function trailingslashit($string) + { + return rtrim($string, '/\\').'/'; + } +} \ No newline at end of file diff --git a/inc/wp-hook-functions.php b/inc/wp-hook-functions.php new file mode 100644 index 0000000..4067bd9 --- /dev/null +++ b/inc/wp-hook-functions.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @author Giuseppe Mazzapica + * @license http://opensource.org/licenses/MIT MIT + * @package BrainMonkey + */ + +use Brain\Monkey; + +if ( ! function_exists('add_action')) { + function add_action($action, ...$args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToAdded(Monkey\Hooks\HookStorage::ACTIONS, $action, $args); + $container->hookExpectationExecutor()->executeAddAction($action, $args); + + return true; + } +} + +if ( ! function_exists('add_filter')) { + function add_filter($filter, ...$args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToAdded(Monkey\Hooks\HookStorage::FILTERS, $filter, $args); + $container->hookExpectationExecutor()->executeAddFilter($filter, $args); + + return true; + } +} + +if ( ! function_exists('do_action')) { + function do_action($action, ...$args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToDone(Monkey\Hooks\HookStorage::ACTIONS, $action, $args); + $container->hookExpectationExecutor()->executeDoAction($action, $args); + } +} + +if ( ! function_exists('do_action_ref_array')) { + function do_action_ref_array($action, array $args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToDone(Monkey\Hooks\HookStorage::ACTIONS, $action, $args); + $container->hookExpectationExecutor()->executeDoAction($action, $args); + } +} + +if ( ! function_exists('apply_filters')) { + function apply_filters($filter, ...$args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToDone(Monkey\Hooks\HookStorage::FILTERS, $filter, $args); + + return $container->hookExpectationExecutor()->executeApplyFilters($filter, $args); + } +} + +if ( ! function_exists('apply_filters_ref_array')) { + function apply_filters_ref_array($filter, array $args) + { + $container = Monkey\Container::instance(); + $container->hookStorage()->pushToDone(Monkey\Hooks\HookStorage::FILTERS, $filter, $args); + + return $container->hookExpectationExecutor()->executeApplyFilters($filter, $args); + } +} + +if ( ! function_exists('has_action')) { + function has_action($action, $callback = null) + { + return Monkey\Actions\has($action, $callback); + } +} + +if ( ! function_exists('has_filter')) { + function has_filter($filter, $callback = null) + { + return Monkey\Filters\has($filter, $callback); + } +} + +if ( ! function_exists('did_action')) { + function did_action($action) + { + return Monkey\Actions\did($action); + } +} + +if ( ! function_exists('remove_action')) { + function remove_action($action, ...$args) + { + return Monkey\Container::instance() + ->hookStorage() + ->removeFromAdded(Monkey\Hooks\HookStorage::ACTIONS, $action, $args); + } +} + +if ( ! function_exists('remove_filter')) { + function remove_filter($filter, ...$args) + { + return Monkey\Container::instance() + ->hookStorage() + ->removeFromAdded(Monkey\Hooks\HookStorage::FILTERS, $filter, $args); + } +} + +if ( ! function_exists('doing_action')) { + function doing_action($action) + { + return Monkey\Actions\doing($action); + } +} + +if ( ! function_exists('doing_filter')) { + function doing_filter($filter) + { + return Monkey\Filters\doing($filter); + } +} + +if ( ! function_exists('current_filter')) { + function current_filter() + { + return Monkey\Container::instance()->hookRunningStack()->last() ? : false; + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fcffda3..2a2b7f0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - - tests/unit + + tests/src/Api diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..bafcb6f --- /dev/null +++ b/src/Container.php @@ -0,0 +1,107 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class Container +{ + + /** + * @var Container + */ + private static $instance; + + /** + * @var array + */ + private $services = []; + + /** + * Static instance lookup. + * + * @return static + */ + public static function instance() + { + self::$instance or self::$instance = new static(); + + return self::$instance; + } + + /** + * @return \Brain\Monkey\Expectation\ExpectationFactory + */ + public function expectationFactory() + { + return $this->service(__FUNCTION__, new Expectation\ExpectationFactory()); + } + + /** + * @return \Brain\Monkey\Hooks\HookRunningStack + */ + public function hookRunningStack() + { + return $this->service(__FUNCTION__, new Hooks\HookRunningStack()); + } + + /** + * @return \Brain\Monkey\Hooks\HookStorage + */ + public function hookStorage() + { + return $this->service(__FUNCTION__, new Hooks\HookStorage()); + } + + /** + * @return \Brain\Monkey\Hooks\HookExpectationExecutor + */ + public function hookExpectationExecutor() + { + return $this->service(__FUNCTION__, new Hooks\HookExpectationExecutor( + $this->hookRunningStack(), + $this->expectationFactory() + )); + } + + /** + * @return \Brain\Monkey\Expectation\FunctionStubFactory + */ + public function functionStubFactory() + { + return $this->service(__FUNCTION__, new Expectation\FunctionStubFactory()); + } + + public function reset() + { + $this->expectationFactory()->reset(); + $this->hookRunningStack()->reset(); + $this->hookStorage()->reset(); + $this->functionStubFactory()->reset(); + } + + /** + * @param string $id + * @param mixed $service + * @return mixed + */ + private function service($id, $service) + { + if ( ! array_key_exists($id, $this->services)) { + $this->services[$id] = $service; + } + + return $this->services[$id]; + } +} \ No newline at end of file diff --git a/src/Exception.php b/src/Exception.php new file mode 100644 index 0000000..877d9aa --- /dev/null +++ b/src/Exception.php @@ -0,0 +1,21 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class Exception extends \Exception +{ + +} \ No newline at end of file diff --git a/src/Expectation/Exception/Exception.php b/src/Expectation/Exception/Exception.php new file mode 100644 index 0000000..046472d --- /dev/null +++ b/src/Expectation/Exception/Exception.php @@ -0,0 +1,37 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class Exception extends BaseException +{ + + /** + * + * @param \Exception $exception + * @return static + */ + public static function becauseOf(\Exception $exception) + { + return new static( + $exception->getMessage(), + $exception->getCode(), + $exception + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/ExpectationArgsRequired.php b/src/Expectation/Exception/ExpectationArgsRequired.php new file mode 100644 index 0000000..c1d2152 --- /dev/null +++ b/src/Expectation/Exception/ExpectationArgsRequired.php @@ -0,0 +1,51 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class ExpectationArgsRequired extends Exception +{ + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return static + */ + public static function forExpectationType(ExpectationTarget $target) + { + $type = ''; + + switch ($target->type()) { + case ExpectationTarget::TYPE_ACTION_ADDED: + $type = "added action"; + break; + case ExpectationTarget::TYPE_ACTION_DONE: + $type = "done action"; + break; + case ExpectationTarget::TYPE_FILTER_ADDED: + $type = "added filter"; + break; + case ExpectationTarget::TYPE_FILTER_APPLIED: + $type = "applied filter"; + break; + } + + return new static( + "Can't use `withNoArgs()` for {$type} expectations: they require at least one argument." + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/InvalidArgumentForStub.php b/src/Expectation/Exception/InvalidArgumentForStub.php new file mode 100644 index 0000000..e53a86f --- /dev/null +++ b/src/Expectation/Exception/InvalidArgumentForStub.php @@ -0,0 +1,21 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidArgumentForStub extends Exception +{ + +} \ No newline at end of file diff --git a/src/Expectation/Exception/InvalidExpectationName.php b/src/Expectation/Exception/InvalidExpectationName.php new file mode 100644 index 0000000..194d8a5 --- /dev/null +++ b/src/Expectation/Exception/InvalidExpectationName.php @@ -0,0 +1,39 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidExpectationName extends Exception +{ + + /** + * @param mixed $name + * @param string $type + * @return static + */ + public static function forNameAndType($name, $type) + { + return new static( + sprintf( + '%s name to set expectation for must be in a string, got %s.', + $type === ExpectationTarget::TYPE_FUNCTION ? 'Function' : 'Hook', + is_object($name) ? 'instance of '.get_class($name) : gettype($name) + ) + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/InvalidExpectationType.php b/src/Expectation/Exception/InvalidExpectationType.php new file mode 100644 index 0000000..e277ed0 --- /dev/null +++ b/src/Expectation/Exception/InvalidExpectationType.php @@ -0,0 +1,35 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidExpectationType extends Exception +{ + + /** + * @param string $type + * @return static + */ + public static function forType($type) + { + return new static( + sprintf( + '%s method is not allowed for Brain Monkey expectation.', + $type + ) + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/MissedPatchworkReplace.php b/src/Expectation/Exception/MissedPatchworkReplace.php new file mode 100644 index 0000000..4eaebf7 --- /dev/null +++ b/src/Expectation/Exception/MissedPatchworkReplace.php @@ -0,0 +1,32 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class MissedPatchworkReplace extends Exception +{ + + /** + * @param string $function_name + * @return static + */ + public static function forFunction($function_name) + { + return new static( + "Patchwork was not able to replace '{$function_name}', try to load Patchwork earlier." + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/NotAllowedMethod.php b/src/Expectation/Exception/NotAllowedMethod.php new file mode 100644 index 0000000..850f5be --- /dev/null +++ b/src/Expectation/Exception/NotAllowedMethod.php @@ -0,0 +1,54 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class NotAllowedMethod extends Exception +{ + + const CODE_METHOD = 1; + const CODE_RETURNING_METHOD = 2; + + /** + * @param string $method_name + * @return static + */ + public static function forMethod($method_name) + { + return new static( + sprintf( + '%s method is not allowed for Brain Monkey expectation.', + $method_name + ), + self::CODE_METHOD + ); + } + + /** + * @param string $method_name + * @return static + */ + public static function forReturningMethod($method_name) + { + return new static( + sprintf( + 'Bad usage of "%s" method: returning expectation can only be used for functions or applied filters expectations.', + $method_name + ), + self::CODE_RETURNING_METHOD + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Exception/NotAllowedWhenHappen.php b/src/Expectation/Exception/NotAllowedWhenHappen.php new file mode 100644 index 0000000..b461329 --- /dev/null +++ b/src/Expectation/Exception/NotAllowedWhenHappen.php @@ -0,0 +1,45 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class NotAllowedWhenHappen extends Exception +{ + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return static + */ + public static function forExpectationType(ExpectationTarget $target) + { + $type = ''; + + switch ($target->type()) { + case ExpectationTarget::TYPE_FUNCTION: + $type = "function"; + break; + case ExpectationTarget::TYPE_FILTER_APPLIED: + $type = "applied filter"; + break; + } + + return new static( + "Can't use `whenHappen()` for {$type} expectations: use `andReturnUsing()` instead." + ); + } + +} \ No newline at end of file diff --git a/src/Expectation/Expectation.php b/src/Expectation/Expectation.php new file mode 100644 index 0000000..f535145 --- /dev/null +++ b/src/Expectation/Expectation.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Brain\Monkey\Expectation; + +use Mockery\ExpectationInterface; + +/** + * A wrap around Mockery expectation. + * + * Acts as "man in the middle" between Monkey API and Mockery expectation, preventing calls to + * some methods and do some checks before calling other methods. + * finally, some additional methods are added like `andAlsoExpect` to overcome the not allowed + * `getMock()` and `andReturnFirstArg()` to facilitate the creation of expectation for applied + * filter hooks. + * + * @author Giuseppe Mazzapica + * @license http://opensource.org/licenses/MIT MIT + * @package BrainMonkey + * + * @method Expectation once() + * @method Expectation twice() + * @method Expectation atLeast() + * @method Expectation atMost() + * @method Expectation times(int $times) + * @method Expectation never() + * @method Expectation ordered() + * @method Expectation between(int $min, int $max) + * @method Expectation zeroOrMoreTimes() + * @method Expectation with(...$args) + * @method Expectation withAnyArgs() + * @method Expectation andReturn(...$args) + * @method Expectation andReturnNull() + * @method Expectation andReturnValues(...$args) + * @method Expectation andReturnUsing(callable ...$args) + * @method Expectation andThrow(\Throwable $throwable) + */ +class Expectation +{ + + const RETURNING_EXPECTATION_TYPES = [ + ExpectationTarget::TYPE_FILTER_APPLIED, + ExpectationTarget::TYPE_FUNCTION + ]; + + const NO_ARGS_EXPECTATION_TYPES = [ + ExpectationTarget::TYPE_ACTION_DONE, + ExpectationTarget::TYPE_FUNCTION + ]; + + const NOT_ALLOWED_METHODS = [ + 'shouldReceive', + 'andSet', + 'set', + 'shouldExpect', + 'mock', + 'getMock', + 'byDefault' + ]; + + /** + * @var \Mockery\Expectation|\Mockery\ExpectationInterface + */ + private $expectation; + + /** + * @var \Brain\Monkey\Expectation\ExpectationTarget + */ + private $target; + + /** + * @var bool + */ + private $default = true; + + /** + * @param \Mockery\ExpectationInterface $expectation + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + */ + public function __construct(ExpectationInterface $expectation, ExpectationTarget $target) + { + $this->expectation = $expectation; + $this->target = $target; + } + + /** + * Ensure full cloning. + */ + public function __clone() + { + $this->expectation = clone $this->expectation; + $this->target = clone $this->target; + } + + /** + * Delegate method to wrapped expectation, after some checks. + * + * @param string $name + * @param array $arguments + * @return static + * @throws \Brain\Monkey\Expectation\Exception\NotAllowedMethod + */ + public function __call($name, array $arguments = []) + { + if (in_array($name, self::NOT_ALLOWED_METHODS, true)) { + throw Exception\NotAllowedMethod::forMethod($name); + } + + if ( + stristr($name, 'return') + && ! in_array($this->target->type(), self::RETURNING_EXPECTATION_TYPES, true) + ) { + throw Exception\NotAllowedMethod::forReturningMethod($name); + } + + if ($this->default) { + $this->default = false; + $this->andAlsoExpectIt(); + } + + $this->expectation = ([$this->expectation, $name])(...$arguments); + + return $this; + } + + /** + * @return \Mockery\Expectation|\Mockery\CompositeExpectation + */ + public function mockeryExpectation() + { + return $this->expectation; + } + + /** + * Mockery expectation allow chaining different expectations with by chaining `getMock()` + * method. + * Since `getMock()` is disabled for Brain Monkey expectation this methods provides a way to + * chain expectations. + * + * @return static + */ + public function andAlsoExpectIt() + { + $method = $this->target->mockMethodName(); + /** @noinspection PhpMethodParametersCountMismatchInspection */ + $this->expectation = $this->expectation->getMock()->shouldReceive($method); + + return $this; + } + + /** + * WordPress action and filters addition and filters applying requires at least one argument, + * and setting an expectation of no arguments for those triggers an error in Brain Monkey. + * + * @return static + * @throws \Brain\Monkey\Expectation\Exception\ExpectationArgsRequired + */ + public function withNoArgs() + { + if ( ! in_array($this->target->type(), self::NO_ARGS_EXPECTATION_TYPES, true)) { + throw Exception\ExpectationArgsRequired::forExpectationType($this->target); + } + + $this->expectation = $this->expectation->withNoArgs(); + + return $this; + } + + /** + * Brain Monkey doesn't allow return expectation for actions (added/done) nor for added + * filters. + * However, it is desirable to do something when the expected callback is used, this is the + * reason to be of this method. + * + * ``` + * Actions::expectDone('some_action')->once()->whenHappen(function($some_arg) { + * echo "{$some_arg} was passed to " . current_filter(); + * }); + * ``` + * + * Snippet above will not change the return of `do_action('some_action', $some_arg)` + * like a normal return expectation would do, but allows to catch expected events with a + * callback. + * + * For expectation types that allows return expectation (functions, applied filters) this method + * becomes just an alias for Mockery `andReturnUsing()`. + * + * @param callable $callback + * @return static + * @throws \Brain\Monkey\Expectation\Exception\NotAllowedWhenHappen + */ + public function whenHappen(callable $callback) + { + if (in_array($this->target->type(), self::RETURNING_EXPECTATION_TYPES, true)) { + throw Exception\NotAllowedWhenHappen::forExpectationType($this->target); + } + + $this->expectation->andReturnUsing($callback); + + return $this; + } + + /** + * @return static + * @throws \Brain\Monkey\Expectation\Exception\NotAllowedMethod + */ + public function andReturnFirstArg() + { + if ( ! in_array($this->target->type(), self::RETURNING_EXPECTATION_TYPES, true)) { + throw Exception\NotAllowedMethod::forReturningMethod('andReturnFirstParam'); + } + + $this->expectation->andReturnUsing(function ($arg = null) { + return $arg; + }); + + return $this; + } +} diff --git a/src/Expectation/ExpectationFactory.php b/src/Expectation/ExpectationFactory.php new file mode 100644 index 0000000..dc2d382 --- /dev/null +++ b/src/Expectation/ExpectationFactory.php @@ -0,0 +1,137 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class ExpectationFactory +{ + + /** + * @var \Brain\Monkey\Expectation\Expectation[] + */ + private $expectations = []; + + /** + * @param string $function + * @return \Brain\Monkey\Expectation\Expectation; + */ + public function forFunctionExecuted($function) + { + return $this->create( + new ExpectationTarget(ExpectationTarget::TYPE_FUNCTION, $function) + ); + } + + /** + * @param string $action + * @return \Brain\Monkey\Expectation\Expectation; + */ + public function forActionAdded($action) + { + return $this->create( + new ExpectationTarget(ExpectationTarget::TYPE_ACTION_ADDED, $action) + ); + } + + /** + * @param string $action + * @return \Brain\Monkey\Expectation\Expectation; + */ + public function forActionDone($action) + { + return $this->create( + new ExpectationTarget(ExpectationTarget::TYPE_ACTION_DONE, $action) + ); + } + + /** + * @param string $filter + * @return \Brain\Monkey\Expectation\Expectation; + */ + public function forFilterAdded($filter) + { + return $this->create( + new ExpectationTarget(ExpectationTarget::TYPE_FILTER_ADDED, $filter) + ); + } + + /** + * @param string $filter + * @return \Brain\Monkey\Expectation\Expectation; + */ + public function forFilterApplied($filter) + { + return $this->create( + new ExpectationTarget(ExpectationTarget::TYPE_FILTER_APPLIED, $filter) + ); + } + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return \Mockery\MockInterface|mixed + */ + public function hasMockFor(ExpectationTarget $target) + { + return array_key_exists($target->identifier(), $this->expectations); + } + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return \Mockery\MockInterface|mixed + */ + public function mockFor(ExpectationTarget $target) + { + return $this->hasMockFor($target) + ? $this->expectations[$target->identifier()]->mockeryExpectation()->getMock() + : \Mockery::mock(); + } + + public function reset() + { + $this->expectations = []; + } + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return \Brain\Monkey\Expectation\Expectation + */ + private function create(ExpectationTarget $target) + { + $id = $target->identifier(); + + /** @noinspection PhpMethodParametersCountMismatchInspection */ + $expectation = $this->mockFor($target) + ->shouldReceive($target->mockMethodName()) + ->atLeast() + ->once(); + + if ($target->type() === ExpectationTarget::TYPE_FILTER_APPLIED) { + $expectation = $expectation->andReturnUsing(function ($arg) { + return $arg; + }); + } + + $expectation = $expectation->byDefault(); + + $this->expectations[$id] = new Expectation($expectation, $target); + + return $this->expectations[$id]; + } + +} \ No newline at end of file diff --git a/src/Expectation/ExpectationTarget.php b/src/Expectation/ExpectationTarget.php new file mode 100644 index 0000000..39f6f10 --- /dev/null +++ b/src/Expectation/ExpectationTarget.php @@ -0,0 +1,188 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class ExpectationTarget +{ + + const TYPE_ACTION_ADDED = 'add_action'; + const TYPE_ACTION_DONE = 'do_action'; + const TYPE_FILTER_ADDED = 'add_filter'; + const TYPE_FILTER_APPLIED = 'apply_filters'; + const TYPE_FUNCTION = 'function'; + const TYPE_NULL = ''; + + const TYPES = [ + self::TYPE_FUNCTION, + self::TYPE_ACTION_ADDED, + self::TYPE_ACTION_DONE, + self::TYPE_FILTER_ADDED, + self::TYPE_FILTER_APPLIED, + ]; + + const HOOK_SANITIZE_MAP = [ + '-' => '_hyphen_', + ' ' => '_space_', + '/' => '_slash_', + '\\' => '_backslash_', + '.' => '_dot_', + '!' => '_exclamation_mark_', + '"' => '_double_quote_', + '\'' => '_single_quote_', + '£' => '_pound_', + '$' => '_dollar_', + '%' => '_percent_', + '=' => '_equal_', + '?' => '_question_mark_', + '*' => '_asterisk_', + '@' => '_slug_', + '#' => '_sharp_', + '+' => '_plus_', + '|' => '_pipe_', + '<' => '_lt_', + '>' => '_gt_', + ',' => '_comma_', + ';' => '_colon_', + '~' => '_tilde_', + '(' => '_bracket_open_', + ')' => '_bracket_close_', + '[' => '_square_bracket_open_', + ']' => '_square_bracket_close_', + '{' => '_curly_bracket_open_', + '}' => '_curly_bracket_close_', + ]; + + /** + * @var string + */ + private $type; + + /** + * @var callable|string + */ + private $name; + + /** + * @var string + */ + private $original_name; + + /** + * @param string $type + * @param string $name + * @throws \Brain\Monkey\Expectation\Exception\InvalidExpectationName + * @throws \Brain\Monkey\Expectation\Exception\InvalidExpectationType + */ + public function __construct($type, $name) + { + if ( ! in_array($type, self::TYPES, true)) { + throw Exception\InvalidExpectationType::forType($name); + } + + if ( ! is_string($name)) { + throw Exception\InvalidExpectationName::forNameAndType($name, $type); + } + + $this->type = $type; + + if ($type === self::TYPE_FUNCTION) { + $nameObject = new FunctionName($name); + $namespace = str_replace('\\', '_', $nameObject->getNamespace()); + $this->name = "{$namespace}_".$nameObject->shortName(); + $this->original_name = $nameObject->fullyQualifiedName(); + + return; + } + + $this->original_name = $name; + $replaced = strtr($name, self::HOOK_SANITIZE_MAP); + $this->name = preg_replace('/[^a-zA-Z0-9_]/', '__', $replaced); + + } + + /** + * @return string + */ + public function identifier() + { + return md5($this->original_name.$this->type); + } + + /** + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * @return string + */ + public function mockMethodName() + { + $name = $this->name(); + + switch ($this->type()) { + case ExpectationTarget::TYPE_FUNCTION: + break; + case ExpectationTarget::TYPE_ACTION_ADDED: + $name = "add_action_{$name}"; + break; + case ExpectationTarget::TYPE_ACTION_DONE: + $name = "do_action_{$name}"; + break; + case ExpectationTarget::TYPE_FILTER_ADDED: + $name = "add_filter_{$name}"; + break; + case ExpectationTarget::TYPE_FILTER_APPLIED: + $name = "apply_filters_{$name}"; + break; + default : + throw new \UnexpectedValueException(sprintf('Unexpected %s type.', __CLASS__)); + } + + return $name; + } + + /** + * @return string + */ + public function type() + { + return $this->type; + } + + /** + * @param \Brain\Monkey\Expectation\ExpectationTarget $target + * @return bool + */ + public function equals(ExpectationTarget $target) + { + return + $this->original_name === $target->original_name + && $this->type === $target->type; + } +} \ No newline at end of file diff --git a/src/Expectation/FunctionStub.php b/src/Expectation/FunctionStub.php new file mode 100644 index 0000000..2e5e2f1 --- /dev/null +++ b/src/Expectation/FunctionStub.php @@ -0,0 +1,243 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class FunctionStub +{ + + /** + * @var \Brain\Monkey\Names\FunctionName + */ + private $function_name; + + /** + * @param FunctionName $function_name + */ + public function __construct(FunctionName $function_name) + { + $this->function_name = $function_name; + $name = $this->function_name->shortName(); + $namespace = $this->function_name->getNamespace(); + + if (function_exists($function_name->fullyQualifiedName())) { + return; + } + + $function = <<function_name->fullyQualifiedName(); + } + + /** + * Redefine target function replacing it on the fly with a given callable. + * + * @param callable $callback + */ + public function alias(callable $callback) + { + $fqn = $this->function_name->fullyQualifiedName(); + \Patchwork\redefine($fqn, $callback); + $this->assertRedefined($fqn); + } + + /** + * Redefine target function replacing it with a function that execute Brain Monkey expectation + * target method on the mock associated with given Brain Monkey expectation. + * + * @param \Brain\Monkey\Expectation\Expectation $expectation + * @return void + */ + public function replaceUsingExpectation(Expectation $expectation) + { + $fqn = $this->function_name->fullyQualifiedName(); + + $this->alias(function (...$args) use ($expectation, $fqn) { + + $mock = $expectation->mockeryExpectation()->getMock(); + $target = new ExpectationTarget(ExpectationTarget::TYPE_FUNCTION, $fqn); + + return $mock->{$target->mockMethodName()}(...$args); + }); + } + + /** + * Redefine target function making it return an arbitrary value. + * + * @param mixed $return + */ + public function justReturn($return = null) + { + $fqn = ltrim($this->function_name->fullyQualifiedName(), '\\'); + + \Patchwork\redefine($fqn, function () use ($return) { + return $return; + }); + + $this->assertRedefined($fqn); + } + + /** + * Redefine target function making it echo an arbitrary value. + * + * @param mixed $value + * @throws \Brain\Monkey\Expectation\Exception\InvalidArgumentForStub + */ + public function justEcho($value = null) + { + is_null($value) and $value = ''; + $fqn = ltrim($this->function_name->fullyQualifiedName(), '\\'); + + $this->assertPrintable($value, 'provided to justEcho'); + + \Patchwork\redefine($fqn, function () use ($value) { + echo $value; + }); + + $this->assertRedefined($fqn); + } + + /** + * Redefine target function making it return one of the received arguments, the first by + * default. Redefined function will throw an exception if the function does not receive desired + * argument. + * + * @param int $arg_num The position (1-based) of the argument to return + */ + public function returnArg($arg_num = 1) + { + $arg_num = $this->assertValidArgNum($arg_num); + + $fqn = $this->function_name->fullyQualifiedName(); + + \Patchwork\redefine($fqn, function (...$args) use ($fqn, $arg_num) { + if ( ! array_key_exists($arg_num - 1, $args)) { + $count = count($args); + throw new \RuntimeException( + "{$fqn} was called with {$count} params, can't return argument \"{$arg_num}\"." + ); + } + + return $args[$arg_num - 1]; + }); + $this->assertRedefined($fqn); + } + + /** + * Redefine target function making it echo one of the received arguments, the first by default. + * Redefined function will throw an exception if the function does not receive desired argument. + * + * @param int $arg_num The position (1-based) of the argument to echo + * @throws \Brain\Monkey\Expectation\Exception\InvalidArgumentForStub + */ + public function echoArg($arg_num = 1) + { + $arg_num = $this->assertValidArgNum($arg_num); + + $fqn = $this->function_name->fullyQualifiedName(); + + \Patchwork\redefine($fqn, function (...$args) use ($fqn, $arg_num) { + + if ( ! array_key_exists($arg_num - 1, $args)) { + $count = count($args); + throw new \RuntimeException( + "{$fqn} was called with {$count} params, can't return argument \"{$arg_num}\"." + ); + } + + $arg = $args[$arg_num - 1]; + + $this->assertPrintable($arg, "passed as argument {$arg_num} to {$fqn}"); + + echo (string) $arg; + }); + + $this->assertRedefined($fqn); + } + + /** + * @param int $arg_num + * @return bool + * @throws \Brain\Monkey\Expectation\Exception\InvalidArgumentForStub + */ + private function assertValidArgNum($arg_num) + { + if ( ! is_int($arg_num) || $arg_num <= 0) { + throw new Exception\InvalidArgumentForStub( + sprintf('`%s::returnArg()` first parameter must be a positiver integer.', __CLASS__) + ); + } + + return $arg_num; + } + + /** + * @param string $function_name + * @throws \Brain\Monkey\Expectation\Exception\MissedPatchworkReplace + */ + private function assertRedefined($function_name) + { + if (\Patchwork\hasMissed($function_name)) { + throw Exception\MissedPatchworkReplace::forFunction($function_name); + } + } + + /** + * @param $value + * @param string $coming + * @throws \Brain\Monkey\Expectation\Exception\InvalidArgumentForStub + */ + private function assertPrintable($value, $coming = '') + { + if (is_scalar($value)) { + return; + } + + $printable = + is_object($value) + && method_exists($value, '__toString') + && is_callable([$value, '__toString']); + + if ( ! $printable) { + throw new Exception\InvalidArgumentForStub( + sprintf( + "%s, %s, is not printable.", + is_object($value) ? 'Instance of '.get_class($value) : gettype($value), + $coming + ) + ); + } + + } +} \ No newline at end of file diff --git a/src/Expectation/FunctionStubFactory.php b/src/Expectation/FunctionStubFactory.php new file mode 100644 index 0000000..d3b8044 --- /dev/null +++ b/src/Expectation/FunctionStubFactory.php @@ -0,0 +1,94 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class FunctionStubFactory +{ + + const SCOPE_STUB = 'a stub'; + const SCOPE_EXPECTATION = 'an expectation'; + + /** + * @var array + */ + private $storage = []; + + /** + * @param \Brain\Monkey\Names\FunctionName $name + * @param string $scope + * @return \Brain\Monkey\Expectation\FunctionStub + * @throws \Brain\Monkey\Expectation\Exception\Exception + */ + public function create(FunctionName $name, $scope) + { + $stored_type = $this->storedType($name); + + if ( ! $stored_type) { + + $stub = new FunctionStub($name); + $this->storage[$name->fullyQualifiedName()] = [$stub, $scope]; + + return $stub; + } + + if ($scope !== $stored_type) { + throw new Exception\Exception( + 'It was not possible to create %s for function "%s" because %s for it already exists.', + $scope, + $name->fullyQualifiedName(), + $stored_type + ); + } + + list($stub) = $this->storage[$name->fullyQualifiedName()]; + + return $stub; + } + + /** + * @param \Brain\Monkey\Names\FunctionName $name + * @return bool + */ + public function has(FunctionName $name) + { + return array_key_exists($name->fullyQualifiedName(), $this->storage); + } + + /** + * @return void + */ + public function reset() + { + $this->storage = []; + } + + /** + * @param \Brain\Monkey\Names\FunctionName $name + * @return string + */ + private function storedType(FunctionName $name) + { + if ( ! $this->has($name)) { + return ''; + } + + list(, $stored_type) = $this->storage[$name->fullyQualifiedName()]; + + return $stored_type; + } +} \ No newline at end of file diff --git a/src/Hooks/Exception/Exception.php b/src/Hooks/Exception/Exception.php new file mode 100644 index 0000000..405d305 --- /dev/null +++ b/src/Hooks/Exception/Exception.php @@ -0,0 +1,24 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class Exception extends BaseException +{ + +} \ No newline at end of file diff --git a/src/Hooks/Exception/InvalidAddedHookArgument.php b/src/Hooks/Exception/InvalidAddedHookArgument.php new file mode 100644 index 0000000..93c2f73 --- /dev/null +++ b/src/Hooks/Exception/InvalidAddedHookArgument.php @@ -0,0 +1,87 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidAddedHookArgument extends InvalidHookArgument +{ + + const CODE_WRONG_ARGS_COUNT = 1; + const CODE_MISSING_CALLBACK = 2; + const CODE_INVALID_PRIORITY = 3; + const CODE_INVALID_ACCEPTED_ARGS = 4; + + /** + * @param string $type + * @return static + */ + public static function forWrongArgumentsCount($type) + { + return new static( + sprintf( + '"%s" must be called at with hook name and at maximum three other arguments: callback, priority, and accepted args num.', + $type === HookStorage::ACTIONS ? "add_action" : "add_filter" + ), + self::CODE_WRONG_ARGS_COUNT + ); + } + + /** + * @param string $type + * @return static + */ + public static function forMissingCallback($type) + { + return new static( + sprintf( + 'A callback parameter is required for "%s".', + $type === HookStorage::ACTIONS ? "add_action" : "add_filter" + ), + self::CODE_MISSING_CALLBACK + ); + } + + /** + * @param string $type + * @return static + */ + public static function forInvalidPriority($type) + { + return new static( + sprintf( + 'Priority parameter passed to "%s" must be an integer.', + $type === HookStorage::ACTIONS ? "add_action" : "add_filter" + ), + self::CODE_INVALID_PRIORITY + ); + } + + /** + * @param string $type + * @return static + */ + public static function forInvalidAcceptedArgs($type) + { + return new static( + sprintf( + 'Accepted args number parameter passed to "%s" must be an integer.', + $type === HookStorage::ACTIONS ? "add_action" : "add_filter" + ), + self::CODE_INVALID_ACCEPTED_ARGS + ); + } +} \ No newline at end of file diff --git a/src/Hooks/Exception/InvalidHookArgument.php b/src/Hooks/Exception/InvalidHookArgument.php new file mode 100644 index 0000000..6850dd3 --- /dev/null +++ b/src/Hooks/Exception/InvalidHookArgument.php @@ -0,0 +1,79 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidHookArgument extends Exception +{ + + /** + * @param mixed $type + * @return static + */ + public static function forInvalidType($type) + { + return new static( + sprintf( + 'HookStorage hook type must either HookStorage::ACTIONS or HookStorage::FILTERS, got %s.', + is_object($type) ? ' instance of '.get_class($type) : gettype($type) + ) + ); + } + + /** + * @param mixed $type + * @return static + */ + public static function forInvalidHook($type) + { + return new static( + sprintf( + 'Hook name must be in a string, got %s.', + is_object($type) ? ' instance of '.get_class($type) : gettype($type) + ) + ); + } + + /** + * @param string $key + * @param string $type + * @return static + */ + public static function forEmptyArguments($key, $type) + { + $function = $missing = ''; + + switch ($type) { + case HookStorage::ACTIONS: + $missing = 'callback'; + $function = $key === HookStorage::ADDED ? "'add_action'" : "'do_action'"; + break; + case HookStorage::FILTERS: + $missing = $key === HookStorage::ADDED ? 'callback' : 'first'; + $function = $key === HookStorage::ADDED ? "'add_filter'" : "'apply_filters'"; + break; + } + + return new static( + sprintf( + 'Missing %s required argument for %s.', + $missing, + $function + ) + ); + } +} \ No newline at end of file diff --git a/src/Hooks/HookExpectationExecutor.php b/src/Hooks/HookExpectationExecutor.php new file mode 100644 index 0000000..b7cfb09 --- /dev/null +++ b/src/Hooks/HookExpectationExecutor.php @@ -0,0 +1,116 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class HookExpectationExecutor +{ + + /** + * @var \Brain\Monkey\Hooks\HookRunningStack + */ + private $stack; + + /** + * @var \Brain\Monkey\Expectation\ExpectationFactory + */ + private $factory; + + /** + * @param \Brain\Monkey\Hooks\HookRunningStack $stack + * @param \Brain\Monkey\Expectation\ExpectationFactory $factory + */ + public function __construct(HookRunningStack $stack, ExpectationFactory $factory) + { + $this->stack = $stack; + $this->factory = $factory; + } + + /** + * @param string $action + * @param array $args + */ + public function executeAddAction($action, array $args) + { + $this->execute(ExpectationTarget::TYPE_ACTION_ADDED, $action, $args); + } + + /** + * @param string $action + * @param array $args + */ + public function executeAddFilter($action, array $args) + { + $this->execute(ExpectationTarget::TYPE_FILTER_ADDED, $action, $args); + } + + /** + * @param string $action + * @param array $args + */ + public function executeDoAction($action, array $args = []) + { + $is_running = $this->stack->has(); + $this->stack->push($action); + $this->execute(ExpectationTarget::TYPE_ACTION_DONE, $action, $args); + $is_running or $this->stack->reset(); + } + + /** + * @param string $filter + * @param array $args + * @return mixed|null + */ + public function executeApplyFilters($filter, array $args) + { + + $is_running = $this->stack->has(); + $this->stack->push($filter); + $return = $this->execute(ExpectationTarget::TYPE_FILTER_APPLIED, $filter, $args); + $is_running or $this->stack->reset(); + + return $return; + } + + /** + * @param string $type + * @param string $hook + * @param array $args + * @return mixed + */ + private function execute($type, $hook, array $args) + { + $target = new ExpectationTarget($type, $hook); + if ($this->factory->hasMockFor($target)) { + $method = $target->mockMethodName(); + + return $this->factory->mockFor($target)->{$method}(...$args); + } + + if ($type === ExpectationTarget::TYPE_FILTER_APPLIED) { + return reset($args); + } + + return null; + + } +} \ No newline at end of file diff --git a/src/Hooks/HookRunningStack.php b/src/Hooks/HookRunningStack.php new file mode 100644 index 0000000..f6b69c5 --- /dev/null +++ b/src/Hooks/HookRunningStack.php @@ -0,0 +1,76 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class HookRunningStack +{ + + /** + * @var array + */ + private $stack = []; + + /** + * @param string $hook_name + * @return static + */ + public function push($hook_name) + { + $this->stack[] = $hook_name; + + return $this; + } + + /** + * @return string + */ + public function last() + { + if ( ! $this->stack) { + return ''; + } + + return end($this->stack); + } + + /** + * @param string $hook_name + * @return bool + */ + public function has($hook_name = null) + { + if ( ! $this->stack) { + return false; + } + + return $hook_name === null ? true : in_array($hook_name, $this->stack, true); + } + + /** + * @return static + */ + public function reset() + { + $this->stack = []; + + return $this; + } +} \ No newline at end of file diff --git a/src/Hooks/HookStorage.php b/src/Hooks/HookStorage.php new file mode 100644 index 0000000..5d2662e --- /dev/null +++ b/src/Hooks/HookStorage.php @@ -0,0 +1,230 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class HookStorage +{ + + const ACTIONS = 'actions'; + const FILTERS = 'filters'; + const ADDED = 'added'; + const DONE = 'done'; + + private $storage = [ + self::ADDED => [], + self::DONE => [] + ]; + + /** + * @return void + */ + public function reset() + { + $this->storage = [ + self::ADDED => [], + self::DONE => [] + ]; + } + + /** + * @param string $type + * @param string $hook + * @param array $args + * @return static + */ + public function pushToAdded($type, $hook, array $args) + { + return $this->pushToStorage(self::ADDED, $type, $hook, $args); + } + + /** + * @param string $type + * @param string $hook + * @param array $args + * @return bool + */ + public function removeFromAdded($type, $hook, array $args) + { + if ( ! $this->isHookAdded($type, $hook)) { + return false; + } + + if ( ! $args) { + unset($this->storage[self::ADDED][$type][$hook]); + + return true; + } + + $args = $this->parseArgsToAdd($args, self::ADDED, $type); + + $all = $this->storage[self::ADDED][$type][$hook]; + $removed = 0; + + /** + * @var CallbackStringForm $callback + */ + foreach ($all as $key => list($callback, $priority)) { + if ($callback->equals($args[0]) && $priority === $args[1]) { + unset($all[$key]); + $removed++; + } + } + + $removed and $this->storage[self::ADDED][$type][$hook] = array_values($all); + + return $removed > 0; + } + + /** + * @param string $type + * @param string $hook + * @param array $args + * @return static + */ + public function pushToDone($type, $hook, array $args) + { + return $this->pushToStorage(self::DONE, $type, $hook, $args); + } + + /** + * @param string $type + * @param string $hook + * @param callable|null $function + * @return bool + */ + public function isHookAdded($type, $hook, $function = null) + { + return $this->isInStorage(self::ADDED, $type, $hook, $function); + } + + /** + * @param $type + * @param $hook + * @return int + */ + public function isHookDone($type, $hook) + { + return $this->isInStorage(self::DONE, $type, $hook); + } + + /** + * @param string $key + * @param string $type + * @param string $hook + * @param array $args + * @return static + * @throws \Brain\Monkey\Hooks\Exception\InvalidHookArgument + */ + private function pushToStorage($key, $type, $hook, array $args) + { + if ( ! is_string($type)) { + throw Exception\InvalidHookArgument::forInvalidType($type); + } + + if ( ! is_string($hook)) { + throw Exception\InvalidHookArgument::forInvalidHook($hook); + } + + // do_action() is the only of target functions that can be called without additional arguments + if ( ! $args && $key !== self::DONE && $type !== self::ACTIONS) { + throw Exception\InvalidHookArgument::forEmptyArguments($key, $type); + } + + $storage = &$this->storage[$key]; + + array_key_exists($type, $storage) or $storage[$type] = []; + array_key_exists($hook, $storage[$type]) or $storage[$type][$hook] = []; + + if ($key === self::ADDED) { + $args = $this->parseArgsToAdd($args, $key, $type); + } + + $storage[$type][$hook][] = $args; + + return $this; + } + + /** + * @param string $key + * @param string $type + * @param string $hook + * @param callable|null $function + * @return int|bool + * @throws \Brain\Monkey\Hooks\Exception\InvalidHookArgument + */ + private function isInStorage($key, $type, $hook, $function = null) + { + $storage = $this->storage[$key]; + + if ( ! in_array($type, [self::ACTIONS, self::FILTERS], true)) { + throw Exception\InvalidHookArgument::forInvalidType($type); + } + + if ( ! array_key_exists($type, $storage) || ! array_key_exists($hook, $storage[$type])) { + return $key === self::ADDED ? false : 0; + } + + if ($function === null) { + return $key === self::ADDED ? true : count($storage[$type][$hook]); + } + + $filter = function (array $args) use ($function) { + return $args[0]->equals(new CallbackStringForm($function)); + }; + + $matching = array_filter($storage[$type][$hook], $filter); + + return $key === self::ADDED ? (bool)$matching : count($matching); + } + + /** + * @param array $args + * @param string $key + * @param string $type + * @return array + * @throws \Brain\Monkey\Hooks\Exception\InvalidHookArgument + */ + private function parseArgsToAdd(array $args, $key, $type) + { + if ( ! $args) { + throw Exception\InvalidHookArgument::forEmptyArguments($key, $type); + } + + if ( ! count($args) > 3) { + throw Exception\InvalidAddedHookArgument::forWrongArgumentsCount($type); + } + + $args = array_replace([null, 10, 1], array_values($args)); + + if ( ! $args[0]) { + throw Exception\InvalidAddedHookArgument::forMissingCallback($type); + } + + $args[0] = new CallbackStringForm($args[0]); + + if ( ! is_int($args[1])) { + throw Exception\InvalidAddedHookArgument::forInvalidPriority($type); + } + + if ( ! is_int($args[2])) { + throw Exception\InvalidAddedHookArgument::forInvalidAcceptedArgs($type); + } + + return $args; + } +} \ No newline at end of file diff --git a/src/Monkey.php b/src/Monkey.php deleted file mode 100644 index 019c4fa..0000000 --- a/src/Monkey.php +++ /dev/null @@ -1,162 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain; - -use Brain\Monkey\WP\Hooks; -use Brain\Monkey\Functions; -use Patchwork; -use Mockery; -use ReflectionClass as Reflection; -use ReflectionMethod as M; -use RuntimeException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - * - * @method Functions when() - * @method \Mockery\Expectation expect() - */ -class Monkey -{ - /** - * @var array|null - */ - private $proxied; - - /** - * Include WordPress functions file, only needed to mock WordPress hooks. - */ - public static function setUp() - { - $vendor = dirname(dirname(dirname(__DIR__))); - $patchwork = '/antecedent/patchwork/Patchwork.php'; - if (file_exists($vendor.$patchwork)) { - /** @noinspection PhpIncludeInspection */ - @require_once $vendor.$patchwork; // normal installation - } elseif (file_exists(dirname(dirname(__DIR__)).$patchwork)) { - /** @noinspection PhpIncludeInspection */ - @require_once dirname(dirname(__DIR__)).$patchwork; // root installation - } - if (! function_exists('Patchwork\replace')) { - throw new RuntimeException( - 'Brain Monkey was unable to load Patchwork. Please require Patchwork.php by yourself before running tests.' - ); - } - } - - /** - * Include WordPress functions file, only needed to mock WordPress hooks. - */ - public static function setUpWP() - { - self::setUp(); - require_once dirname(__DIR__).'/inc/wp-functions.php'; - } - - /** - * Clean up Functions and Hooks statics: is always needed when using this class. - */ - public static function tearDown() - { - Functions::__flush(); - Patchwork\restoreAll(); - Mockery::close(); - } - - /** - * Clean up Functions and Hooks statics: is always needed when using this class. - */ - public static function tearDownWP() - { - Hooks::tearDown(); - self::tearDown(); - } - - /** - * An alias for Hooks::instance(Hooks::ACTION), allows to only use this class inside tests. - * - * @return \Brain\Monkey\WP\Actions - */ - public static function actions() - { - return Hooks::instance(Hooks::ACTION); - } - - /** - * An alias for Hooks::instance(Hooks::FILTER), allows to only use this class inside tests. - * - * @return \Brain\Monkey\WP\Filters - */ - public static function filters() - { - return Hooks::instance(Hooks::FILTER); - } - - /** - * Returns an instance of current class, that thanks to __call() implementation allows to call - * static Functions class methods on the returned instance. - * This way this method allows exactly same syntax of actions() and filters(). - * - * @return \Brain\Monkey - */ - public static function functions() - { - return new self('Brain\Monkey\Functions'); - } - - /** - * Constructor. - * - * Passing a target class, it will be possible to call dynamically on the obtained instance - * methods that will be proxied to target class static methods. - * - * @param string|null $target - */ - public function __construct($target = null) - { - if (is_string($target) && class_exists($target)) { - $this->proxied[$target] = array_map( - function ($method) { - return $method->name; - }, - (new Reflection($target))->getMethods(M::IS_STATIC | M::IS_PUBLIC) - ); - } - } - - /** - * When a target object is set, allows to call static methods on target class by calling - * same-named dynamic methods on this class. - * Mainly used to allows for functions() same syntax of actions() and filters(). - * - * @param string $name - * @param array $arguments - * @return mixed - */ - public function __call($name, array $arguments = []) - { - if (! empty($this->proxied) && in_array($name, reset($this->proxied), true)) { - return call_user_func_array([key($this->proxied), $name], $arguments); - } - $backtrace = debug_backtrace(0, 2); - trigger_error( - sprintf( - "Call to undefined method %s() in %s line %d. Fired ", - __CLASS__.'::'.$name, - $backtrace[1]['file'], - $backtrace[1]['line'] - ), - E_USER_ERROR - ); - } -} diff --git a/src/Monkey/Functions.php b/src/Monkey/Functions.php deleted file mode 100644 index 1ed79b5..0000000 --- a/src/Monkey/Functions.php +++ /dev/null @@ -1,234 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey; - -use Patchwork; -use Mockery; -use InvalidArgumentException; -use RuntimeException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - */ -class Functions -{ - /** - * @var array - */ - private static $functions = []; - - /** - * @var string Name of the function to mock - */ - private $name; - - /** - * Clean up mocked functions array. - */ - public static function __flush() - { - self::$functions = []; - } - - /** - * Factory method: receives the name of the function to mock and return an instance of - * FunctionMock where is possible to call any of the mocking functions. - * - * @param string $functionName the name of the function to mock - * @return \Brain\Monkey\Functions - */ - public static function when($functionName) - { - $names = self::check($functionName); - $name = array_pop($names); - $namespace = empty($names) ? false : implode('\\', $names); - $instance = new static(); - $instance->name = $namespace ? $namespace.'\\'.$name : $name; - if (! function_exists($instance->name)) { - $fn = $namespace ? "namespace {$namespace};\n" : ''; - $fn .= "function {$name}() {"; - $fn .= " trigger_error('{$name} is not defined nor mocked in this test.', E_USER_ERROR);"; - $fn .= "}"; - eval($fn); - } - - return $instance; - } - - /** - * Returns a Mockery Expectation object, where is possible to set all the expectations, using - * Mockery methods. - * - * @param string $functionName the name of the function to mock - * @return \Mockery\Expectation - * @see http://docs.mockery.io/en/latest/reference/expectations.html - */ - public static function expect($functionName) - { - if (! isset(self::$functions[$functionName])) { - self::when($functionName); - $mockery = Mockery::mock($functionName); - Patchwork\redefine($functionName, function () use (&$mockery, $functionName) { - return call_user_func_array([$mockery, $functionName], func_get_args()); - }); - self::$functions[$functionName] = $mockery; - } - /** @var \Mockery\MockInterface $mockery */ - $mockery = self::$functions[$functionName]; - /** @var \Mockery\ExpectationInterface $expectation */ - $expectation = $mockery->shouldReceive($functionName); - - return new MockeryBridge($expectation); - } - - /** - * Checks the name of a function and throw an exception if is not valid. When name is valid - * returns an array of the name itself and its namespace parts. - * - * @param string $functionName - * @return array - */ - private static function check($functionName) - { - $names = is_string($functionName) ? explode('\\', $functionName) : false; - $valid = function ($n) { - return is_string($n) && preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $n); - }; - if (! $names || array_filter($names, $valid) !== $names) { - $name = $names ? $functionName : 'The value passed to '.__CLASS__; - throw new InvalidArgumentException("'{$name}' is not a valid function name."); - } - - return $names; - } - - /** - * Mocks the function and makes it return an arbitrary value. - * - * @param mixed $return - */ - public function justReturn($return = null) - { - Patchwork\redefine($this->name, function () use ($return) { - return $return; - }); - } - - /** - * Mocks the function and makes it echo an arbitrary value. - * - * @param mixed $value - */ - public function justEcho($value = null) - { - is_null($value) and $value = ''; - if (! is_scalar($value) && ! (is_object($value) && method_exists($value, '__toString'))) { - throw new InvalidArgumentException( - sprintf( - "Please, use a string with %s, can't echo a var of type %s.", - __METHOD__, - gettype($value) - ) - ); - } - $this->justReturn($value); - $cb = $this->name; - echo (string) $cb(); - } - - /** - * Mocks the function making it return one of the received arguments, the first by default. - * Throw an exception if the function does not receive desired argument. - * - * @param int $n The position (1-based) of the argument to return - */ - public function returnArg($n = 1) - { - $name = $this->name; - $n = $this->ensureArg($n); - Patchwork\redefine($name, function () use ($n, $name) { - $count = func_num_args(); - $n0 = $n - 1; - if ($count < $n0) { - throw new RuntimeException( - "{$name} was called with {$count} params, can't return arg ".($n)."." - ); - } - - return func_num_args() > $n0 ? func_get_arg($n0) : null; - }); - } - - /** - * Mocks the function making it echo one of the received arguments, the first by default. - * - * @param int $n The position (1-based) of the argument to echo - */ - public function echoArg($n = 1) - { - $name = $this->name; - $n = $this->ensureArg($n); - Patchwork\redefine($name, function () use ($n, $name) { - $count = func_num_args(); - $n0 = $n - 1; - if ($count < $n0) { - throw new RuntimeException( - "{$name} was called with {$count} params, can't return arg ".($n)."." - ); - } - - $value = func_num_args() > $n0 ? func_get_arg($n0) : ''; - - if ( - ! is_scalar($value) - && ! (is_object($value) && method_exists($value, '__toString')) - ) { - throw new RuntimeException( - sprintf( - "%s received as argument %d a %s, can't echo it.", - $name, - $n, - gettype($value) - ) - ); - } - - echo (string) $value; - }); - } - - /** - * Mocks the function replacing it on the fly with a given callable. - * - * @param callable $callback - */ - public function alias(callable $callback) - { - Patchwork\redefine($this->name, $callback); - } - - /** - * @param int $n - * @return int - */ - private function ensureArg($n) - { - if (! is_int($n) || $n < 1) { - throw new InvalidArgumentException( - "Argument number for {$this->name} must be a greater than 1 integer." - ); - } - - return $n; - } -} diff --git a/src/Monkey/MockeryBridge.php b/src/Monkey/MockeryBridge.php deleted file mode 100644 index 631c4a1..0000000 --- a/src/Monkey/MockeryBridge.php +++ /dev/null @@ -1,138 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey; - -use BadMethodCallException; -use Mockery\ExpectationInterface; -use RuntimeException; -use LogicException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - * - * @method MockeryBridge once() - * @method MockeryBridge twice() - * @method MockeryBridge times() - * @method MockeryBridge atLeast() - * @method MockeryBridge atMost() - * @method MockeryBridge between() - * @method MockeryBridge zeroOrMoreTimes() - * @method MockeryBridge never() - * @method MockeryBridge with() - * @method MockeryBridge withNoArgs() - * @method MockeryBridge withAnyArgs() - * @method MockeryBridge andReturn() - * @method MockeryBridge andReturnNull() - * @method MockeryBridge andReturnValues() - * @method MockeryBridge andReturnUsing() - * @method MockeryBridge andThrow() - */ -class MockeryBridge -{ - /** - * @var \Mockery\Expectation - */ - private $expectation; - - /** - * @var bool - */ - private $isHook = false; - - /** - * @var bool - */ - private $isAction = false; - - /** - * Constructor. - * - * @param \Mockery\ExpectationInterface $expectation - * @param string|null $parent - */ - public function __construct(ExpectationInterface $expectation, $parent = null) - { - $this->expectation = $expectation; - $name = $this->expectation->__toString(); - if (is_string($parent) && class_exists($parent)) { - $parent = trim($parent, '\\'); - $reflection = new \ReflectionClass($parent); - $this->isHook = $reflection->isSubclassOf('Brain\Monkey\WP\Hooks'); - $this->isAction = - $parent === 'Brain\Monkey\WP\Actions' - || is_subclass_of($parent, 'Brain\Monkey\WP\Actions'); - } - $this->isAddedHook = $this->isHook && strpos($name, '[add_') === 0; - } - - /** - * shouldReceive() can't be called on Mockery expectation object or everything will break. - * - * @param string $name - * @param array $arguments - * @return \Brain\Monkey\MockeryBridge $this - */ - public function __call($name, array $arguments = []) - { - $notAllowed = ['shouldreceive', 'andset', 'shouldexpect']; - if (in_array(strtolower($name), $notAllowed, true)) { - throw new BadMethodCallException( - "shouldReceive(), shouldExpect() and andSet() methods are not allowed in Brain Monkey." - ); - } - if (stristr($name, 'return') && $this->isHook) { - $this->checkReturn($name); - } - $this->expectation = call_user_func_array([$this->expectation, $name], $arguments); - - return $this; - } - - /** - * WordPress testing function used to avoid return expectations on action hooks. - * Throws exception for WordPress filters and generic PHP function testing. - * - * @param callable $callback - * @return \Mockery\Expectation - */ - public function whenHappen(callable $callback) - { - if (! $this->isAddedHook && ! $this->isAction) { - throw new RuntimeException( - 'whenHappen() can only be used for WordPress actions or added filters expectations.' - ); - } - - return $this->expectation->andReturnUsing($callback); - } - - /** - * WordPress testing function, ignored for generic PHP function testing. - * Used to avoid return expectations on added hooks. - * - * @param string $name - */ - private function checkReturn($name) - { - if ($this->isAction) { - throw new LogicException( - "Don't use return expectations on actions, use whenHappen() instead." - ); - } - if (strpos($name, 'add_') === 0) { - throw new LogicException( - "Don't use return expectations on added hooks." - ); - } - } -} diff --git a/src/Monkey/WP/Actions.php b/src/Monkey/WP/Actions.php deleted file mode 100644 index 60d28b1..0000000 --- a/src/Monkey/WP/Actions.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey\WP; - -use Mockery; -use InvalidArgumentException; -use LogicException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - */ -class Actions extends Hooks -{ - /** - * Retrieves an Mockery object that allows to set expectations on specific hook fired, and even - * run a specific callback as response of hook firing. - * - * @param string $action Action name, e.g. 'init' - * @return \Brain\Monkey\WP\MockeryHookBridge - */ - public static function expectFired($action) - { - return self::createBridgeFor(self::ACTION, $action, 'run'); - } - - /** - * Retrieves an Mockery object that allows to set expectations on specific hook added. - * - * @param string $action Action name, e.g. 'init' - * @return \Brain\Monkey\WP\MockeryHookBridge - */ - public static function expectAdded($action) - { - return self::createBridgeFor(self::ACTION, $action, 'add'); - } - - /** - * Adds an action hook. - * - * @return bool Always true, because so do WordPress. - */ - public function add() - { - $args = func_get_args(); - array_unshift($args, self::ACTION); - - return call_user_func_array([$this, 'addHook'], $args); - } - - /** - * Removes an action hook. - * - * @return bool True when the hook exists and is been removed. - */ - public function remove() - { - $args = func_get_args(); - array_unshift($args, self::ACTION); - - return call_user_func_array([$this, 'removeHook'], $args); - } - - /** - * Fires an action. - */ - public function run() - { - $args = func_get_args(); - array_unshift($args, self::ACTION); - call_user_func_array([$this, 'runHook'], $args); - } - - /** - * Fires an action using an array for arguments. - */ - public function runRef() - { - if (func_num_args() < 2 || ! is_array(func_get_arg(1))) { - throw new LogicException('do_action_ref_array() needs an array as second argument.'); - } - $args = func_get_arg(1); - array_unshift($args, func_get_arg(0)); - - call_user_func_array([$this, 'run'], $args); - } - - /** - * Checks if an action has been added. - * - * @return bool - */ - public function has() - { - $args = func_get_args(); - array_unshift($args, self::ACTION); - - return call_user_func_array([$this, 'hasHook'], $args); - } - - /** - * Checks if a specific action has been triggered, - * - * @param string $action - * @return int - */ - public function did($action) - { - if (empty($action) || ! is_string($action)) { - throw new InvalidArgumentException("Action name must be in a string."); - } - - return in_array($action, $this->done, true) ? array_count_values($this->done)[$action] : 0; - } - - /** - * Cleanup. - */ - public function clean() - { - $this->cleanInstance($this); - } -} diff --git a/src/Monkey/WP/Filters.php b/src/Monkey/WP/Filters.php deleted file mode 100644 index 3d1e4a5..0000000 --- a/src/Monkey/WP/Filters.php +++ /dev/null @@ -1,121 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey\WP; - -use LogicException; -use InvalidArgumentException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - */ -class Filters extends Hooks -{ - /** - * @param string $filter - * @return \Brain\Monkey\WP\MockeryHookBridge - */ - public static function expectApplied($filter) - { - return self::createBridgeFor(self::FILTER, $filter, 'run'); - } - - /** - * @param string $filter - * @return \Brain\Monkey\WP\MockeryHookBridge - */ - public static function expectAdded($filter) - { - return self::createBridgeFor(self::FILTER, $filter, 'add'); - } - - /** - * @inheritdoc - */ - public function add() - { - $args = func_get_args(); - array_unshift($args, self::FILTER); - - return call_user_func_array([$this, 'addHook'], $args); - } - - /** - * @inheritdoc - */ - public function remove() - { - $args = func_get_args(); - array_unshift($args, self::FILTER); - - return call_user_func_array([$this, 'removeHook'], $args); - } - - /** - * @inheritdoc - */ - public function run() - { - $args = func_get_args(); - array_unshift($args, self::FILTER); - - return call_user_func_array([$this, 'runHook'], $args); - } - - /** - * @inheritdoc - */ - public function runRef() - { - if (func_num_args() < 2 || ! is_array(func_get_arg(1))) { - throw new LogicException('apply_filters_ref_array() needs an array as second argument.'); - } - $args = func_get_arg(1); - array_unshift($args, func_get_arg(0)); - - return call_user_func_array([$this, 'run'], $args); - } - - /** - * @inheritdoc - */ - public function has() - { - $args = func_get_args(); - array_unshift($args, self::FILTER); - - return call_user_func_array([$this, 'hasHook'], $args); - } - - /** - * Checks if a specific action has been triggered. - * - * @param string $filter - * @return int - */ - public function applied($filter) - { - if (empty($filter) || ! is_string($filter)) { - throw new InvalidArgumentException("Action name must be in a string."); - } - - return in_array($filter, $this->done, true) ? array_count_values($this->done)[$filter] : 0; - } - - /** - * @inheritdoc - */ - public function clean() - { - $this->cleanInstance($this); - } -} diff --git a/src/Monkey/WP/Hooks.php b/src/Monkey/WP/Hooks.php deleted file mode 100644 index 5a81f55..0000000 --- a/src/Monkey/WP/Hooks.php +++ /dev/null @@ -1,423 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey\WP; - -use Brain\Monkey\MockeryBridge; -use Mockery; -use Closure; -use InvalidArgumentException; -use LogicException; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - */ -abstract class Hooks -{ - const ACTION = 'action'; - const FILTER = 'filter'; - - /** - * @var array - */ - private static $instances = []; - - /** - * @var array - */ - private static $names = []; - - /** - * @var array - */ - private static $classes = [ - self::ACTION => 'Brain\Monkey\WP\Actions', - self::FILTER => 'Brain\Monkey\WP\Filters', - ]; - - private static $sanitize_map = [ - '-' => '_', - ' ' => '_sp_', - '/' => '_sl_', - '\\' => '_bs_', - '.' => '_po_', - '!' => '_es_', - '"' => '_q_', - '\'' => '_sq_', - '£' => '_pou_', - '$' => '_do_', - '%' => '_pe_', - '(' => '_op_', - ')' => '_cp_', - '=' => '_eq_', - '?' => '_qm_', - '^' => '_ca_', - '*' => '_as_', - '@' => '_at_', - '°' => '_deg_', - '#' => '_sh_', - '[' => '_ob_', - ']' => '_cb_', - '+' => '_and_', - '|' => '_pi_', - '<' => '_lt_', - '>' => '_gt_', - ',' => '_co_', - ';' => '_sc_', - '{' => '_ocb_', - '}' => '_ccb_', - '~' => '_ti_', - ]; - - /** - * @var bool - */ - private static $current = false; - - /** - * @var array - */ - protected $hooks = []; - - /** - * @var array - */ - protected $done = []; - - /** - * @var array - */ - protected $mocks = []; - - /** - * @param string $name - * @param array $args - * @return mixed - */ - public static function __callStatic($name, $args) - { - $type = strstr($name, self::ACTION) ? self::ACTION : self::FILTER; - $method = substr($name, 0, -1 * strlen($type)); - - return call_user_func_array([self::instance($type), $method], $args); - } - - /** - * @param string $type - * @return \Brain\Monkey\WP\Actions|\Brain\Monkey\WP\Filters - */ - public static function instance($type) - { - if (! isset(self::$instances[$type])) { - $class = self::$classes[$type]; - self::$instances[$type] = new $class(); - } - - return self::$instances[$type]; - } - - public static function tearDown() - { - if (isset(self::$instances[self::ACTION])) { - /** @var \Brain\Monkey\WP\Actions $actions */ - $actions = self::$instances[self::ACTION]; - $actions->clean(); - self::$instances[self::ACTION] = null; - } - if (isset(self::$instances[self::FILTER])) { - /** @var \Brain\Monkey\WP\Filters $filters */ - $filters = self::$instances[self::FILTER]; - $filters->clean(); - self::$instances[self::FILTER] = null; - } - self::$instances = []; - self::$names = []; - self::$current = null; - } - - /** - * @return string|bool - */ - public static function current() - { - $current = $name = self::$current; - if (is_string($current) && isset(self::$names[$current])) { - $name = self::$names[$current]; - } - - return $name; - } - - abstract public function add(); - - abstract public function remove(); - - abstract public function run(); - - abstract public function runRef(); - - abstract public function has(); - - abstract public function clean(); - - /** - * @param string $type - * @return bool - */ - protected function addHook($type) - { - /** @var \Brain\Monkey\WP\Actions|\Brain\Monkey\WP\Filters $instance */ - $instance = self::instance($type); - $parsed = $this->args(array_slice(func_get_args(), 1), $type); - $data = reset($parsed); - // hook name, e.g. 'init' - $hook = self::sanitizeHookName($data['hook']); - if (! isset($instance->hooks[$hook])) { - $instance->hooks[$hook] = []; - self::$names[$hook] = $data['hook']; - } - $instance->hooks[$hook][key($parsed)] = $data; - if (isset($instance->mocks[$hook]) && isset($instance->mocks[$hook]['add'])) { - /** @var \Mockery\Expectation $mock */ - $mock = $instance->mocks[$hook]['add']; - call_user_func_array([$mock, "add_{$type}_{$hook}"], array_slice(func_get_args(), 2)); - } - - return true; - } - - /** - * @param string $type - * @return bool - */ - protected function removeHook($type) - { - /** @var \Brain\Monkey\WP\Actions|\Brain\Monkey\WP\Filters $instance */ - $instance = self::instance($type); - $parsed = $this->args(array_slice(func_get_args(), 1), $type, true); - $data = reset($parsed); - // hook name, e.g. 'init' - $hook = self::sanitizeHookName($data['hook']); - if (isset($instance->hooks[$hook]) && is_array($instance->hooks[$hook])) { - $hooks = $instance->hooks[$hook]; - foreach ($hooks as $key => $hookData) { - if ($key === key($parsed) && $hookData === $data) { - unset($instance->hooks[$hook][$key]); - - return true; - }; - } - } - - return false; - } - - /** - * @param string $type - * @return mixed|null - */ - protected function runHook($type) - { - /** @var \Brain\Monkey\WP\Actions|\Brain\Monkey\WP\Filters $instance */ - $instance = self::instance($type); - $args = array_slice(func_get_args(), 1); - if (empty($args) || ! is_string(reset($args))) { - throw new LogicException("To fire a {$type} its name is required and has to be a string."); - } - // hook name, e.g. 'init' - $rawHook = array_shift($args); - // sanitized hook name, where anything that does not match [a-zA-Z0-9_] is removed. - // this is done because hooks becomes class methods, and special chars are not allowed there - $hook = self::sanitizeHookName($rawHook); - if ($rawHook !== $hook && ! isset(self::$names[$hook])) { - self::$names[$hook] = $rawHook; - } - // returning value is always null for actions - $value = $type === self::FILTER && func_num_args() > 2 ? func_get_arg(2) : null; - self::$current = $hook; - // This will be used to mock `did_action` so we have to store the raw hook - $instance->done[] = $rawHook; - if (isset($instance->mocks[$hook]) && isset($instance->mocks[$hook]['run'])) { - /** @var \Mockery\Expectation $mock */ - $mock = $instance->mocks[$hook]['run']; - $verb = $type === self::FILTER ? 'apply' : 'do'; - $result = call_user_func_array([$mock, "{$verb}_{$type}_{$hook}"], $args); - $value = $type === self::FILTER ? $result : null; - } - self::$current = false; - - return $value; - } - - /** - * @param string $type - * @return bool - */ - protected function hasHook($type) - { - /** @var \Brain\Monkey\WP\Actions|\Brain\Monkey\WP\Filters $instance */ - $instance = self::instance($type); - $hookArgs = array_slice(func_get_args(), 1); - $hookArgsCount = count($hookArgs); - // We are checking just if the hook has *any* callback attached - if ($hookArgsCount === 1 && is_string($hookArgs[0])) { - return ! empty($instance->hooks[self::sanitizeHookName($hookArgs[0])]); - } - $parsed = $this->args($hookArgs, $type, true); - $data = reset($parsed); - // hook name, e.g. 'init' - $hook = self::sanitizeHookName($data['hook']); - if (isset($instance->hooks[$hook]) && is_array($instance->hooks[$hook])) { - foreach ($instance->hooks[$hook] as $key => $hookData) { - if ($hookData === $data) { - return true; - }; - } - } - - return false; - } - - /** - * @param \Brain\Monkey\WP\Hooks $instance - */ - protected function cleanInstance(Hooks $instance) - { - $instance->hooks = null; - if (! empty($instance->mocks)) { - foreach (array_keys($instance->mocks) as $key) { - $instance->mocks[$key] = null; - } - } - $instance->mocks = null; - $instance->done = null; - } - - /** - * Receive variadic arguments and format an array with all the information about hook. - * Missing values are filled with default. - * - * @param array $args - * @param string $type - * @param bool $getId - * @return array - */ - private function args(array $args, $type, $getId = false) - { - if (empty($args)) { - throw new InvalidArgumentException("{$type} name and callback are required."); - } - $hook = array_shift($args); - if (empty($hook) || ! is_string($hook)) { - throw new InvalidArgumentException("{$type} name must be in a string."); - } - $callback = empty($args) ?: array_shift($args); - if (is_callable($callback)) { - $getId = false; - } - if (($getId && ! is_string($callback)) || (! $getId && ! is_callable($callback))) { - throw new InvalidArgumentException("A callback is required to add a {$type}."); - } - $callbackId = $getId ? [$callback] : $this->callbackId($callback); - $callbackUId = $getId ? $callback.'__'.uniqid() : $callbackId[0].'__'.$callbackId[1]; - $priority = empty($args) ? 10 : array_shift($args); - if (! is_numeric($priority)) { - throw new InvalidArgumentException("To add a {$type} priority must be an integer."); - } - $argsNum = empty($args) ? 1 : array_shift($args); - if (! is_numeric($argsNum)) { - throw new InvalidArgumentException("To add a {$type} accepted args must be an integer."); - } - - return [ - $callbackUId => [ - 'hook' => $hook, - 'id' => $callbackId[0], - 'priority' => $priority, - 'args_num' => $argsNum, - ] - ]; - } - - /** - * @param callable $callback - * @return array - */ - private function callbackId(callable $callback) - { - $hash = ''; - $id = ''; - if (is_string($callback)) { - $id = $callback; - } elseif ($callback instanceof Closure) { - /** @var object $callback */ - $hash = spl_object_hash($callback); - $id = "function()"; - } elseif (is_object($callback)) { - /** @var object $callback */ - $hash = spl_object_hash($callback); - $id = get_class($callback)."()"; - } elseif (is_array($callback) && is_object($callback[0])) { - $hash = spl_object_hash($callback[0]); - $id = get_class($callback[0])."->{$callback[1]}()"; - } elseif (is_array($callback)) { - $id = "{$callback[0]}::{$callback[1]}()"; - } - - return [$id, $hash]; - } - - /** - * @param string $type - * @param string $hook - * @param string $action - * @return \Brain\Monkey\WP\MockeryHookBridge - */ - protected static function createBridgeFor($type, $hook, $action = 'add') - { - $hook = self::sanitizeHookName($hook); - /** @var static $instance */ - $instance = self::instance($type); - $prefix = $action; - ($action === 'run') and $prefix = $type === self::FILTER ? 'apply' : 'do'; - $method = "{$prefix}_{$type}_{$hook}"; - - $mock = null; - is_array($instance->mocks) or $instance->mocks = []; - if (! isset($instance->mocks[$hook]) || ! isset($instance->mocks[$hook][$action])) { - isset($instance->mocks[$hook]) or $instance->mocks[$hook] = []; - $mock = Mockery::mock("{$prefix}_{$hook}"); - $instance->mocks[$hook][$action] = $mock; - } - - $mock = $instance->mocks[$hook][$action]; - $expectation = $mock->shouldReceive($method); - $parent = $type === self::FILTER ? '\Brain\Monkey\WP\Filters' : '\Brain\Monkey\WP\Actions'; - - return new MockeryHookBridge(new MockeryBridge($expectation, $parent)); - } - - /** - * @param string $name - * @return string - */ - private static function sanitizeHookName($name) - { - $replaced = strtr($name, self::$sanitize_map); - $clean = preg_replace('/[^a-z0-9_]/i', '__', $replaced); - if (is_numeric($clean[0])) { - $clean = '_'.$clean; - } - - return $clean; - } -} diff --git a/src/Monkey/WP/MockeryHookBridge.php b/src/Monkey/WP/MockeryHookBridge.php deleted file mode 100644 index 193f4c1..0000000 --- a/src/Monkey/WP/MockeryHookBridge.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Brain\Monkey\WP; - -use Brain\Monkey\MockeryBridge; -use Mockery; - -/** - * @author Giuseppe Mazzapica - * @license http://opensource.org/licenses/MIT MIT - * @package BrainMonkey - * - * @method MockeryBridge once() - * @method MockeryBridge twice() - * @method MockeryBridge times() - * @method MockeryBridge atLeast() - * @method MockeryBridge atMost() - * @method MockeryBridge between() - * @method MockeryBridge zeroOrMoreTimes() - * @method MockeryBridge never() - * @method MockeryBridge with() - * @method MockeryBridge withNoArgs() - * @method MockeryBridge withAnyArgs() - * @method MockeryBridge andReturn() - * @method MockeryBridge andReturnNull() - * @method MockeryBridge andReturnValues() - * @method MockeryBridge andReturnUsing() - * @method MockeryBridge andThrow() - * @method Mockery\Expectation whenHappen() - */ -class MockeryHookBridge -{ - /** - * @var \Brain\Monkey\MockeryBridge - */ - private $bridge; - - public function __construct(MockeryBridge $bridge) - { - $this->bridge = $bridge; - } - - public function __call($name, array $arguments = []) - { - return call_user_func_array([$this->bridge, $name], $arguments); - } -} diff --git a/src/Names/CallbackStringForm.php b/src/Names/CallbackStringForm.php new file mode 100644 index 0000000..ce6a08b --- /dev/null +++ b/src/Names/CallbackStringForm.php @@ -0,0 +1,181 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class CallbackStringForm +{ + + /** + * @var string + */ + private $parsed; + + /** + * @param callable $callback + */ + public function __construct($callback) + { + $this->parsed = $this->parseCallback($callback); + } + + /** + * @param \Brain\Monkey\Names\CallbackStringForm $callback + * @return bool + */ + public function equals(CallbackStringForm $callback) + { + return (string)$this === (string)$callback; + } + + /** + * @return string + * @throws \Brain\Monkey\Names\Exception\NotInvokableObjectAsCallback + */ + public function __toString() + { + return $this->parsed; + } + + /** + * @param $callback + * @return string + * @throws \Brain\Monkey\Names\Exception\InvalidCallable + * @throws \Brain\Monkey\Names\Exception\NotInvokableObjectAsCallback + */ + private function parseCallback($callback) + { + if ( ! is_callable($callback, true)) { + throw Exception\InvalidCallable::forCallable($callback); + } + + if (is_string($callback)) { + return $this->parseString($callback); + } + + $is_object = is_object($callback); + + if ($is_object && ! is_callable($callback)) { + throw new Exception\NotInvokableObjectAsCallback(); + } + + if ($is_object) { + return $callback instanceof \Closure + ? (new ClosureStringForm($callback))->name() + : get_class($callback).'()'; + } + + list($object, $method) = $callback; + + $method_name = (new MethodName($method))->name(); + + if (is_string($object)) { + $class_name = (new ClassName($object))->fullyQualifiedName(); + + $this->assertMethodCallable($class_name, $method_name, $callback); + + return "{$class_name}::{$method_name}()"; + } + + if ( ! is_callable([$object, $method_name])) { + throw new Exception\NotInvokableObjectAsCallback(); + } + + $class_name = (new ClassName(get_class($object)))->fullyQualifiedName(); + + return "{$class_name}->{$method_name}()"; + } + + /** + * @param string $callback + * @return bool|string + * @throws \Brain\Monkey\Names\Exception\InvalidCallable + * @throws \Brain\Monkey\Names\Exception\NotInvokableObjectAsCallback + */ + private function parseString($callback) + { + $callback = trim($callback); + + // First check if this is a closure is string form, and just return it if so + $closure = strpos($callback, 'function') === 0 && substr($callback, -1) === ')' + ? $callback + : ''; + $closure_normalized = $closure ? ClosureStringForm::normalizeString($callback) : ''; + if ($closure && ! $closure_normalized) { + throw Exception\InvalidCallable::forCallable($callback); + } elseif ($closure_normalized) { + return $closure_normalized; + } + + // If this is not a string in normalized form, we just check is a valid function name + if (substr($callback, -2) !== '()') { + return (new FunctionName($callback))->fullyQualifiedName(); + } + + // remove parenthesis + $callback = substr($callback, 0, -2); + + $is_dynamic_method = substr_count($callback, '->') === 1; + $is_static_method = substr_count($callback, '::') === 1; + + // If this is a normalized form of a static or dynamic method let's check that both class + // and method names are fine + if ($is_dynamic_method || $is_static_method) { + $separator = $is_dynamic_method ? '->' : '::'; + list($class, $method) = explode($separator, $callback); + $class_name = (new ClassName($class))->fullyQualifiedName(); + $method_name = (new MethodName($method))->name(); + $this->assertMethodCallable($class_name, $method, "{$callback}()"); + + return "{$class_name}{$separator}{$method_name}()"; + } + + // Last chance is that the string is fully qualified name of an invokable object. + $class_name = (new ClassName($callback))->fullyQualifiedName(); + // Check `__invoke` method existence only if class is available + if (class_exists($class_name) && ! method_exists($class_name, '__invoke')) { + throw new Exception\NotInvokableObjectAsCallback(); + } + + return "{$class_name}()"; + } + + /** + * Ensure method existence only if class is available. + * + * @param string $class_name + * @param string $method + * @param string|array $callable + * @throws \Brain\Monkey\Names\Exception\InvalidCallable + * @throws \Brain\Monkey\Names\Exception\NotInvokableObjectAsCallback + */ + private function assertMethodCallable($class_name, $method, $callable) + { + if ( + class_exists($class_name) + && ! (method_exists($class_name, $method) || is_callable([$class_name, $method])) + ) { + throw Exception\InvalidCallable::forCallable($callable); + } + } +} \ No newline at end of file diff --git a/src/Names/ClassName.php b/src/Names/ClassName.php new file mode 100644 index 0000000..29d9a78 --- /dev/null +++ b/src/Names/ClassName.php @@ -0,0 +1,72 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class ClassName +{ + + /** + * @var \Brain\Monkey\Names\FunctionName + */ + private $function_name; + + /** + * @param string $class_name + * @throws \Brain\Monkey\Names\Exception\InvalidName + */ + public function __construct($class_name) + { + try { + $this->function_name = new FunctionName($class_name); + } catch (Exception\InvalidName $e) { + throw Exception\InvalidName::forClass($class_name); + } + } + + /** + * @return string + */ + public function fullyQualifiedName() + { + return $this->function_name->fullyQualifiedName(); + } + + /** + * @return string + */ + public function shortName() + { + return $this->function_name->shortName(); + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->function_name->getNamespace(); + } + + /** + * @param \Brain\Monkey\Names\ClassName $name + * @return bool + */ + public function equals(ClassName $name) + { + return $this->fullyQualifiedName() === $name->fullyQualifiedName(); + } +} \ No newline at end of file diff --git a/src/Names/ClosureStringForm.php b/src/Names/ClosureStringForm.php new file mode 100644 index 0000000..803c50f --- /dev/null +++ b/src/Names/ClosureStringForm.php @@ -0,0 +1,115 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class ClosureStringForm +{ + + const VALID_PARAM_PATTERN = '/^\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/'; + + /** + * @var string + */ + private $name; + + /** + * @param string $closure_string + * @return string + */ + public static function normalizeString($closure_string) + { + if ( + ! is_string($closure_string) + || ! preg_match('/^function\((.*?)\)$/', trim($closure_string), $matches) + ) { + return ''; + } + + $raw_params = trim($matches[1]); + + if ( ! $raw_params) { + return "function()"; + } + + $params = array_map('trim', explode(',', $raw_params)); + $normalized = 'function('; + $had_variadic = false; + + foreach ($params as $param) { + $variadic = substr($param, 0, 3) === '...'; + if ($variadic && $had_variadic) { + return ''; + } + if ($variadic) { + $had_variadic = true; + $param = ltrim($param, '. '); + } + if ( ! preg_match(self::VALID_PARAM_PATTERN, $param)) { + return ''; + } + $normalized .= $variadic ? "...{$param}, " : "{$param}, "; + } + + return trim($normalized, ', ').')'; + } + + /** + * @param \Closure $closure + */ + public function __construct(\Closure $closure) + { + $this->name = $this->buildName($closure); + } + + /** + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * @param \Brain\Monkey\Names\ClosureStringForm $name + * @return bool + */ + public function equals(ClosureStringForm $name) + { + return $this->name() === $name->name(); + } + + /** + * Checks the name of a function and throw an exception if is not valid. + * When name is valid returns an array of the name itself and its namespace parts. + * + * @param \Closure $closure + * @return string + */ + private function buildName(\Closure $closure) + { + $reflection = new \ReflectionFunction($closure); + $arguments = $reflection->getParameters(); + $name = 'function('; + foreach ($arguments as $argument) { + $n = '$'.$argument->getName().', '; + $argument->isVariadic() and $n = "...{$n}"; + $name .= $n; + } + + return trim($name, ', ').')'; + } +} \ No newline at end of file diff --git a/src/Names/Exception/Exception.php b/src/Names/Exception/Exception.php new file mode 100644 index 0000000..aea3c20 --- /dev/null +++ b/src/Names/Exception/Exception.php @@ -0,0 +1,24 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class Exception extends BaseException +{ + +} \ No newline at end of file diff --git a/src/Names/Exception/InvalidCallable.php b/src/Names/Exception/InvalidCallable.php new file mode 100644 index 0000000..b7b68a1 --- /dev/null +++ b/src/Names/Exception/InvalidCallable.php @@ -0,0 +1,41 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidCallable extends Exception +{ + + /** + * @param $callback + * @return \Brain\Monkey\Names\Exception\InvalidCallable|\Brain\Monkey\Names\Exception\NotInvokableObjectAsCallback + */ + public static function forCallable($callback) + { + if (is_object($callback)) { + return new NotInvokableObjectAsCallback(); + } + + return new static( + sprintf( + 'Given %s "%s" is not a valid PHP callable.', + gettype($callback), + is_string($callback) ? "{$callback}" : var_export($callback, true) + ) + ); + + } + +} \ No newline at end of file diff --git a/src/Names/Exception/InvalidName.php b/src/Names/Exception/InvalidName.php new file mode 100644 index 0000000..1104164 --- /dev/null +++ b/src/Names/Exception/InvalidName.php @@ -0,0 +1,83 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class InvalidName extends Exception +{ + + const CODE_FOR_FUNCTION = 1; + const CODE_FOR_CLASS = 2; + const CODE_FOR_METHOD = 3; + + /** + * @param string $function + * @return \Brain\Monkey\Names\Exception\InvalidName + */ + public static function forFunction($function) + { + return self::createFor($function, self::CODE_FOR_FUNCTION); + } + + /** + * @param $class + * @return \Brain\Monkey\Names\Exception\InvalidName + */ + public static function forClass($class) + { + return self::createFor($class, self::CODE_FOR_CLASS); + } + + /** + * @param $function + * @return \Brain\Monkey\Names\Exception\InvalidName + */ + public static function forMethod($function) + { + return self::createFor($function, self::CODE_FOR_METHOD); + } + + /** + * @param string $thing + * @param int $code + * @return static + */ + private static function createFor($thing, $code) + { + switch ($code) { + case self::CODE_FOR_CLASS: + $type = 'class'; + break; + case self::CODE_FOR_METHOD: + $type = 'class method'; + break; + case self::CODE_FOR_FUNCTION: + default: + $type = 'function'; + break; + } + + $name = "'{$thing}'"; + if ( ! is_string($thing)) { + $name = is_object($thing) + ? 'An instance of '.get_class($thing) + : 'A variable of type '.gettype($thing); + } + + return new static(sprintf('%s is not a valid %s name.', $name, $type), $code); + } + +} \ No newline at end of file diff --git a/src/Names/Exception/NotInvokableObjectAsCallback.php b/src/Names/Exception/NotInvokableObjectAsCallback.php new file mode 100644 index 0000000..62146c7 --- /dev/null +++ b/src/Names/Exception/NotInvokableObjectAsCallback.php @@ -0,0 +1,29 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +class NotInvokableObjectAsCallback extends Exception +{ + + public function __construct() + { + parent::__construct( + 'Only closures and invokable objects can be used as callbacks for hooks.' + ); + } + +} \ No newline at end of file diff --git a/src/Names/FunctionName.php b/src/Names/FunctionName.php new file mode 100644 index 0000000..0f6eae8 --- /dev/null +++ b/src/Names/FunctionName.php @@ -0,0 +1,98 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class FunctionName +{ + + const VALID_NAME_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/'; + + /** + * @var string + */ + private $function_name = ''; + + /** + * @var string + */ + private $namespace = ''; + + /** + * @param string $function_name + */ + public function __construct($function_name) + { + list($this->function_name, $this->namespace) = $this->parseName($function_name); + } + + /** + * @return string + */ + public function fullyQualifiedName() + { + return "{$this->namespace}\\{$this->function_name}"; + } + + /** + * @return string + */ + public function shortName() + { + return $this->function_name; + } + + /** + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * @param \Brain\Monkey\Names\FunctionName $name + * @return bool + */ + public function equals(FunctionName $name) + { + return $this->fullyQualifiedName() === $name->fullyQualifiedName(); + } + + /** + * Checks the name of a function and throw an exception if is not valid. + * When name is valid returns an array of the name itself and its namespace parts. + * + * @param string $function_name + * @return \string[] + * @throws \Brain\Monkey\Names\Exception\InvalidName + */ + private function parseName($function_name) + { + $chunks = is_string($function_name) ? explode('\\', ltrim($function_name, '\\')) : null; + $valid = $chunks ? preg_filter(self::VALID_NAME_PATTERN, '$0', $chunks) : null; + + if ( ! $valid || $valid !== $chunks) { + $name = is_string($function_name) + ? "'{$function_name}'" + : 'Variable of type '.gettype($function_name); + + throw Exception\InvalidName::forFunction($name); + } + + return [array_pop($chunks), implode('\\', $chunks)]; + } +} \ No newline at end of file diff --git a/src/Names/MethodName.php b/src/Names/MethodName.php new file mode 100644 index 0000000..26fa998 --- /dev/null +++ b/src/Names/MethodName.php @@ -0,0 +1,62 @@ + + * @package BrainMonkey + * @license http://opensource.org/licenses/MIT MIT + */ +final class MethodName +{ + + /** + * @var string + */ + private $name; + + /** + * @param string $method_name + * @throws \Brain\Monkey\Names\Exception\InvalidName + */ + public function __construct($method_name) + { + try { + $function_name = new FunctionName($method_name); + } catch (Exception\InvalidName $e) { + throw Exception\InvalidName::forMethod($method_name); + } + + if ($function_name->getNamespace()) { + throw Exception\InvalidName::forMethod($method_name); + } + + $this->name = $function_name->shortName(); + } + + /** + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * @param \Brain\Monkey\Names\MethodName $name + * @return bool + */ + public function equals(MethodName $name) + { + return $this->name() === $name->name(); + } +} \ No newline at end of file