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 classes like QuizController 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 QuizController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
10 | class QuizController extends GameController |
||
11 | { |
||
12 | /** |
||
13 | * |
||
14 | * @var gameService |
||
15 | */ |
||
16 | protected $gameService; |
||
17 | |||
18 | public function __construct(ServiceLocatorInterface $locator) |
||
22 | |||
23 | public function playAction() |
||
24 | { |
||
25 | // the quiz is done for the first time in this entry |
||
26 | $firstTime = true; |
||
27 | $playError = null; |
||
28 | $entry = $this->getGameService()->play($this->game, $this->user, $playError); |
||
29 | if (!$entry) { |
||
30 | $reason = ""; |
||
|
|||
31 | if ($playError === -1) { |
||
32 | // the user has already taken part to this game and the participation limit has been reached |
||
33 | $this->flashMessenger()->addMessage('Vous avez déjà participé'); |
||
34 | $reason = '?playLimitReached=1'; |
||
35 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
36 | $this->game->getClassType().'/result', |
||
37 | array( |
||
38 | 'id' => $this->game->getIdentifier(), |
||
39 | ) |
||
40 | ) .$reason; |
||
41 | } elseif ($playError === -2) { |
||
42 | // the user has not accepted the mandatory rules of the game |
||
43 | $this->flashMessenger()->addMessage('Vous devez accepter le réglement'); |
||
44 | $reason = '?NoOptin=1'; |
||
45 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
46 | $this->game->getClassType(), |
||
47 | array( |
||
48 | 'id' => $this->game->getIdentifier(), |
||
49 | ) |
||
50 | ) .$reason; |
||
51 | } elseif ($playError === -3) { |
||
52 | // the user has enough points to buy an entry to this game |
||
53 | $this->flashMessenger()->addMessage("Vous ne pouvez pas acheter la partie"); |
||
54 | $reason = '?NotPaid=1'; |
||
55 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
56 | $this->game->getClassType(), |
||
57 | array( |
||
58 | 'id' => $this->game->getIdentifier(), |
||
59 | ) |
||
60 | ) .$reason; |
||
61 | View Code Duplication | } else { |
|
62 | $this->flashMessenger()->addMessage("An error occurred. Please try again later"); |
||
63 | $reason = '?Error=1'; |
||
64 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
65 | $this->game->getClassType(), |
||
66 | array( |
||
67 | 'id' => $this->game->getIdentifier(), |
||
68 | ) |
||
69 | ) .$reason; |
||
70 | } |
||
71 | |||
72 | return $this->redirect()->toUrl($noEntryRedirect); |
||
73 | } |
||
74 | |||
75 | $reply = $this->getGameService()->getQuizReplyMapper()->getLastGameReply($entry); |
||
76 | $userAnswers = array(); |
||
77 | if ($reply) { |
||
78 | $firstTime = false; |
||
79 | foreach ($reply->getAnswers() as $answer) { |
||
80 | $userAnswers[$answer->getQuestionId()][$answer->getAnswerId()] = true; |
||
81 | $userAnswers[$answer->getQuestionId()]['answer'] = $answer->getAnswer(); |
||
82 | } |
||
83 | } |
||
84 | |||
85 | $questions = $this->game->getQuestions(); |
||
86 | $totalQuestions = count($questions); |
||
87 | |||
88 | $form = new Form(); |
||
89 | |||
90 | $inputFilter = new \Zend\InputFilter\InputFilter(); |
||
91 | $factory = new InputFactory(); |
||
92 | |||
93 | $i = 0; |
||
94 | $j = 0; |
||
95 | $elementData = array(); |
||
96 | $explanations = array(); |
||
97 | $data = $this->getRequest()->getPost()->toArray(); |
||
98 | $anticheat = array(); |
||
99 | |||
100 | foreach ($questions as $q) { |
||
101 | if (($this->game->getQuestionGrouping() > 0 && $i % $this->game->getQuestionGrouping() === 0) |
||
102 | || ($i === 0 && $this->game->getQuestionGrouping() === 0) |
||
103 | ) { |
||
104 | $fieldsetName = 'questionGroup' . ++ $j; |
||
105 | $fieldset = new Fieldset($fieldsetName); |
||
106 | } |
||
107 | |||
108 | if ($this->getRequest()->isPost()) { |
||
109 | $jsonData = json_decode($q->getJsonData(), true); |
||
110 | // décalage de 2h avec UTC |
||
111 | $date = (isset($jsonData['stopdate'])) ? strtotime($jsonData['stopdate']) : false; |
||
112 | |||
113 | if ($date) { |
||
114 | $now = time(); |
||
115 | if ($now > $date) { |
||
116 | $anticheat[] = $q->getId(); |
||
117 | continue; |
||
118 | } |
||
119 | } |
||
120 | } |
||
121 | |||
122 | $name = 'q' . $q->getId(); |
||
123 | $fieldsetFilter = new \Zend\InputFilter\InputFilter(); |
||
124 | |||
125 | if ($q->getType() === 0) { |
||
126 | $element = new Element\Radio($name); |
||
127 | $values = array(); |
||
128 | $valuesSortedByPosition = array(); |
||
129 | $position = 0; |
||
130 | foreach ($q->getAnswers() as $a) { |
||
131 | $status = ( |
||
132 | isset($userAnswers[$q->getId()]) && |
||
133 | isset($userAnswers[$q->getId()][$a->getId()]) |
||
134 | )? true:false; |
||
135 | $pos = ($a->getPosition() == 0 && isset($values[$a->getPosition()])) ? $position : $a->getPosition(); |
||
136 | $values[$pos] = array( |
||
137 | 'id' => $a->getId(), |
||
138 | 'position' => $pos, |
||
139 | 'answer' => $a->getAnswer(), |
||
140 | 'checked' => $status |
||
141 | ); |
||
142 | $explanations[$a->getAnswer()] = $a->getExplanation(); |
||
143 | ++$position; |
||
144 | } |
||
145 | ksort($values); |
||
146 | foreach ($values as $key => $value) { |
||
147 | $valuesSortedByPosition[$value['id']] = $value['answer']; |
||
148 | if ($value['checked']) { |
||
149 | $element->setValue($value['id']); |
||
150 | } |
||
151 | } |
||
152 | $element->setValueOptions($valuesSortedByPosition); |
||
153 | $element->setLabelOptions(array("disable_html_escape"=>true)); |
||
154 | $elementData[$q->getId()] = new Element\Hidden($name.'-data'); |
||
155 | } elseif ($q->getType() === 1) { |
||
156 | $element = new Element\MultiCheckbox($name); |
||
157 | $values = array(); |
||
158 | $valuesSortedByPosition = array(); |
||
159 | foreach ($q->getAnswers() as $a) { |
||
160 | $values[$a->getId()] = array( |
||
161 | 'id' => $a->getId(), |
||
162 | 'position' => $a->getPosition(), |
||
163 | 'answer' => $a->getAnswer(), |
||
164 | ); |
||
165 | $explanations[$a->getAnswer()] = $a->getExplanation(); |
||
166 | $elementData[$a->getId()] = new Element\Hidden($name.'-'.$a->getId().'-data'); |
||
167 | } |
||
168 | |||
169 | foreach ($values as $key => $value) { |
||
170 | $valuesSortedByPosition[$value['id']] = $value['answer']; |
||
171 | } |
||
172 | |||
173 | $element->setValueOptions($valuesSortedByPosition); |
||
174 | $element->setLabelOptions(array("disable_html_escape"=>true)); |
||
175 | } elseif ($q->getType() == 2) { |
||
176 | $element = new Element\Textarea($name); |
||
177 | if (isset($userAnswers[$q->getId()])) { |
||
178 | $element->setValue($userAnswers[$q->getId()]['answer']); |
||
179 | } |
||
180 | $elementData[$q->getId()] = new Element\Hidden($name.'-data'); |
||
181 | } |
||
182 | |||
183 | $element->setLabel($q->getQuestion()); |
||
184 | $fieldset->add($element); |
||
185 | if (is_array($elementData)) { |
||
186 | foreach ($elementData as $id => $e) { |
||
187 | $fieldset->add($e); |
||
188 | } |
||
189 | } else { |
||
190 | $fieldset->add($elementData); |
||
191 | } |
||
192 | |||
193 | $fieldsetFilter->add( |
||
194 | $factory->createInput( |
||
195 | [ |
||
196 | 'name' => $name, |
||
197 | 'required' => true, |
||
198 | 'validators' => [ |
||
199 | [ |
||
200 | 'name' =>'NotEmpty', |
||
201 | 'options' => [ |
||
202 | 'messages' => [ |
||
203 | 'isEmpty' => 'Merci de répondre à la question.', |
||
204 | ], |
||
205 | ], |
||
206 | ], |
||
207 | ] |
||
208 | ] |
||
209 | ) |
||
210 | ); |
||
211 | |||
212 | $i ++; |
||
213 | if (($this->game->getQuestionGrouping() > 0 && $i % $this->game->getQuestionGrouping() == 0 && $i > 0) |
||
214 | || $i == $totalQuestions |
||
215 | ) { |
||
216 | $form->add($fieldset); |
||
217 | $inputFilter->add($fieldsetFilter, $fieldsetName); |
||
218 | } |
||
219 | } |
||
220 | |||
221 | $form->setInputFilter($inputFilter); |
||
222 | |||
223 | if ($this->getRequest()->isPost()) { |
||
224 | foreach ($anticheat as $id) { |
||
225 | $j = 0; |
||
226 | $i = 0; |
||
227 | foreach ($questions as $q) { |
||
228 | if (($this->game->getQuestionGrouping() > 0 && $i % $this->game->getQuestionGrouping() == 0) || ($i == 0 && $this->game->getQuestionGrouping() == 0)) { |
||
229 | $fieldsetName = 'questionGroup' . ++ $j; |
||
230 | } |
||
231 | if ($q->getId() == $id) { |
||
232 | unset($data[$fieldsetName]['q'.$q->getId()]); |
||
233 | } |
||
234 | $i++; |
||
235 | } |
||
236 | } |
||
237 | $action = $this->params('action'); |
||
238 | |||
239 | // On POST, if the anonymousUser has not been created yet, I try to create it now |
||
240 | // Maybe is there only one form for the quiz and the player data... I try... |
||
241 | // And if the formPlayer data was included in the form, I remove it |
||
242 | if (!$this->user && $this->game->getAnonymousAllowed() && $this->game->getAnonymousIdentifier()) { |
||
243 | $session = new \Zend\Session\Container('anonymous_identifier'); |
||
244 | if (empty($session->offsetGet('anonymous_identifier'))) { |
||
245 | $controller = __NAMESPACE__ . '\\' . ucfirst($this->game->getClassType()); |
||
246 | $registerUser = $this->forward()->dispatch( |
||
247 | $controller, |
||
248 | array( |
||
249 | 'action' => 'register', |
||
250 | 'id' => $this->game->getIdentifier() |
||
251 | ) |
||
252 | ); |
||
253 | |||
254 | foreach ($data as $index => $el) { |
||
255 | if (! is_array($el)) { |
||
256 | unset($data[$index]); |
||
257 | } |
||
258 | } |
||
259 | $playError = null; |
||
260 | $entry = $this->getGameService()->play($this->game, $this->user, $playError); |
||
261 | View Code Duplication | if (!$entry) { |
|
262 | $reason = ""; |
||
263 | if ($playError === -1) { |
||
264 | // the user has already taken part to this game and the participation limit has been reached |
||
265 | $this->flashMessenger()->addMessage('Vous avez déjà participé'); |
||
266 | $reason = '?playLimitReached=1'; |
||
267 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
268 | $this->game->getClassType().'/result', |
||
269 | array( |
||
270 | 'id' => $this->game->getIdentifier(), |
||
271 | ) |
||
272 | ) .$reason; |
||
273 | } elseif ($playError === -2) { |
||
274 | // the user has not accepted the mandatory rules of the game |
||
275 | $this->flashMessenger()->addMessage('Vous devez accepter le réglement'); |
||
276 | $reason = '?NoOptin=1'; |
||
277 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
278 | $this->game->getClassType(), |
||
279 | array( |
||
280 | 'id' => $this->game->getIdentifier(), |
||
281 | ) |
||
282 | ) .$reason; |
||
283 | } elseif ($playError === -3) { |
||
284 | // the user has enough points to buy an entry to this game |
||
285 | $this->flashMessenger()->addMessage("Vous ne pouvez pas acheter la partie"); |
||
286 | $reason = '?NotPaid=1'; |
||
287 | $noEntryRedirect = $this->frontendUrl()->fromRoute( |
||
288 | $this->game->getClassType(), |
||
289 | array( |
||
290 | 'id' => $this->game->getIdentifier(), |
||
291 | ) |
||
292 | ) .$reason; |
||
293 | } |
||
294 | |||
295 | return $this->redirect()->toUrl($noEntryRedirect); |
||
296 | } |
||
297 | } |
||
298 | } |
||
299 | |||
300 | $form->setData($data); |
||
301 | |||
302 | // Improve it : I don't validate the form in a timer quiz as no answer is mandatory |
||
303 | if ($this->game->getTimer() || $form->isValid()) { |
||
304 | unset($data['submitForm']); |
||
305 | $entry = $this->getGameService()->createQuizReply($data, $this->game, $this->user); |
||
306 | } |
||
307 | |||
308 | return $this->redirect()->toUrl( |
||
309 | $this->frontendUrl()->fromRoute( |
||
310 | $this->game->getClassType() . '/'. $this->game->nextStep($action), |
||
311 | array('id' => $this->game->getIdentifier()) |
||
312 | ) |
||
313 | ); |
||
314 | } |
||
315 | |||
316 | $viewModel = $this->buildView($this->game); |
||
317 | $viewModel->setVariables( |
||
318 | [ |
||
319 | 'firstTime' => $firstTime, |
||
320 | 'questions' => $questions, |
||
321 | 'form' => $form, |
||
322 | 'explanations' => $explanations, |
||
323 | 'flashMessages' => $this->flashMessenger()->getMessages(), |
||
324 | ] |
||
325 | ); |
||
326 | |||
327 | return $viewModel; |
||
328 | } |
||
329 | |||
330 | public function resultAction() |
||
331 | { |
||
332 | $playLimitReached = false; |
||
333 | if ($this->getRequest()->getQuery()->get('playLimitReached')) { |
||
334 | $playLimitReached = true; |
||
335 | } |
||
336 | $statusMail = null; |
||
337 | $prediction = false; |
||
338 | $userTimer = array(); |
||
339 | |||
340 | $lastEntry = $this->getGameService()->findLastEntry($this->game, $this->user); |
||
341 | if (!$lastEntry) { |
||
342 | return $this->redirect()->toUrl( |
||
343 | $this->frontendUrl()->fromRoute( |
||
344 | 'quiz', |
||
345 | array('id' => $this->game->getIdentifier()), |
||
346 | array('force_canonical' => true) |
||
347 | ) |
||
348 | ); |
||
349 | } |
||
350 | |||
351 | // je compte les bonnes réponses et le ratio |
||
352 | $maxCorrectAnswers = $this->game->getMaxCorrectAnswers(); |
||
353 | $winner = $lastEntry->getWinner(); |
||
354 | $reply = $this->getGameService()->getQuizReplyMapper()->getLastGameReply($lastEntry); |
||
355 | $userCorrectAnswers = 0; |
||
356 | $correctAnswers = array(); |
||
357 | $userAnswers = array(); |
||
358 | |||
359 | if ($reply !== null) { |
||
360 | foreach ($reply->getAnswers() as $answer) { |
||
361 | if ($answer->getCorrect()) { |
||
362 | $correctAnswers[$answer->getQuestionId()][$answer->getAnswerId()] = true; |
||
363 | ++$userCorrectAnswers; |
||
364 | } |
||
365 | $userAnswers[$answer->getQuestionId()][$answer->getAnswerId()] = true; |
||
366 | $userAnswers[$answer->getQuestionId()]['answer'] = $answer->getAnswer(); |
||
367 | } |
||
368 | } |
||
369 | |||
370 | $ratioCorrectAnswers = 0; |
||
371 | if ($maxCorrectAnswers > 0) { |
||
372 | $ratioCorrectAnswers = ($userCorrectAnswers / $maxCorrectAnswers) * 100; |
||
373 | } else { |
||
374 | $ratioCorrectAnswers = 100; |
||
375 | } |
||
376 | |||
377 | if ($this->game->getTimer()) { |
||
378 | $timer = $this->getGameService()->getEntryMapper()->findOneBy( |
||
379 | array('game' => $this->game, 'user'=> $this->user) |
||
380 | ); |
||
381 | $start = $timer->getCreatedAt()->format('U'); |
||
382 | $end = $timer->getUpdatedAt()->format('U'); |
||
383 | $userTimer = array( |
||
384 | 'ratio' => $ratioCorrectAnswers, |
||
385 | 'timer' => $end - $start, |
||
386 | ); |
||
387 | } |
||
388 | |||
389 | // The distribution of answers for each question |
||
390 | $distribution = $this->getGameService()->getAnswersDistribution($this->game); |
||
391 | |||
392 | // Je prépare le tableau des bonnes réponses trouvées et non trouvées |
||
393 | $ga = array(); |
||
394 | $questions = $this->game->getQuestions(); |
||
395 | foreach ($questions as $q) { |
||
396 | foreach ($q->getAnswers() as $a) { |
||
397 | if ($a->getCorrect()) { |
||
398 | $ga[$q->getId()]['question'] = $q; |
||
399 | $ga[$q->getId()]['answers'][$a->getId()]['answer'] = $a->getAnswer(); |
||
400 | $ga[$q->getId()]['answers'][$a->getId()]['explanation'] = $a->getExplanation(); |
||
401 | $ga[$q->getId()]['answers'][$a->getId()]['userAnswer'] = isset($userAnswers[$q->getId()]) ? |
||
402 | $userAnswers[$q->getId()]['answer'] : |
||
403 | false; |
||
404 | |||
405 | View Code Duplication | if (isset($correctAnswers[$q->getId()]) && isset($correctAnswers[$q->getId()][$a->getId()])) { |
|
406 | $ga[$q->getId()]['answers'][$a->getId()]['found'] = true; |
||
407 | } else { |
||
408 | $ga[$q->getId()]['answers'][$a->getId()]['found'] = false; |
||
409 | } |
||
410 | |||
411 | View Code Duplication | if (isset($userAnswers[$q->getId()]) && isset($userAnswers[$q->getId()][$a->getId()])) { |
|
412 | $ga[$q->getId()]['answers'][$a->getId()]['yourChoice'] = true; |
||
413 | } else { |
||
414 | $ga[$q->getId()]['answers'][$a->getId()]['yourChoice'] = false; |
||
415 | } |
||
416 | |||
417 | $ga[$q->getId()]['answers'][$a->getId()]['correctAnswers'] = true; |
||
418 | } else { |
||
419 | $ga[$q->getId()]['question'] = $q; |
||
420 | $ga[$q->getId()]['answers'][$a->getId()]['answer'] = $a->getAnswer(); |
||
421 | $ga[$q->getId()]['answers'][$a->getId()]['explanation'] = $a->getExplanation(); |
||
422 | $ga[$q->getId()]['answers'][$a->getId()]['correctAnswers'] = false; |
||
423 | $ga[$q->getId()]['answers'][$a->getId()]['userAnswer'] = isset($userAnswers[$q->getId()]) ? |
||
424 | $userAnswers[$q->getId()]['answer'] : |
||
425 | false; |
||
426 | |||
427 | View Code Duplication | if (isset($userAnswers[$q->getId()]) && isset($userAnswers[$q->getId()][$a->getId()])) { |
|
428 | $ga[$q->getId()]['answers'][$a->getId()]['yourChoice'] = true; |
||
429 | } else { |
||
430 | $ga[$q->getId()]['answers'][$a->getId()]['yourChoice'] = false; |
||
431 | } |
||
432 | } |
||
433 | } |
||
434 | // if only one question is a prediction, we can't determine if it's a winner or looser |
||
435 | if ($q->getPrediction()) { |
||
436 | $prediction = true; |
||
437 | } |
||
438 | } |
||
439 | |||
440 | $form = $this->getServiceLocator()->get('playgroundgame_sharemail_form'); |
||
441 | $form->setAttribute('method', 'post'); |
||
442 | |||
443 | $viewModel = $this->buildView($this->game); |
||
444 | |||
445 | // TODO: Change the way we know if the play step has been rejected |
||
446 | $messages = $this->flashMessenger()->getMessages(); |
||
447 | if (!isset($messages[0]) || substr($messages[0], 0, 9) != 'Vous avez') { |
||
448 | $this->getGameService()->sendMail($this->game, $this->user, $lastEntry); |
||
449 | } |
||
450 | |||
451 | $viewModel->setVariables( |
||
452 | [ |
||
453 | 'entry' => $lastEntry, |
||
454 | 'statusMail' => $statusMail, |
||
455 | 'form' => $form, |
||
456 | 'winner' => $winner, |
||
457 | 'prediction' => $prediction, |
||
458 | 'userCorrectAnswers' => $userCorrectAnswers, |
||
459 | 'maxCorrectAnswers' => $maxCorrectAnswers, |
||
460 | 'ratioCorrectAnswers' => $ratioCorrectAnswers, |
||
461 | 'gameCorrectAnswers' => $ga, |
||
462 | 'userTimer' => $userTimer, |
||
463 | 'userAnswers' => $userAnswers, |
||
464 | 'flashMessages' => $this->flashMessenger()->getMessages(), |
||
465 | 'playLimitReached' => $playLimitReached, |
||
466 | 'distribution' => $distribution, |
||
467 | ] |
||
468 | ); |
||
469 | |||
470 | return $viewModel; |
||
471 | } |
||
472 | |||
473 | View Code Duplication | public function fbshareAction() |
|
494 | |||
495 | View Code Duplication | public function fbrequestAction() |
|
515 | |||
516 | View Code Duplication | public function tweetAction() |
|
536 | |||
537 | View Code Duplication | public function googleAction() |
|
557 | |||
558 | public function getGameService() |
||
566 | } |
||
567 |
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
Both the
$myVar
assignment in line 1 and the$higher
assignment in line 2 are dead. The first because$myVar
is never used and the second because$higher
is always overwritten for every possible time line.