From a0fca92f34eee3c68beecbb0f5e851e9d618fed5 Mon Sep 17 00:00:00 2001 From: Basil Suter Date: Wed, 21 Dec 2016 16:19:33 +0100 Subject: [PATCH] Add Hook mechanism in order to trigger contents. #1120 --- CHANGELOG.md | 1 + core/Hook.php | 132 ++++++++++++++++++++++++++++++++++++++++ core/base/HookEvent.php | 82 +++++++++++++++++++++++++ tests/core/HookTest.php | 104 +++++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 core/Hook.php create mode 100644 core/base/HookEvent.php create mode 100644 tests/core/HookTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d38d268cf..8ccf0272d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The changelog contains informations about bug fixes, new features or bc breaking ### Added +- [#1120](https://github.com/luyadev/luya/issues/1120) Add Hook mechanism in order to trigger contents. - [#1115](https://github.com/luyadev/luya/issues/1115) ActiveDataProvider default sorting configuration for news article overview. - [#1018](https://github.com/luyadev/luya/issues/1018) NgRest SelectModel Plugins where conditions, labelFields and labelTemplate properties added. - [#1010](https://github.com/luyadev/luya/issues/1010) Ability to soft delete admin languages if its not the system default language. diff --git a/core/Hook.php b/core/Hook.php new file mode 100644 index 000000000..c8202c0a3 --- /dev/null +++ b/core/Hook.php @@ -0,0 +1,132 @@ + 'Hello', 1 => 'World') + * ``` + * + * @since 1.0.0 + * @author Basil Suter + */ +class Hook extends Object +{ + private static $_hooks = []; + + /** + * Register a hook listener. + * + * Define the name of the type of listener. + * + * @param string $name The name of the hook. + * @param callable|array $value An array with `[$object, 'method']` or a callable function `function($hook) {}`. + * @param boolean $prepend Whether to prepend the item to the start or not, defaults to false. + */ + public static function on($name, $value, $prepend = false) + { + /// @TODO implement prepend + static::$_hooks[$name][] = new HookEvent(['handler' => $value]); + } + + /** + * Trigger the hooks and returns the {{luya\base\HookEvent}} objects. + * + * @param string $name The hook name. + * @return array An array with {{luya\base\HookEvent}} objects. + */ + protected static function trigger($name) + { + if (isset(static::$_hooks[$name])) { + + $events = []; + + foreach (static::$_hooks[$name] as $hookEvent) { + + if ($hookEvent->isHandled) { + continue; + } + + if (is_callable($hookEvent->handler)) { + $hookEvent->output = call_user_func($hookEvent->handler, $hookEvent); + } elseif (is_array($hookValue)) { + $hookEvent->output = call_user_func_array($hookEvent->handler, [$hookEvent]); + } + + $hookEvent->isHandled = true; + + if ($hookEvent->isValid) { + $events[] = $hookEvent; + } + } + + return $events; + } + + return []; + } + + /** + * Get the string output of the hooks. + * + * @param string $name The name of the hook to trigger. + * @return string + */ + public static function string($name) + { + $buffer = []; + foreach (self::trigger($name) as $hook) { + $buffer[] = $hook->output; + } + + return implode("", $buffer); + } + + /** + * Get the array output of iteration hooks. + * + * @param string $name The name of the hook to trigger. + * @return array + */ + public static function iterate($name) + { + $buffer = []; + foreach (self::trigger($name) as $hook) { + $buffer = array_merge($buffer, $hook->getIterations()); + } + + return $buffer; + } +} \ No newline at end of file diff --git a/core/base/HookEvent.php b/core/base/HookEvent.php new file mode 100644 index 000000000..c8c1c1156 --- /dev/null +++ b/core/base/HookEvent.php @@ -0,0 +1,82 @@ + + */ +final class HookEvent extends Object implements \ArrayAccess +{ + /** + * @var array|callable The executable handler to use when calling the hook event in order to generate the $output. + */ + public $handler; + + /** + * @var string The output which is generated when the $handler is called, this is used in order to return string outputs. + */ + public $output; + + /** + * @var boolean Whether the hook event is handled already or not. + */ + public $isHandled = false; + + /** + * @var boolean Whether the current event is valid or not, if set to false, this event will not be triggered on output. + */ + public $isValid = true; + + private $_iterations = []; + + /** + * Getter method for iteration. + * + * @return array + */ + public function getIterations() + { + return $this->_iterations; + } + + /** + * Add Iteration + * @param mixed $value The value of the key + * @param string $key The key of the array + */ + public function iteration($value, $key = null) + { + if (is_null($key)) { + $this->_iterations[] = $value; + } else { + $this->_iterations[$key] = $value; + } + } + + public function offsetSet($offset, $value) + { + $this->iteration($value, $offset); + } + + public function offsetExists($offset) + { + return isset($this->_iterations[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->_iterations[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->_iterations[$offset]) ? $this->_iterations[$offset] : null; + } +} \ No newline at end of file diff --git a/tests/core/HookTest.php b/tests/core/HookTest.php new file mode 100644 index 000000000..d6c88cffa --- /dev/null +++ b/tests/core/HookTest.php @@ -0,0 +1,104 @@ +iteration('hello'); + } + + public function worldHook($hook) + { + $hook->iteration('world'); + } +} + +class HookTest extends LuyaWebTestCase +{ + public function testStringHooks() + { + Hook::on('fooBar', function($hook) { + return 'Hello World'; + }); + + $this->assertSame('Hello World', Hook::string('fooBar')); + + Hook::on('fooBar', function($hook) { + return 'Another'; + }); + Hook::on('fooBar', function($hook) { + return 'Test'; + }); + + $this->assertSame('AnotherTest', Hook::string('fooBar')); + } + + public function testIterationHooks() + { + Hook::on('fooBar', function($hook) { + $hook->iteration('hello'); + $hook->iteration('world'); + }); + + $this->assertSame(['hello', 'world'], Hook::iterate('fooBar')); + + + Hook::on('testIterator', function($hook) { + $hook->iteration('hello'); + $hook->iteration('world'); + }); + + Hook::on('testIterator', function($hook) { + $hook->iteration('another'); + $hook->iteration('world'); + }); + + $this->assertSame(['hello', 'world', 'another', 'world'], Hook::iterate('testIterator')); + } + + public function testIterationWithObjectHooks() + { + $obj = new TestClass(); + Hook::on('fooBar', [$obj, 'helloHook']); + Hook::on('fooBar', [$obj, 'worldHook']); + + $this->assertSame(['hello', 'world'], Hook::iterate('fooBar')); + } + + public function testIterationArrayAccessHook() + { + Hook::on('fooBar', function($hook) { + $hook[] = 'Hello'; + $hook[] = 'World'; + + $hook['foo'] = 'Bar'; + $hook['unfoo'] = 'Unfoo'; + + if ($hook['unfoo']) { + unset($hook['unfoo']); + } + }); + + $this->assertSame(['Hello', 'World', 'foo' => 'Bar'], Hook::iterate('fooBar')); + } + + public function testIterationOverrideKey() + { + Hook::on('fooBar', function($hook) { + $hook['test'] = 'value1'; + $hook['test'] = 'value2'; + }); + + Hook::on('fooBar', function($hook) { + $hook['test'] = 'value3'; + $hook['test'] = 'value4'; + }); + + $this->assertSame(['test' => 'value4'], Hook::iterate('fooBar')); + } +} \ No newline at end of file