SubscriptionsService   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 693
Duplicated Lines 0 %

Test Coverage

Coverage 95.26%

Importance

Changes 0
Metric Value
eloc 237
c 0
b 0
f 0
dl 0
loc 693
ccs 261
cts 274
cp 0.9526
rs 2
wmc 88

20 Methods

Rating   Name   Duplication   Size   Complexity  
A unmuteNotifications() 0 2 1
A hasMutedNotifications() 0 2 1
A filterMutedNotifications() 0 39 5
A __construct() 0 5 1
A getSubscriptionsForContainer() 0 28 6
C getNotificationEventSubscriptions() 0 50 12
A addSubscription() 0 28 5
A hasSubscription() 0 16 4
A removeSubscriptions() 0 18 3
A getMethodRelationships() 0 13 5
A removeSubscription() 0 21 5
A getSubscriptionRecords() 0 24 4
A filterTimedMutedSubscribers() 0 23 1
A hasSubscriptions() 0 27 4
B assertValidTypeSubtypeActionForSubscription() 0 12 7
A muteNotifications() 0 10 1
A filterDelayedEmailSubscribers() 0 11 3
C getEntitySubscriptions() 0 51 15
A getSubscribers() 0 23 3
A filterSubscriptions() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like SubscriptionsService 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.

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 SubscriptionsService, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Elgg\Notifications;
4
5
use Elgg\Database;
6
use Elgg\Database\RelationshipsTable;
7
use Elgg\Database\Select;
8
use Elgg\Database\Delete;
9
use Elgg\Database\QueryBuilder;
10
use Elgg\Database\Entities;
11
use Elgg\EventsService;
12
use Elgg\Exceptions\InvalidArgumentException;
13
14
/**
15
 * Subscription service
16
 *
17
 * @internal
18
 * @since 1.9.0
19
 */
20
class SubscriptionsService {
21
22
	/**
23
	 * @var string Elgg has historically stored subscriptions as relationships with the prefix 'notify'
24
	 */
25
	const RELATIONSHIP_PREFIX = 'notify';
26
27
	/**
28
	 * @var string Used when an entity no longer wishes to recieve notifications
29
	 */
30
	const MUTE_NOTIFICATIONS_RELATIONSHIP = 'mute_notifications';
31
32
	/**
33
	 * Constructor
34
	 *
35
	 * @param Database           $db                 Database service
36
	 * @param RelationshipsTable $relationshipsTable Relationship database table
37
	 * @param EventsService      $events             Events service
38
	 */
39 247
	public function __construct(
40
		protected Database $db,
41
		protected RelationshipsTable $relationshipsTable,
42
		protected EventsService $events
43
	) {
44 247
	}
45
46
	/**
47
	 * Get the subscriptions for this notification event
48
	 *
49
	 * The return array is of the form:
50
	 *
51
	 * array(
52
	 *     <user guid> => array('email', 'sms', 'ajax'),
53
	 * );
54
	 *
55
	 * @param NotificationEvent $event                     Notification event
56
	 * @param array             $methods                   Notification methods
57
	 * @param array             $exclude_guids_for_records GUIDs to exclude from fetching subscription records
58
	 *
59
	 * @return array
60
	 */
61 16
	public function getNotificationEventSubscriptions(NotificationEvent $event, array $methods, array $exclude_guids_for_records = []) {
62
63 16
		if (empty($methods)) {
64 1
			return [];
65
		}
66
67 15
		$object = $event->getObject();
68 15
		if (!$object instanceof \ElggData) {
69 1
			return [];
70
		}
71
72
		// get subscribers only for \ElggEntity if it isn't private
73 14
		if (!$object instanceof \ElggEntity || $object->access_id === ACCESS_PRIVATE) {
74
			return [];
75
		}
76
77 14
		$guids = [
78 14
			$object->owner_guid,
79 14
			$object->container_guid,
80 14
		];
81 14
		if ($object instanceof \ElggObject || $object instanceof \ElggGroup) {
82 13
			$guids[] = $object->guid;
83
		}
84
		
85 14
		$guids = array_diff($guids, $exclude_guids_for_records);
86 14
		if (empty($guids)) {
87
			return [];
88
		}
89
		
90 14
		$subscriptions = [];
91 14
		$records = $this->getSubscriptionRecords($guids, $methods, $object->type, $object->subtype, $event->getAction(), $event->getActorGUID());
92 14
		foreach ($records as $record) {
93 12
			if (empty($record->guid)) {
94
				// happens when no records are found
95
				continue;
96
			}
97
			
98 12
			if (!isset($subscriptions[$record->guid])) {
99 12
				$subscriptions[$record->guid] = [];
100
			}
101
			
102 12
			$deliveryMethods = explode(',', $record->methods);
103 12
			foreach ($deliveryMethods as $relationship) {
104 12
				$relationship_array = explode(':', $relationship);
105
				
106 12
				$subscriptions[$record->guid][] = end($relationship_array);
107
			}
108
		}
109
110 14
		return $subscriptions;
111
	}
112
113
	/**
114
	 * Get the subscriptions for the content created inside this container.
115
	 *
116
	 * The return array is of the form:
117
	 *
118
	 * array(
119
	 *     <user guid> => array('email', 'sms', 'ajax'),
120
	 * );
121
	 *
122
	 * @param int         $container_guid GUID of the entity acting as a container
123
	 * @param array       $methods        Notification methods
124
	 * @param null|string $type           (optional) entity type
125
	 * @param null|string $subtype        (optional) entity subtype
126
	 * @param null|string $action         (optional) notification action (eg. 'create')
127
	 * @param int         $actor_guid     (optional) Notification event actor to exclude from the database subscriptions
128
	 *
129
	 * @return array User GUIDs (keys) and their subscription types (values).
130
	 */
131 3
	public function getSubscriptionsForContainer(int $container_guid, array $methods, ?string $type = null, ?string $subtype = null, ?string $action = null, int $actor_guid = 0) {
132
133 3
		if (empty($methods)) {
134 1
			return [];
135
		}
136
137 2
		$subscriptions = [];
138
139 2
		$records = $this->getSubscriptionRecords([$container_guid], $methods, $type, $subtype, $action, $actor_guid);
140 2
		foreach ($records as $record) {
141 2
			if (empty($record->guid)) {
142
				// happens when no records are found
143
				continue;
144
			}
145
			
146 2
			if (!isset($subscriptions[$record->guid])) {
147 2
				$subscriptions[$record->guid] = [];
148
			}
149
			
150 2
			$deliveryMethods = explode(',', $record->methods);
151 2
			foreach ($deliveryMethods as $relationship) {
152 2
				$relationship_array = explode(':', $relationship);
153
				
154 2
				$subscriptions[$record->guid][] = end($relationship_array);
155
			}
156
		}
157
158 2
		return $subscriptions;
159
	}
160
161
	/**
162
	 * Subscribe a user to notifications about a target entity
163
	 *
164
	 * This method will return false if the subscription already exists.
165
	 *
166
	 * @param int         $user_guid   The GUID of the user to subscribe to notifications
167
	 * @param string      $method      The delivery method of the notifications
168
	 * @param int         $target_guid The entity to receive notifications about
169
	 * @param null|string $type        (optional) entity type
170
	 * @param null|string $subtype     (optional) entity subtype
171
	 * @param null|string $action      (optional) notification action (eg. 'create')
172
	 *
173
	 * @return bool
174
	 * @throws InvalidArgumentException
175
	 */
176 378
	public function addSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null) {
177 378
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
178
		
179 372
		$rel = [
180 372
			self::RELATIONSHIP_PREFIX,
181 372
		];
182
		
183 372
		if (!_elgg_services()->notifications->isRegisteredMethod($method)) {
184 4
			return false;
185
		}
186
		
187
		// remove the muted notification relationship
188 371
		$this->unmuteNotifications($user_guid, $target_guid);
189
		
190 371
		if (!empty($type) && !empty($subtype) && !empty($action)) {
191 20
			$rel[] = $type;
192 20
			$rel[] = $subtype;
193 20
			$rel[] = $action;
194
		}
195
		
196 371
		$rel[] = $method;
197
		
198 371
		$relationship = new \ElggRelationship();
199 371
		$relationship->guid_one = $user_guid;
200 371
		$relationship->relationship = implode(':', $rel);
201 371
		$relationship->guid_two = $target_guid;
202
		
203 371
		return $relationship->save();
204
	}
205
	
206
	/**
207
	 * Check if a subscription exists
208
	 *
209
	 * @param int         $user_guid   The GUID of the user to check subscriptions for
210
	 * @param string      $method      The delivery method of the notifications
211
	 * @param int         $target_guid The entity to receive notifications about
212
	 * @param null|string $type        (optional) entity type
213
	 * @param null|string $subtype     (optional) entity subtype
214
	 * @param null|string $action      (optional) notification action (eg. 'create')
215
	 *
216
	 * @return bool
217
	 * @throws InvalidArgumentException
218
	 * @since 4.0
219
	 */
220 38
	public function hasSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null): bool {
221 38
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
222
		
223 32
		$rel = [
224 32
			self::RELATIONSHIP_PREFIX,
225 32
		];
226
		
227 32
		if (!empty($type) && !empty($subtype) && !empty($action)) {
228 13
			$rel[] = $type;
229 13
			$rel[] = $subtype;
230 13
			$rel[] = $action;
231
		}
232
		
233 32
		$rel[] = $method;
234
		
235 32
		return $this->relationshipsTable->check($user_guid, implode(':', $rel), $target_guid) instanceof \ElggRelationship;
236
	}
237
	
238
	/**
239
	 * Check if any subscription exists
240
	 *
241
	 * @param int   $user_guid   The GUID of the user to check subscriptions for
242
	 * @param int   $target_guid The entity to receive notifications about
243
	 * @param array $methods     The delivery method of the notifications
244
	 *
245
	 * @return bool
246
	 * @since 4.0
247
	 */
248 9
	public function hasSubscriptions(int $user_guid, int $target_guid, array $methods = []): bool {
249 9
		if (empty($methods)) {
250
			// all currently registered methods
251
			$methods = _elgg_services()->notifications->getMethods();
252
		}
253
		
254 9
		if (empty($methods)) {
255
			// no methods available
256
			return false;
257
		}
258
		
259 9
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
260 9
		$select->select('count(*) as total')
261 9
			->where($select->compare('guid_one', '=', $user_guid, ELGG_VALUE_GUID))
262 9
			->andWhere($select->compare('guid_two', '=', $target_guid, ELGG_VALUE_GUID));
263
		
264 9
		$ors = [];
265 9
		foreach ($methods as $method) {
266 9
			$ors[] = $select->compare('relationship', '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
267 9
			$ors[] = $select->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
268
		}
269
		
270 9
		$select->andWhere($select->merge($ors, 'OR'));
271
		
272 9
		$result = $this->db->getDataRow($select);
273
		
274 9
		return (bool) $result->total;
275
	}
276
277
	/**
278
	 * Unsubscribe a user to notifications about a target entity
279
	 *
280
	 * @param int         $user_guid   The GUID of the user to unsubscribe to notifications
281
	 * @param string      $method      The delivery method of the notifications to stop
282
	 * @param int         $target_guid The entity to stop receiving notifications about
283
	 * @param null|string $type        (optional) entity type
284
	 * @param null|string $subtype     (optional) entity subtype
285
	 * @param null|string $action      (optional) notification action (eg. 'create')
286
	 *
287
	 * @return bool
288
	 * @throws InvalidArgumentException
289
	 */
290 21
	public function removeSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null) {
291 21
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
292
		
293 15
		$rel = [
294 15
			self::RELATIONSHIP_PREFIX,
295 15
		];
296
		
297 15
		if (!empty($type) && !empty($subtype) && !empty($action)) {
298 5
			$rel[] = $type;
299 5
			$rel[] = $subtype;
300 5
			$rel[] = $action;
301
		}
302
		
303 15
		$rel[] = $method;
304
		
305 15
		if (!$this->relationshipsTable->check($user_guid, implode(':', $rel), $target_guid)) {
306
			// subscription doesn't exist
307 14
			return true;
308
		}
309
		
310 14
		return $this->relationshipsTable->remove($user_guid, implode(':', $rel), $target_guid);
311
	}
312
	
313
	/**
314
	 * Unsubscribe a user from all notifications about the target entity
315
	 *
316
	 * @param int   $user_guid   The GUID of the user to unsubscribe to notifications
317
	 * @param int   $target_guid The entity to stop receiving notifications about
318
	 * @param array $methods     (optional) The delivery method of the notifications to stop
319
	 *
320
	 * @return bool
321
	 * @since 4.0
322
	 */
323 78
	public function removeSubscriptions(int $user_guid, int $target_guid, array $methods = []): bool {
324 78
		$delete = Delete::fromTable(RelationshipsTable::TABLE_NAME);
325 78
		$delete->where($delete->compare('guid_one', '=', $user_guid, ELGG_VALUE_GUID))
326 78
			->andWhere($delete->compare('guid_two', '=', $target_guid, ELGG_VALUE_GUID));
327
		
328 78
		if (empty($methods)) {
329 77
			$delete->andWhere($delete->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING));
330
		} else {
331 3
			$ors = [];
332 3
			foreach ($methods as $method) {
333 3
				$ors[] = $delete->compare('relationship', '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
334 3
				$ors[] = $delete->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
335
			}
336
			
337 3
			$delete->andWhere($delete->merge($ors, 'OR'));
338
		}
339
		
340 78
		return (bool) $this->db->deleteData($delete);
341
	}
342
	
343
	/**
344
	 * Get all subscribers of the target guid
345
	 *
346
	 * @param int   $target_guid the entity of the subscriptions
347
	 * @param array $methods     (optional) The delivery method of the notifications
348
	 *
349
	 * @return \ElggEntity[]
350
	 */
351 12
	public function getSubscribers(int $target_guid, array $methods = []): array {
352 12
		return elgg_get_entities([
353 12
			'limit' => false,
354 12
			'wheres' => [
355 12
				function(QueryBuilder $qb, $main_alias) use ($target_guid) {
356 12
					$rel = $qb->joinRelationshipTable($main_alias, 'guid', null, true);
357
					
358 12
					return $qb->compare("{$rel}.guid_two", '=', $target_guid, ELGG_VALUE_GUID);
359 12
				},
360 12
				function(QueryBuilder $qb, $main_alias) use ($methods) {
361 12
					$rel = $qb->joinRelationshipTable($main_alias, 'guid', null, true);
362
					
363 12
					if (empty($methods)) {
364 9
						return $qb->compare("{$rel}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING);
365
					}
366
					
367 9
					$ors = [];
368 9
					foreach ($methods as $method) {
369 9
						$ors[] = $qb->compare("{$rel}.relationship", '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
370 9
						$ors[] = $qb->compare("{$rel}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
371
					}
372
					
373 9
					return $qb->merge($ors, 'OR');
374 12
				},
375 12
			],
376 12
		]);
377
	}
378
	
379
	/**
380
	 * Get the current subscriptions for the given entity
381
	 *
382
	 * @param int         $target_guid The GUID of the entity to get subscriptions for
383
	 * @param int         $user_guid   The GUID of the user to check subscriptions for
384
	 * @param array       $methods     The delivery method of the notifications
385
	 * @param null|string $type        (optional) entity type
386
	 * @param null|string $subtype     (optional) entity subtype
387
	 * @param null|string $action      (optional) notification action (eg. 'create')
388
	 *
389
	 * @return \ElggRelationship[]
390
	 * @throws InvalidArgumentException
391
	 */
392 15
	public function getEntitySubscriptions(int $target_guid = 0, int $user_guid = 0, array $methods = [], ?string $type = null, ?string $subtype = null, ?string $action = null): array {
393 15
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
394
		
395 9
		if (empty($target_guid) && empty($user_guid)) {
396
			return [];
397
		}
398
		
399 9
		if (empty($target_guid)) {
400
			$target_guid = ELGG_ENTITIES_ANY_VALUE;
401
		}
402
		
403 9
		return elgg_get_relationships([
404 9
			'limit' => false,
405 9
			'wheres' => [
406 9
				function(QueryBuilder $qb, $main_alias) use ($target_guid) {
407 9
					if (empty($target_guid)) {
408
						return;
409
					}
410
					
411 9
					return $qb->compare("{$main_alias}.guid_two", '=', $target_guid, ELGG_VALUE_GUID);
412 9
				},
413 9
				function(QueryBuilder $qb, $main_alias) use ($user_guid) {
414 9
					if (empty($user_guid)) {
415 9
						return;
416
					}
417
					
418
					return $qb->compare("{$main_alias}.guid_one", '=', $user_guid, ELGG_VALUE_GUID);
419 9
				},
420 9
				function(QueryBuilder $qb, $main_alias) use ($methods, $type, $subtype, $action) {
421 9
					if (empty($methods) && (empty($type) || empty($subtype) || empty($action))) {
422 9
						return $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING);
423
					}
424
					
425 6
					if (!empty($methods)) {
426 3
						if (empty($type) || empty($subtype) || empty($action)) {
427
							// only methods
428 3
							$ors = [];
429 3
							foreach ($methods as $method) {
430 3
								$ors[] = $qb->compare("{$main_alias}.relationship", '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
431 3
								$ors[] = $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
432
							}
433
							
434 3
							return $qb->merge($ors, 'OR');
435
						} else {
436
							// with type limitation
437
							return $qb->compare("{$main_alias}.relationship", 'in', $this->getMethodRelationships($methods, $type, $subtype, $action), ELGG_VALUE_STRING);
438
						}
439
					}
440
					
441
					// only type limitation
442 3
					return $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ":{$type}:{$subtype}:{$action}:%", ELGG_VALUE_STRING);
443 9
				},
444 9
			],
445 9
		]);
446
	}
447
	
448
	/**
449
	 * Mute notifications about events affecting the target
450
	 *
451
	 * @param int $user_guid   The GUID of the user to mute notifications for
452
	 * @param int $target_guid The GUID of the entity to for which to mute notifications
453
	 *
454
	 * @return bool
455
	 */
456 15
	public function muteNotifications(int $user_guid, int $target_guid): bool {
457
		// remove all current subscriptions
458 15
		$this->removeSubscriptions($user_guid, $target_guid);
459
		
460 15
		$rel = new \ElggRelationship();
461 15
		$rel->guid_one = $user_guid;
462 15
		$rel->relationship = self::MUTE_NOTIFICATIONS_RELATIONSHIP;
463 15
		$rel->guid_two = $target_guid;
464
		
465 15
		return $rel->save();
466
	}
467
	
468
	/**
469
	 * No longer nute notifications about events affecting the target
470
	 *
471
	 * @param int $user_guid   The GUID of the user to unmute notifcations for
472
	 * @param int $target_guid The GUID of the entity to for which to unmute notifications
473
	 *
474
	 * @return bool
475
	 */
476 372
	public function unmuteNotifications(int $user_guid, int $target_guid): bool {
477 372
		return $this->relationshipsTable->remove($user_guid, self::MUTE_NOTIFICATIONS_RELATIONSHIP, $target_guid);
478
	}
479
	
480
	/**
481
	 * Check if the user has notifications muted about events affecting the target
482
	 *
483
	 * @param int $user_guid   The GUID of the user to check muted notifcations for
484
	 * @param int $target_guid The GUID of the entity to for which to check muted notifications
485
	 *
486
	 * @return bool
487
	 */
488 28
	public function hasMutedNotifications(int $user_guid, int $target_guid): bool {
489 28
		return $this->relationshipsTable->check($user_guid, self::MUTE_NOTIFICATIONS_RELATIONSHIP, $target_guid) instanceof \ElggRelationship;
490
	}
491
	
492
	/**
493
	 * Apply filtering to subscriptions, like muted notifications etc
494
	 *
495
	 * @param array             $subscriptions List of subscribers to filter
496
	 * @param NotificationEvent $event         Notification event from which to get information
497
	 * @param bool              $filter_muted  Should the muting rules be applied to the subscriptions (default: true)
498
	 *
499
	 * @return array
500
	 */
501 46
	public function filterSubscriptions(array $subscriptions, NotificationEvent $event, bool $filter_muted = true): array {
502
		// sanitize
503
		// make methods unique and remove empties
504 46
		$subscriptions = array_map(function($user_methods) {
505 46
			return array_values(array_filter(array_unique($user_methods)));
506 46
		}, $subscriptions);
507 46
		$subscriptions = $this->filterDelayedEmailSubscribers($subscriptions);
508
		
509
		// apply filters
510 46
		if ($filter_muted) {
511 41
			$subscriptions = $this->filterMutedNotifications($subscriptions, $event);
512 41
			$subscriptions = $this->filterTimedMutedSubscribers($subscriptions);
513
		}
514
		
515 46
		return $subscriptions;
516
	}
517
	
518
	/**
519
	 * Filter subscriptions based on muted notification settings related to the notification event
520
	 *
521
	 * This filters out muted notifications based on:
522
	 * - Event actor
523
	 * - Event entity
524
	 * - Event entity owner
525
	 * - Event entity container
526
	 *
527
	 * @param array             $subscriptions List of subscribers to filter
528
	 * @param NotificationEvent $event         Notification event from which to get information
529
	 *
530
	 * @return array
531
	 */
532 45
	protected function filterMutedNotifications(array $subscriptions, NotificationEvent $event): array {
533 45
		$guids_to_check = [];
534
		
535
		// Event actor
536 45
		$guids_to_check[] = $event->getActorGUID();
537
		
538
		// Event object
539 45
		$entity = false;
540 45
		$object = $event->getObject();
541 45
		if ($object instanceof \ElggEntity) {
542 27
			$entity = $object;
543 18
		} elseif ($object instanceof \ElggAnnotation) {
544 2
			$entity = $object->getEntity();
545
		}
546
		
547 45
		if ($entity instanceof \ElggEntity) {
548 29
			$guids_to_check[] = $entity->guid;
549 29
			$guids_to_check[] = $entity->owner_guid;
550 29
			$guids_to_check[] = $entity->container_guid;
551
		}
552
		
553
		// are there GUIDs to check
554 45
		$guids_to_check = array_filter($guids_to_check);
555 45
		if (empty($guids_to_check)) {
556
			return $subscriptions;
557
		}
558
		
559
		// get muted relations
560 45
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
561 45
		$select->select('guid_one')
562 45
			->where($select->compare('relationship', '=', self::MUTE_NOTIFICATIONS_RELATIONSHIP, ELGG_VALUE_STRING))
563 45
			->andWhere($select->compare('guid_two', 'in', $guids_to_check, ELGG_VALUE_GUID));
564
		
565 45
		$muted = $this->db->getData($select, function($row) {
566 9
			return (int) $row->guid_one;
567 45
		});
568
		
569
		// filter subscriptions
570 45
		return array_diff_key($subscriptions, array_flip($muted));
571
	}
572
	
573
	/**
574
	 * When a user has both 'email' and 'delayed_email' subscription remove the delayed email as it would be a duplicate
575
	 *
576
	 * @param array $subscriptions List of subscribers to filter
577
	 *
578
	 * @return array
579
	 */
580 47
	protected function filterDelayedEmailSubscribers(array $subscriptions): array {
581 47
		return array_map(function ($user_methods) {
582 47
			if (!in_array('delayed_email', $user_methods) || !in_array('email', $user_methods)) {
583 47
				return $user_methods;
584
			}
585
			
586 1
			$pos = array_search('delayed_email', $user_methods);
587 1
			unset($user_methods[$pos]);
588
			
589 1
			return array_values($user_methods);
590 47
		}, $subscriptions);
591
	}
592
	
593
	/**
594
	 * Filter users who have set a period in which not to receive notifications
595
	 *
596
	 * @param array $subscriptions List of subscribers to filter
597
	 *
598
	 * @return array
599
	 */
600 41
	protected function filterTimedMutedSubscribers(array $subscriptions): array {
601 41
		$muted = Entities::find([
602 41
			'type' => 'user',
603 41
			'guids' => array_keys($subscriptions),
604 41
			'limit' => false,
605 41
			'callback' => function ($row) {
606 1
				return (int) $row->guid;
607 41
			},
608 41
			'metadata_name_value_pairs' => [
609 41
				[
610 41
					'name' => 'timed_muting_start',
611 41
					'value' => time(),
612 41
					'operand' => '<=',
613 41
				],
614 41
				[
615 41
					'name' => 'timed_muting_end',
616 41
					'value' => time(),
617 41
					'operand' => '>=',
618 41
				],
619 41
			],
620 41
		]);
621
		
622 41
		return array_diff_key($subscriptions, array_flip($muted));
623
	}
624
625
	/**
626
	 * Get subscription records from the database
627
	 *
628
	 * Records are an object with two vars: guid and methods with the latter
629
	 * being a comma-separated list of subscription relationship names.
630
	 *
631
	 * @param int[]       $container_guid The GUID of the subscription target
632
	 * @param array       $methods        Notification methods
633
	 * @param null|string $type           (optional) entity type
634
	 * @param null|string $subtype        (optional) entity subtype
635
	 * @param null|string $action         (optional) notification action (eg. 'create')
636
	 * @param int         $actor_guid     (optional) Notification event actor to exclude from the database subscriptions
637
	 *
638
	 * @return array
639
	 */
640 16
	protected function getSubscriptionRecords(array $container_guid, array $methods, ?string $type = null, ?string $subtype = null, ?string $action = null, int $actor_guid = 0): array {
641
		// create IN clause
642 16
		$rels = $this->getMethodRelationships($methods, $type, $subtype, $action);
643 16
		if (!$rels) {
644
			return [];
645
		}
646
		
647 16
		$container_guid = array_unique(array_filter($container_guid));
648 16
		if (empty($container_guid)) {
649 1
			return [];
650
		}
651
652 15
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
653 15
		$select->select('guid_one AS guid')
654 15
			->addSelect("GROUP_CONCAT(relationship SEPARATOR ',') AS methods")
655 15
			->where($select->compare('guid_two', 'in', $container_guid, ELGG_VALUE_GUID))
656 15
			->andWhere($select->compare('relationship', 'in', $rels, ELGG_VALUE_STRING))
657 15
			->groupBy('guid_one');
658
		
659 15
		if (!empty($actor_guid)) {
660 11
			$select->andWhere($select->compare('guid_one', '!=', $actor_guid, ELGG_VALUE_GUID));
661
		}
662
		
663 15
		return $this->db->getData($select);
664
	}
665
666
	/**
667
	 * Get the relationship names for notifications
668
	 *
669
	 * @param array       $methods Notification methods
670
	 * @param null|string $type    (optional) entity type
671
	 * @param null|string $subtype (optional) entity subtype
672
	 * @param null|string $action  (optional) notification action (eg. 'create')
673
	 *
674
	 * @return array
675
	 */
676 16
	protected function getMethodRelationships(array $methods, ?string $type = null, ?string $subtype = null, ?string $action = null): array {
677 16
		$prefix = self::RELATIONSHIP_PREFIX;
678
		
679 16
		$names = [];
680 16
		foreach ($methods as $method) {
681 16
			$names[] = "{$prefix}:{$method}";
682
			
683 16
			if (!empty($type) && !empty($subtype) && !empty($action)) {
684 12
				$names[] = "{$prefix}:{$type}:{$subtype}:{$action}:{$method}";
685
			}
686
		}
687
		
688 16
		return $names;
689
	}
690
	
691
	/**
692
	 * Validate subscription input for type, subtype and action
693
	 *
694
	 * @param string $type    entity type
695
	 * @param string $subtype entity subtype
696
	 * @param string $action  notification action (eg. 'create')
697
	 *
698
	 * @return void
699
	 * @throws InvalidArgumentException
700
	 */
701 396
	protected function assertValidTypeSubtypeActionForSubscription($type, $subtype, $action): void {
702 396
		if (empty($type) && empty($subtype) && empty($action)) {
703
			// all empty, this is valid
704 370
			return;
705
		}
706
		
707 44
		if (!empty($type) && !empty($subtype) && !empty($action)) {
708
			// all set, also valid
709 20
			return;
710
		}
711
		
712 24
		throw new InvalidArgumentException('$type, $subtype and $action need to all be empty or all have a value');
713
	}
714
}
715