Completed
Push — master ( 82b9ae...b792a1 )
by J.D.
02:52
created

WordPoints_Hook_Extension_Periods   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 439
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4
Metric Value
wmc 38
lcom 1
cbo 4
dl 0
loc 439
rs 8.4

12 Methods

Rating   Name   Duplication   Size   Complexity  
A get_ui_script_data() 0 18 1
B validate_periods() 0 26 4
B validate_period() 0 32 5
B validate_period_args() 0 36 5
A should_hit() 0 21 4
B has_period_ended() 0 26 3
A get_arg_values() 0 19 3
B get_period() 0 34 3
B get_period_by_reaction() 0 42 3
A after_hit() 0 21 3
A get_period_signature() 0 15 2
B add_period() 0 33 2
1
<?php
2
3
/**
4
 * Periods hook extension.
5
 *
6
 * @package wordpoints-hooks-api
7
 * @since 1.0.0
8
 */
9
10
/**
11
 * Limits the number of times that targets can be hit in a given time period.
12
 *
13
 * @since 1.0.0
14
 */
15
class WordPoints_Hook_Extension_Periods extends WordPoints_Hook_Extension {
16
17
	/**
18
	 * @since 1.0.0
19
	 */
20
	protected $slug = 'periods';
21
22
	/**
23
	 * @since 1.0.0
24
	 */
25
	public function get_ui_script_data() {
26
27
		$periods = array(
28
			MINUTE_IN_SECONDS   => __( 'Minute', 'wordpoints' ),
29
			HOUR_IN_SECONDS     => __( 'Hour',   'wordpoints' ),
30
			DAY_IN_SECONDS      => __( 'Day',    'wordpoints' ),
31
			WEEK_IN_SECONDS     => __( 'Week',   'wordpoints' ),
32
			30 * DAY_IN_SECONDS => __( 'Month',  'wordpoints' ),
33
		);
34
35
		return array(
36
			'periods' => $periods,
37
			'l10n' => array(
38
				// TODO this should be supplied per-reactor
39
				'label' => __( 'Award each user no more than once per:', 'wordpoints' ),
40
			),
41
		);
42
	}
43
44
	/**
45
	 * Validate the periods.
46
	 *
47
	 * @since 1.0.0
48
	 *
49
	 * @param array $periods The periods.
50
	 *
51
	 * @return array The validated periods.
52
	 */
53
	protected function validate_periods( $periods ) {
54
55
		if ( ! is_array( $periods ) ) {
56
57
			$this->validator->add_error(
58
				__( 'Periods do not match expected format.', 'wordpoints' )
59
			);
60
61
			return array();
62
		}
63
64
		foreach ( $periods as $index => $period ) {
65
66
			$this->validator->push_field( $index );
67
68
			$period = $this->validate_period( $period );
69
70
			if ( $period ) {
71
				$periods[ $index ] = $period;
72
			}
73
74
			$this->validator->pop_field();
75
		}
76
77
		return $periods;
78
	}
79
80
	/**
81
	 * Validate the settings for a period.
82
	 *
83
	 * @since 1.0.0
84
	 *
85
	 * @param array $period The period.
86
	 *
87
	 * @return array|false The validated period, or false if invalid.
88
	 */
89
	protected function validate_period( $period ) {
90
91
		if ( ! is_array( $period ) ) {
92
			$this->validator->add_error(
93
				__( 'Period does not match expected format.', 'wordpoints' )
94
			);
95
96
			return false;
97
		}
98
99
		if ( isset( $period['args'] ) ) {
100
			$this->validate_period_args( $period['args'] );
101
		}
102
103
		if ( ! isset( $period['length'] ) ) {
104
105
			$this->validator->add_error(
106
				__( 'Period length setting is missing.', 'wordpoints' )
107
			);
108
109
		} elseif ( false === wordpoints_posint( $period['length'] ) ) {
110
111
			$this->validator->add_error(
112
				__( 'Period length must be a positive integer.', 'wordpoints' )
113
				, 'length'
114
			);
115
116
			return false;
117
		}
118
119
		return $period;
120
	}
121
122
	/**
123
	 * Validate the period args.
124
	 *
125
	 * @since 1.0.0
126
	 *
127
	 * @param mixed $args The args the period is related to.
128
	 */
129
	protected function validate_period_args( $args ) {
130
131
		if ( ! is_array( $args ) ) {
132
133
			$this->validator->add_error(
134
				__( 'Period does not match expected format.', 'wordpoints' )
135
				, 'args'
136
			);
137
138
			return;
139
		}
140
141
		$this->validator->push_field( 'args' );
142
143
		foreach ( $args as $index => $hierarchy ) {
144
145
			$this->validator->push_field( $index );
146
147
			if ( ! is_array( $hierarchy ) ) {
148
149
				$this->validator->add_error(
150
					__( 'Period does not match expected format.', 'wordpoints' )
151
				);
152
153
			} elseif ( ! $this->event_args->get_from_hierarchy( $hierarchy ) ) {
154
155
				$this->validator->add_error(
156
					__( 'Invalid period.', 'wordpoints' ) // TODO better error message
157
				);
158
			}
159
160
			$this->validator->pop_field();
161
		}
162
163
		$this->validator->pop_field();
164
	}
165
166
	/**
167
	 * @since 1.0.0
168
	 */
169
	public function should_hit(
170
		WordPoints_Hook_Reaction_Validator $reaction,
171
		WordPoints_Hook_Event_Args $event_args
172
	) {
173
174
		$periods = $reaction->get_meta( 'periods' );
175
176
		if ( empty( $periods ) ) {
177
			return true;
178
		}
179
180
		$this->event_args = $event_args;
181
182
		foreach ( $periods as $period ) {
183
			if ( ! $this->has_period_ended( $period, $reaction ) ) {
184
				return false;
185
			}
186
		}
187
188
		return true;
189
	}
190
191
	/**
192
	 * Check whether a period has ended.
193
	 *
194
	 * @since 1.0.0
195
	 *
196
	 * @param array                              $settings The period's settings.
197
	 * @param WordPoints_Hook_Reaction_Validator $reaction The reaction object.
198
	 *
199
	 * @return bool Whether the period has ended.
200
	 */
201
	protected function has_period_ended(
202
		array $settings,
203
		WordPoints_Hook_Reaction_Validator $reaction
204
	) {
205
206
		$period = $this->get_period_by_reaction(
207
			$this->get_period_signature( $settings, $reaction )
208
			, $reaction
209
		);
210
211
		// If the period isn't found, we know that we can still fire.
212
		if ( ! $period ) {
213
			return true;
214
		}
215
216
		$now = current_time( 'timestamp' );
217
218
		if ( ! empty( $settings['relative'] ) ) {
219
			return ( $period->hit_time < $now - $settings['length'] );
220
		} else {
221
			return (
222
				(int) ( $period->hit_time / $settings['length'] )
223
				< (int) ( $now / $settings['length'] )
224
			);
225
		}
226
	}
227
228
	/**
229
	 * Get the values of the args that a period relates to.
230
	 *
231
	 * @since 1.0.0
232
	 *
233
	 * @param array $period_args The args this period relates to.
234
	 *
235
	 * @return array The arg values.
236
	 */
237
	protected function get_arg_values( array $period_args ) {
238
239
		$values = array();
240
241
		foreach ( $period_args as $arg_hierarchy ) {
242
243
			$arg = $this->event_args->get_from_hierarchy(
244
				$arg_hierarchy
245
			);
246
247
			if ( ! $arg instanceof WordPoints_EntityishI ) {
248
				continue;
249
			}
250
251
			$values[ implode( '.', $arg_hierarchy ) ] = $arg->get_the_value();
252
		}
253
254
		return $values;
255
	}
256
257
	/**
258
	 * Get a a period from the database by ID.
259
	 *
260
	 * @since 1.0.0
261
	 *
262
	 * @param int $period_id The ID of a period.
263
	 *
264
	 * @return object|false The period data, or false if not found.
265
	 */
266
	protected function get_period( $period_id ) {
267
268
		$period = wp_cache_get( $period_id, 'wordpoints_hook_period' );
269
270
		if ( ! $period ) {
271
272
			global $wpdb;
273
274
			$period = $wpdb->get_row(
275
				$wpdb->prepare(
276
					"
277
						SELECT `id`, `reaction_id`, `signature`, `hit_time`, `meta`
278
						FROM `{$wpdb->wordpoints_hook_periods}`
279
						WHERE `id` = %d
280
					"
281
					, $period_id
282
				)
283
			);
284
285
			if ( ! $period ) {
286
				return false;
287
			}
288
289
			wp_cache_set(
290
				"{$period->reaction_id}-{$period->signature}"
291
				, $period->id
292
				, 'wordpoints_hook_period_ids'
293
			);
294
295
			wp_cache_set( $period->id, $period, 'wordpoints_hook_periods' );
296
		}
297
298
		return $period;
299
	}
300
301
	/**
302
	 * Get a period from the database by args reaction ID.
303
	 *
304
	 * @since 1.0.0
305
	 *
306
	 * @param string                             $signature The values of the args
307
	 *                                                     this period relates to.
308
	 * @param WordPoints_Hook_Reaction_Validator $reaction  The reaction object.
309
	 *
310
	 * @return object|false The period data, or false if not found.
311
	 */
312
	protected function get_period_by_reaction(
313
		$signature,
314
		WordPoints_Hook_Reaction_Validator $reaction
315
	) {
316
317
		$reaction_id = $reaction->get_id();
318
319
		$cache_key = "{$reaction_id}-{$signature}";
320
321
		// Before we run the query, we try to lookup the ID in the cache.
322
		$period_id = wp_cache_get( $cache_key, 'wordpoints_hook_period_ids' );
323
324
		// If we found it, we can retrieve the period by ID instead.
325
		if ( $period_id ) {
326
			return $this->get_period( $period_id );
327
		}
328
329
		global $wpdb;
330
331
		// Otherwise, we have to run this query.
332
		$period = $wpdb->get_row(
333
			$wpdb->prepare(
334
				"
335
					SELECT `id`, `reaction_id`, `signature`, `hit_time`, `meta`
336
					FROM `{$wpdb->wordpoints_hook_periods}`
337
					WHERE `reaction_id` = %d
338
						AND `signature` = %s
339
				"
340
				, $reaction_id
341
				, $signature
342
			)
343
		);
344
345
		if ( ! $period ) {
346
			return false;
347
		}
348
349
		wp_cache_set( $cache_key, $period->id, 'wordpoints_hook_period_ids' );
350
		wp_cache_set( $period->id, $period, 'wordpoints_hook_periods' );
351
352
		return $period;
353
	}
354
355
	/**
356
	 * @since 1.0.0
357
	 */
358
	public function after_hit(
359
		WordPoints_Hook_Reaction_Validator $reaction,
360
		WordPoints_Hook_Event_Args $event_args
361
	) {
362
363
		$periods = $reaction->get_meta( 'periods' );
364
365
		if ( empty( $periods ) ) {
366
			return;
367
		}
368
369
		$this->event_args = $event_args;
370
371
		foreach ( $periods as $settings ) {
372
373
			$this->add_period(
374
				$this->get_period_signature( $settings, $reaction )
375
				, $reaction
376
			);
377
		}
378
	}
379
380
	/**
381
	 * Get the signature for a period.
382
	 *
383
	 * The period signature is a hash value calculated based on the values of the
384
	 * event args to which that period is related. This is calculated as a hash so
385
	 * that it can be easily stored and queried at a fixed length.
386
	 *
387
	 * @since 1.0.0
388
	 *
389
	 * @param array                              $settings The period settings.
390
	 * @param WordPoints_Hook_Reaction_Validator $reaction The reaction.
391
	 *
392
	 * @return string The period signature.
393
	 */
394
	protected function get_period_signature(
395
		array $settings,
396
		WordPoints_Hook_Reaction_Validator $reaction
397
	) {
398
399
		if ( isset( $settings['args'] ) ) {
400
			$period_args = $settings['args'];
401
		} else {
402
			$period_args = array( $reaction->get_meta( 'target' ) );
403
		}
404
405
		return wordpoints_hash(
406
			wp_json_encode( $this->get_arg_values( $period_args ) )
407
		);
408
	}
409
410
	/**
411
	 * Add a period to the database.
412
	 *
413
	 * @since 1.0.0
414
	 *
415
	 * @param string                             $signature The period signature.
416
	 * @param WordPoints_Hook_Reaction_Validator $reaction  The reaction object.
417
	 *
418
	 * @return false|object The period data, or false if not found.
419
	 */
420
	protected function add_period(
421
		$signature,
422
		WordPoints_Hook_Reaction_Validator $reaction
423
	) {
424
425
		global $wpdb;
426
427
		$reaction_id = $reaction->get_id();
428
429
		$inserted = $wpdb->insert(
430
			$wpdb->wordpoints_hook_periods
431
			, array(
432
				'reaction_id' => $reaction_id,
433
				'signature'   => $signature,
434
				'hit_time'    => current_time( 'timestamp' ),
435
			)
436
			, array( '%d', '%s', '%d' )
437
		);
438
439
		if ( ! $inserted ) {
440
			return false;
441
		}
442
443
		$period_id = $wpdb->insert_id;
444
445
		wp_cache_set(
446
			"{$reaction_id}-{$signature}"
447
			, $period_id
448
			, 'wordpoints_hook_period_ids'
449
		);
450
451
		return $period_id;
452
	}
453
}
454
455
// EOF
456