Completed
Push — fm-matches ( f867e2...7fc64f )
by Vladimir
14:12
created

MessageController   F

Complexity

Total Complexity 57

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 30

Test Coverage

Coverage 71.22%

Importance

Changes 5
Bugs 0 Features 3
Metric Value
wmc 57
c 5
b 0
f 3
lcom 1
cbo 30
dl 0
loc 429
ccs 99
cts 139
cp 0.7122
rs 1.3043

17 Methods

Rating   Name   Duplication   Size   Complexity  
B teamRefreshAction() 0 24 3
A setup() 0 4 1
A prepareTwig() 0 20 1
A listAction() 0 4 1
C composeAction() 0 45 7
C showAction() 0 63 9
A leaveAction() 0 18 3
A teamLeaveAction() 0 20 3
B kickAction() 0 30 5
A searchAction() 0 16 3
B showInviteForm() 0 29 5
A showRenameForm() 0 19 2
A showMessageForm() 0 20 4
A assertCanParticipate() 0 7 2
A sendMessage() 0 21 2
A getErrorMessage() 0 14 4
A assertCanEdit() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like MessageController 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 MessageController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use BZIon\Event\ConversationAbandonEvent;
4
use BZIon\Event\ConversationJoinEvent;
5
use BZIon\Event\ConversationKickEvent;
6
use BZIon\Event\ConversationRenameEvent;
7
use BZIon\Event\Events;
8
use BZIon\Event\NewMessageEvent;
9
use BZIon\Form\Creator\ConversationFormCreator;
10
use BZIon\Form\Creator\ConversationInviteFormCreator;
11
use BZIon\Form\Creator\ConversationRenameFormCreator;
12
use BZIon\Form\Creator\MessageFormCreator;
13
use BZIon\Form\Creator\MessageSearchFormCreator;
14
use BZIon\Search\MessageSearch;
15
use Symfony\Component\Form\Form;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\Request;
19
20
class MessageController extends JSONController
21
{
22 1
    public function setup()
23
    {
24 1
        $this->requireLogin();
25 1
    }
26
27 1
    protected function prepareTwig()
28
    {
29 1
        $currentPage = $this->getRequest()->query->get('page', 1);
30
31 1
        $query = $this->getQueryBuilder('Conversation')
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class QueryBuilder as the method forPlayer() does only exist in the following sub-classes of QueryBuilder: ConversationQueryBuilder, MessageQueryBuilder. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
32 1
            ->forPlayer($this->getMe())
33 1
            ->sortBy('last_activity')->reverse()
34 1
            ->limit(5)->fromPage($currentPage);
35
36 1
        $creator = new MessageSearchFormCreator();
37 1
        $searchForm = $creator->create();
38
39 1
        $twig = $this->container->get('twig');
40 1
        $twig->addGlobal("conversations", $query->getModels());
41 1
        $twig->addGlobal("currentPage", $currentPage);
42 1
        $twig->addGlobal("totalPages", $query->countPages());
43 1
        $twig->addGlobal("searchForm", $searchForm->createView());
44
45 1
        return $twig;
46
    }
47
48
    public function listAction()
49
    {
50
        return array();
51
    }
52
53 1
    public function composeAction(Player $me, Request $request)
54
    {
55 1
        if (!$me->hasPermission(Permission::SEND_PRIVATE_MSG)) {
56 1
            throw new ForbiddenException("You are not allowed to send messages");
57
        }
58
59 1
        $creator = new ConversationFormCreator($me);
60 1
        $form = $creator->create()->handleRequest($request);
61
62 1
        if ($form->isSubmitted()) {
63 1
            if ($form->isValid()) {
64 1
                $subject = $form->get('Subject')->getData();
65 1
                $content = $form->get('Message')->getData();
66 1
                $recipients = $form->get('Recipients')->getData();
67
68 1
                $conversation_to = Conversation::createConversation($subject, $me->getId(), $recipients);
69 1
                $message = $conversation_to->sendMessage($me, $content);
70
71 1
                $event = new NewMessageEvent($message, true);
72 1
                $this->dispatch(Events::MESSAGE_NEW, $event);
73
74 1
                if ($this->isJson()) {
75
                    return new JsonResponse(array(
76
                        'success' => true,
77
                        'message' => 'Your message was sent successfully',
78
                        'id'      => $conversation_to->getId()
79
                    ));
80
                } else {
81 1
                    return new RedirectResponse($conversation_to->getUrl());
82
                }
83 1
            } elseif ($this->isJson()) {
84 1
                throw new BadRequestException($this->getErrorMessage($form));
85
            }
86
        } else {
87
            // Load the list of recipients from the URL
88 1
            if ($request->query->has('recipients')) {
89
                $form->get('Recipients')->setData($this->decompose(
90
                    $request->query->get('recipients'),
91
                    array('Player', 'Team')
92
                ));
93
            }
94
        }
95
96 1
        return array("form" => $form->createView());
97
    }
98
99 1
    public function showAction(Conversation $conversation, Player $me, Request $request)
100
    {
101 1
        $this->assertCanParticipate($me, $conversation);
102 1
        $conversation->markReadBy($me->getId());
103
104 1
        $form = $this->showMessageForm($conversation, $me);
105 1
        $inviteForm = $this->showInviteForm($conversation, $me);
106 1
        $renameForm = $this->showRenameForm($conversation, $me);
107
108 1
        $messages = $this->getQueryBuilder('AbstractMessage')
109 1
                  ->where('conversation')->is($conversation)
110 1
                  ->sortBy('time')->reverse()
111 1
                  ->limit(10)->fromPage($request->query->get('page', 1))
112 1
                  ->startAt($request->query->get('end'))
113 1
                  ->endAt($request->query->get('start'))
114 1
                  ->getModels();
115
116
        // Hide the details (author, timestamp) of the first message if they're
117
        // already shown in the previous message (useful for AJAX calls)
118 1
        if ($request->query->getBoolean('hideFirstDetails')) {
119
            $previousMessage = Message::get($request->query->get('end'));
120
        } else {
121 1
            $previousMessage = null;
122
        }
123
124
        $params = array(
125 1
            "form"            => $form->createView(),
126 1
            "inviteForm"      => $inviteForm->createView(),
127 1
            "renameForm"      => $renameForm->createView(),
128 1
            "conversation"    => $conversation,
129 1
            "messages"        => $messages,
130 1
            "previousMessage" => $previousMessage
131
        );
132
133 1
        if ($request->query->has('nolayout')) {
134
            if ($request->query->getBoolean('reviewLastDetails')) {
135
                // An AJAX call has asked us to check if details (author,
136
                // timestamp) will need to be shown for the next message
137
138
                $nextMessage = Message::get($request->query->get('start'));
139
                $lastMessage = reset($messages);
140
141
                if ($lastMessage !== false
142
                    && $lastMessage->isMessage()
143
                    && $nextMessage->isMessage()
144
                    && $lastMessage->getTimestamp()->isSameDay($nextMessage->getTimestamp())
145
                    && $lastMessage->getAuthor()->isSameAs($nextMessage->getAuthor())
146
                ) {
147
                    $hideLastDetails = true;
148
                } else {
149
                    $hideLastDetails = false;
150
                }
151
152
                // Add $hideLastDetails to the list of JSON parameters
153
                $this->attributes->set('hideLastDetails', $hideLastDetails);
154
            }
155
156
            // Don't show the layout so that ajax can just load the messages
157
            return $this->render('Message/messages.html.twig', $params);
158
        } else {
159 1
            return $params;
160
        }
161
    }
162
163
    public function leaveAction(Player $me, Conversation $conversation)
164
    {
165
        if (!$conversation->isMember($me, $distinct = true)) {
166
            throw new ForbiddenException("You are not a member of this discussion.");
167
        } elseif ($conversation->getCreator()->isSameAs($me)) {
168
            throw new ForbiddenException("You can't abandon the conversation you started!");
169
        }
170
171
        return $this->showConfirmationForm(function () use ($conversation, $me) {
172
            $conversation->removeMember($me);
173
174
            $event = new ConversationAbandonEvent($conversation, $me);
175
            Service::getDispatcher()->dispatch(Events::CONVERSATION_ABANDON, $event);
176
177
            return new RedirectResponse(Service::getGenerator()->generate('message_list'));
178
        },  "Are you sure you want to abandon this conversation?",
179
            "You will no longer receive messages from this conversation", "Leave");
180
    }
181
182
    public function teamRefreshAction(Player $me, Conversation $conversation)
183
    {
184
        $team = $me->getTeam();
185
        $missing = $conversation->getMissingTeamMembers($team);
0 ignored issues
show
Bug introduced by
It seems like $team defined by $me->getTeam() on line 184 can be null; however, Conversation::getMissingTeamMembers() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
186
187
        if (empty($missing)) {
188
            throw new ForbiddenException("All members of the specified team are already part of that conversation.");
189
        }
190
191
        $names = array();
192
        foreach ($missing as $player) {
193
            $names[] = $player->getEscapedUsername();
194
        }
195
        $names = implode(', ', $names);
196
197
        return $this->showConfirmationForm(function () use ($conversation, $missing, $team) {
198
            $conversation->addMember($team); // does all the hard work
199
200
            $event = new ConversationJoinEvent($conversation, $missing);
201
            Service::getDispatcher()->dispatch(Events::CONVERSATION_JOIN, $event);
202
203
            return new RedirectResponse($conversation->getURL());
204
        }, "Are you sure you want to invite $names from {$team->getEscapedName()} to that conversation?");
205
    }
206
207
    public function teamLeaveAction(Player $me, Conversation $conversation)
208
    {
209
        $team = $me->getTeam();
210
211
        if (!$me->canEdit($team)) {
0 ignored issues
show
Bug introduced by
It seems like $team defined by $me->getTeam() on line 209 can be null; however, Player::canEdit() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
212
            throw new ForbiddenException("You are not allowed to remove your team from this conversation.");
213
        } elseif (!$conversation->isMember($team)) {
0 ignored issues
show
Bug introduced by
It seems like $team defined by $me->getTeam() on line 209 can be null; however, Conversation::isMember() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
214
            throw new ForbiddenException("That team is not participating in this conversation.");
215
        }
216
217
        return $this->showConfirmationForm(function () use ($conversation, $team) {
218
            $conversation->removeMember($team);
219
220
            $event = new ConversationAbandonEvent($conversation, $team);
221
            Service::getDispatcher()->dispatch(Events::CONVERSATION_ABANDON, $event);
222
223
            return new RedirectResponse($conversation->getURL());
224
        },  "Are you sure you want to remove {$team->getEscapedName()} from this conversation?",
225
            "Your team is no longer participating in that conversation.", "Remove team");
226
    }
227
228
    public function kickAction(Conversation $conversation, Player $me, $type, $member)
229
    {
230
        $this->assertCanEdit($me, $conversation, "You are not allowed to kick a member off that discussion!");
231
232
        if (strtolower($type) === 'player') {
233
            $member = Player::fetchFromSlug($member);
234
        } elseif (strtolower($type) === 'team') {
235
            $member = Team::fetchFromSlug($member);
236
        } else {
237
            throw new BadRequestException("Unrecognized member type.");
238
        }
239
240
        if ($conversation->isCreator($member->getId())) {
241
            throw new ForbiddenException("You can't leave your own conversation.");
242
        }
243
244
        if (!$conversation->isMember($member)) {
0 ignored issues
show
Documentation introduced by
$member is of type object<AliasModel>|null, but the function expects a object<Player>|object<Team>.

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...
245
            throw new ForbiddenException("The specified player is not a member of this conversation.");
246
        }
247
248
        return $this->showConfirmationForm(function () use ($conversation, $member, $me) {
249
            $conversation->removeMember($member);
250
251
            $event = new ConversationKickEvent($conversation, $member, $me);
252
            Service::getDispatcher()->dispatch(Events::CONVERSATION_KICK, $event);
253
254
            return new RedirectResponse($conversation->getUrl());
255
        },  "Are you sure you want to kick {$member->getEscapedName()} from the discussion?",
256
            "{$member->getName()} has been kicked from the conversation", "Kick");
257
    }
258
259 1
    public function searchAction(Player $me, Request $request)
260
    {
261 1
        $query = $request->query->get('q');
262
263 1
        if (strlen($query) < 3 && !$this->isDebug()) {
264
            // TODO: Find a better error message
265
            throw new BadRequestException('The search term you have provided is too short');
266
        }
267
268 1
        $search  = new MessageSearch($this->getQueryBuilder(), $me);
0 ignored issues
show
Compatibility introduced by
$this->getQueryBuilder() of type object<QueryBuilder> is not a sub-type of object<MessageQueryBuilder>. It seems like you assume a child class of the class QueryBuilder to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
269 1
        $results = $search->search($query);
270
271
        return array(
272 1
            'messages' => $results
273
        );
274
    }
275
276
    /**
277
     * @param Conversation  $conversation
278
     * @param Player $me
279
     *
280
     * @return $this|Form|\Symfony\Component\Form\FormInterface
281
     */
282 1
    private function showInviteForm($conversation, $me)
283
    {
284 1
        $creator = new ConversationInviteFormCreator($conversation);
285 1
        $form = $creator->create()->handleRequest($this->getRequest());
286
287 1
        if ($form->isValid()) {
288
            $this->assertCanEdit($me, $conversation);
289
            $invitees = array();
290
291
            foreach ($form->get('players')->getData() as $player) {
292
                if (!$conversation->isMember($player, $distinct = true)) {
293
                    $conversation->addMember($player);
294
                    $invitees[] = $player;
295
                }
296
            }
297
298
            if (!empty($invitees)) {
299
                $event = new ConversationJoinEvent($conversation, $invitees);
300
                Service::getDispatcher()->dispatch(Events::CONVERSATION_JOIN, $event);
301
            }
302
303
            $this->getFlashBag()->add('success', "The conversation has been updated");
304
305
            // Reset the form fields
306
            return $creator->create();
307
        }
308
309 1
        return $form;
310
    }
311
312
    /**
313
     * @param Conversation  $conversation
314
     * @param Player $me
315
     */
316 1
    private function showRenameForm($conversation, $me)
317
    {
318 1
        $creator = new ConversationRenameFormCreator($conversation);
319 1
        $form = $creator->create()->handleRequest($this->getRequest());
320
321 1
        if ($form->isValid()) {
322
            $this->assertCanEdit($me, $conversation);
323
324
            $newName = $form->get('subject')->getData();
325
326
            $event = new ConversationRenameEvent($conversation, $conversation->getSubject(), $newName, $me);
327
            $conversation->setSubject($newName);
328
            Service::getDispatcher()->dispatch(Events::CONVERSATION_RENAME, $event);
329
330
            $this->getFlashBag()->add('success', "The conversation has been updated");
331
        }
332
333 1
        return $form;
334
    }
335
336
    /**
337
     * @param Conversation  $conversation
338
     * @param Player $me
339
     */
340 1
    private function showMessageForm($conversation, $me)
341
    {
342
        // Create the form to send a message to the conversation
343 1
        $creator = new MessageFormCreator($conversation);
344 1
        $form = $creator->create();
345
346
        // Keep a cloned version so we can come back to it later, if we need
347
        // to reset the fields of the form
348 1
        $cloned = clone $form;
349 1
        $form->handleRequest($this->getRequest());
350
351 1
        if ($form->isValid()) {
352
            // The player wants to send a message
353 1
            $this->sendMessage($me, $conversation, $form, $cloned);
354 1
        } elseif ($form->isSubmitted() && $this->isJson()) {
355
            throw new BadRequestException($this->getErrorMessage($form));
356
        }
357
358 1
        return $form;
359
    }
360
361
    /**
362
     * Make sure that a player can participate in a conversation
363
     *
364
     * Throws an exception if a player is not an admin or a member of that conversation
365
     * @todo Permission for spying on other people's conversations?
366
     * @param  Player        $player  The player to test
367
     * @param  Conversation         $conversation   The message conversation
368
     * @param  string        $message The error message to show
369
     * @throws HTTPException
370
     * @return void
371
     */
372 1
    private function assertCanParticipate(Player $player, Conversation $conversation,
373
        $message = "You are not allowed to participate in that discussion"
374
    ) {
375 1
        if (!$conversation->isMember($player)) {
376
            throw new ForbiddenException($message);
377
        }
378 1
    }
379
380
    /**
381
     * Sends a message to a conversation
382
     *
383
     * @param  Player        $from   The sender
384
     * @param  Conversation         $to     The conversation that will receive the message
385
     * @param  Form          $form   The message's form
386
     * @param  Form          $form   The form before it handled the request
387
     * @param  Form          $cloned
388
     * @throws HTTPException Thrown if the user doesn't have the
389
     *                              SEND_PRIVATE_MSG permission
390
     * @return void
391
     */
392 1
    private function sendMessage(Player $from, Conversation $to, &$form, $cloned)
393
    {
394 1
        if (!$from->hasPermission(Permission::SEND_PRIVATE_MSG)) {
395 1
            throw new ForbiddenException("You are not allowed to send messages");
396
        }
397
398 1
        $message = $form->get('message')->getData();
399 1
        $message = $to->sendMessage($from, $message);
400
401 1
        $this->getFlashBag()->add('success', "Your message was sent successfully");
402
403
        // Let javascript know the message's ID
404 1
        $this->attributes->set('id', $message->getId());
405
406
        // Reset the form
407 1
        $form = $cloned;
408
409
        // Notify everyone that we sent a new message
410 1
        $event = new NewMessageEvent($message, false);
411 1
        $this->dispatch(Events::MESSAGE_NEW, $event);
412 1
    }
413
414
    /**
415
     * @return string|null
416
     */
417
    private function getErrorMessage(Form $form)
418
    {
419
        foreach ($form->all() as $child) {
420
            foreach ($child->getErrors() as $error) {
421
                return $error->getMessage();
422
            }
423
        }
424
425
        foreach ($form->getErrors() as $error) {
426
            return $error->getMessage();
427
        }
428
429
        return "Unknown Error";
430
    }
431
432
    /**
433
     * Make sure that a player can edit a conversation
434
     *
435
     * Throws an exception if a player is not an admin or the leader of a team
436
     * @param  Player        $player  The player to test
437
     * @param  Conversation         $conversation   The team
438
     * @param  string        $message The error message to show
439
     * @throws HTTPException
440
     * @return void
441
     */
442
    private function assertCanEdit(Player $player, Conversation $conversation, $message = "You are not allowed to edit the discussion")
443
    {
444
        if ($conversation->getCreator()->getId() != $player->getId()) {
445
            throw new ForbiddenException($message);
446
        }
447
    }
448
}
449