Completed
Push — master ( ea7c62...226003 )
by J.D.
03:16
created

WordPoints_Hook_Router   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 37
c 3
b 0
f 0
lcom 1
cbo 10
dl 0
loc 311
rs 8.6

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __call() 0 12 2
C route_action() 0 54 11
C fire_event() 0 52 8
C add_action() 0 51 10
A remove_action() 0 18 4
A add_event_to_action() 0 3 1
A remove_event_from_action() 0 3 1
1
<?php
2
3
/**
4
 * Hook action router class.
5
 *
6
 * @package wordpoints-hooks-api
7
 * @since 1.0.0
8
 */
9
10
/**
11
 * Routes WordPress actions to WordPoints hook actions, and finally to hook events.
12
 *
13
 * Each WordPress action can have several different WordPoints hook actions hooked to
14
 * it. This router handles hooking into the WordPress action, and making sure the
15
 * hook actions are processed when it is fired. This allows us to hook to each action
16
 * once, even if multiple hook actions are registered for it.
17
 *
18
 * When a hook action is fired, the router then loops through the events which are
19
 * registered to fire on that hook action, and fires each of them.
20
 *
21
 * This arrangement allows for events and actions to be decoupled from WordPress
22
 * actions, and from each other as well. As a result, action classes don't have to
23
 * be loaded until the router is called on the action that they are attached to. The
24
 * event classes can be lazy-loaded as well.
25
 *
26
 * It also makes it possible for a hook action to abort firing any events if it
27
 * chooses to do so.
28
 *
29
 * @since 1.0.0
30
 */
31
class WordPoints_Hook_Router {
32
33
	/**
34
	 * The actions registry object.
35
	 *
36
	 * @since 1.0.0
37
	 *
38
	 * @var WordPoints_Hook_Actions
39
	 */
40
	protected $actions;
41
42
	/**
43
	 * The events registry object.
44
	 *
45
	 * @since 1.0.0
46
	 *
47
	 * @var WordPoints_Hook_Events
48
	 */
49
	protected $events;
50
51
	/**
52
	 * The actions, indexed by WordPress action/filter hooks.
53
	 *
54
	 * The indexes are of this format: "$action_or_filter_name,$priority".
55
	 *
56
	 * @since 1.0.0
57
	 *
58
	 * @var array
59
	 */
60
	protected $action_index = array();
61
62
	/**
63
	 * The events, indexed by action slug and action type.
64
	 *
65
	 * @since 1.0.0
66
	 *
67
	 * @var array
68
	 */
69
	protected $event_index = array();
70
71
	/**
72
	 * @since 1.0.0
73
	 */
74
	public function __call( $name, $args ) {
75
76
		$this->route_action( $name, $args );
77
78
		// Return the first value, in case it is hooked to a filter.
79
		$return = null;
80
		if ( isset( $args[0] ) ) {
81
			$return = $args[0];
82
		}
83
84
		return $return;
85
	}
86
87
	/**
88
	 * Routes a WordPress action to WordPoints hook actions, and fires their events.
89
	 *
90
	 * @since 1.0.0
91
	 *
92
	 * @param string $name The action ID. This is not the slug of a hook action, but
93
	 *                     rather a unique ID for the WordPress action based on the
94
	 *                     action name and the priority.
95
	 * @param array  $args The args the action was fired with.
96
	 */
97
	protected function route_action( $name, $args ) {
98
99
		if ( ! isset( $this->action_index[ $name ] ) ) {
100
			return;
101
		}
102
103
		// We might normally do this in the constructor, however, the events
104
		// registry attempts to access the router in its own constructor. The result
105
		// of attempting to do this before the router itself has been fully
106
		// constructed is that the events registry gets null instead of the router.
107
		if ( ! isset( $this->actions ) ) {
108
109
			$hooks = wordpoints_hooks();
110
111
			$this->events  = $hooks->events;
112
			$this->actions = $hooks->actions;
113
		}
114
115
		foreach ( $this->action_index[ $name ]['actions'] as $slug => $data ) {
116
117
			if ( ! isset( $this->event_index[ $slug ] ) ) {
118
				continue;
119
			}
120
121
			$action_object = $this->actions->get( $slug, $args, $data );
122
123
			if ( ! ( $action_object instanceof WordPoints_Hook_ActionI ) ) {
124
				continue;
125
			}
126
127
			if ( ! $action_object->should_fire() ) {
128
				continue;
129
			}
130
131
			foreach ( $this->event_index[ $slug ] as $type => $events ) {
132
				foreach ( $events as $event_slug => $unused ) {
133
134
					if ( ! $this->events->is_registered( $event_slug ) ) {
135
						continue;
136
					}
137
138
					$event_args = $this->events->args->get_children( $event_slug, array( $action_object ) );
139
140
					if ( empty( $event_args ) ) {
141
						continue;
142
					}
143
144
					$event_args = new WordPoints_Hook_Event_Args( $event_args );
145
146
					$this->fire_event( $type, $event_slug, $event_args );
147
				}
148
			}
149
		}
150
	}
151
152
	/**
153
	 * Fire an event at each of the reactions.
154
	 *
155
	 * @since 1.0.0
156
	 *
157
	 * @param string                     $action_type The type of action triggering
158
	 *                                                this fire of this event.
159
	 * @param string                     $event_slug  The slug of the event.
160
	 * @param WordPoints_Hook_Event_Args $event_args  The event args.
161
	 */
162
	public function fire_event(
163
		$action_type,
164
		$event_slug,
165
		WordPoints_Hook_Event_Args $event_args
166
	) {
167
168
		$hooks = wordpoints_hooks();
169
170
		/** @var WordPoints_Hook_Reactor $reactor */
171
		foreach ( $hooks->reactors->get_all() as $reactor ) {
172
173
			if ( ! in_array( $action_type, $reactor->get_action_types(), true ) ) {
174
				continue;
175
			}
176
177
			foreach ( $reactor->get_all_reactions_to_event( $event_slug ) as $reaction ) {
178
179
				$validator = new WordPoints_Hook_Reaction_Validator(
180
					$reaction
181
					, $reactor
182
					, true
183
				);
184
185
				$validator->validate();
186
187
				if ( $validator->had_errors() ) {
188
					continue;
189
				}
190
191
				unset( $validator );
192
193
				$fire = new WordPoints_Hook_Fire( $action_type, $event_args, $reaction );
194
195
				/** @var WordPoints_Hook_Extension[] $extensions */
196
				$extensions = $hooks->extensions->get_all();
197
198
				foreach ( $extensions as $extension ) {
199
					if ( ! $extension->should_hit( $fire ) ) {
200
						continue 2;
201
					}
202
				}
203
204
				$fire->hit();
205
206
				$reactor->hit( $fire );
207
208
				foreach ( $extensions as $extension ) {
209
					$extension->after_hit( $fire );
210
				}
211
			}
212
		}
213
	}
214
215
	/**
216
	 * Register an action with the router.
217
	 *
218
	 * The arg number will be automatically determined based on $data['arg_index']
219
	 * and $data['requirements']. So in most cases $arg_number may be omitted.
220
	 *
221
	 * @since 1.0.0
222
	 *
223
	 * @param string $slug The slug of the action.
224
	 * @param array  $args {
225
	 *        Other arguments.
226
	 *
227
	 *        @type string $action     The name of the WordPress action for this hook action.
228
	 *        @type int    $priority   The priority for the WordPress action. Default: 10.
229
	 *        @type int    $arg_number The number of args the action object expects. Default: 1.
230
	 *        @type array  $data {
231
	 *              Args that will be passed to the action object's constructor.
232
	 *
233
	 *              @type int[] $arg_index    List of args (starting from 0), indexed by slug.
234
	 *              @type array $requirements List of requirements, indexed by arg index (from 0).
235
	 *        }
236
	 * }
237
	 */
238
	public function add_action( $slug, array $args ) {
239
240
		$priority = 10;
241
		if ( isset( $args['priority'] ) ) {
242
			$priority = $args['priority'];
243
		}
244
245
		if ( ! isset( $args['action'] ) ) {
246
			return;
247
		}
248
249
		$method = "{$args['action']},{$priority}";
250
251
		$this->action_index[ $method ]['actions'][ $slug ] = array();
252
253
		$arg_number = 1;
254
255
		if ( isset( $args['data'] ) ) {
256
257
			if ( isset( $args['data']['arg_index'] ) ) {
258
				$arg_number = 1 + max( $args['data']['arg_index'] );
259
			}
260
261
			if ( isset( $args['data']['requirements'] ) ) {
262
				$requirements = 1 + max( array_keys( $args['data']['requirements'] ) );
263
264
				if ( $requirements > $arg_number ) {
265
					$arg_number = $requirements;
266
				}
267
			}
268
269
			$this->action_index[ $method ]['actions'][ $slug ] = $args['data'];
270
		}
271
272
		if ( isset( $args['arg_number'] ) ) {
273
			$arg_number = $args['arg_number'];
274
		}
275
276
		// If this action is already being routed, and will have enough args, we
277
		// don't need to hook to it again.
278
		if (
279
			isset( $this->action_index[ $method ]['arg_number'] )
280
			&& $this->action_index[ $method ]['arg_number'] >= $arg_number
281
		) {
282
			return;
283
		}
284
285
		$this->action_index[ $method ]['arg_number'] = $arg_number;
286
287
		add_action( $args['action'], array( $this, $method ), $priority, $arg_number );
288
	}
289
290
	/**
291
	 * Deregister an action with the router.
292
	 *
293
	 * @since 1.0.0
294
	 *
295
	 * @param string $slug The action slug.
296
	 */
297
	public function remove_action( $slug ) {
298
299
		foreach ( $this->action_index as $method => $data ) {
300
			if ( isset( $data['actions'][ $slug ] ) ) {
301
302
				unset( $this->action_index[ $method ]['actions'][ $slug ] );
303
304
				if ( empty( $this->action_index[ $method ]['actions'] ) ) {
305
306
					unset( $this->action_index[ $method ] );
307
308
					list( $action, $priority ) = explode( ',', $method );
309
310
					remove_action( $action, array( $this, $method ), $priority );
311
				}
312
			}
313
		}
314
	}
315
316
	/**
317
	 * Hook an event to an action.
318
	 *
319
	 * @since 1.0.0
320
	 *
321
	 * @param string $event_slug The slug of the event.
322
	 * @param string $action_slug The slug of the action.
323
	 * @param string $action_type The type of action. Default is 'fire'.
324
	 */
325
	public function add_event_to_action( $event_slug, $action_slug, $action_type = 'fire' ) {
326
		$this->event_index[ $action_slug ][ $action_type ][ $event_slug ] = true;
327
	}
328
329
	/**
330
	 * Unhook an event from an action.
331
	 *
332
	 * @since 1.0.0
333
	 *
334
	 * @param string $event_slug  The slug of the event.
335
	 * @param string $action_slug The slug of the action.
336
	 * @param string $action_type The type of action. Default is 'fire'.
337
	 */
338
	public function remove_event_from_action( $event_slug, $action_slug, $action_type = 'fire' ) {
339
		unset( $this->event_index[ $action_slug ][ $action_type ][ $event_slug ] );
340
	}
341
}
342
343
// EOF
344