├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── EventEmitter.php ├── EventEmitterInterface.php └── EventEmitterTrait.php └── test └── EventEmitterTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.0 6 | - 5.6 7 | - 5.5 8 | - 5.4 9 | 10 | notifications: 11 | slack: downshiftllc:iPuazhi14R86Oh753YLQppb7 12 | 13 | install: composer install --no-interaction --prefer-source 14 | 15 | script: vendor/bin/phpunit 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP EventEmitter 2 | 3 | An object oriented event emitter for WordPress actions 4 | 5 | ## Motivation 6 | 7 | A familiar event interface that delegates to the global `add_action` and `do_action` functions of WordPress. It also presents 8 | a much more testable interface as it only uses the WP functions if they are available. 9 | 10 | ## Methods 11 | 12 | ### on 13 | 14 | Delegate to WordPress' [add_action](https://codex.wordpress.org/Function_Reference/add_action) function. In test environments a local 15 | collection of listeners will be used. 16 | 17 | ### emit 18 | 19 | Delegate to WordPress' [do_action](https://codex.wordpress.org/Function_Reference/do_action) function. In test environments a local 20 | collection of listeners will be used. 21 | 22 | ### filter 23 | 24 | Delegate to WordPress' [add_filter](https://codex.wordpress.org/Function_Reference/add_filter) function. In test environments a local 25 | collection of listeners will be used. 26 | 27 | ### applyFilters 28 | 29 | Delegate to WordPress' [apply_filters](https://codex.wordpress.org/Function_Reference/apply_filters) function. In test environments a 30 | local collection of listeners will be used. 31 | 32 | ## Tests 33 | 34 | Tests use PHPUnit 35 | 36 | ``` 37 | $ vendor/bin/phpunit 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "downshiftorg/wp-event-emitter", 3 | "description": "An object oriented event emitter for WordPress actions", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Brian Scaturro", 9 | "email": "scaturrob@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "DownShift\\WordPress\\": "src/" 18 | } 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^4.6", 22 | "symfony/var-dumper": "~2.6.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./test/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/EventEmitter.php: -------------------------------------------------------------------------------- 1 | addListener($event, $function_to_add, $priority); 32 | krsort($this->listeners[$event]); 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function off($event, $function_to_remove, $priority = 10) 41 | { 42 | if (function_exists('remove_action')) { 43 | remove_action($event, $function_to_remove, $priority); 44 | return; 45 | } 46 | 47 | $this->removeListener($event, $function_to_remove, $priority); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | * 53 | * @param string $name 54 | * @param mixed $function_to_add 55 | * @param int $priority 56 | * @return $this 57 | */ 58 | public function filter($name, $function_to_add, $priority = 10, $acceptedArgs = 1) 59 | { 60 | if (function_exists('add_filter')) { 61 | add_filter($name, $function_to_add, $priority, $acceptedArgs); 62 | return $this; 63 | } 64 | 65 | $this->on($name, $function_to_add, $priority); 66 | return $this; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | * 72 | * @param $name 73 | * @param $value 74 | * @return $this|mixed 75 | */ 76 | public function applyFilters($name, $value /** ...args */) 77 | { 78 | $args = func_get_args(); 79 | $rest = array_slice($args, 1); 80 | 81 | if (function_exists('apply_filters')) { 82 | return call_user_func_array('apply_filters', $args); 83 | } 84 | 85 | return $this->invokeHook($name, $rest, 'filter'); 86 | } 87 | 88 | 89 | /** 90 | * {@inheritdoc} 91 | * 92 | * @param $tag 93 | */ 94 | public function emit($event /** ... args */) 95 | { 96 | $args = func_get_args(); 97 | $rest = array_slice($args, 1); 98 | 99 | if (function_exists('do_action')) { 100 | call_user_func_array('do_action', $args); 101 | return $this; 102 | } 103 | 104 | $this->invokeHook($event, $rest, 'action'); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | * 112 | * @todo handle $function_to_check when using internal test listeners 113 | */ 114 | public function hasEventListener($event, $function_to_check = false) 115 | { 116 | return $this->hasListener('action', $event, $function_to_check); 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | * 122 | * @todo handle $function_to_check when using internal test listeners 123 | */ 124 | public function hasFilter($name, $function_to_check = false) 125 | { 126 | return $this->hasListener('filter', $name, $function_to_check); 127 | } 128 | 129 | /** 130 | * Check if listener exists for type, hook, and function 131 | * 132 | * Delegates to WordPress `has_action` and `has_filter` when present, 133 | * or falls back to internal listener queue for testing purposes 134 | * 135 | * @param string $type 136 | * @param string $hook 137 | * @param mixed $function_to_check 138 | * @return boolean 139 | */ 140 | protected function hasListener($type, $hook, $function_to_check = false) 141 | { 142 | $wp_has_listener = 'has_' . $type; 143 | 144 | if (function_exists($wp_has_listener)) { 145 | return call_user_func($wp_has_listener, $hook, $function_to_check); 146 | } 147 | 148 | return $this->hasInternalListener($hook, $function_to_check); 149 | } 150 | 151 | /** 152 | * Check if an internal listener exists for hook and optional function 153 | * 154 | * If the optional second $function_to_check arg is passed, check 155 | * if that specific callable has been registered as a listener. 156 | * 157 | * @param string $hook 158 | * @param mixed $function_to_check 159 | * @return boolean 160 | */ 161 | protected function hasInternalListener($hook, $function_to_check) 162 | { 163 | $listeners = $this->listeners($hook); 164 | 165 | if (!$listeners) { 166 | return false; 167 | } 168 | 169 | if ($function_to_check === false) { 170 | return true; 171 | } 172 | 173 | return $this->hasMatchingListener($listeners, $function_to_check); 174 | } 175 | 176 | /** 177 | * Does a specific callable exist in an array of prioritized listeners 178 | * 179 | * @param array $listeners 180 | * @param callable $function_to_check 181 | * @return boolean 182 | */ 183 | protected function hasMatchingListener(array $listeners, $function_to_check) 184 | { 185 | $listeners = array_reduce($listeners, 'array_merge', array()); 186 | 187 | return (bool) array_filter($listeners, function ($listener) use ($function_to_check) { 188 | return $listener == $function_to_check; 189 | }); 190 | } 191 | 192 | /** 193 | * Return the listeners for a given hook 194 | * 195 | * @param $hook 196 | * @return array 197 | */ 198 | protected function listeners($hook) 199 | { 200 | return isset($this->listeners[$hook]) ? $this->listeners[$hook] : array(); 201 | } 202 | 203 | /** 204 | * Add a prioritized listener 205 | * 206 | * @param $hook 207 | * @param $function_to_add 208 | * @param $priority 209 | */ 210 | protected function addListener($hook, $function_to_add, $priority) 211 | { 212 | if (!isset($this->listeners[$hook])) { 213 | $this->listeners[$hook] = array(); 214 | } 215 | 216 | if (!isset($this->listeners[$hook][$priority])) { 217 | $this->listeners[$hook][$priority] = array(); 218 | } 219 | 220 | $this->listeners[$hook][$priority][] = $function_to_add; 221 | } 222 | 223 | /** 224 | * Remove a listener 225 | * 226 | * @param string $event 227 | * @param mixed $function_to_remove 228 | * @param int $priority 229 | * @return void 230 | */ 231 | protected function removeListener($event, $function_to_remove, $priority) 232 | { 233 | if (!isset($this->listeners[$event])) { 234 | return; 235 | } 236 | 237 | if (!isset($this->listeners[$event][$priority])) { 238 | return; 239 | } 240 | 241 | $listeners = array(); 242 | foreach ($this->listeners[$event][$priority] as $listener) { 243 | if ($listener !== $function_to_remove) { 244 | $listeners[] = $listener; 245 | } 246 | } 247 | 248 | $this->listeners[$event][$priority] = $listeners; 249 | } 250 | 251 | /** 252 | * Invoke all listeners for a given hook 253 | * 254 | * @param string $hook 255 | * @param array $argument 256 | * @param string $type 257 | * @return mixed 258 | */ 259 | protected function invokeHook($hook, array $arguments, $type) 260 | { 261 | $listeners = $this->listeners($hook); 262 | 263 | $value = isset($arguments[0]) ? $arguments[0] : ''; 264 | 265 | foreach ($listeners as $key => $set) { 266 | $value = $this->invokeListeners($set, $arguments, $type); 267 | } 268 | 269 | return $value; 270 | } 271 | 272 | /** 273 | * Invoke listeners for a given hook priority 274 | * 275 | * @param array $listeners 276 | * @param array $arguments 277 | * @param string $type 278 | * @return mixed 279 | */ 280 | protected function invokeListeners(array $listeners, array $arguments, $type) 281 | { 282 | $value = ''; 283 | 284 | foreach ($listeners as $listener) { 285 | $value = call_user_func_array($listener, $arguments); 286 | if ($type === 'filter') { 287 | $arguments[0] = $value; 288 | } 289 | } 290 | 291 | return $value; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /test/EventEmitterTest.php: -------------------------------------------------------------------------------- 1 | emitter = new EventEmitter(); 15 | } 16 | 17 | public function testEventPriority() 18 | { 19 | $result = null; 20 | 21 | $this->emitter->on('init', function () use (&$result) { 22 | $result = 'early'; 23 | }); 24 | 25 | $this->emitter->on('init', function () use (&$result) { 26 | $result = 'late'; 27 | }, 11); 28 | 29 | $this->emitter->emit('init'); 30 | 31 | $this->assertEquals('early', $result); 32 | } 33 | 34 | public function testEmitIsVariadicLikeWordPressDoAction() 35 | { 36 | $result = null; 37 | 38 | $this->emitter->on('foo', function () use (&$result) { 39 | $result = func_get_args(); 40 | }); 41 | 42 | $this->emitter->emit('foo', 'grace jones', 'andre the giant'); 43 | 44 | $this->assertSame(array('grace jones', 'andre the giant'), $result); 45 | } 46 | 47 | public function testFilters() 48 | { 49 | $this->emitter->filter('the_content', function ($content, $append) { 50 | return $content . ' ' . $append; 51 | }); 52 | 53 | $this->emitter->filter('the_content', function ($content) { 54 | return $content . ' yolo'; 55 | }); 56 | 57 | $content = $this->emitter->applyFilters('the_content', 'ham', 'sandwich'); 58 | 59 | $this->assertEquals('ham sandwich yolo', $content); 60 | } 61 | 62 | public function testPhpNoticesOrWarningsNotEmittedWhenApplyingFiltersToHookWithNoFilter() 63 | { 64 | $this->emitter->applyFilters('jim_jam', 'yolo neckbeard'); 65 | } 66 | 67 | public function testMultipleEmits() 68 | { 69 | $test = null; 70 | 71 | $this->emitter->on('foo', function ($arg) use (&$test) { 72 | $test = $arg; 73 | }); 74 | 75 | $this->emitter->on('foo', function ($arg) use (&$test) { 76 | $test = $arg; 77 | }); 78 | 79 | $this->emitter->emit('foo', 'bar'); 80 | 81 | $this->assertNotEmpty($test); 82 | } 83 | 84 | public function testCallingApplyFiltersWithNoFiltersAddedReturnsValue() 85 | { 86 | $toFilter = 'foobar'; 87 | 88 | $filtered = $this->emitter->applyFilters('foo', $toFilter); 89 | 90 | $this->assertSame($toFilter, $filtered); 91 | } 92 | 93 | public function testHasEventListenerReturnsFalseWhenNoListenerAdded() 94 | { 95 | $hasListener = $this->emitter->hasEventListener('foo'); 96 | 97 | $this->assertFalse($hasListener); 98 | } 99 | 100 | public function testHasEventListenerReturnsTrueWhenListenerAdded() 101 | { 102 | $this->emitter->on('foo', 'phpinfo'); 103 | 104 | $hasListener = $this->emitter->hasEventListener('foo'); 105 | 106 | $this->assertTrue($hasListener); 107 | } 108 | 109 | public function testHasFilterReturnsFalseWhenNoFilterAdded() 110 | { 111 | $hasFilter = $this->emitter->hasFilter('foo'); 112 | 113 | $this->assertFalse($hasFilter); 114 | } 115 | 116 | public function testHasFilterReturnsTrueWhenFilterAdded() 117 | { 118 | $this->emitter->filter('foo', 'strtoupper'); 119 | 120 | $hasFilter = $this->emitter->hasFilter('foo'); 121 | 122 | $this->assertTrue($hasFilter); 123 | } 124 | 125 | public function testHasEventListenerReturnsFalseWhenStringFunctionToCheckDoesNotMatch() 126 | { 127 | $this->emitter->on('foo', 'phpinfo'); 128 | 129 | $hasListener = $this->emitter->hasEventListener('foo', 'some_other_func'); 130 | 131 | $this->assertFalse($hasListener); 132 | } 133 | 134 | public function testHasEventListenerReturnsTrueWhenStringFunctionToCheckMatches() 135 | { 136 | $this->emitter->on('foo', 'phpinfo'); 137 | 138 | $hasListener = $this->emitter->hasEventListener('foo', 'phpinfo'); 139 | 140 | $this->assertTrue($hasListener); 141 | } 142 | 143 | public function testHasEventListenerReturnsTrueWhenClosuresSame() 144 | { 145 | $saySomething = function () { 146 | echo 'something'; 147 | }; 148 | 149 | $this->emitter->on('foo', $saySomething); 150 | 151 | $hasListener = $this->emitter->hasEventListener('foo', $saySomething); 152 | 153 | $this->assertTrue($hasListener); 154 | } 155 | 156 | public function testHasEventListenerReturnsFalseWhenComparingDifferentClosures() 157 | { 158 | $this->emitter->on('foo', function () { 159 | echo 'foo'; 160 | }); 161 | 162 | $hasListener = $this->emitter->hasEventListener('foo', function () { 163 | echo 'foo'; 164 | }); 165 | 166 | $this->assertFalse($hasListener); 167 | } 168 | 169 | public function testHasEventListenerReturnsTrueWhenPassedSameArrayCallable() 170 | { 171 | $this->emitter->on('foo', array($this, 'listener1')); 172 | 173 | $hasListener = $this->emitter->hasEventListener('foo', array($this, 'listener1')); 174 | 175 | $this->assertTrue($hasListener); 176 | } 177 | 178 | public function testHasEventListenerReturnsFalseWhenPassedDifferentArrayCallable() 179 | { 180 | $this->emitter->on('foo', array($this, 'listener1')); 181 | 182 | $hasListener = $this->emitter->hasEventListener('foo', array($this, 'listener2')); 183 | 184 | $this->assertFalse($hasListener); 185 | } 186 | 187 | /** 188 | * @runInSeparateProcess 189 | */ 190 | public function testWhenWordpressApplyFiltersExistsReturnsResultOfCallingThatFunction() 191 | { 192 | eval('function apply_filters() { return "foobar"; }'); 193 | 194 | $filtered = $this->emitter->applyFilters('some_filter', 'jimjam'); 195 | 196 | $this->assertSame('foobar', $filtered); 197 | } 198 | 199 | /** 200 | * @runInSeparateProcess 201 | */ 202 | public function testCorrectArgsPassedToWordpressFunctionWhenPresent() 203 | { 204 | eval('function apply_filters() { return func_get_args(); }'); 205 | 206 | $filtered = $this->emitter->applyFilters('foo', 'bar'); 207 | 208 | $this->assertSame(array('foo', 'bar'), $filtered); 209 | } 210 | 211 | public function testOffRemovesAllListeners() 212 | { 213 | $function = function() {}; 214 | $this->emitter->on('foo', $function); 215 | 216 | $this->emitter->off('foo', $function); 217 | 218 | $this->assertFalse($this->emitter->hasEventListener('foo', $function)); 219 | } 220 | 221 | public function listener1() 222 | { 223 | // do a thing 224 | } 225 | 226 | public function listener2() 227 | { 228 | // do another thing 229 | } 230 | } 231 | --------------------------------------------------------------------------------