SubscriptionsService   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 693
Duplicated Lines 0 %

Test Coverage

Coverage 70.44%

Importance

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

20 Methods

Rating   Name   Duplication   Size   Complexity  
A unmuteNotifications() 0 2 1
A hasMutedNotifications() 0 2 1
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 getMethodRelationships() 0 13 5
A removeSubscription() 0 21 5
A getSubscriptionRecords() 0 24 4
A filterTimedMutedSubscribers() 0 23 1
B assertValidTypeSubtypeActionForSubscription() 0 12 7
A muteNotifications() 0 10 1
A filterDelayedEmailSubscribers() 0 11 3
A filterSubscriptions() 0 15 2
A removeSubscriptions() 0 18 3
A hasSubscriptions() 0 27 4
C getEntitySubscriptions() 0 51 15
A getSubscribers() 0 23 3
A filterMutedNotifications() 0 39 5

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 203
	public function __construct(
40
		protected Database $db,
41
		protected RelationshipsTable $relationshipsTable,
42
		protected EventsService $events
43
	) {
44 203
	}
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 20
	public function getNotificationEventSubscriptions(NotificationEvent $event, array $methods, array $exclude_guids_for_records = []) {
62
63 20
		if (empty($methods)) {
64 2
			return [];
65
		}
66
67 18
		$object = $event->getObject();
68 18
		if (!$object instanceof \ElggData) {
69 2
			return [];
70
		}
71
72
		// get subscribers only for \ElggEntity if it isn't private
73 16
		if (!$object instanceof \ElggEntity || $object->access_id === ACCESS_PRIVATE) {
74
			return [];
75
		}
76
77 16
		$guids = [
78 16
			$object->owner_guid,
79 16
			$object->container_guid,
80 16
		];
81 16
		if ($object instanceof \ElggObject || $object instanceof \ElggGroup) {
82 15
			$guids[] = $object->guid;
83
		}
84
		
85 16
		$guids = array_diff($guids, $exclude_guids_for_records);
86 16
		if (empty($guids)) {
87
			return [];
88
		}
89
		
90 16
		$subscriptions = [];
91 16
		$records = $this->getSubscriptionRecords($guids, $methods, $object->type, $object->subtype, $event->getAction(), $event->getActorGUID());
92 16
		foreach ($records as $record) {
93 13
			if (empty($record->guid)) {
94
				// happens when no records are found
95
				continue;
96
			}
97
			
98 13
			if (!isset($subscriptions[$record->guid])) {
99 13
				$subscriptions[$record->guid] = [];
100
			}
101
			
102 13
			$deliveryMethods = explode(',', $record->methods);
103 13
			foreach ($deliveryMethods as $relationship) {
104 13
				$relationship_array = explode(':', $relationship);
105
				
106 13
				$subscriptions[$record->guid][] = end($relationship_array);
107
			}
108
		}
109
110 16
		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 5
	public function getSubscriptionsForContainer(int $container_guid, array $methods, ?string $type = null, ?string $subtype = null, ?string $action = null, int $actor_guid = 0) {
132
133 5
		if (empty($methods)) {
134 2
			return [];
135
		}
136
137 3
		$subscriptions = [];
138
139 3
		$records = $this->getSubscriptionRecords([$container_guid], $methods, $type, $subtype, $action, $actor_guid);
140 3
		foreach ($records as $record) {
141 3
			if (empty($record->guid)) {
142
				// happens when no records are found
143
				continue;
144
			}
145
			
146 3
			if (!isset($subscriptions[$record->guid])) {
147 3
				$subscriptions[$record->guid] = [];
148
			}
149
			
150 3
			$deliveryMethods = explode(',', $record->methods);
151 3
			foreach ($deliveryMethods as $relationship) {
152 3
				$relationship_array = explode(':', $relationship);
153
				
154 3
				$subscriptions[$record->guid][] = end($relationship_array);
155
			}
156
		}
157
158 3
		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 251
	public function addSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null) {
177 251
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
178
		
179 239
		$rel = [
180 239
			self::RELATIONSHIP_PREFIX,
181 239
		];
182
		
183 239
		if (!_elgg_services()->notifications->isRegisteredMethod($method)) {
184 1
			return false;
185
		}
186
		
187
		// remove the muted notification relationship
188 238
		$this->unmuteNotifications($user_guid, $target_guid);
189
		
190 238
		if (!empty($type) && !empty($subtype) && !empty($action)) {
191 5
			$rel[] = $type;
192 5
			$rel[] = $subtype;
193 5
			$rel[] = $action;
194
		}
195
		
196 238
		$rel[] = $method;
197
		
198 238
		$relationship = new \ElggRelationship();
199 238
		$relationship->guid_one = $user_guid;
200 238
		$relationship->relationship = implode(':', $rel);
201 238
		$relationship->guid_two = $target_guid;
202
		
203 238
		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 26
	public function hasSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null): bool {
221 26
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
222
		
223 14
		$rel = [
224 14
			self::RELATIONSHIP_PREFIX,
225 14
		];
226
		
227 14
		if (!empty($type) && !empty($subtype) && !empty($action)) {
228 4
			$rel[] = $type;
229 4
			$rel[] = $subtype;
230 4
			$rel[] = $action;
231
		}
232
		
233 14
		$rel[] = $method;
234
		
235 14
		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
	public function hasSubscriptions(int $user_guid, int $target_guid, array $methods = []): bool {
249
		if (empty($methods)) {
250
			// all currently registered methods
251
			$methods = _elgg_services()->notifications->getMethods();
252
		}
253
		
254
		if (empty($methods)) {
255
			// no methods available
256
			return false;
257
		}
258
		
259
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
260
		$select->select('count(*) as total')
261
			->where($select->compare('guid_one', '=', $user_guid, ELGG_VALUE_GUID))
262
			->andWhere($select->compare('guid_two', '=', $target_guid, ELGG_VALUE_GUID));
263
		
264
		$ors = [];
265
		foreach ($methods as $method) {
266
			$ors[] = $select->compare('relationship', '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
267
			$ors[] = $select->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
268
		}
269
		
270
		$select->andWhere($select->merge($ors, 'OR'));
271
		
272
		$result = $this->db->getDataRow($select);
273
		
274
		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 18
	public function removeSubscription(int $user_guid, string $method, int $target_guid, ?string $type = null, ?string $subtype = null, ?string $action = null) {
291 18
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
292
		
293 6
		$rel = [
294 6
			self::RELATIONSHIP_PREFIX,
295 6
		];
296
		
297 6
		if (!empty($type) && !empty($subtype) && !empty($action)) {
298 2
			$rel[] = $type;
299 2
			$rel[] = $subtype;
300 2
			$rel[] = $action;
301
		}
302
		
303 6
		$rel[] = $method;
304
		
305 6
		if (!$this->relationshipsTable->check($user_guid, implode(':', $rel), $target_guid)) {
306
			// subscription doesn't exist
307 5
			return true;
308
		}
309
		
310 5
		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 19
	public function removeSubscriptions(int $user_guid, int $target_guid, array $methods = []): bool {
324 19
		$delete = Delete::fromTable(RelationshipsTable::TABLE_NAME);
325 19
		$delete->where($delete->compare('guid_one', '=', $user_guid, ELGG_VALUE_GUID))
326 19
			->andWhere($delete->compare('guid_two', '=', $target_guid, ELGG_VALUE_GUID));
327
		
328 19
		if (empty($methods)) {
329 19
			$delete->andWhere($delete->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING));
330
		} else {
331
			$ors = [];
332
			foreach ($methods as $method) {
333
				$ors[] = $delete->compare('relationship', '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
334
				$ors[] = $delete->compare('relationship', 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
335
			}
336
			
337
			$delete->andWhere($delete->merge($ors, 'OR'));
338
		}
339
		
340 19
		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
	public function getSubscribers(int $target_guid, array $methods = []): array {
352
		return elgg_get_entities([
353
			'limit' => false,
354
			'wheres' => [
355
				function(QueryBuilder $qb, $main_alias) use ($target_guid) {
356
					$rel = $qb->joinRelationshipTable($main_alias, 'guid', null, true);
357
					
358
					return $qb->compare("{$rel}.guid_two", '=', $target_guid, ELGG_VALUE_GUID);
359
				},
360
				function(QueryBuilder $qb, $main_alias) use ($methods) {
361
					$rel = $qb->joinRelationshipTable($main_alias, 'guid', null, true);
362
					
363
					if (empty($methods)) {
364
						return $qb->compare("{$rel}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING);
365
					}
366
					
367
					$ors = [];
368
					foreach ($methods as $method) {
369
						$ors[] = $qb->compare("{$rel}.relationship", '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
370
						$ors[] = $qb->compare("{$rel}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
371
					}
372
					
373
					return $qb->merge($ors, 'OR');
374
				},
375
			],
376
		]);
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 12
	public function getEntitySubscriptions(int $target_guid = 0, int $user_guid = 0, array $methods = [], ?string $type = null, ?string $subtype = null, ?string $action = null): array {
393 12
		$this->assertValidTypeSubtypeActionForSubscription($type, $subtype, $action);
394
		
395
		if (empty($target_guid) && empty($user_guid)) {
396
			return [];
397
		}
398
		
399
		if (empty($target_guid)) {
400
			$target_guid = ELGG_ENTITIES_ANY_VALUE;
401
		}
402
		
403
		return elgg_get_relationships([
404
			'limit' => false,
405
			'wheres' => [
406
				function(QueryBuilder $qb, $main_alias) use ($target_guid) {
407
					if (empty($target_guid)) {
408
						return;
409
					}
410
					
411
					return $qb->compare("{$main_alias}.guid_two", '=', $target_guid, ELGG_VALUE_GUID);
412
				},
413
				function(QueryBuilder $qb, $main_alias) use ($user_guid) {
414
					if (empty($user_guid)) {
415
						return;
416
					}
417
					
418
					return $qb->compare("{$main_alias}.guid_one", '=', $user_guid, ELGG_VALUE_GUID);
419
				},
420
				function(QueryBuilder $qb, $main_alias) use ($methods, $type, $subtype, $action) {
421
					if (empty($methods) && (empty($type) || empty($subtype) || empty($action))) {
422
						return $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%', ELGG_VALUE_STRING);
423
					}
424
					
425
					if (!empty($methods)) {
426
						if (empty($type) || empty($subtype) || empty($action)) {
427
							// only methods
428
							$ors = [];
429
							foreach ($methods as $method) {
430
								$ors[] = $qb->compare("{$main_alias}.relationship", '=', self::RELATIONSHIP_PREFIX . ':' . $method, ELGG_VALUE_STRING);
431
								$ors[] = $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ':%:' . $method, ELGG_VALUE_STRING);
432
							}
433
							
434
							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
					return $qb->compare("{$main_alias}.relationship", 'like', self::RELATIONSHIP_PREFIX . ":{$type}:{$subtype}:{$action}:%", ELGG_VALUE_STRING);
443
				},
444
			],
445
		]);
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 12
	public function muteNotifications(int $user_guid, int $target_guid): bool {
457
		// remove all current subscriptions
458 12
		$this->removeSubscriptions($user_guid, $target_guid);
459
		
460 12
		$rel = new \ElggRelationship();
461 12
		$rel->guid_one = $user_guid;
462 12
		$rel->relationship = self::MUTE_NOTIFICATIONS_RELATIONSHIP;
463 12
		$rel->guid_two = $target_guid;
464
		
465 12
		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 239
	public function unmuteNotifications(int $user_guid, int $target_guid): bool {
477 239
		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 20
	public function hasMutedNotifications(int $user_guid, int $target_guid): bool {
489 20
		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 49
	public function filterSubscriptions(array $subscriptions, NotificationEvent $event, bool $filter_muted = true): array {
502
		// sanitize
503
		// make methods unique and remove empties
504 49
		$subscriptions = array_map(function($user_methods) {
505 49
			return array_values(array_filter(array_unique($user_methods)));
506 49
		}, $subscriptions);
507 49
		$subscriptions = $this->filterDelayedEmailSubscribers($subscriptions);
508
		
509
		// apply filters
510 49
		if ($filter_muted) {
511 10
			$subscriptions = $this->filterMutedNotifications($subscriptions, $event);
512 10
			$subscriptions = $this->filterTimedMutedSubscribers($subscriptions);
513
		}
514
		
515 49
		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 14
	protected function filterMutedNotifications(array $subscriptions, NotificationEvent $event): array {
533 14
		$guids_to_check = [];
534
		
535
		// Event actor
536 14
		$guids_to_check[] = $event->getActorGUID();
537
		
538
		// Event object
539 14
		$entity = false;
540 14
		$object = $event->getObject();
541 14
		if ($object instanceof \ElggEntity) {
542 14
			$entity = $object;
543
		} elseif ($object instanceof \ElggAnnotation) {
544
			$entity = $object->getEntity();
545
		}
546
		
547 14
		if ($entity instanceof \ElggEntity) {
548 14
			$guids_to_check[] = $entity->guid;
549 14
			$guids_to_check[] = $entity->owner_guid;
550 14
			$guids_to_check[] = $entity->container_guid;
551
		}
552
		
553
		// are there GUIDs to check
554 14
		$guids_to_check = array_filter($guids_to_check);
555 14
		if (empty($guids_to_check)) {
556
			return $subscriptions;
557
		}
558
		
559
		// get muted relations
560 14
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
561 14
		$select->select('guid_one')
562 14
			->where($select->compare('relationship', '=', self::MUTE_NOTIFICATIONS_RELATIONSHIP, ELGG_VALUE_STRING))
563 14
			->andWhere($select->compare('guid_two', 'in', $guids_to_check, ELGG_VALUE_GUID));
564
		
565 14
		$muted = $this->db->getData($select, function($row) {
566 9
			return (int) $row->guid_one;
567 14
		});
568
		
569
		// filter subscriptions
570 14
		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 50
	protected function filterDelayedEmailSubscribers(array $subscriptions): array {
581 50
		return array_map(function ($user_methods) {
582 50
			if (!in_array('delayed_email', $user_methods) || !in_array('email', $user_methods)) {
583 50
				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 50
		}, $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 10
	protected function filterTimedMutedSubscribers(array $subscriptions): array {
601 10
		$muted = Entities::find([
602 10
			'type' => 'user',
603 10
			'guids' => array_keys($subscriptions),
604 10
			'limit' => false,
605 10
			'callback' => function ($row) {
606 1
				return (int) $row->guid;
607 10
			},
608 10
			'metadata_name_value_pairs' => [
609 10
				[
610 10
					'name' => 'timed_muting_start',
611 10
					'value' => time(),
612 10
					'operand' => '<=',
613 10
				],
614 10
				[
615 10
					'name' => 'timed_muting_end',
616 10
					'value' => time(),
617 10
					'operand' => '>=',
618 10
				],
619 10
			],
620 10
		]);
621
		
622 10
		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 19
	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 19
		$rels = $this->getMethodRelationships($methods, $type, $subtype, $action);
643 19
		if (!$rels) {
644
			return [];
645
		}
646
		
647 19
		$container_guid = array_unique(array_filter($container_guid));
648 19
		if (empty($container_guid)) {
649 1
			return [];
650
		}
651
652 18
		$select = Select::fromTable(RelationshipsTable::TABLE_NAME);
653 18
		$select->select('guid_one AS guid')
654 18
			->addSelect("GROUP_CONCAT(relationship SEPARATOR ',') AS methods")
655 18
			->where($select->compare('guid_two', 'in', $container_guid, ELGG_VALUE_GUID))
656 18
			->andWhere($select->compare('relationship', 'in', $rels, ELGG_VALUE_STRING))
657 18
			->groupBy('guid_one');
658
		
659 18
		if (!empty($actor_guid)) {
660 11
			$select->andWhere($select->compare('guid_one', '!=', $actor_guid, ELGG_VALUE_GUID));
661
		}
662
		
663 18
		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 19
	protected function getMethodRelationships(array $methods, ?string $type = null, ?string $subtype = null, ?string $action = null): array {
677 19
		$prefix = self::RELATIONSHIP_PREFIX;
678
		
679 19
		$names = [];
680 19
		foreach ($methods as $method) {
681 19
			$names[] = "{$prefix}:{$method}";
682
			
683 19
			if (!empty($type) && !empty($subtype) && !empty($action)) {
684 12
				$names[] = "{$prefix}:{$type}:{$subtype}:{$action}:{$method}";
685
			}
686
		}
687
		
688 19
		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 287
	protected function assertValidTypeSubtypeActionForSubscription($type, $subtype, $action): void {
702 287
		if (empty($type) && empty($subtype) && empty($action)) {
703
			// all empty, this is valid
704 237
			return;
705
		}
706
		
707 53
		if (!empty($type) && !empty($subtype) && !empty($action)) {
708
			// all set, also valid
709 5
			return;
710
		}
711
		
712 48
		throw new InvalidArgumentException('$type, $subtype and $action need to all be empty or all have a value');
713
	}
714
}
715