Completed
Push — feature/update_all_the_things ( 324e2a...b9ab83 )
by Lucas
30:14 queued 13:01
created

onKernelResponse()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 7.0027

Importance

Changes 3
Bugs 2 Features 0
Metric Value
c 3
b 2
f 0
dl 0
loc 41
ccs 25
cts 26
cp 0.9615
rs 6.7272
cc 7
eloc 19
nc 7
nop 1
crap 7.0027
1
<?php
2
/**
3
 * Response listener that adds an eventStatus to Link header if necessary, creates an EventStatus resource
4
 * and publishes the change to the queue
5
 */
6
7
namespace Graviton\RabbitMqBundle\Listener;
8
9
use Doctrine\ODM\MongoDB\DocumentManager;
10
use Graviton\DocumentBundle\Service\ExtReferenceConverter;
11
use Graviton\RabbitMqBundle\Document\QueueEvent;
12
use Graviton\RestBundle\HttpFoundation\LinkHeader;
13
use Graviton\RestBundle\HttpFoundation\LinkHeaderItem;
14
use Graviton\SecurityBundle\Authentication\Strategies\CookieFieldStrategy;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
17
use OldSound\RabbitMqBundle\RabbitMq\ProducerInterface;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\RequestStack;
20
use Symfony\Component\Routing\RouterInterface;
21
22
/**
23
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
24
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
25
 * @link     http://swisscom.ch
26
 */
27
class EventStatusLinkResponseListener
28
{
29
30
    /**
31
     * @var ProducerInterface Producer for publishing messages.
32
     */
33
    private $rabbitMqProducer = null;
34
35
    /**
36
     * @var RouterInterface Router to generate resource URLs
37
     */
38
    private $router = null;
39
40
    /**
41
     * @var Request request
42
     */
43
    private $request;
44
45
    /**
46
     * @var QueueEvent queueevent document
47
     */
48
    private $queueEventDocument;
49
50
    /**
51
     * @var array
52
     */
53
    private $eventMap;
54
55
    /**
56
     * @var ExtReferenceConverter ExtReferenceConverter
57
     */
58
    private $extRefConverter;
59
60
    /**
61
     * @var string classname of the EventWorker document
62
     */
63
    private $eventWorkerClassname;
64
65
    /**
66
     * @var string classname of the EventStatus document
67
     */
68
    private $eventStatusClassname;
69
70
    /**
71
     * @var string classname of the EventStatusStatus document
72
     */
73
    private $eventStatusStatusClassname;
74
75
    /**
76
     * @var string classname of the EventStatusEventResource document
77
     */
78
    private $eventStatusEventResourceClassname;
79
80
    /**
81
     * @var string route name of the /event/status route
82
     */
83
    private $eventStatusRouteName;
84
85
    /**
86
     * @var DocumentManager Document manager
87
     */
88
    private $documentManager;
89
90
    /**
91
     * @param ProducerInterface     $rabbitMqProducer                  RabbitMQ dependency
92
     * @param RouterInterface       $router                            Router dependency
93
     * @param RequestStack          $requestStack                      Request stack
94
     * @param DocumentManager       $documentManager                   Doctrine document manager
95
     * @param ExtReferenceConverter $extRefConverter                   instance of the ExtReferenceConverter service
96
     * @param QueueEvent            $queueEventDocument                queueevent document
97
     * @param array                 $eventMap                          eventmap
98
     * @param string                $eventWorkerClassname              classname of the EventWorker document
99
     * @param string                $eventStatusClassname              classname of the EventStatus document
100
     * @param string                $eventStatusStatusClassname        classname of the EventStatusStatus document
101
     * @param string                $eventStatusEventResourceClassname classname of the E*S*E*Resource document
102
     * @param string                $eventStatusRouteName              name of the route to EventStatus
103
     */
104 2
    public function __construct(
105
        ProducerInterface $rabbitMqProducer,
106
        RouterInterface $router,
107
        RequestStack $requestStack,
108
        DocumentManager $documentManager,
109
        ExtReferenceConverter $extRefConverter,
110
        QueueEvent $queueEventDocument,
111
        array $eventMap,
112
        $eventWorkerClassname,
113
        $eventStatusClassname,
114
        $eventStatusStatusClassname,
115
        $eventStatusEventResourceClassname,
116
        $eventStatusRouteName
117
    ) {
118 2
        $this->rabbitMqProducer = $rabbitMqProducer;
119 2
        $this->router = $router;
120 2
        $this->request = $requestStack->getCurrentRequest();
121 2
        $this->documentManager = $documentManager;
122 2
        $this->extRefConverter = $extRefConverter;
123 2
        $this->queueEventDocument = $queueEventDocument;
124 2
        $this->eventMap = $eventMap;
125 2
        $this->eventWorkerClassname = $eventWorkerClassname;
126 2
        $this->eventStatusClassname = $eventStatusClassname;
127 2
        $this->eventStatusStatusClassname = $eventStatusStatusClassname;
128 2
        $this->eventStatusEventResourceClassname = $eventStatusEventResourceClassname;
129 2
        $this->eventStatusRouteName = $eventStatusRouteName;
130 2
    }
131
132
    /**
133
     * add a rel=eventStatus Link header to the response if necessary
134
     *
135
     * @param FilterResponseEvent $event response listener event
136
     *
137
     * @return void
138
     */
139 2
    public function onKernelResponse(FilterResponseEvent $event)
140
    {
141
        // exit if not master request or uninteresting method..
142 2
        if (!$event->isMasterRequest() || $this->isNotConcerningRequest()) {
143
            return;
144
        }
145
146
        // we can always safely call this, it doesn't need much resources.
147
        // only if we have subscribers, it will create more load as it persists an EventStatus
148 2
        $queueEvent = $this->createQueueEventObject();
149
150
        /**
151
         * @var Response $response
152
         */
153 2
        $response = $event->getResponse();
154
155 2
        if (!empty($queueEvent->getStatusurl()) && !empty($queueEvent->getEvent())) {
156 2
            $linkHeader = LinkHeader::fromResponse($response);
157 2
            $linkHeader->add(
158 2
                new LinkHeaderItem(
159 2
                    $queueEvent->getStatusurl(),
160 2
                    array('rel' => 'eventStatus')
161 1
                )
162 1
            );
163
164 2
            $response->headers->set(
165 2
                'Link',
166 1
                (string) $linkHeader
167 1
            );
168 1
        }
169
170
        // let's send it to the queue(s) if appropriate
171 2
        if (!empty($queueEvent->getEvent())) {
172 2
            $queuesForEvent = $this->getSubscribedWorkerIds($queueEvent);
173 2
            foreach ($queuesForEvent as $queueForEvent) {
174
                // declare the Queue for the Event if its not there already declared
175 2
                $this->rabbitMqProducer->getChannel()->queue_declare($queueForEvent, false, true, false, false);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OldSound\RabbitMqBundle\RabbitMq\ProducerInterface as the method getChannel() does only exist in the following implementations of said interface: Graviton\RabbitMqBundle\Producer\Dummy, OldSound\RabbitMqBundle\RabbitMq\Producer.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
176 2
                $this->rabbitMqProducer->publish(json_encode($queueEvent), $queueForEvent);
177 1
            }
178 1
        }
179 2
    }
180
181
    /**
182
     * we only want to do something if we have a mapped event..
183
     *
184
     * @return boolean true if it should not concern us, false otherwise
185
     */
186 2
    private function isNotConcerningRequest()
187
    {
188 2
        return is_null($this->generateRoutingKey());
189
    }
190
191
    /**
192
     * Creates the structured object that will be sent to the queue (eventually..)
193
     *
194
     * @return QueueEvent event
195
     */
196 2
    private function createQueueEventObject()
197 1
    {
198 2
        $obj = clone $this->queueEventDocument;
199 2
        $obj->setEvent($this->generateRoutingKey());
200 2
        $obj->setDocumenturl($this->request->get('selfLink'));
201 2
        $obj->setStatusurl($this->getStatusUrl($obj));
202 2
        $obj->setCoreUserId($this->getCoreUserId());
203
204 2
        return $obj;
205
    }
206
207
    /**
208
     * compose our routingKey. this will have the form of 'document.[bundle].[document].[event]'
209
     * rules:
210
     *  * always 4 parts divided by points.
211
     *  * in this context (doctrine/odm stuff) we prefix with 'document.'
212
     *
213
     * @return string routing key
214
     */
215 2
    private function generateRoutingKey()
216
    {
217 2
        $routeParts = explode('.', $this->request->get('_route'));
218 2
        $action = array_pop($routeParts);
219 2
        $baseRoute = implode('.', $routeParts);
220
221
        // find our route in the map
222 2
        $routingKey = null;
223
224 2
        foreach ($this->eventMap as $mapElement) {
225 2
            if ($mapElement['baseRoute'] == $baseRoute &&
226 2
                isset($mapElement['events'][$action])
227 1
            ) {
228 2
                $routingKey = $mapElement['events'][$action];
229 2
                break;
230
            }
231 1
        }
232
233 2
        return $routingKey;
234
    }
235
236
    /**
237
     * Creates a EventStatus object that gets persisted..
238
     *
239
     * @param QueueEvent $queueEvent queueEvent object
240
     *
241
     * @return string
242
     */
243 2
    private function getStatusUrl($queueEvent)
244
    {
245
        // this has to be checked after cause we should not call getSubscribedWorkerIds() if above is true
246 2
        $workerIds = $this->getSubscribedWorkerIds($queueEvent);
247 2
        if (empty($workerIds)) {
248
            return '';
249
        }
250
251
        // we have subscribers; create the EventStatus entry
252
        /** @var \GravitonDyn\EventStatusBundle\Document\EventStatus $eventStatus **/
253 2
        $eventStatus = new $this->eventStatusClassname();
254 2
        $eventStatus->setCreatedate(new \DateTime());
255 2
        $eventStatus->setEventname($queueEvent->getEvent());
256
257
        // if available, transport the ref document to the eventStatus instance
258 2
        if (!empty($queueEvent->getDocumenturl())) {
259 2
            $eventStatusResource = new $this->eventStatusEventResourceClassname();
260 2
            $eventStatusResource->setRef($this->extRefConverter->getExtReference($queueEvent->getDocumenturl()));
261 2
            $eventStatus->setEventresource($eventStatusResource);
262 1
        }
263
264 2
        foreach ($workerIds as $workerId) {
265
            /** @var \GravitonDyn\EventStatusBundle\Document\EventStatusStatus $eventStatusStatus **/
266 2
            $eventStatusStatus = new $this->eventStatusStatusClassname();
267 2
            $eventStatusStatus->setWorkerid($workerId);
268 2
            $eventStatusStatus->setStatus('opened');
269 2
            $eventStatus->addStatus($eventStatusStatus);
270 1
        }
271
272 2
        $this->documentManager->persist($eventStatus);
273 2
        $this->documentManager->flush();
274
275
        // get the url..
276 2
        $url = $this->router->generate(
277 2
            $this->eventStatusRouteName,
278
            [
279 2
                'id' => $eventStatus->getId()
280 1
            ],
281 1
            true
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282 1
        );
283
284 2
        return $url;
285
    }
286
287
    /**
288
     * Checks EventWorker for worker that are subscribed to our event and returns
289
     * their workerIds as array
290
291
     * @param QueueEvent $queueEvent queueEvent object
292
     *
293
     * @return array array of worker ids
294
     */
295 2
    private function getSubscribedWorkerIds(QueueEvent $queueEvent)
296
    {
297
        // compose our regex to match stars ;-)
298
        // results in = /((\*|document)+)\.((\*|dude)+)\.((\*|config)+)\.((\*|update)+)/
299 2
        $routingArgs = explode('.', $queueEvent->getEvent());
300
        $regex =
301
            '/'.
302 2
            implode(
303 2
                '\.',
304 1
                array_map(
305 2
                    function ($arg) {
306 2
                        return '((\*|'.$arg.')+)';
307 2
                    },
308
                    $routingArgs
309 1
                )
310 1
            ).
311 2
            '/';
312
313
        // look up workers by class name
314 2
        $qb = $this->documentManager->createQueryBuilder($this->eventWorkerClassname);
315
        $data = $qb
316 2
            ->select('id')
317 2
            ->field('subscription.event')
318 2
            ->equals(new \MongoRegex($regex))
319 2
            ->getQuery()
320 2
            ->execute()
321 2
            ->toArray();
322
323 2
        return array_keys($data);
324
    }
325
326
    /**
327
     * Find current request attribute to User Core Id
328
     *
329
     * @return string
330
     */
331 2
    public function getCoreUserId()
332
    {
333 2
        $attributes = $this->request->attributes;
334 2
        if (!$attributes) {
335 2
            return '';
336
        }
337
        $value = $attributes->get(CookieFieldStrategy::CONFIGURATION_PARAMETER_CORE_ID);
338
        if ($value) {
339
            return (string) $value;
340
        }
341
        return '';
342
    }
343
}
344