Completed
Push — master ( b3af43...6273ca )
by Todd
19:01
created

Event   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 96.49%

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 48
c 6
b 1
f 0
lcom 1
cbo 0
dl 0
loc 375
ccs 110
cts 114
cp 0.9649
rs 8.4865

13 Methods

Rating   Name   Duplication   Size   Complexity  
A bind() 0 5 1
A functionExists() 0 7 3
A hasHandler() 0 4 2
A methodExists() 0 9 3
A reset() 0 4 1
B callUserFuncArray() 0 31 5
C bindClass() 0 40 11
A dumpHandlers() 0 10 2
A fire() 0 16 4
A fireArray() 0 15 4
A fireFilter() 0 15 4
B getEventname() 0 17 5
A getHandlers() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like Event often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Event, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2014 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden;
9
10
/**
11
 * Contains methods for binding and firing to events.
12
 *
13
 * Addons can create callbacks that bind to events which are calle througout the code to allow
14
 * extension of the application and framework.
15
 */
16
class Event {
17
    /// Constants ///
18
19
    const PRIORITY_LOW = 1000;
20
    const PRIORITY_NORMAL = 100;
21
    const PRIORITY_HIGH = 10;
22
23
    /// Properties ///
24
25
    /**
26
     * All of the event handlers that have been registered.
27
     * @var array An array of event handlers.
28
     */
29
    protected static $handlers = array();
30
31
    /**
32
     * All of the event handlers that still need to be sorted by priority.
33
     * @var array An array of event handler names that need to be sorted.
34
     */
35
    protected static $toSort = array();
36
37
    /// Methods ///
38
39
    /**
40
     * Call a callback with an array of parameters, checking for events to be called with the callback.
41
     *
42
     * This method is similar to {@link call_user_func_array()} but it will fire events that can happen
43
     * before and/or after the callback and can call an event instead of the callback itself to override it.
44
     *
45
     * In order to use events with this method they must be bound with a particular naming convention.
46
     *
47
     * **Modify a function**
48
     *
49
     * - functionname_before
50
     * - functionname
51
     * - functionname_after
52
     *
53
     * **Modify a method call**
54
     *
55
     * - classname_methodname_before
56
     * - classname_methodname
57
     * - classname_methodname_after
58
     *
59
     * @param callable $callback The {@link callable} to be called.
60
     * @param array $args The arguments to be passed to the callback, as an indexed array.
61
     * @return mixed Returns the return value of the callback.
62
     */
63 18
    public static function callUserFuncArray($callback, $args = []) {
64
        // Figure out the event name from the callback.
65 18
        $event_name = static::getEventname($callback);
66 18
        if (!$event_name) {
67 1
            return call_user_func_array($callback, $args);
68
        }
69
70
        // The events could have different args because the event handler can take the object as the first parameter.
71 17
        $event_args = $args;
72
        // If the callback is an object then it gets passed as the first argument.
73 17
        if (is_array($callback) && is_object($callback[0])) {
74 16
            array_unshift($event_args, $callback[0]);
75
        }
76
77
        // Fire before events.
78 17
        self::fireArray($event_name.'_before', $event_args);
79
80
        // Call the function.
81 17
        if (static::hasHandler($event_name)) {
82
            // The callback was overridden so fire it.
83 1
            $result = static::fireArray($event_name, $event_args);
84
        } else {
85
            // The callback was not overridden so just call the passed callback.
86 16
            $result = call_user_func_array($callback, $args);
87
        }
88
89
        // Fire after events.
90 17
        self::fireArray($event_name.'_after', $event_args);
91
92 17
        return $result;
93
    }
94
95
    /**
96
     * Bind an event handler to an event.
97
     *
98
     * @param string $event The naame of the event to bind to.
99
     * @param callback $callback The callback of the event.
100
     * @param int $priority The priority of the event.
101
     */
102 41
    public static function bind($event, $callback, $priority = Event::PRIORITY_NORMAL) {
103 41
        $event = strtolower($event);
104 41
        self::$handlers[$event][$priority][] = $callback;
105 41
        self::$toSort[$event] = true;
106 41
    }
107
108
    /**
109
     * Bind a class' declared event handlers.
110
     *
111
     * Plugin classes declare event handlers in the following way:
112
     *
113
     * ```
114
     * // Bind to a normal event.
115
     * public function eventname_handler($arg1, $arg2, ...) { ... }
116
     *
117
     * // Add/override a method called with Event::callUserFuncArray().
118
     * public function ClassName_methodName($sender, $arg1, $arg2) { ... }
119
     * public function ClassName_methodName_create($sender, $arg1, $arg2) { ... } // deprecated
120
     *
121
     * // Call the handler before or after a method called with Event::callUserFuncArray().
122
     * public function ClassName_methodName_before($sender, $arg1, $arg2) { ... }
123
     * public function ClassName_methodName_after($sender, $arg1, $arg2) { ... }
124
     * ```
125
     *
126
     * @param mixed $class The class name or an object instance.
127
     * @param int $priority The priority of the event.
128
     * @throws \InvalidArgumentException Throws an exception when binding to a class name with no `instance()` method.
129
     */
130 2
    public static function bindClass($class, $priority = Event::PRIORITY_NORMAL) {
131 2
        $method_names = get_class_methods($class);
132
133
        // Grab an instance of the class so there is something to bind to.
134 2
        if (is_string($class)) {
135 1
            if (method_exists($class, 'instance')) {
136
                // TODO: Make the instance lazy load.
137 1
                $instance = call_user_func(array($class, 'instance'));
138
            } else {
139 1
                throw new \InvalidArgumentException('Event::bindClass(): The class for argument #1 must have an instance() method or be passed as an object.', 422);
140
            }
141
        } else {
142 1
            $instance = $class;
143
        }
144
145 2
        foreach ($method_names as $method_name) {
146 2
            if (strpos($method_name, '_') === false) {
147 2
                continue;
148
            }
149
150 2
            $parts = explode('_', strtolower($method_name));
151 2
            switch (end($parts)) {
152
                case 'handler':
153
                case 'create':
154
                case 'override':
155 2
                    array_pop($parts);
156 2
                    $event_name = implode('_', $parts);
157 2
                    break;
158
                case 'before':
159 2
                case 'after':
160
                default:
161 2
                    $event_name = implode('_', $parts);
162 2
                    break;
163
            }
164
            // Bind the event if we have one.
165 2
            if ($event_name) {
166 2
                static::bind($event_name, array($instance, $method_name), $priority);
167
            }
168
        }
169 2
    }
170
171
    /**
172
     * Dumps all of the bound handlers.
173
     *
174
     * This method is meant for debugging.
175
     *
176
     * @return array Returns an array of all handlers indexed by event name.
177
     */
178 1
    public static function dumpHandlers() {
179 1
        $result = [];
180
181 1
        foreach (self::$handlers as $event_name => $nested) {
182 1
            $handlers = call_user_func_array('array_merge', static::getHandlers($event_name));
183 1
            $result[$event_name] = array_map('format_callback', $handlers);
184
        }
185
186 1
        return $result;
187
    }
188
189
    /**
190
     * Fire an event.
191
     *
192
     * @param string $event The name of the event.
193
     * @return mixed Returns the result of the last event handler.
194
     */
195 38
    public static function fire($event) {
196 38
        $handlers = self::getHandlers($event);
197 38
        if (!$handlers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $handlers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
198 1
            return null;
199
        }
200
201
        // Grab the handlers and call them.
202 37
        $args = array_slice(func_get_args(), 1);
203 37
        $result = null;
204 37
        foreach ($handlers as $callbacks) {
205 37
            foreach ($callbacks as $callback) {
206 37
                $result = call_user_func_array($callback, $args);
207
            }
208
        }
209 37
        return $result;
210
    }
211
212
    /**
213
     * Fire an event with an array of arguments.
214
     *
215
     * This method is to {@link Event::fire()} as {@link call_user_func_array()} is to {@link call_user_funct()}.
216
     * The main purpose though is to allow you to have event handlers that can take references.
217
     *
218
     * @param string $event The name of the event.
219
     * @param array $args The arguments for the event handlers.
220
     * @return mixed Returns the result of the last event handler.
221
     */
222 18
    public static function fireArray($event, $args = []) {
223 18
        $handlers = self::getHandlers($event);
224 18
        if (!$handlers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $handlers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
225 18
            return null;
226
        }
227
228
        // Grab the handlers and call them.
229 2
        $result = null;
230 2
        foreach ($handlers as $callbacks) {
231 2
            foreach ($callbacks as $callback) {
232 2
                $result = call_user_func_array($callback, $args);
233
            }
234
        }
235 2
        return $result;
236
    }
237
238
    /**
239
     * Chain several event handlers together.
240
     *
241
     * This method will fire the first handler and pass its result as the first argument
242
     * to the next event handler and so on. A chained event handler can have more than one parameter,
243
     * but must have at least one parameter.
244
     *
245
     * @param string $event The name of the event to fire.
246
     * @param mixed $value The value to pass into the filter.
247
     * @return mixed The result of the chained event or `$value` if there were no handlers.
248
     */
249 2
    public static function fireFilter($event, $value) {
250 2
        $handlers = self::getHandlers($event);
251 2
        if (!$handlers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $handlers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
252 2
            return $value;
253
        }
254
255 1
        $args = array_slice(func_get_args(), 1);
256 1
        foreach ($handlers as $callbacks) {
257 1
            foreach ($callbacks as $callback) {
258 1
                $value = call_user_func_array($callback, $args);
259 1
                $args[0] = $value;
260
            }
261
        }
262 1
        return $value;
263
    }
264
265
    /**
266
     * Fire an event meant to override another function or method.
267
     * The event handler should return true if it wants to say it is overridding the method.
268
     *
269
     * @param string $event The name of the event to fire.
270
     * @return bool Whether or not the event has been overridden.
271
     */
272
//   public static function fireOverride($event) {
273
//      $handlers = self::getHandlers($event);
274
//      if ($handlers === false)
275
//         return $value;
276
//
277
//      $args = array_slice(func_get_args(), 1);
278
//      foreach ($handlers as $callbacks) {
279
//         foreach ($callbacks as $callback) {
280
//            $overridden = call_user_func_array($callback, $args);
281
//            if ($overridden)
282
//               return true;
283
//         }
284
//      }
285
//      return false;
286
//   }
287
288
    /**
289
     * Checks if a function exists or there is a replacement event for it.
290
     *
291
     * @param string $function_name The function name.
292
     * @param bool $only_events Whether or not to only check events.
293
     * @return boolean Returns `true` if the function given by `function_name` has been defined, `false` otherwise.
294
     * @see http://ca1.php.net/manual/en/function.function-exists.php
295
     */
296 2
    public static function functionExists($function_name, $only_events = false) {
297 2
        if (!$only_events && function_exists($function_name)) {
298 1
            return true;
299
        } else {
300 1
            return static::hasHandler($function_name);
301
        }
302
    }
303
304
    /**
305
     * Get the event name for a callback.
306
     *
307
     * @param string|array|callable $callback The callback or an array in the form of a callback.
308
     * @return string The name of the callback.
309
     */
310 29
    protected static function getEventname($callback) {
311 29
        if (is_string($callback)) {
312 1
            return strtolower($callback);
313 29
        } elseif (is_array($callback)) {
314 28
            if (is_string($callback[0])) {
315 1
                $classname = $callback[0];
316
            } else {
317 27
                $classname = get_class($callback[0]);
318
            }
319 28
            $eventclass = trim(strrchr($classname, '\\'), '\\');
320 28
            if (!$eventclass) {
321 26
                $eventclass = $classname;
322
            }
323 28
            return strtolower($eventclass.'_'.$callback[1]);
324
        }
325 1
        return '';
326
    }
327
328
    /**
329
     * Get all of the handlers bound to an event.
330
     *
331
     * @param string $name The name of the event.
332
     * @return array Returns the handlers that are watching {@link $name}.
333
     */
334 42
    public static function getHandlers($name) {
335 42
        $name = strtolower($name);
336
337 42
        if (!isset(self::$handlers[$name])) {
338 19
            return [];
339
        }
340
341
        // See if the handlers need to be sorted.
342 41
        if (isset(self::$toSort[$name])) {
343 41
            ksort(self::$handlers[$name]);
344 41
            unset(self::$toSort[$name]);
345
        }
346
347 41
        return self::$handlers[$name];
348
    }
349
350
    /**
351
     * Checks if an event has a handler.
352
     *
353
     * @param string $event The name of the event.
354
     * @return bool Returns `true` if the event has at least one handler, `false` otherwise.
355
     */
356 29
    public static function hasHandler($event) {
357 29
        $event = strtolower($event);
358 29
        return array_key_exists($event, self::$handlers) && !empty(self::$handlers[$event]);
359
    }
360
361
    /**
362
     * Checks if a class method exists or there is a replacement event for it.
363
     *
364
     * @param mixed $object An object instance or a class name.
365
     * @param string $method_name The method name.
366
     * @param bool $only_events Whether or not to only check events.
367
     * @return boolean Returns `true` if the method given by method_name has been defined for the given object,
368
     * `false` otherwise.
369
     * @see http://ca1.php.net/manual/en/function.method-exists.php
370
     */
371 29
    public static function methodExists($object, $method_name, $only_events = false) {
372 29
        if (!$only_events && method_exists($object, $method_name)) {
373 27
            return true;
374
        } else {
375
            // Check to see if there is an event bound to the method.
376 13
            $event_name = self::getEventname([$object, $method_name]);
377 13
            return static::hasHandler($event_name);
378
        }
379
    }
380
381
    /**
382
     * Clear all of the event handlers.
383
     *
384
     * This method resets the event object to its original state.
385
     */
386 10
    public static function reset() {
387 10
        self::$handlers = [];
388 10
        self::$toSort = [];
389 10
    }
390
}
391