Completed
Branch FET-9046-messages-queue (bbfce3)
by
unknown
947:38 queued 926:24
created

EE_Messages_Queue::execute()   C

Complexity

Conditions 11
Paths 38

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 48
rs 5.2653
cc 11
eloc 29
nc 38
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php if ( ! defined('EVENT_ESPRESSO_VERSION')) { exit('No direct script access allowed'); }
2
3
/**
4
 * This class is used for managing and interacting with the EE_Messages Queue.  An instance
5
 * of this object is used for interacting with a specific batch of EE_Message objects.
6
 *
7
 * @package    Event Espresso
8
 * @subpackage messages
9
 * @author     Darren Ethier
10
 * @since      4.9.0
11
 */
12
class EE_Messages_Queue {
13
14
15
	/**
16
	 * @type    string  reference for sending action
17
	 */
18
	const action_sending = 'sending';
19
20
	/**
21
	 * @type    string  reference for generation action
22
	 */
23
	const action_generating = 'generation';
24
25
26
27
	/**
28
	 * @type EE_Message_Repository $_queue
29
	 */
30
	protected $_queue;
31
32
	/**
33
	 * Sets the limit of how many messages are generated per process.
34
	 * @type int
35
	 */
36
	protected $_batch_count;
37
38
	/**
39
	 * Sets the limit of how many messages can be sent per hour.
40
	 * @type int
41
	 */
42
	protected $_rate_limit;
43
44
	/**
45
	 * This is an array of cached queue items being stored in this object.
46
	 * The array keys will be the ID of the EE_Message in the db if saved.  If the EE_Message
47
	 * is not saved to the db then its key will be an increment of "UNS" (i.e. UNS1, UNS2 etc.)
48
	 * @type EE_Message[]
49
	 */
50
	protected $_cached_queue_items;
51
52
	/**
53
	 * Tracks the number of unsaved queue items.
54
	 * @type int
55
	 */
56
	protected $_unsaved_count = 0;
57
58
	/**
59
	 * used to record if a do_messenger_hooks has already been called for a message type.  This prevents multiple
60
	 * hooks getting fired if users have setup their action/filter hooks to prevent duplicate calls.
61
	 *
62
	 * @type array
63
	 */
64
	protected $_did_hook = array();
65
66
67
68
	/**
69
	 * Constructor.
70
	 * Setup all the initial properties and load a EE_Message_Repository.
71
	 *
72
	 * @param \EE_Message_Repository       $message_repository
73
	 */
74
	public function __construct( EE_Message_Repository $message_repository ) {
75
		$this->_batch_count = apply_filters( 'FHEE__EE_Messages_Queue___batch_count', 50 );
76
		$this->_rate_limit = $this->get_rate_limit();
77
		$this->_queue = $message_repository;
78
	}
79
80
81
82
	/**
83
	 * Add a EE_Message object to the queue
84
	 *
85
	 * @param EE_Message    $message
86
	 * @param array         $data     This will be an array of data to attach to the object in the repository.  If the
87
	 *                                object is persisted, this data will be saved on an extra_meta object related to
88
	 *                                EE_Message.
89
	 * @param  bool         $preview  Whether this EE_Message represents a preview or not.
90
	 * @param  bool         $test_send This indicates whether to do a test send instead of actual send. A test send will
91
	 *                                 use the messenger send method but typically is based on preview data.
92
	 * @return bool          Whether the message was successfully added to the repository or not.
93
	 */
94
	public function add( EE_Message $message, $data = array(), $preview = false, $test_send = false ) {
95
		$data['preview'] = $preview;
96
		$data['test_send'] = $test_send;
97
		return $this->_queue->add( $message, $data );
98
	}
99
100
101
102
103
	/**
104
	 * Removes EE_Message from _queue that matches the given EE_Message if the pointer is on a matching EE_Message
105
	 * @param EE_Message    $message    The message to detach from the queue
106
	 * @param bool          $persist    This flag indicates whether to attempt to delete the object from the db as well.
107
	 * @return bool
108
	 */
109
	public function remove( EE_Message $message, $persist = false ) {
110
		if ( $persist && $this->_queue->current() !== $message ) {
111
			//get pointer on right message
112
			if ( $this->_queue->has( $message ) ) {
113
				$this->_queue->rewind();
114
				while( $this->_queue->valid() ) {
115
					if ( $this->_queue->current() === $message ) {
116
						break;
117
					}
118
					$this->_queue->next();
119
				}
120
			} else {
121
				return false;
122
			}
123
		}
124
		return $persist ? $this->_queue->delete() : $this->_queue->remove( $message );
125
	}
126
127
128
129
130
	/**
131
	 * Persists all queued EE_Message objects to the db.
132
	 * @return array()  @see EE_Messages_Repository::saveAll() for return values.
0 ignored issues
show
Documentation introduced by
The doc-type array() could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
133
	 */
134
	public function save() {
135
		return $this->_queue->saveAll();
136
	}
137
138
139
140
141
142
	/**
143
	 * @return EE_Message_Repository
144
	 */
145
	public function get_queue() {
146
		return $this->_queue;
147
	}
148
149
150
151
152
	/**
153
	 * This does the following things:
154
	 * 1. Checks if there is a lock on generation (prevents race conditions).  If there is a lock then exits (return false).
155
	 * 2. If no lock, sets lock, then retrieves a batch of non-generated EE_Message objects and adds to queue
156
	 * 3. Returns bool.  True = batch ready.  False = no batch ready (or nothing available for generation).
157
	 *
158
	 * Note: Callers should make sure they release the lock otherwise batch generation will be prevented from continuing.
159
	 *       The lock is on a transient that is set to expire after one hour as a fallback in case locks are not removed.
160
	 *
161
	 * @return bool  true if successfully retrieved batch, false no batch ready.
162
	 */
163
	public function get_batch_to_generate() {
164
		if ( $this->is_locked( EE_Messages_Queue::action_generating ) ) {
165
			return false;
166
		}
167
168
		//lock batch generation to prevent race conditions.
169
		$this->lock_queue( EE_Messages_Queue::action_generating );
170
171
		$query_args = array(
172
			// key 0 = where conditions
173
			0 => array( 'STS_ID' => EEM_Message::status_incomplete ),
174
			'order_by' => $this->_get_priority_orderby(),
175
			'limit' => $this->_batch_count
176
		);
177
		$messages = EEM_Message::instance()->get_all( $query_args );
178
179
		if ( ! $messages ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $messages of type EE_Base_Class[] 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...
180
			return false; //nothing to generate
181
		}
182
183
		foreach ( $messages as $message ) {
184
			if ( $message instanceof EE_Message ) {
185
				$data = $message->all_extra_meta_array();
186
				$this->add( $message, $data );
187
			}
188
		}
189
		return true;
190
	}
191
192
193
	/**
194
	 * This does the following things:
195
	 * 1. Checks if there is a lock on sending (prevents race conditions).  If there is a lock then exits (return false).
196
	 * 2. Grabs the allowed number of messages to send for the rate_limit.  If cannot send any more messages, then return false.
197
	 * 2. If no lock, sets lock, then retrieves a batch of EE_Message objects, adds to queue and triggers execution.
198
	 * 3. On success or unsuccessful send, sets status appropriately.
199
	 * 4. Saves messages via the queue
200
	 * 5. Releases lock.
201
	 *
202
	 * @return bool  true on success, false if something preventing sending (i.e. lock set).  Note: true does not necessarily
203
	 *               mean that all messages were successfully sent.  It just means that this method successfully completed.
204
	 *               On true, client may want to call $this->count_STS_in_queue( EEM_Message::status_failed ) to see if
205
	 *               any failed EE_Message objects.  Each failed message object will also have a saved error message on it
206
	 *               to assist with notifying user.
207
	 */
208
	public function get_to_send_batch_and_send() {
209
		if ( $this->is_locked( EE_Messages_Queue::action_sending ) || $this->_rate_limit < 1 ) {
210
			return false;
211
		}
212
213
		$this->lock_queue( EE_Messages_Queue::action_sending );
214
215
		$batch = $this->_batch_count < $this->_rate_limit ? $this->_batch_count : $this->_rate_limit;
216
217
		$query_args = array(
218
			// key 0 = where conditions
219
			0 => array( 'STS_ID' => array( 'IN', EEM_Message::instance()->stati_indicating_to_send() ) ),
220
			'order_by' => $this->_get_priority_orderby(),
221
			'limit' => $batch
222
		);
223
224
		$messages_to_send = EEM_Message::instance()->get_all( $query_args );
225
226
227
		//any to send?
228
		if ( ! $messages_to_send ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $messages_to_send of type EE_Base_Class[] 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...
229
			$this->unlock_queue( EE_Messages_Queue::action_sending );
230
			return false;
231
		}
232
233
		//add to queue.
234
		foreach ( $messages_to_send as $message ) {
235
			if ( $message instanceof EE_Message ) {
236
				$this->add( $message );
237
			}
238
		}
239
240
		//send messages  (this also updates the rate limit)
241
		$this->execute();
242
243
		//release lock
244
		$this->unlock_queue( EE_Messages_Queue::action_sending );
245
		return true;
246
	}
247
248
249
250
251
	/**
252
	 * Locks the queue so that no other queues can call the "batch" methods.
253
	 *
254
	 * @param   string  $type   The type of queue being locked.
255
	 */
256
	public function lock_queue( $type = EE_Messages_Queue::action_generating ) {
257
		set_transient( $this->_get_lock_key( $type ), 1, $this->_get_lock_expiry( $type ) );
258
	}
259
260
261
262
263
	/**
264
	 * Unlocks the queue so that batch methods can be used.
265
	 *
266
	 * @param   string  $type   The type of queue being unlocked.
267
	 */
268
	public function unlock_queue( $type = EE_Messages_Queue::action_generating ) {
269
		delete_transient( $this->_get_lock_key( $type ) );
270
	}
271
272
273
274
275
	/**
276
	 * Retrieve the key used for the lock transient.
277
	 * @param string $type  The type of lock.
278
	 * @return string
279
	 */
280
	protected function _get_lock_key( $type = EE_Messages_Queue::action_generating ) {
281
		return '_ee_lock_' . $type;
282
	}
283
284
285
286
287
	/**
288
	 * Retrieve the expiry time for the lock transient.
289
	 * @param string $type  The type of lock
290
	 * @return int   time to expiry in seconds.
291
	 */
292
	protected function _get_lock_expiry( $type = EE_Messages_Queue::action_generating ) {
293
		return (int) apply_filters( 'FHEE__EE_Messages_Queue__lock_expiry', HOUR_IN_SECONDS, $type );
294
	}
295
296
297
	/**
298
	 * Returns the key used for rate limit transient.
299
	 * @return string
300
	 */
301
	protected function _get_rate_limit_key() {
302
		return '_ee_rate_limit';
303
	}
304
305
306
	/**
307
	 * Returns the rate limit expiry time.
308
	 * @return int
309
	 */
310
	protected function _get_rate_limit_expiry() {
311
		return (int) apply_filters( 'FHEE__EE_Messages_Queue__rate_limit_expiry', HOUR_IN_SECONDS );
312
	}
313
314
315
316
317
	/**
318
	 * Returns the default rate limit for sending messages.
319
	 * @return int
320
	 */
321
	protected function _default_rate_limit() {
322
		return (int) apply_filters( 'FHEE__EE_Messages_Queue___rate_limit', 200 );
323
	}
324
325
326
327
328
	/**
329
	 * Return the orderby array for priority.
330
	 * @return array
331
	 */
332
	protected function _get_priority_orderby() {
333
		return array(
334
			'MSG_priority' => 'ASC',
335
			'MSG_modified' => 'DESC'
336
		);
337
	}
338
339
340
341
342
	/**
343
	 * Returns whether batch methods are "locked" or not.
344
	 *
345
	 * @param  string $type The type of lock being checked for.
346
	 * @return bool
347
	 */
348
	public function is_locked( $type = EE_Messages_Queue::action_generating ) {
349
		return (bool) get_transient( $this->_get_lock_key( $type ) );
350
	}
351
352
353
354
355
356
357
358
	/**
359
	 * Retrieves the rate limit that may be cached as a transient.
360
	 * If the rate limit is not set, then this sets the default rate limit and expiry and returns it.
361
	 * @return int
362
	 */
363
	public function get_rate_limit() {
364
		if ( ! $rate_limit = get_transient( $this->_get_rate_limit_key() ) ) {
365
			$rate_limit = $this->_default_rate_limit();
366
			set_transient( $this->_get_rate_limit_key(), $rate_limit, $this->_get_rate_limit_key() );
367
		}
368
		return $rate_limit;
369
	}
370
371
372
373
374
	/**
375
	 * This updates existing rate limit with the new limit which is the old minus the batch.
376
	 * @param int $batch_completed  This sets the new rate limit based on the given batch that was completed.
377
	 */
378
	public function set_rate_limit( $batch_completed ) {
379
		//first get the most up to date rate limit (in case its expired and reset)
380
		$rate_limit = $this->get_rate_limit();
381
		$new_limit = $rate_limit - $batch_completed;
382
		//updating the transient option directly to avoid resetting the expiry.
383
		update_option( '_transient_' . $this->_get_rate_limit_key(), $new_limit );
384
	}
385
386
387
	/**
388
	 * This method checks the queue for ANY EE_Message objects with a priority matching the given priority passed in.
389
	 * If that exists, then we immediately initiate a non-blocking request to do the requested action type.
390
	 *
391
	 * Note: Keep in mind that there is the possibility that the request will not execute if there is already another request
392
	 * running on a queue for the given task.
393
	 * @param string $task This indicates what type of request is going to be initiated.
394
	 * @param int    $priority  This indicates the priority that triggers initiating the request.
395
	 */
396
	public function initiate_request_by_priority( $task = 'generate', $priority = EEM_Message::priority_high ) {
397
		//determine what status is matched with the priority as part of the trigger conditions.
398
		$status = $task == 'generate'
399
			? EEM_Message::status_incomplete
400
			: EEM_Message::instance()->stati_indicating_to_send();
401
		// always make sure we save because either this will get executed immediately on a separate request
402
		// or remains in the queue for the regularly scheduled queue batch.
403
		$this->save();
404
		if ( $this->_queue->count_by_priority_and_status( $priority, $status ) ) {
405
			EE_Messages_Scheduler::initiate_scheduled_non_blocking_request( $task );
406
		}
407
	}
408
409
410
411
	/**
412
	 *  Loops through the EE_Message objects in the _queue and calls the messenger send methods for each message.
413
	 *
414
	 * @param   bool $save                      Used to indicate whether to save the message queue after sending
415
	 *                                          (default will save).
416
	 * @param   mixed $sending_messenger 		(optional) When the sending messenger is different than
417
	 *                                          what is on the EE_Message object in the queue.
418
	 *                                          For instance, showing the browser view of an email message,
419
	 *                                          or giving a pdf generated view of an html document.
420
	 *                                     		This should be an instance of EE_Messenger
421
	 * @param   bool|int $by_priority           When set, this indicates that only messages
422
	 *                                          matching the given priority should be executed.
423
	 *
424
	 * @return int        Number of messages sent.  Note, 0 does not mean that no messages were processed.
425
	 *                    Also, if the messenger is an request type messenger (or a preview),
426
	 * 					  its entirely possible that the messenger will exit before
427
	 */
428
	public function execute( $save = true, $sending_messenger = null, $by_priority = false ) {
429
		$messages_sent = 0;
430
		$this->_did_hook = array();
431
		$this->_queue->rewind();
432
		while ( $this->_queue->valid() ) {
433
			$error_messages = array();
434
			/** @type EE_Message $message */
435
			$message = $this->_queue->current();
436
			//if the message in the queue has a sent status, then skip
437
			if ( in_array( $message->STS_ID(), EEM_Message::instance()->stati_indicating_sent() ) ) {
438
				continue;
439
			}
440
			//if $by_priority is set and does not match then continue;
441
			if ( $by_priority && $by_priority != $message->priority() ) {
442
				continue;
443
			}
444
			//error checking
445
			if ( ! $message->valid_messenger() ) {
446
				$error_messages[] = sprintf(
447
					__( 'The %s messenger is not active at time of sending.', 'event_espresso' ),
448
					$message->messenger()
449
				);
450
			}
451
			if ( ! $message->valid_message_type() ) {
452
				$error_messages[] = sprintf(
453
					__( 'The %s message type is not active at the time of sending.', 'event_espresso' ),
454
					$message->message_type()
455
				);
456
			}
457
			// if there was supposed to be a sending messenger for this message, but it was invalid/inactive,
458
			// then it will instead be an EE_Error object, so let's check for that
459
			if ( $sending_messenger instanceof EE_Error ) {
460
				$error_messages[] = $sending_messenger->getMessage();
461
			}
462
			// if there are no errors, then let's process the message
463
			if ( empty( $error_messages ) && $this->_process_message( $message, $sending_messenger ) ) {
464
				$messages_sent++;
465
			}
466
			$this->_set_error_message( $message, $error_messages );
467
			//add modified time
468
			$message->set_modified( time() );
469
			$this->_queue->next();
470
		}
471
		if ( $save ) {
472
			$this->save();
473
		}
474
		return $messages_sent;
475
	}
476
477
478
479
	/**
480
	 * _process_message
481
	 *
482
	 * @param EE_Message $message
483
	 * @param mixed 	 $sending_messenger (optional)
484
	 * @return bool
485
	 */
486
	protected function _process_message( EE_Message $message, $sending_messenger = null ) {
487
		// these *should* have been validated in the execute() method above
488
		$messenger = $message->messenger_object();
489
		$message_type = $message->message_type_object();
490
		//do actions for sending messenger if it differs from generating messenger and swap values.
491
		if (
492
			$sending_messenger instanceof EE_Messenger
493
			&& $messenger instanceof EE_Messenger
494
			&& $sending_messenger->name != $messenger->name
495
		) {
496
			$messenger->do_secondary_messenger_hooks( $sending_messenger->name );
497
			$messenger = $sending_messenger;
498
		}
499
		// send using messenger, but double check objects
500
		if ( $messenger instanceof EE_Messenger && $message_type instanceof EE_Message_Type ) {
501
			//set hook for message type (but only if not using another messenger to send).
502
			if ( ! isset( $this->_did_hook[ $message_type->name ] ) ) {
503
				$message_type->do_messenger_hooks( $messenger );
504
				$this->_did_hook[ $message_type->name ] = 1;
505
			}
506
			//if preview then use preview method
507
			return $this->_queue->is_preview()
508
				? $this->_do_preview( $message, $messenger, $message_type, $this->_queue->is_test_send() )
509
				: $this->_do_send( $message, $messenger, $message_type );
510
		}
511
		return false;
512
	}
513
514
515
516
	/**
517
	 * The intention of this method is to count how many EE_Message objects
518
	 * are in the queue with a given status.
519
	 *
520
	 * Example usage:
521
	 * After a caller calls the "EE_Message_Queue::execute()" method, the caller can check if there were any failed sends
522
	 * by calling $queue->count_STS_in_queue( EEM_Message_Queue::status_failed ).
523
	 *
524
	 * @param array $status  Stati to check for in queue
525
	 * @return int  Count of EE_Message's matching the given status.
526
	 */
527
	public function count_STS_in_queue( $status ) {
528
		$count = 0;
529
		$status = is_array( $status ) ? $status : array( $status );
530
		foreach( $this->_queue as $message ) {
531
			if ( in_array( $message->STS_ID(), $status ) ) {
532
				$count++;
533
			}
534
		}
535
		return $count;
536
	}
537
538
539
	/**
540
	 * Executes the get_preview method on the provided messenger.
541
	 *
542
*@param EE_Message            $message
543
	 * @param EE_Messenger    $messenger
544
	 * @param EE_message_type $message_type
545
	 * @param $test_send
546
	 * @return bool   true means all went well, false means, not so much.
547
	 */
548
	protected function _do_preview( EE_Message $message, EE_Messenger $messenger, EE_message_type $message_type, $test_send ) {
549
		if ( $preview = $messenger->get_preview( $message, $message_type, $test_send ) ) {
550
			if ( ! $test_send ) {
551
				$message->set_content( $preview );
0 ignored issues
show
Bug introduced by
It seems like $preview defined by $messenger->get_preview(...ssage_type, $test_send) on line 549 can also be of type boolean; however, EE_Message::set_content() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
552
			}
553
			$message->set_STS_ID( EEM_Message::status_sent );
554
			return true;
555
		} else {
556
			$message->set_STS_ID( EEM_Message::status_failed );
557
			return false;
558
		}
559
	}
560
561
562
563
564
	/**
565
	 * Executes the send method on the provided messenger
566
	 *
567
*@param EE_Message            $message
568
	 * @param EE_Messenger    $messenger
569
	 * @param EE_message_type $message_type
570
	 * @return bool true means all went well, false means, not so much.
571
	 */
572
	protected function _do_send( EE_Message $message, EE_Messenger $messenger, EE_message_type $message_type ) {
573
		if ( $messenger->send_message( $message, $message_type ) ) {
574
			$message->set_STS_ID( EEM_Message::status_sent );
575
			return true;
576
		} else {
577
			$message->set_STS_ID( EEM_Message::status_retry );
578
			return false;
579
		}
580
	}
581
582
583
584
585
586
	/**
587
	 * This sets any necessary error messages on the message object and its status to failed.
588
	 * @param EE_Message $message
589
	 * @param array      $error_messages the response from the messenger.
590
	 */
591
	protected function _set_error_message( EE_Message $message, $error_messages ) {
592
		$error_messages = (array) $error_messages;
593
		if ( $message->STS_ID() === EEM_Message::status_failed || $message->STS_ID() === EEM_Message::status_retry ) {
594
			$notices = EE_Error::has_notices();
595
			$error_messages[] = __( 'Messenger and Message Type were valid and active, but the messenger send method failed.', 'event_espresso' );
596
			if ( $notices === 1 ) {
597
				$notices = EE_Error::get_vanilla_notices();
598
				$notices['errors'] = isset( $notices['errors'] ) ? $notices['errors'] : array();
599
				$error_messages[] = implode( "\n", $notices['errors'] );
600
			}
601
		}
602
		if ( count( $error_messages ) > 0 ) {
603
			$msg = __( 'Message was not executed successfully.', 'event_espresso' );
604
			$msg = $msg . "\n" . implode( "\n", $error_messages );
605
			$message->set_error_message( $msg );
606
		}
607
	}
608
609
} //end EE_Messages_Queue class