Matthias Andreas Benkard | 7b2a3a1 | 2021-08-16 10:57:25 +0200 | [diff] [blame^] | 1 | <?php |
| 2 | |
| 3 | namespace LdapRecord\Events; |
| 4 | |
| 5 | use LdapRecord\Support\Arr; |
| 6 | |
| 7 | /** |
| 8 | * Class Dispatcher. |
| 9 | * |
| 10 | * Handles event listening and dispatching. |
| 11 | * |
| 12 | * This code was taken out of the Laravel Framework core |
| 13 | * with broadcasting and queuing omitted to remove |
| 14 | * an extra dependency that would be required. |
| 15 | * |
| 16 | * @author Taylor Otwell |
| 17 | * |
| 18 | * @see https://github.com/laravel/framework |
| 19 | */ |
| 20 | class Dispatcher implements DispatcherInterface |
| 21 | { |
| 22 | /** |
| 23 | * The registered event listeners. |
| 24 | * |
| 25 | * @var array |
| 26 | */ |
| 27 | protected $listeners = []; |
| 28 | |
| 29 | /** |
| 30 | * The wildcard listeners. |
| 31 | * |
| 32 | * @var array |
| 33 | */ |
| 34 | protected $wildcards = []; |
| 35 | |
| 36 | /** |
| 37 | * The cached wildcard listeners. |
| 38 | * |
| 39 | * @var array |
| 40 | */ |
| 41 | protected $wildcardsCache = []; |
| 42 | |
| 43 | /** |
| 44 | * @inheritdoc |
| 45 | */ |
| 46 | public function listen($events, $listener) |
| 47 | { |
| 48 | foreach ((array) $events as $event) { |
| 49 | if (strpos($event, '*') !== false) { |
| 50 | $this->setupWildcardListen($event, $listener); |
| 51 | } else { |
| 52 | $this->listeners[$event][] = $this->makeListener($listener); |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Setup a wildcard listener callback. |
| 59 | * |
| 60 | * @param string $event |
| 61 | * @param mixed $listener |
| 62 | * |
| 63 | * @return void |
| 64 | */ |
| 65 | protected function setupWildcardListen($event, $listener) |
| 66 | { |
| 67 | $this->wildcards[$event][] = $this->makeListener($listener, true); |
| 68 | |
| 69 | $this->wildcardsCache = []; |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * @inheritdoc |
| 74 | */ |
| 75 | public function hasListeners($eventName) |
| 76 | { |
| 77 | return isset($this->listeners[$eventName]) || isset($this->wildcards[$eventName]); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * @inheritdoc |
| 82 | */ |
| 83 | public function until($event, $payload = []) |
| 84 | { |
| 85 | return $this->dispatch($event, $payload, true); |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * @inheritdoc |
| 90 | */ |
| 91 | public function fire($event, $payload = [], $halt = false) |
| 92 | { |
| 93 | return $this->dispatch($event, $payload, $halt); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * @inheritdoc |
| 98 | */ |
| 99 | public function dispatch($event, $payload = [], $halt = false) |
| 100 | { |
| 101 | // When the given "event" is actually an object we will assume it is an event |
| 102 | // object and use the class as the event name and this event itself as the |
| 103 | // payload to the handler, which makes object based events quite simple. |
| 104 | [$event, $payload] = $this->parseEventAndPayload( |
| 105 | $event, |
| 106 | $payload |
| 107 | ); |
| 108 | |
| 109 | $responses = []; |
| 110 | |
| 111 | foreach ($this->getListeners($event) as $listener) { |
| 112 | $response = $listener($event, $payload); |
| 113 | |
| 114 | // If a response is returned from the listener and event halting is enabled |
| 115 | // we will just return this response, and not call the rest of the event |
| 116 | // listeners. Otherwise we will add the response on the response list. |
| 117 | if ($halt && ! is_null($response)) { |
| 118 | return $response; |
| 119 | } |
| 120 | |
| 121 | // If a boolean false is returned from a listener, we will stop propagating |
| 122 | // the event to any further listeners down in the chain, else we keep on |
| 123 | // looping through the listeners and firing every one in our sequence. |
| 124 | if ($response === false) { |
| 125 | break; |
| 126 | } |
| 127 | |
| 128 | $responses[] = $response; |
| 129 | } |
| 130 | |
| 131 | return $halt ? null : $responses; |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * Parse the given event and payload and prepare them for dispatching. |
| 136 | * |
| 137 | * @param mixed $event |
| 138 | * @param mixed $payload |
| 139 | * |
| 140 | * @return array |
| 141 | */ |
| 142 | protected function parseEventAndPayload($event, $payload) |
| 143 | { |
| 144 | if (is_object($event)) { |
| 145 | [$payload, $event] = [[$event], get_class($event)]; |
| 146 | } |
| 147 | |
| 148 | return [$event, Arr::wrap($payload)]; |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * @inheritdoc |
| 153 | */ |
| 154 | public function getListeners($eventName) |
| 155 | { |
| 156 | $listeners = $this->listeners[$eventName] ?? []; |
| 157 | |
| 158 | $listeners = array_merge( |
| 159 | $listeners, |
| 160 | $this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName) |
| 161 | ); |
| 162 | |
| 163 | return class_exists($eventName, false) |
| 164 | ? $this->addInterfaceListeners($eventName, $listeners) |
| 165 | : $listeners; |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Get the wildcard listeners for the event. |
| 170 | * |
| 171 | * @param string $eventName |
| 172 | * |
| 173 | * @return array |
| 174 | */ |
| 175 | protected function getWildcardListeners($eventName) |
| 176 | { |
| 177 | $wildcards = []; |
| 178 | |
| 179 | foreach ($this->wildcards as $key => $listeners) { |
| 180 | if ($this->wildcardContainsEvent($key, $eventName)) { |
| 181 | $wildcards = array_merge($wildcards, $listeners); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | return $this->wildcardsCache[$eventName] = $wildcards; |
| 186 | } |
| 187 | |
| 188 | /** |
| 189 | * Determine if the wildcard matches or contains the given event. |
| 190 | * |
| 191 | * This function is a direct excerpt from Laravel's Str::is(). |
| 192 | * |
| 193 | * @param string $wildcard |
| 194 | * @param string $eventName |
| 195 | * |
| 196 | * @return bool |
| 197 | */ |
| 198 | protected function wildcardContainsEvent($wildcard, $eventName) |
| 199 | { |
| 200 | $patterns = Arr::wrap($wildcard); |
| 201 | |
| 202 | if (empty($patterns)) { |
| 203 | return false; |
| 204 | } |
| 205 | |
| 206 | foreach ($patterns as $pattern) { |
| 207 | // If the given event is an exact match we can of course return true right |
| 208 | // from the beginning. Otherwise, we will translate asterisks and do an |
| 209 | // actual pattern match against the two strings to see if they match. |
| 210 | if ($pattern == $eventName) { |
| 211 | return true; |
| 212 | } |
| 213 | |
| 214 | $pattern = preg_quote($pattern, '#'); |
| 215 | |
| 216 | // Asterisks are translated into zero-or-more regular expression wildcards |
| 217 | // to make it convenient to check if the strings starts with the given |
| 218 | // pattern such as "library/*", making any string check convenient. |
| 219 | $pattern = str_replace('\*', '.*', $pattern); |
| 220 | |
| 221 | if (preg_match('#^'.$pattern.'\z#u', $eventName) === 1) { |
| 222 | return true; |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | return false; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Add the listeners for the event's interfaces to the given array. |
| 231 | * |
| 232 | * @param string $eventName |
| 233 | * @param array $listeners |
| 234 | * |
| 235 | * @return array |
| 236 | */ |
| 237 | protected function addInterfaceListeners($eventName, array $listeners = []) |
| 238 | { |
| 239 | foreach (class_implements($eventName) as $interface) { |
| 240 | if (isset($this->listeners[$interface])) { |
| 241 | foreach ($this->listeners[$interface] as $names) { |
| 242 | $listeners = array_merge($listeners, (array) $names); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | return $listeners; |
| 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Register an event listener with the dispatcher. |
| 252 | * |
| 253 | * @param \Closure|string $listener |
| 254 | * @param bool $wildcard |
| 255 | * |
| 256 | * @return \Closure |
| 257 | */ |
| 258 | public function makeListener($listener, $wildcard = false) |
| 259 | { |
| 260 | if (is_string($listener)) { |
| 261 | return $this->createClassListener($listener, $wildcard); |
| 262 | } |
| 263 | |
| 264 | return function ($event, $payload) use ($listener, $wildcard) { |
| 265 | if ($wildcard) { |
| 266 | return $listener($event, $payload); |
| 267 | } |
| 268 | |
| 269 | return $listener(...array_values($payload)); |
| 270 | }; |
| 271 | } |
| 272 | |
| 273 | /** |
| 274 | * Create a class based listener. |
| 275 | * |
| 276 | * @param string $listener |
| 277 | * @param bool $wildcard |
| 278 | * |
| 279 | * @return \Closure |
| 280 | */ |
| 281 | protected function createClassListener($listener, $wildcard = false) |
| 282 | { |
| 283 | return function ($event, $payload) use ($listener, $wildcard) { |
| 284 | if ($wildcard) { |
| 285 | return call_user_func($this->createClassCallable($listener), $event, $payload); |
| 286 | } |
| 287 | |
| 288 | return call_user_func_array( |
| 289 | $this->createClassCallable($listener), |
| 290 | $payload |
| 291 | ); |
| 292 | }; |
| 293 | } |
| 294 | |
| 295 | /** |
| 296 | * Create the class based event callable. |
| 297 | * |
| 298 | * @param string $listener |
| 299 | * |
| 300 | * @return callable |
| 301 | */ |
| 302 | protected function createClassCallable($listener) |
| 303 | { |
| 304 | [$class, $method] = $this->parseListenerCallback($listener); |
| 305 | |
| 306 | return [new $class(), $method]; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Parse the class listener into class and method. |
| 311 | * |
| 312 | * @param string $listener |
| 313 | * |
| 314 | * @return array |
| 315 | */ |
| 316 | protected function parseListenerCallback($listener) |
| 317 | { |
| 318 | return strpos($listener, '@') !== false |
| 319 | ? explode('@', $listener, 2) |
| 320 | : [$listener, 'handle']; |
| 321 | } |
| 322 | |
| 323 | /** |
| 324 | * @inheritdoc |
| 325 | */ |
| 326 | public function forget($event) |
| 327 | { |
| 328 | if (strpos($event, '*') !== false) { |
| 329 | unset($this->wildcards[$event]); |
| 330 | } else { |
| 331 | unset($this->listeners[$event]); |
| 332 | } |
| 333 | } |
| 334 | } |