Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

Notifier   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 410
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 45
cbo 11
dl 0
loc 410
ccs 0
cts 216
cp 0
rs 8.8
c 0
b 0
f 0
lcom 1

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A setOptions() 0 15 3
A getThrottleTime() 0 4 1
A compose() 0 4 1
A notify() 0 18 4
A hasAdapter() 0 4 1
A injectAdapter() 0 18 3
A getAdapter() 0 8 2
A getAdapters() 0 15 4
A postNotification() 0 22 3
A getNotifications() 0 20 2
A getNotification() 0 13 2
A deleteNotification() 0 17 2
A throttleSubscriptions() 0 14 1
A getSubscription() 0 9 2
A getSubscriptions() 0 8 2
B getAllSubscriptions() 0 31 6
B subscribeNode() 0 52 5

How to fix   Complexity   

Complex Class

Complex classes like Notifier 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Notification;
13
14
use Balloon\App\Notification\Adapter\AdapterInterface;
15
use Balloon\Filesystem\Node\Collection;
16
use Balloon\Filesystem\Node\NodeInterface;
17
use Balloon\Server;
18
use Balloon\Server\User;
19
use InvalidArgumentException;
20
use MongoDB\BSON\ObjectId;
21
use MongoDB\BSON\UTCDateTime;
22
use MongoDB\Database;
23
use Psr\Log\LoggerInterface;
24
25
class Notifier
26
{
27
    /**
28
     * Notifications.
29
     *
30
     * @var array
31
     */
32
    protected $notifications = [];
33
34
    /**
35
     * Adapter.
36
     *
37
     * @var array
38
     */
39
    protected $adapter = [];
40
41
    /**
42
     * Logger.
43
     *
44
     * @var LoggerInterface
45
     */
46
    protected $logger;
47
48
    /**
49
     * Database.
50
     *
51
     * @var Database
52
     */
53
    protected $db;
54
55
    /**
56
     * Server.
57
     *
58
     * @var Server
59
     */
60
    protected $server;
61
62
    /**
63
     * Collection name.
64
     *
65
     * @var string
66
     */
67
    protected $collection_name = 'notification';
68
69
    /**
70
     * Message handler.
71
     *
72
     * @var TemplateHandler
73
     */
74
    protected $template;
75
76
    /**
77
     * Notification throttle.
78
     *
79
     * @var int
80
     */
81
    protected $notification_throttle = 120;
82
83
    /**
84
     * Constructor.
85
     */
86
    public function __construct(Database $db, Server $server, LoggerInterface $logger, TemplateHandler $template, array $config = [])
87
    {
88
        $this->logger = $logger;
89
        $this->db = $db;
90
        $this->server = $server;
91
        $this->template = $template;
92
        $this->setOptions($config);
93
    }
94
95
    /**
96
     * Set config.
97
     */
98
    public function setOptions(array $config = []): self
99
    {
100
        foreach ($config as $option => $value) {
101
            switch ($option) {
102
                case 'notification_throttle':
103
                    $this->{$option} = (int) $value;
104
105
                break;
106
                default:
107
                    throw new InvalidArgumentException('invalid option '.$option.' given');
108
            }
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * Get notification throttle time.
116
     */
117
    public function getThrottleTime(): int
118
    {
119
        return $this->notification_throttle;
120
    }
121
122
    /**
123
     * Node message factory.
124
     */
125
    public function compose(string $type, array $context = []): MessageInterface
126
    {
127
        return new Message($type, $this->template, $context);
128
    }
129
130
    /**
131
     * Send notification.
132
     */
133
    public function notify(iterable $receiver, ?User $sender, MessageInterface $message): bool
134
    {
135
        if (0 === count($this->adapter)) {
136
            throw new Exception\NoAdapterAvailable('there are no notification adapter enabled, notification can not be sent');
137
        }
138
139
        foreach ($receiver as $user) {
140
            foreach ($this->adapter as $name => $adapter) {
141
                $this->logger->debug('send notification to user ['.$user->getId().'] via adapter ['.$name.']', [
142
                    'category' => get_class($this),
143
                ]);
144
145
                $adapter->notify($user, $sender, $message);
146
            }
147
        }
148
149
        return true;
150
    }
151
152
    /**
153
     * Has adapter.
154
     */
155
    public function hasAdapter(string $name): bool
156
    {
157
        return isset($this->adapter[$name]);
158
    }
159
160
    /**
161
     * Inject adapter.
162
     */
163
    public function injectAdapter(AdapterInterface $adapter, ?string $name = null): self
164
    {
165
        if (null === $name) {
166
            $name = get_class($adapter);
167
        }
168
169
        $this->logger->debug('inject notification adapter ['.$name.'] of type ['.get_class($adapter).']', [
170
            'category' => get_class($this),
171
        ]);
172
173
        if ($this->hasAdapter($name)) {
174
            throw new Exception\AdapterNotUnique('adapter '.$name.' is already registered');
175
        }
176
177
        $this->adapter[$name] = $adapter;
178
179
        return $this;
180
    }
181
182
    /**
183
     * Get adapter.
184
     */
185
    public function getAdapter(string $name): AdapterInterface
186
    {
187
        if (!$this->hasAdapter($name)) {
188
            throw new Exception\AdapterNotFound('adapter '.$name.' is not registered');
189
        }
190
191
        return $this->adapter[$name];
192
    }
193
194
    /**
195
     * Get adapters.
196
     */
197
    public function getAdapters(array $adapters = []): array
0 ignored issues
show
Unused Code introduced by
The parameter $adapters is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
198
    {
199
        if (empty($adapter)) {
0 ignored issues
show
Bug introduced by
The variable $adapter does not exist. Did you mean $adapters?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
200
            return $this->adapter;
201
        }
202
        $list = [];
203
        foreach ($adapter as $name) {
204
            if (!$this->hasAdapter($name)) {
205
                throw new Exception\AdapterNotFound('adapter '.$name.' is not registered');
206
            }
207
            $list[$name] = $this->adapter[$name];
208
        }
209
210
        return $list;
211
    }
212
213
    /**
214
     * Add notification.
215
     */
216
    public function postNotification(User $receiver, ?User $sender, MessageInterface $message): ObjectId
217
    {
218
        $data = [
219
            'subject' => $message->getSubject($receiver),
220
            'body' => $message->getBody($receiver),
221
            'receiver' => $receiver->getId(),
222
            'locale' => $receiver->getAttributes()['locale'],
223
            'type' => $message->getType(),
224
        ];
225
226
        if ($sender instanceof User) {
227
            $data['sender'] = $sender->getId();
228
        }
229
230
        if (isset($message->getContext()['node'])) {
231
            $data['node'] = $message->getContext()['node']->getId();
232
        }
233
234
        $result = $this->db->{$this->collection_name}->insertOne($data);
235
236
        return $result->getInsertedId();
237
    }
238
239
    /**
240
     * Get notifications.
241
     */
242
    public function getNotifications(User $user, array $query = [], ?int $offset = null, ?int $limit = null, ?int &$total = null): iterable
243
    {
244
        $filter = ['receiver' => $user->getId()];
245
        if (!empty($query)) {
246
            $filter = [
247
                '$and' => [
248
                    $filter,
249
                    $query,
250
                ],
251
            ];
252
        }
253
254
        $total = $this->db->{$this->collection_name}->count($filter);
255
        $result = $this->db->{$this->collection_name}->find($filter, [
256
            'skip' => $offset,
257
            'limit' => $limit,
258
        ]);
259
260
        return $result;
261
    }
262
263
    /**
264
     * Get notification.
265
     */
266
    public function getNotification(ObjectId $id): array
267
    {
268
        $result = $this->db->{$this->collection_name}->findOne([
269
            '_id' => $id,
270
            'receiver' => $this->server->getIdentity()->getId(),
271
        ]);
272
273
        if ($result === null) {
274
            throw new Exception\NotificationNotFound('notification not found');
275
        }
276
277
        return $result;
278
    }
279
280
    /**
281
     * Get notifications.
282
     */
283
    public function deleteNotification(ObjectId $id): bool
284
    {
285
        $result = $this->db->{$this->collection_name}->deleteOne([
286
            '_id' => $id,
287
            'receiver' => $this->server->getIdentity()->getId(),
288
        ]);
289
290
        if (null === $result) {
291
            throw new Exception\NotificationNotFound('notification not found');
292
        }
293
294
        $this->logger->debug('notification ['.$id.'] removed from user ['.$this->server->getIdentity()->getId().']', [
295
            'category' => get_class($this),
296
        ]);
297
298
        return true;
299
    }
300
301
    /**
302
     * Throttle subscriptions.
303
     */
304
    public function throttleSubscriptions(array $subscriptions): Notifier
305
    {
306
        $this->db->subscription->updateMany([
307
            '_id' => [
308
                '$in' => $subscriptions,
309
            ],
310
        ], [
311
            '$set' => [
312
                'last_notification' => new UTCDateTime(),
313
            ],
314
        ]);
315
316
        return $this;
317
    }
318
319
    /**
320
     * Get subscription.
321
     */
322
    public function getSubscription(NodeInterface $node, User $user): ?array
323
    {
324
        $node_id = $node->isReference() ? $node->getShareId() : $node->getId();
325
326
        return $this->db->subscription->findOne([
327
            'node' => $node_id,
328
            'user' => $user->getId(),
329
        ]);
330
    }
331
332
    /**
333
     * Get subscriptions.
334
     */
335
    public function getSubscriptions(NodeInterface $node): iterable
336
    {
337
        $node_id = $node->isReference() ? $node->getShareId() : $node->getId();
338
339
        return $this->db->subscription->find([
340
            'node' => $node_id,
341
        ]);
342
    }
343
344
    /**
345
     * Get subscriptions.
346
     */
347
    public function getAllSubscriptions(NodeInterface $node): iterable
348
    {
349
        $sub_id = $node->isReference() ? $node->getShareId() : $node->getId();
350
351
        $ids = [$sub_id];
352
        foreach ($node->getParents() as $parent) {
353
            $ids[] = $parent->isReference() ? $parent->getShareId() : $parent->getId();
354
355
            if ($parent->isShare()) {
356
                break;
357
            }
358
        }
359
360
        $parents = [$ids[0]];
361
362
        if (count($ids) > 1) {
363
            $parents[] = $ids[1];
364
        }
365
366
        return $this->db->subscription->find([
367
            '$or' => [
368
                [
369
                    'node' => ['$in' => $ids],
370
                    'recursive' => true,
371
                ],
372
                [
373
                    'node' => ['$in' => $parents],
374
                ],
375
            ],
376
        ]);
377
    }
378
379
    /**
380
     * Subscribe to node updates.
381
     */
382
    public function subscribeNode(NodeInterface $node, bool $subscribe = true, bool $exclude_me = true, bool $recursive = false, ?int $throttle = null): bool
383
    {
384
        $node_id = $node->isReference() ? $node->getShareId() : $node->getId();
385
        $user_id = $this->server->getIdentity()->getId();
386
387
        if (true === $subscribe) {
388
            $this->logger->debug('user ['.$this->server->getIdentity()->getId().'] subscribes node ['.$node->getId().']', [
389
                'category' => get_class($this),
390
            ]);
391
392
            $subscription = [
393
                'timestamp' => new UTCDateTime(),
394
                'exclude_me' => $exclude_me,
395
                'recursive' => $recursive,
396
                'throttle' => $throttle,
397
                'user' => $user_id,
398
                'node' => $node_id,
399
            ];
400
401
            $this->db->subscription->replaceOne(
402
                [
403
                'user' => $subscription['user'],
404
                'node' => $subscription['node'],
405
            ],
406
                $subscription,
407
                [
408
                'upsert' => true,
409
            ]
410
            );
411
        } else {
412
            $this->logger->debug('user ['.$this->server->getIdentity()->getId().'] unsubscribes node ['.$node->getId().']', [
413
                'category' => get_class($this),
414
            ]);
415
416
            $this->db->subscription->deleteOne([
417
                'user' => $user_id,
418
                'node' => $node_id,
419
            ]);
420
421
            if ($node instanceof Collection && $recursive === true) {
422
                $db = $this->db;
423
                $node->doRecursiveAction(function ($child) use ($db, $node_id, $user_id) {
0 ignored issues
show
Unused Code introduced by
The parameter $child is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
424
                    $db->subscription->deleteOne([
425
                        'user' => $user_id,
426
                        'node' => $node_id,
427
                    ]);
428
                });
429
            }
430
        }
431
432
        return true;
433
    }
434
}
435