Completed
Push — feature/player-elo ( 21d941...60e1be )
by Vladimir
11:51
created

MatchController   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 222
Duplicated Lines 21.17 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 33.63%

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 15
dl 47
loc 222
ccs 37
cts 110
cp 0.3363
rs 7.6361
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A showAction() 0 4 1
B recalculateAction() 0 33 4
B listAction() 0 34 4
B createAction() 16 16 5
A editAction() 16 16 3
A deleteAction() 15 15 4
A log() 0 6 1
A getMessages() 0 15 4
D validate() 0 40 10
B validateEdit() 0 10 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
3
use Symfony\Component\Form\FormError;
4
use Symfony\Component\HttpFoundation\RedirectResponse;
5
use Symfony\Component\HttpFoundation\Request;
6
use Symfony\Component\HttpFoundation\StreamedResponse;
7
8
class MatchController extends CRUDController
9
{
10
    /**
11
     * Whether the last edited match has had its ELO changed, requiring an ELO
12
     * recalculation
13
     *
14
     * This is useful so that a confirmation form is shown, asking the user if
15
     * they want to recalculate ELOs
16
     *
17
     * @var bool
18
     */
19
    public $recalculateNeeded = false;
20
21
    public function listAction(Request $request, Player $me, Team $team = null, Player $player = null, $type = null)
22
    {
23
        $qb = $this->getQueryBuilder();
24
25
        $currentPage = $request->query->get('page', 1);
26
27
        if ($player) {
28
            $team = $player;
29
        }
30
31
        $query = $qb->sortBy('time')->reverse()
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 with() does only exist in the following sub-classes of QueryBuilder: MatchQueryBuilder. 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
               ->with($team, $type)
33
               ->limit(50)->fromPage($currentPage);
34
35
        $matchType = $request->query->get('type', 'all');
36
37
        if (in_array($matchType, array(Match::FUN, Match::OFFICIAL, Match::SPECIAL))) {
38
            $query->where('type')->is($matchType);
39
        }
40
41
        $matches = $query->getModels($fast = true);
42
43
        foreach ($matches as $match) {
44
            // Don't show wrong labels for matches
45
            $match->getOriginalTimestamp()->setTimezone($me->getTimezone());
46
        }
47
48
        return array(
49
            "matches"     => $matches,
50
            "team"        => $team,
51
            "currentPage" => $currentPage,
52
            "totalPages"  => $qb->countPages()
53
        );
54
    }
55
56 1
    public function showAction(Match $match)
57
    {
58 1
        return array("match" => $match);
59
    }
60
61 1 View Code Duplication
    public function createAction(Player $me)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
62
    {
63
        return $this->create($me, function (Match $match) use ($me) {
64 1
            if ($me->canEdit($match)
65 1
                && $match->isOfficial()
66 1
                && (!$match->getTeamA()->isLastMatch($match)
67 1
                || !$match->getTeamB()->isLastMatch($match))
68
            ) {
69
                $url = Service::getGenerator()->generate('match_recalculate', array(
70
                    'match' => $match->getId(),
71
                ));
72
73
                return new RedirectResponse($url);
74
            }
75 1
        });
76
    }
77
78 View Code Duplication
    public function deleteAction(Player $me, Match $match)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
79
    {
80
        return $this->delete($match, $me, function () use ($match, $me) {
81
            if ($match->getTeamA()->isLastMatch($match)
82
                && $match->getTeamB()->isLastMatch($match)) {
83
                $match->resetELOs();
84
            } elseif ($me->canEdit($match)) {
85
                $url = Service::getGenerator()->generate('match_recalculate', array(
86
                    'match' => $match->getId(),
87
                ));
88
89
                return new RedirectResponse($url);
90
            }
91
        });
92
    }
93
94 View Code Duplication
    public function editAction(Player $me, Match $match)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
95
    {
96
        // TODO: Generating this response is unnecessary
97
        $response = $this->edit($match, $me, "match");
98
99
        if ($this->recalculateNeeded && $match->isOfficial()) {
100
            // Redirect to a confirmation form if we are assigning a new leader
101
            $url = Service::getGenerator()->generate('match_recalculate', array(
102
                'match' => $match->getId(),
103
            ));
104
105
            return new RedirectResponse($url);
106
        }
107
108
        return $response;
109
    }
110
111
    public function recalculateAction(Player $me, $match)
112
    {
113
        $match = Match::get($match); // get a match even if it's deleted
114
115
        if (!$me->canEdit($match)) {
116
            throw new ForbiddenException("You are not allowed to edit that match.");
117
        }
118
119
        if (!$match->isOfficial()) {
120
            throw new BadRequestException("You can't recalculate ELO history for a special match.");
121
        }
122
123
        return $this->showConfirmationForm(function () use ($match) {
124
            $response = new StreamedResponse();
125
            $response->headers->set('Content-Type', 'text/plain');
126
            $response->setCallback(function () use ($match) {
127
                $this->log(Match::recalculateMatchesSince($match));
128
            });
129
            $response->send();
130
        }, "Do you want to recalculate ELO history for all teams and matches after the specified match?",
131
            "ELO history recalculated",
132
            "Recalculate ELOs",
133
            function () use ($match) {
134
                if ($match->isDeleted()) {
135
                    return new RedirectResponse($match->getURL('list'));
136
                }
137
138
                return new RedirectResponse($match->getURL('show'));
139
            },
140
            "Match/recalculate.html.twig",
141
            $noButton = true
142
        );
143
    }
144
145
    /**
146
     * Echo a string and flush the buffers
147
     *
148
     * Useful for streamed AJAX responses
149
     *
150
     * @param string $string The string to echo
151
     */
152
    private function log($string)
153
    {
154
        echo $string;
155
        ob_flush();
156
        flush();
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 1
    protected function getMessages($type, $name = '')
163
    {
164 1
        $messages = parent::getMessages($type, $name);
165
166
        // Don't show the match info on the successful create/edit message
167 1
        foreach ($messages as &$action) {
168 1
            foreach ($action as &$status) {
169 1
                if (isset($status['named'])) {
170 1
                    $status['named'] = $status['unnamed'];
171
                }
172
            }
173
        }
174
175 1
        return $messages;
176
    }
177
178 1
    protected function validate($form)
179
    {
180
        // Make sure that two different teams participated in a match, i.e. a team
181
        // didn't match against itself
182 1
        $firstTeam  = $form->get('first_team')->get('team')->getData();
183 1
        $secondTeam = $form->get('second_team')->get('team')->getData();
184
185 1
        if (!$firstTeam || !$secondTeam) {
186
            return;
187
        }
188
189 1
        if ($firstTeam->isSameAs($secondTeam)) {
190 1
            $message = "You can't report a match where a team played against itself!";
191 1
            $form->addError(new FormError($message));
192
        }
193
194 1
        $matchType = $form->get('type')->getData();
195
196 1
        foreach (array('first_team', 'second_team') as $team) {
197 1
            $input = $form->get($team);
198 1
            $teamInput = $input->get('team');
199 1
            $teamParticipants = $input->get('participants');
200
201 1
            if ($matchType === Match::FUN) {
202 1
                if (!$teamInput->getData() instanceof ColorTeam) {
203
                    $message = "Please enter a team color for fun and special matches.";
204 1
                    $teamInput->addError(new FormError($message));
205
                }
206 1
            } elseif ($matchType === Match::OFFICIAL) {
207 1
                if ($teamInput->getData() instanceof ColorTeam) {
208 1
                    $participants = $teamParticipants->getData();
209
210 1
                    if (empty($participants)) {
211 1
                        $message = 'A player roster is necessary for a color team for a mixed official match.';
212 1
                        $teamInput->addError(new FormError($message));
213
                    }
214
                }
215
            }
216
        }
217 1
    }
218
219
    protected function validateEdit($form, $match)
220
    {
221
        if ($match->isOfficial() && $form->get('type')->getData() !== Match::OFFICIAL) {
222
            $message = "You cannot change this match's type.";
223
            $form->get('type')->addError(new FormError($message));
224
        } elseif (!$match->isOfficial() && $form->get('type')->getData() === Match::OFFICIAL) {
225
            $message = "You can't make this an official match.";
226
            $form->get('type')->addError(new FormError($message));
227
        }
228
    }
229
}
230