Passed
Pull Request — master (#17)
by BENOIT
01:48
created

SubscriptionsController::__invoke()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 66
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 44
nc 6
nop 1
dl 0
loc 66
rs 8.5937
c 1
b 0
f 0

How to fix   Long Method   

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
2
3
namespace BenTools\MercurePHP\Controller;
4
5
use BenTools\MercurePHP\Exception\Http\AccessDeniedHttpException;
6
use BenTools\MercurePHP\Hub\Hub;
7
use BenTools\MercurePHP\Model\Subscription;
8
use BenTools\MercurePHP\Security\Authenticator;
9
use BenTools\MercurePHP\Security\TopicMatcher;
10
use BenTools\MercurePHP\Storage\StorageInterface;
11
use Psr\Http\Message\RequestInterface;
12
use Psr\Http\Message\ResponseInterface;
13
use Psr\Http\Message\ServerRequestInterface;
14
use React\Http\Message\Response;
15
use React\Stream\ThroughStream;
16
use React\Stream\WritableStreamInterface;
17
18
use function BenTools\MercurePHP\nullify;
19
20
final class SubscriptionsController extends AbstractController
21
{
22
    private const PATH = '/.well-known/mercure/subscriptions';
23
    private const TOPIC_SELECTOR = '/.well-known/mercure/subscriptions/{topic}/{subscriber}';
24
    private Hub $hub;
25
    private Authenticator $authenticator;
26
27
    public function __construct(Hub $hub, Authenticator $authenticator)
28
    {
29
        $this->hub = $hub;
30
        $this->authenticator = $authenticator;
31
    }
32
33
    public function __invoke(ServerRequestInterface $request): ResponseInterface
34
    {
35
        if (!\in_array($request->getMethod(), ['GET', 'HEAD', 'OPTIONS'])) {
36
            return new Response(405);
37
        }
38
39
        $path = $request->getUri()->getPath();
40
        $filters = \explode('/', \trim(\strtr($path, [self::PATH => '']), '/'), 2);
41
        $topic = nullify($filters[0]);
42
        $subscriber = nullify($filters[1] ?? null);
43
44
        $token = $this->authenticator->authenticate($request);
45
46
        if (null === $token) {
47
            throw new AccessDeniedHttpException('You must be authenticated to access the Subscription API.');
48
        }
49
50
        $claim = (array) $token->getClaim('mercure');
51
        $allowedTopics = $claim['subscribe'] ?? [];
52
        $deniedTopics = $claim['subscribe_exclude'] ?? [];
53
        $topicSelector = $path === self::PATH ? self::TOPIC_SELECTOR : $path;
54
        $matchAllowedTopics = TopicMatcher::matchesTopicSelectors($topicSelector, $allowedTopics);
55
        $matchDeniedTopics = TopicMatcher::matchesTopicSelectors($topicSelector, $deniedTopics);
56
        if (!$matchAllowedTopics || $matchDeniedTopics) {
57
            throw new AccessDeniedHttpException('You are not authorized to display these subscriptions.');
58
        }
59
60
        $stream = new ThroughStream();
61
        $this->hub->hook(
62
            function () use ($stream, $path, $subscriber, $topic, $allowedTopics, $deniedTopics) {
63
                $this->hub->getLastEventID()->then(
64
                    function (?string $lastEventId) use (
65
                        $topic,
66
                        $subscriber,
67
                        $stream,
68
                        $path,
69
                        $allowedTopics,
70
                        $deniedTopics
71
                    ) {
72
                        $this->hub->getActiveSubscriptions($topic, $subscriber)
73
                            ->then(
74
                                function (iterable $subscriptions) use (
75
                                    $stream,
76
                                    $path,
77
                                    $allowedTopics,
78
                                    $deniedTopics,
79
                                    $lastEventId
80
                                ) {
81
                                    $subscriptions = $this->filterSubscriptions(
82
                                        $subscriptions,
83
                                        $allowedTopics,
84
                                        $deniedTopics
85
                                    );
86
                                    $this->sendResult($path, $lastEventId, $subscriptions, $stream);
87
                                }
88
                            );
89
                    }
90
                );
91
            }
92
        );
93
94
        $headers = [
95
            'Content-Type' => 'application/ld+json',
96
        ];
97
98
        return new Response(200, $headers, $stream);
99
    }
100
101
    public function matchRequest(RequestInterface $request): bool
102
    {
103
        return 0 === \strpos($request->getUri()->getPath(), self::PATH);
104
    }
105
106
    private function filterSubscriptions(iterable $subscriptions, array $allowedTopics, array $deniedTopics): array
107
    {
108
        $subscriptions = \iterable_to_array($subscriptions);
109
        $subscriptions = \array_filter(
110
            $subscriptions,
111
            function (Subscription $subscription) use ($allowedTopics, $deniedTopics) {
112
                $matchAllowedTopics = TopicMatcher::matchesTopicSelectors(
113
                    $subscription->getId(),
114
                    $allowedTopics
115
                );
116
                $matchDeniedTopics = TopicMatcher::matchesTopicSelectors(
117
                    $subscription->getId(),
118
                    $deniedTopics
119
                );
120
121
                return $matchAllowedTopics && !$matchDeniedTopics;
122
            }
123
        );
124
125
        return \array_values($subscriptions);
126
    }
127
128
    private function sendResult(
129
        string $path,
130
        ?string $lastEventId,
131
        array $subscriptions,
132
        WritableStreamInterface $stream
133
    ): void {
134
        $result = [
135
            '@context' => 'https://mercure.rocks/',
136
            'id' => $path,
137
            'type' => 'Subscriptions',
138
            'lastEventID' => $lastEventId ?? StorageInterface::EARLIEST,
139
            'subscriptions' => $subscriptions,
140
        ];
141
        $stream->write(\json_encode($result, \JSON_THROW_ON_ERROR));
142
        $stream->end();
143
        $stream->close();
144
    }
145
}
146