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 EntriesController 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 EntriesController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
43 | class EntriesController extends AppController |
||
44 | { |
||
45 | use RequestActionTrait; |
||
|
|||
46 | |||
47 | public $helpers = ['Posting', 'Text']; |
||
48 | |||
49 | public $actionAuthConfig = [ |
||
50 | 'ajaxToggle' => 'mod', |
||
51 | 'merge' => 'mod', |
||
52 | 'delete' => 'mod' |
||
53 | ]; |
||
54 | |||
55 | /** |
||
56 | * {@inheritDoc} |
||
57 | */ |
||
58 | public function initialize() |
||
59 | { |
||
60 | parent::initialize(); |
||
61 | |||
62 | $this->loadComponent('MarkAsRead'); |
||
63 | $this->loadComponent('Referer'); |
||
64 | $this->loadComponent('Threads'); |
||
65 | } |
||
66 | |||
67 | /** |
||
68 | * posting index |
||
69 | * |
||
70 | * @return void|\Cake\Network\Response |
||
71 | */ |
||
72 | public function index() |
||
73 | { |
||
74 | Stopwatch::start('Entries->index()'); |
||
75 | |||
76 | //= determine user sort order |
||
77 | $sortKey = 'last_answer'; |
||
78 | if (!$this->CurrentUser->get('user_sort_last_answer')) { |
||
79 | $sortKey = 'time'; |
||
80 | } |
||
81 | $order = ['fixed' => 'DESC', $sortKey => 'DESC']; |
||
82 | |||
83 | //= get threads |
||
84 | $threads = $this->Threads->paginate($order); |
||
85 | $this->set('entries', $threads); |
||
86 | |||
87 | $currentPage = (int)$this->request->getQuery('page') ?: 1; |
||
88 | if ($currentPage > 1) { |
||
89 | $this->set('titleForLayout', __('page') . ' ' . $currentPage); |
||
90 | } |
||
91 | if ($currentPage === 1) { |
||
92 | if ($this->MarkAsRead->refresh()) { |
||
93 | return $this->redirect(['action' => 'index']); |
||
94 | } |
||
95 | $this->MarkAsRead->next(); |
||
96 | } |
||
97 | |||
98 | // @bogus |
||
99 | $this->request->getSession()->write('paginator.lastPage', $currentPage); |
||
100 | $this->set('showDisclaimer', true); |
||
101 | $this->set('showBottomNavigation', true); |
||
102 | $this->set('allowThreadCollapse', true); |
||
103 | $this->Slidetabs->show(); |
||
104 | |||
105 | $this->_setupCategoryChooser($this->CurrentUser); |
||
106 | |||
107 | /** @var AutoReloadComponent */ |
||
108 | $autoReload = $this->loadComponent('AutoReload'); |
||
109 | $autoReload->after($this->CurrentUser); |
||
110 | |||
111 | Stopwatch::stop('Entries->index()'); |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Mix view |
||
116 | * |
||
117 | * @param string $tid thread-ID |
||
118 | * @return void|Response |
||
119 | * @throws NotFoundException |
||
120 | */ |
||
121 | public function mix($tid) |
||
122 | { |
||
123 | $tid = (int)$tid; |
||
124 | if ($tid <= 0) { |
||
125 | throw new BadRequestException(); |
||
126 | } |
||
127 | |||
128 | $postings = $this->Entries->treeForNode( |
||
129 | $tid, |
||
130 | ['root' => true, 'complete' => true] |
||
131 | ); |
||
132 | |||
133 | /// redirect sub-posting to mix view of thread |
||
134 | if (!$postings) { |
||
135 | $post = $this->Entries->find() |
||
136 | ->select(['tid']) |
||
137 | ->where(['id' => $tid]) |
||
138 | ->first(); |
||
139 | if (!empty($post)) { |
||
140 | return $this->redirect([$post->get('tid'), '#' => $tid], 301); |
||
141 | } |
||
142 | throw new NotFoundException; |
||
143 | } |
||
144 | |||
145 | // check if anonymous tries to access internal categories |
||
146 | $root = $postings; |
||
147 | if (!$this->CurrentUser->getCategories()->permission('read', $root->get('category'))) { |
||
148 | return $this->_requireAuth(); |
||
149 | } |
||
150 | |||
151 | $this->_setRootEntry($root); |
||
152 | $this->Title->setFromPosting($root, __('view.type.mix')); |
||
153 | |||
154 | $this->set('showBottomNavigation', true); |
||
155 | $this->set('entries', $postings); |
||
156 | |||
157 | $this->_showAnsweringPanel(); |
||
158 | |||
159 | $this->Threads->incrementViews($root, 'thread'); |
||
160 | $this->MarkAsRead->thread($postings); |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * load front page force all entries mark-as-read |
||
165 | * |
||
166 | * @return void |
||
167 | */ |
||
168 | public function update() |
||
169 | { |
||
170 | $this->autoRender = false; |
||
171 | $this->CurrentUser->getLastRefresh()->set(); |
||
172 | $this->redirect('/entries/index'); |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Outputs raw markup of an posting $id |
||
177 | * |
||
178 | * @param string $id posting-ID |
||
179 | * @return void |
||
180 | */ |
||
181 | public function source($id = null) |
||
182 | { |
||
183 | $this->viewBuilder()->enableAutoLayout(false); |
||
184 | $this->view($id); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * View posting. |
||
189 | * |
||
190 | * @param string $id posting-ID |
||
191 | * @return \Cake\Network\Response|void |
||
192 | */ |
||
193 | public function view($id = null) |
||
194 | { |
||
195 | Stopwatch::start('Entries->view()'); |
||
196 | |||
197 | // redirect if no id is given |
||
198 | if (!$id) { |
||
199 | $this->Flash->set(__('Invalid post'), ['element' => 'error']); |
||
200 | |||
201 | return $this->redirect(['action' => 'index']); |
||
202 | } |
||
203 | |||
204 | $entry = $this->Entries->get($id); |
||
205 | |||
206 | // redirect if posting doesn't exists |
||
207 | if ($entry == false) { |
||
208 | $this->Flash->set(__('Invalid post')); |
||
209 | |||
210 | return $this->redirect('/'); |
||
211 | } |
||
212 | |||
213 | if (!$this->CurrentUser->getCategories()->permission('read', $entry->get('category'))) { |
||
214 | return $this->_requireAuth(); |
||
215 | } |
||
216 | |||
217 | $this->set('entry', $entry); |
||
218 | $this->Threads->incrementViews($entry); |
||
219 | $this->_setRootEntry($entry); |
||
220 | $this->_showAnsweringPanel(); |
||
221 | |||
222 | $this->MarkAsRead->posting($entry); |
||
223 | |||
224 | // inline open |
||
225 | if ($this->request->is('ajax')) { |
||
226 | return $this->render('/Element/entry/view_posting'); |
||
227 | } |
||
228 | |||
229 | // full page request |
||
230 | $this->set( |
||
231 | 'tree', |
||
232 | $this->Entries->treeForNode($entry->get('tid'), ['root' => true]) |
||
233 | ); |
||
234 | $this->Title->setFromPosting($entry); |
||
235 | |||
236 | Stopwatch::stop('Entries->view()'); |
||
237 | } |
||
238 | |||
239 | /** |
||
240 | * Add new posting. |
||
241 | * |
||
242 | * @param null|string $id parent-ID if is answer |
||
243 | * @return void|\Cake\Network\Response |
||
244 | * @throws ForbiddenException |
||
245 | */ |
||
246 | public function add($id = null) |
||
247 | { |
||
248 | $titleForPage = __('Write a New Posting'); |
||
249 | |||
250 | if (!empty($this->request->getData())) { |
||
251 | //= insert new posting |
||
252 | $posting = $this->Entries->createPosting( |
||
253 | $this->request->getData(), |
||
254 | $this->CurrentUser |
||
255 | ); |
||
256 | |||
257 | // inserting new posting was successful |
||
258 | if ($posting && !count($posting->getErrors())) { |
||
259 | $id = $posting->get('id'); |
||
260 | $pid = $posting->get('pid'); |
||
261 | $tid = $posting->get('tid'); |
||
262 | |||
263 | if ($this->request->is('ajax')) { |
||
264 | if ($this->Referer->wasAction('index')) { |
||
265 | //= inline answer |
||
266 | $json = json_encode( |
||
267 | ['id' => $id, 'pid' => $pid, 'tid' => $tid] |
||
268 | ); |
||
269 | $this->response = $this->response->withType('json'); |
||
270 | $this->response = $this->response->withStringBody($json); |
||
271 | } |
||
272 | |||
273 | return $this->response; |
||
274 | } else { |
||
275 | //= answering through POST request |
||
276 | $url = ['controller' => 'entries']; |
||
277 | if ($this->Referer->wasAction('mix')) { |
||
278 | //= answer came from mix-view |
||
279 | $url += ['action' => 'mix', $tid, '#' => $id]; |
||
280 | } else { |
||
281 | //= normal posting from entries/add or entries/view |
||
282 | $url += ['action' => 'view', $posting->get('id')]; |
||
283 | } |
||
284 | |||
285 | return $this->redirect($url); |
||
286 | } |
||
287 | } else { |
||
288 | //= Error while trying to save a post |
||
289 | /** @var Entry */ |
||
290 | $posting = $this->Entries->newEntity($this->request->getData()); |
||
291 | |||
292 | if (count($posting->getErrors()) === 0) { |
||
293 | //= Error isn't displayed as form validation error. |
||
294 | $this->Flash->set( |
||
295 | __( |
||
296 | 'Something clogged the tubes. Could not save entry. Try again.' |
||
297 | ), |
||
298 | ['element' => 'error'] |
||
299 | ); |
||
300 | } |
||
301 | } |
||
302 | } else { |
||
303 | //= show form |
||
304 | $posting = $this->Entries->newEntity(); |
||
305 | $isAnswer = $id !== null; |
||
306 | |||
307 | if ($isAnswer) { |
||
308 | //= answer to existing posting |
||
309 | if ($this->request->is('ajax') === false) { |
||
310 | return $this->redirect($this->referer()); |
||
311 | } |
||
312 | |||
313 | $parent = $this->Entries->get($id); |
||
314 | |||
315 | if ($parent->isAnsweringForbidden()) { |
||
316 | throw new ForbiddenException; |
||
317 | } |
||
318 | |||
319 | $this->set('citeSubject', $parent->get('subject')); |
||
320 | $this->set('citeText', $parent->get('text')); |
||
321 | |||
322 | /** @var Entry */ |
||
323 | $posting = $this->Entries->patchEntity( |
||
324 | $posting, |
||
325 | ['pid' => $id] |
||
326 | ); |
||
327 | |||
328 | // set Subnav |
||
329 | $headerSubnavLeftTitle = __( |
||
330 | 'back_to_posting_from_linkname', |
||
331 | $parent->get('user')->get('username') |
||
332 | ); |
||
333 | $this->set('headerSubnavLeftTitle', $headerSubnavLeftTitle); |
||
334 | $titleForPage = __('Write a Reply'); |
||
335 | } else { |
||
336 | // new posting which creates new thread |
||
337 | /** @var Entry */ |
||
338 | $posting = $this->Entries->patchEntity( |
||
339 | $posting, |
||
340 | ['pid' => 0, 'tid' => 0] |
||
341 | ); |
||
342 | } |
||
343 | } |
||
344 | |||
345 | $isInline = $isAnswer = !$posting->isRoot(); |
||
346 | $formId = $posting->get('pid'); |
||
347 | |||
348 | $this->set( |
||
349 | compact('isAnswer', 'isInline', 'formId', 'posting', 'titleForPage') |
||
350 | ); |
||
351 | $this->_setAddViewVars($isAnswer); |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * Get thread-line to insert after an inline-answer |
||
356 | * |
||
357 | * @param string $id posting-ID |
||
358 | * @return void|\Cake\Network\Response |
||
359 | */ |
||
360 | public function threadLine($id = null) |
||
361 | { |
||
362 | $posting = $this->Entries->get($id); |
||
363 | if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) { |
||
364 | return $this->_requireAuth(); |
||
365 | } |
||
366 | |||
367 | $this->set('entrySub', $posting); |
||
368 | // ajax requests so far are always answers |
||
369 | $this->response = $this->response->withType('json'); |
||
370 | $this->set('level', '1'); |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Edit posting |
||
375 | * |
||
376 | * @param string $id posting-ID |
||
377 | * @return void|\Cake\Network\Response |
||
378 | * @throws NotFoundException |
||
379 | * @throws BadRequestException |
||
380 | */ |
||
381 | public function edit($id = null) |
||
382 | { |
||
383 | if (empty($id)) { |
||
384 | throw new BadRequestException; |
||
385 | } |
||
386 | |||
387 | $posting = $this->Entries->get($id, ['return' => 'Entity']); |
||
388 | if (!$posting) { |
||
389 | throw new NotFoundException; |
||
390 | } |
||
391 | |||
392 | // try to save edit |
||
393 | $data = $this->request->getData(); |
||
394 | if (!empty($data)) { |
||
395 | $newEntry = $this->Entries->updatePosting($posting, $data, $this->CurrentUser); |
||
396 | View Code Duplication | if ($newEntry) { |
|
397 | return $this->redirect(['action' => 'view', $id]); |
||
398 | } else { |
||
399 | $this->Flash->set( |
||
400 | __( |
||
401 | 'Something clogged the tubes. Could not save entry. Try again.' |
||
402 | ), |
||
403 | ['element' => 'warning'] |
||
404 | ); |
||
405 | } |
||
406 | } |
||
407 | |||
408 | // show editing form |
||
409 | if (!$posting->toPosting()->isEditingAsUserAllowed()) { |
||
410 | $this->Flash->set( |
||
411 | __('notice_you_are_editing_as_mod'), |
||
412 | ['element' => 'warning'] |
||
413 | ); |
||
414 | } |
||
415 | |||
416 | $this->Entries->patchEntity($posting, $this->request->getData()); |
||
417 | |||
418 | // get text of parent entry for citation |
||
419 | $parentEntryId = $posting->get('pid'); |
||
420 | if ($parentEntryId > 0) { |
||
421 | $parentEntry = $this->Entries->get($parentEntryId); |
||
422 | $this->set('citeText', $parentEntry->get('text')); |
||
423 | } |
||
424 | |||
425 | $isAnswer = !$posting; |
||
426 | $isInline = false; |
||
427 | $formId = $posting->get('pid'); |
||
428 | |||
429 | $this->set(compact('isAnswer', 'isInline', 'formId', 'posting')); |
||
430 | |||
431 | // set headers |
||
432 | $this->set( |
||
433 | 'headerSubnavLeftTitle', |
||
434 | __( |
||
435 | 'back_to_posting_from_linkname', |
||
436 | $posting->get('user')->get('username') |
||
437 | ) |
||
438 | ); |
||
439 | $this->set('headerSubnavLeftUrl', ['action' => 'view', $id]); |
||
440 | $this->set('form_title', __('edit_linkname')); |
||
441 | $this->_setAddViewVars($isAnswer); |
||
442 | $this->render('/Entries/add'); |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * Delete posting |
||
447 | * |
||
448 | * @param string $id posting-ID |
||
449 | * @return void |
||
450 | * @throws NotFoundException |
||
451 | * @throws MethodNotAllowedException |
||
452 | */ |
||
453 | public function delete($id = null) |
||
454 | { |
||
455 | //$this->request->allowMethod(['post', 'delete']); |
||
456 | if (!$id) { |
||
457 | throw new NotFoundException; |
||
458 | } |
||
459 | /* @var Entry $posting */ |
||
460 | $posting = $this->Entries->get($id); |
||
461 | if (!$posting) { |
||
462 | throw new NotFoundException; |
||
463 | } |
||
464 | |||
465 | $success = $this->Entries->treeDeleteNode($id); |
||
466 | |||
467 | if ($success) { |
||
468 | $flashType = 'success'; |
||
469 | if ($posting->isRoot()) { |
||
470 | $message = __('delete_tree_success'); |
||
471 | $redirect = '/'; |
||
472 | } else { |
||
473 | $message = __('delete_subtree_success'); |
||
474 | $redirect = '/entries/view/' . $posting->get('pid'); |
||
475 | } |
||
476 | } else { |
||
477 | $flashType = 'error'; |
||
478 | $message = __('delete_tree_error'); |
||
479 | $redirect = $this->referer(); |
||
480 | } |
||
481 | $this->Flash->set($message, ['element' => $flashType]); |
||
482 | $this->redirect($redirect); |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * Empty function for benchmarking |
||
487 | * |
||
488 | * @return void |
||
489 | */ |
||
490 | public function e() |
||
491 | { |
||
492 | Stopwatch::start('Entries->e()'); |
||
493 | Stopwatch::stop('Entries->e()'); |
||
494 | } |
||
495 | |||
496 | /** |
||
497 | * Marks sub-entry $id as solution to its current root-entry |
||
498 | * |
||
499 | * @param string $id posting-ID |
||
500 | * @return void |
||
501 | * @throws BadRequestException |
||
502 | */ |
||
503 | public function solve($id) |
||
504 | { |
||
505 | $this->autoRender = false; |
||
506 | try { |
||
507 | $posting = $this->Entries->get($id, ['return' => 'Entity']); |
||
508 | |||
509 | if (empty($posting)) { |
||
510 | throw new \InvalidArgumentException('Posting to mark solved not found.'); |
||
511 | } |
||
512 | |||
513 | if ($posting->isRoot()) { |
||
514 | throw new \InvalidArgumentException('Root postings cannot mark themself solved.'); |
||
515 | } |
||
516 | |||
517 | $rootId = $posting->get('tid'); |
||
518 | $rootPosting = $this->Entries->get($rootId); |
||
519 | if ($rootPosting->get('user_id') !== $this->CurrentUser->getId()) { |
||
520 | throw new SaitoForbiddenException( |
||
521 | sprintf('Attempt to mark posting %s as solution.', $posting->get('id')), |
||
522 | ['CurrentUser' => $this->CurrentUser] |
||
523 | ); |
||
524 | } |
||
525 | |||
526 | $success = $this->Entries->toggleSolve($posting); |
||
527 | |||
528 | if (!$success) { |
||
529 | throw new BadRequestException; |
||
530 | } |
||
531 | } catch (\Exception $e) { |
||
532 | throw new BadRequestException(); |
||
533 | } |
||
534 | } |
||
535 | |||
536 | /** |
||
537 | * Merge threads. |
||
538 | * |
||
539 | * @param string $sourceId posting-ID of thread to be merged |
||
540 | * @return void |
||
541 | * @throws NotFoundException |
||
542 | * @td put into admin entries controller |
||
543 | */ |
||
544 | public function merge($sourceId = null) |
||
545 | { |
||
546 | if (!$sourceId) { |
||
547 | throw new NotFoundException(); |
||
548 | } |
||
549 | |||
550 | /* @var Entry */ |
||
551 | $posting = $this->Entries->findById($sourceId)->first(); |
||
552 | |||
553 | if (!$posting || !$posting->isRoot()) { |
||
554 | throw new NotFoundException(); |
||
555 | } |
||
556 | |||
557 | // perform move operation |
||
558 | $targetId = $this->request->getData('targetId'); |
||
559 | if (!empty($targetId)) { |
||
560 | View Code Duplication | if ($this->Entries->threadMerge($sourceId, $targetId)) { |
|
561 | $this->redirect('/entries/view/' . $sourceId); |
||
562 | |||
563 | return; |
||
564 | } else { |
||
565 | $this->Flash->set(__('Error'), ['element' => 'error']); |
||
566 | } |
||
567 | } |
||
568 | |||
569 | $this->viewBuilder()->setLayout('Admin.admin'); |
||
570 | $this->set(compact('posting')); |
||
571 | } |
||
572 | |||
573 | /** |
||
574 | * Toggle posting property via ajax request. |
||
575 | * |
||
576 | * @param string $id posting-ID |
||
577 | * @param string $toggle property |
||
578 | * |
||
579 | * @return \Cake\Network\Response |
||
580 | */ |
||
581 | public function ajaxToggle($id = null, $toggle = null) |
||
604 | |||
605 | /** |
||
606 | * {@inheritDoc} |
||
607 | */ |
||
608 | public function beforeFilter(Event $event) |
||
621 | |||
622 | /** |
||
623 | * set view vars for category chooser |
||
624 | * |
||
625 | * @param CurrentUserInterface $User CurrentUser |
||
626 | * @return void |
||
627 | */ |
||
628 | protected function _setupCategoryChooser(CurrentUserInterface $User) |
||
668 | |||
669 | /** |
||
670 | * set additional vars for creating a new posting |
||
671 | * |
||
672 | * @param bool $isAnswer is new posting answer or root |
||
673 | * @return void |
||
674 | */ |
||
675 | protected function _setAddViewVars($isAnswer) |
||
682 | |||
683 | /** |
||
684 | * Decide if an answering panel is show when rendering a posting |
||
685 | * |
||
686 | * @return void |
||
687 | */ |
||
688 | protected function _showAnsweringPanel() |
||
707 | |||
708 | /** |
||
709 | * makes root posting of $posting avaiable in view |
||
710 | * |
||
711 | * @param Posting $posting posting for root entry |
||
712 | * @return void |
||
713 | */ |
||
714 | protected function _setRootEntry(Posting $posting) |
||
726 | } |
||
727 |
This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.