Completed
Push — master ( 226003...873ec4 )
by J.D.
03:07
created

WordPoints_Hook_Router::fire_event()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 33
rs 8.439
cc 6
eloc 17
nc 5
nop 3
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
	 * The events are the indexes in the arrays for each action type, the values in
66
	 * the arrays are unused.
67
	 *
68
	 * @since 1.0.0
69
	 *
70
	 * @var array[]
71
	 */
72
	protected $event_index = array();
73
74
	/**
75
	 * The reactor hit types, indexed by reactor and action type.
76
	 *
77
	 * Tells us what type of hit to tell a reactor to perform when it is hit by a
78
	 * fire of a particular type of action.
79
	 *
80
	 * @since 1.0.0
81
	 *
82
	 * @var string[][]
83
	 */
84
	protected $reactor_index = array();
85
86
	/**
87
	 * @since 1.0.0
88
	 */
89
	public function __call( $name, $args ) {
90
91
		$this->route_action( $name, $args );
92
93
		// Return the first value, in case it is hooked to a filter.
94
		$return = null;
95
		if ( isset( $args[0] ) ) {
96
			$return = $args[0];
97
		}
98
99
		return $return;
100
	}
101
102
	/**
103
	 * Routes a WordPress action to WordPoints hook actions, and fires their events.
104
	 *
105
	 * @since 1.0.0
106
	 *
107
	 * @param string $name The action ID. This is not the slug of a hook action, but
108
	 *                     rather a unique ID for the WordPress action based on the
109
	 *                     action name and the priority.
110
	 * @param array  $args The args the action was fired with.
111
	 */
112
	protected function route_action( $name, $args ) {
113
114
		if ( ! isset( $this->action_index[ $name ] ) ) {
115
			return;
116
		}
117
118
		// We might normally do this in the constructor, however, the events
119
		// registry attempts to access the router in its own constructor. The result
120
		// of attempting to do this before the router itself has been fully
121
		// constructed is that the events registry gets null instead of the router.
122
		if ( ! isset( $this->actions ) ) {
123
124
			$hooks = wordpoints_hooks();
125
126
			$this->events  = $hooks->events;
127
			$this->actions = $hooks->actions;
128
		}
129
130
		foreach ( $this->action_index[ $name ]['actions'] as $slug => $data ) {
131
132
			if ( ! isset( $this->event_index[ $slug ] ) ) {
133
				continue;
134
			}
135
136
			$action_object = $this->actions->get( $slug, $args, $data );
137
138
			if ( ! ( $action_object instanceof WordPoints_Hook_ActionI ) ) {
139
				continue;
140
			}
141
142
			if ( ! $action_object->should_fire() ) {
143
				continue;
144
			}
145
146
			foreach ( $this->event_index[ $slug ] as $type => $events ) {
147
				foreach ( $events as $event_slug => $unused ) {
148
149
					if ( ! $this->events->is_registered( $event_slug ) ) {
150
						continue;
151
					}
152
153
					$event_args = $this->events->args->get_children( $event_slug, array( $action_object ) );
154
155
					if ( empty( $event_args ) ) {
156
						continue;
157
					}
158
159
					$event_args = new WordPoints_Hook_Event_Args( $event_args );
160
161
					$this->fire_event( $type, $event_slug, $event_args );
162
				}
163
			}
164
		}
165
	}
166
167
	/**
168
	 * Fire an event at each of the reactions.
169
	 *
170
	 * @since 1.0.0
171
	 *
172
	 * @param string                     $action_type The type of action triggering
173
	 *                                                this fire of this event.
174
	 * @param string                     $event_slug  The slug of the event.
175
	 * @param WordPoints_Hook_Event_Args $event_args  The event args.
176
	 */
177
	public function fire_event(
178
		$action_type,
179
		$event_slug,
180
		WordPoints_Hook_Event_Args $event_args
181
	) {
182
183
		$hooks = wordpoints_hooks();
184
185
		foreach ( $hooks->reaction_stores->get_all() as $reaction_stores ) {
186
			foreach ( $reaction_stores as $reaction_store ) {
187
188
				if ( ! $reaction_store instanceof WordPoints_Hook_Reaction_StoreI ) {
189
					continue;
190
				}
191
192
				// Allowing access to stores out-of-context would lead to strange behavior.
193
				if ( false === $reaction_store->get_context_id() ) {
194
					continue;
195
				}
196
197
				foreach ( $reaction_store->get_reactions_to_event( $event_slug ) as $reaction ) {
198
199
					$fire = new WordPoints_Hook_Fire(
200
						$action_type
201
						, $event_args
202
						, $reaction
203
					);
204
205
					$this->fire_reaction( $fire );
206
				}
207
			}
208
		}
209
	}
210
211
	/**
212
	 * Fire for a particular reaction.
213
	 *
214
	 * @since 1.0.0
215
	 *
216
	 * @param WordPoints_Hook_Fire $fire The hook fire object.
217
	 */
218
	protected function fire_reaction( $fire ) {
219
220
		$hooks = wordpoints_hooks();
221
222
		$reactor_slug = $fire->reaction->get_reactor_slug();
223
224
		if ( ! isset( $this->reactor_index[ $reactor_slug ][ $fire->action_type ] ) ) {
225
			return;
226
		}
227
228
		$hit_type = $this->reactor_index[ $reactor_slug ][ $fire->action_type ];
229
230
		$validator = new WordPoints_Hook_Reaction_Validator( $fire->reaction, true );
231
		$validator->validate();
232
233
		if ( $validator->had_errors() ) {
234
			return;
235
		}
236
237
		unset( $validator );
238
239
		/** @var WordPoints_Hook_Extension[] $extensions */
240
		$extensions = $hooks->extensions->get_all();
241
242
		foreach ( $extensions as $extension ) {
243
			if ( ! $extension->should_hit( $fire ) ) {
244
				return;
245
			}
246
		}
247
248
		$fire->hit();
249
250
		/** @var WordPoints_Hook_Reactor $reactor */
251
		$reactor = $hooks->reactors->get( $reactor_slug );
252
253
		$reactor->hit( $hit_type, $fire );
254
255
		foreach ( $extensions as $extension ) {
256
			$extension->after_hit( $fire );
257
		}
258
	}
259
260
	/**
261
	 * Register an action with the router.
262
	 *
263
	 * The arg number will be automatically determined based on $data['arg_index']
264
	 * and $data['requirements']. So in most cases $arg_number may be omitted.
265
	 *
266
	 * @since 1.0.0
267
	 *
268
	 * @param string $slug The slug of the action.
269
	 * @param array  $args {
270
	 *        Other arguments.
271
	 *
272
	 *        @type string $action     The name of the WordPress action for this hook action.
273
	 *        @type int    $priority   The priority for the WordPress action. Default: 10.
274
	 *        @type int    $arg_number The number of args the action object expects. Default: 1.
275
	 *        @type array  $data {
276
	 *              Args that will be passed to the action object's constructor.
277
	 *
278
	 *              @type int[] $arg_index    List of args (starting from 0), indexed by slug.
279
	 *              @type array $requirements List of requirements, indexed by arg index (from 0).
280
	 *        }
281
	 * }
282
	 */
283
	public function add_action( $slug, array $args ) {
284
285
		$priority = 10;
286
		if ( isset( $args['priority'] ) ) {
287
			$priority = $args['priority'];
288
		}
289
290
		if ( ! isset( $args['action'] ) ) {
291
			return;
292
		}
293
294
		$method = "{$args['action']},{$priority}";
295
296
		$this->action_index[ $method ]['actions'][ $slug ] = array();
297
298
		$arg_number = 1;
299
300
		if ( isset( $args['data'] ) ) {
301
302
			if ( isset( $args['data']['arg_index'] ) ) {
303
				$arg_number = 1 + max( $args['data']['arg_index'] );
304
			}
305
306
			if ( isset( $args['data']['requirements'] ) ) {
307
				$requirements = 1 + max( array_keys( $args['data']['requirements'] ) );
308
309
				if ( $requirements > $arg_number ) {
310
					$arg_number = $requirements;
311
				}
312
			}
313
314
			$this->action_index[ $method ]['actions'][ $slug ] = $args['data'];
315
		}
316
317
		if ( isset( $args['arg_number'] ) ) {
318
			$arg_number = $args['arg_number'];
319
		}
320
321
		// If this action is already being routed, and will have enough args, we
322
		// don't need to hook to it again.
323
		if (
324
			isset( $this->action_index[ $method ]['arg_number'] )
325
			&& $this->action_index[ $method ]['arg_number'] >= $arg_number
326
		) {
327
			return;
328
		}
329
330
		$this->action_index[ $method ]['arg_number'] = $arg_number;
331
332
		add_action( $args['action'], array( $this, $method ), $priority, $arg_number );
333
	}
334
335
	/**
336
	 * Deregister an action with the router.
337
	 *
338
	 * @since 1.0.0
339
	 *
340
	 * @param string $slug The action slug.
341
	 */
342
	public function remove_action( $slug ) {
343
344
		foreach ( $this->action_index as $method => $data ) {
345
			if ( isset( $data['actions'][ $slug ] ) ) {
346
347
				unset( $this->action_index[ $method ]['actions'][ $slug ] );
348
349
				if ( empty( $this->action_index[ $method ]['actions'] ) ) {
350
351
					unset( $this->action_index[ $method ] );
352
353
					list( $action, $priority ) = explode( ',', $method );
354
355
					remove_action( $action, array( $this, $method ), $priority );
356
				}
357
			}
358
		}
359
	}
360
361
	/**
362
	 * Hook an event to an action.
363
	 *
364
	 * @since 1.0.0
365
	 *
366
	 * @param string $event_slug The slug of the event.
367
	 * @param string $action_slug The slug of the action.
368
	 * @param string $action_type The type of action. Default is 'fire'.
369
	 */
370
	public function add_event_to_action( $event_slug, $action_slug, $action_type = 'fire' ) {
371
		$this->event_index[ $action_slug ][ $action_type ][ $event_slug ] = true;
372
	}
373
374
	/**
375
	 * Unhook an event from an action.
376
	 *
377
	 * @since 1.0.0
378
	 *
379
	 * @param string $event_slug  The slug of the event.
380
	 * @param string $action_slug The slug of the action.
381
	 * @param string $action_type The type of action. Default is 'fire'.
382
	 */
383
	public function remove_event_from_action( $event_slug, $action_slug, $action_type = 'fire' ) {
384
		unset( $this->event_index[ $action_slug ][ $action_type ][ $event_slug ] );
385
	}
386
387
	/**
388
	 * Hook an action type to a reactor.
389
	 *
390
	 * @since 1.0.0
391
	 *
392
	 * @param string $action_type  The slug of the action type.
393
	 * @param string $reactor_slug The slug of the reactor.
394
	 * @param string $hit_type     The type of hit the reactor should perform when
395
	 *                             hit by this type of event.
396
	 */
397
	public function add_action_type_to_reactor( $action_type, $reactor_slug, $hit_type ) {
398
		$this->reactor_index[ $reactor_slug ][ $action_type ] = $hit_type;
399
	}
400
401
	/**
402
	 * Unhook an action type from a reactor.
403
	 *
404
	 * @since 1.0.0
405
	 *
406
	 * @param string $action_type  The slug of the action type.
407
	 * @param string $reactor_slug The slug of the reactor.
408
	 */
409
	public function remove_action_type_from_reactor( $action_type, $reactor_slug ) {
410
		unset( $this->reactor_index[ $reactor_slug ][ $action_type ] );
411
	}
412
413
	/**
414
	 * Get the event index.
415
	 *
416
	 * @since 1.0.0
417
	 *
418
	 * @return array[] The event index.
419
	 */
420
	public function get_event_index() {
421
422
		if ( empty( $this->reactor_index ) ) {
423
			wordpoints_hooks()->events;
424
		}
425
426
		return $this->event_index;
427
	}
428
429
	/**
430
	 * Get the reactor index.
431
	 *
432
	 * @since 1.0.0
433
	 *
434
	 * @return string[][]
435
	 */
436
	public function get_reactor_index() {
437
438
		if ( empty( $this->reactor_index ) ) {
439
			wordpoints_hooks()->reactors;
440
		}
441
442
		return $this->reactor_index;
443
	}
444
}
445
446
// EOF
447