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 AbstractManager 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 AbstractManager, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | abstract class AbstractManager implements ManagerInterface |
||
15 | { |
||
16 | /** |
||
17 | * Workflow end result code |
||
18 | * |
||
19 | * When workflow is ended, it can have a result code to present status |
||
20 | * like approved or rejected or user canceled. |
||
21 | */ |
||
22 | const RESULT_CODE_NOT_ENDED = 0; |
||
23 | const RESULT_CODE_APPROVED = 1; |
||
24 | const RESULT_CODE_REJECTED = -1; |
||
25 | const RESULT_CODE_CANCELED = -2; |
||
26 | |||
27 | |||
28 | /** |
||
29 | * Storage of disabled actions |
||
30 | * |
||
31 | * {node, action: {}} |
||
32 | * |
||
33 | * @var array |
||
34 | */ |
||
35 | protected $disabledActions = []; |
||
36 | |||
37 | /** |
||
38 | * Workflow model instance |
||
39 | * |
||
40 | * @var ModelInterface |
||
41 | */ |
||
42 | protected $model = null; |
||
43 | |||
44 | /** |
||
45 | * Classname of workflow model |
||
46 | * |
||
47 | * When start a new workflow, this classname is used to create empty model |
||
48 | * instance. |
||
49 | * |
||
50 | * @var string |
||
51 | */ |
||
52 | protected $modelClass = ModelInterface::class; |
||
53 | |||
54 | /** |
||
55 | * Workflow nodes schema array |
||
56 | * |
||
57 | * Should at least have one start node and one end node. |
||
58 | * |
||
59 | * Action name should be unique in all nodes, same action may cause error, |
||
60 | * and confusion for reading code, especially when add controller/view |
||
61 | * action in view or template. |
||
62 | * |
||
63 | * Default value of resultCode is self::RESULT_CODE_NOT_ENDED if not set. |
||
64 | * ResultCode should set only on action relate to end node. When leave end |
||
65 | * node(rollback), resultCode is reset(param default value of move()), or |
||
66 | * user can specify through action. Set resultCode on other action is |
||
67 | * useless. |
||
68 | * |
||
69 | * The end node should not have action 'next' point to itself, that action |
||
70 | * will always trigger both commit() and rollback(). A check for this in |
||
71 | * move() will throw Exception 'end twice'. |
||
72 | * |
||
73 | * @var array |
||
74 | */ |
||
75 | protected $nodes = [ |
||
76 | 'start' => [ |
||
77 | 'title' => 'Started', |
||
78 | 'actions' => [ |
||
79 | 'edit' => [ |
||
80 | 'title' => 'Edit', |
||
81 | 'next' => 'start', |
||
82 | ], |
||
83 | 'submit' => [ |
||
84 | 'title' => 'Submit', |
||
85 | 'next' => 'end', |
||
86 | 'resultCode' => self::RESULT_CODE_APPROVED, |
||
87 | ], |
||
88 | ], |
||
89 | ], |
||
90 | 'end' => [ |
||
91 | 'title' => 'Ended', |
||
92 | ], |
||
93 | ]; |
||
94 | |||
95 | /** |
||
96 | * Not available actions |
||
97 | * |
||
98 | * @var array {action: {title, message or reason}} |
||
99 | */ |
||
100 | protected $notAvailableActions = []; |
||
101 | |||
102 | /** |
||
103 | * Workflow result code title |
||
104 | * |
||
105 | * @var array |
||
106 | */ |
||
107 | protected $resultCodeTitle = [ |
||
108 | self::RESULT_CODE_NOT_ENDED => 'Not Ended', |
||
109 | self::RESULT_CODE_APPROVED => 'Approved', |
||
110 | self::RESULT_CODE_REJECTED => 'Rejected', |
||
111 | self::RESULT_CODE_CANCELED => 'Canceled', |
||
112 | ]; |
||
113 | |||
114 | /** |
||
115 | * Title of workflow class |
||
116 | * |
||
117 | * Usually include the description of what this workflow will do. |
||
118 | * |
||
119 | * @return string |
||
120 | */ |
||
121 | protected $workflowTitle = 'Workflow Title'; |
||
122 | |||
123 | |||
124 | /** |
||
125 | * Constructor |
||
126 | * |
||
127 | * @param string $uuid |
||
128 | */ |
||
129 | public function __construct($uuid = '') |
||
130 | { |
||
131 | $this->load($uuid); |
||
132 | } |
||
133 | |||
134 | |||
135 | /** |
||
136 | * Store change done in commit(), for rollback |
||
137 | */ |
||
138 | protected function afterCommit() |
||
139 | { |
||
140 | // Dummy, do nothing |
||
141 | } |
||
142 | |||
143 | |||
144 | /** |
||
145 | * Prepare to record changes in commit(), for rollback |
||
146 | */ |
||
147 | protected function beforeCommit() |
||
148 | { |
||
149 | // Dummy, do nothing |
||
150 | } |
||
151 | |||
152 | |||
153 | /** |
||
154 | * Process after workflow end and resultCode is approved |
||
155 | * |
||
156 | * In common, this method should write $contents to entity storage. |
||
157 | * |
||
158 | * If use DbDiff to store entity db change, there will have an extra |
||
159 | * UPDATE to db (the former one is save()), by this cost, the workflow got |
||
160 | * possibility to rollback from end node. |
||
161 | * |
||
162 | * Workflow may have no rollback ability, but should commit something, so |
||
163 | * commit() is abstract and must fill by child class, as rollback() is |
||
164 | * default empty. |
||
165 | */ |
||
166 | abstract protected function commit(); |
||
167 | |||
168 | |||
169 | /** |
||
170 | * {@inheritdoc} |
||
171 | */ |
||
172 | public function disableAction($action) |
||
190 | |||
191 | |||
192 | /** |
||
193 | * {@inheritdoc} |
||
194 | */ |
||
195 | View Code Duplication | public function disableActions(array $actions) |
|
218 | |||
219 | |||
220 | /** |
||
221 | * {@inheritdoc} |
||
222 | */ |
||
223 | public function enableAction($action) |
||
234 | |||
235 | |||
236 | /** |
||
237 | * {@inheritdoc} |
||
238 | */ |
||
239 | public function enableActions(array $actions) |
||
247 | |||
248 | |||
249 | /** |
||
250 | * {@inheritdoc} |
||
251 | * |
||
252 | * User can define customized executeAction() method to do extra job like |
||
253 | * generate profile code. this method should not include move() anymore, |
||
254 | * nor should this method set resultCode, that should be set as action |
||
255 | * property in $nodes define array. |
||
256 | */ |
||
257 | public function execute($action) |
||
284 | |||
285 | |||
286 | /** |
||
287 | * Find links/relations the workflow instance have |
||
288 | * |
||
289 | * @return array|null Return null to disable save of link |
||
290 | */ |
||
291 | protected function findLinks() |
||
296 | |||
297 | |||
298 | /** |
||
299 | * Get title of action, will search action in nodes |
||
300 | * |
||
301 | * @param string $action |
||
302 | * @return string |
||
303 | * @throws \Exception If nodes use invalid action |
||
304 | */ |
||
305 | public function getActionTitle($action) |
||
316 | |||
317 | |||
318 | /** |
||
319 | * {@inheritdoc} |
||
320 | */ |
||
321 | public function getAvailableActions() |
||
337 | |||
338 | |||
339 | /** |
||
340 | * Getter of single content |
||
341 | * |
||
342 | * @param string $key |
||
343 | * @return array |
||
344 | */ |
||
345 | public function getContent($key) |
||
349 | |||
350 | |||
351 | /** |
||
352 | * Getter of whole content array |
||
353 | * |
||
354 | * @return array |
||
355 | */ |
||
356 | public function getContents() |
||
360 | |||
361 | |||
362 | /** |
||
363 | * Getter of current node |
||
364 | * |
||
365 | * @return string |
||
366 | */ |
||
367 | public function getCurrentNode() |
||
371 | |||
372 | |||
373 | /** |
||
374 | * Getter of current node title |
||
375 | * |
||
376 | * @return string |
||
377 | */ |
||
378 | public function getCurrentNodeTitle() |
||
384 | |||
385 | |||
386 | /** |
||
387 | * {@inheritdoc} |
||
388 | */ |
||
389 | public function getModel() |
||
393 | |||
394 | |||
395 | /** |
||
396 | * {@inheritdoc} |
||
397 | */ |
||
398 | public function getModelClass() |
||
402 | |||
403 | |||
404 | /** |
||
405 | * Getter of $notAvailableActions |
||
406 | * |
||
407 | * @return array |
||
408 | */ |
||
409 | public function getNotAvailableActions() |
||
413 | |||
414 | |||
415 | /** |
||
416 | * Getter of result code |
||
417 | * |
||
418 | * @return int |
||
419 | */ |
||
420 | public function getResultCode() |
||
424 | |||
425 | |||
426 | /** |
||
427 | * Get title of result code |
||
428 | * |
||
429 | * If $resultCode is null, will get result code of $model, otherwise will |
||
430 | * return title for given result code. |
||
431 | * |
||
432 | * @param int $resultCode |
||
433 | * @return string |
||
434 | */ |
||
435 | public function getResultCodeTitle($resultCode = null) |
||
443 | |||
444 | |||
445 | /** |
||
446 | * Getter of title |
||
447 | * |
||
448 | * @return string |
||
449 | */ |
||
450 | public function getTitle() |
||
454 | |||
455 | |||
456 | /** |
||
457 | * Getter of uuid |
||
458 | * |
||
459 | * @return string |
||
460 | */ |
||
461 | public function getUuid() |
||
465 | |||
466 | |||
467 | /** |
||
468 | * {@inheritdoc} |
||
469 | */ |
||
470 | public function getWorkflowTitle() |
||
474 | |||
475 | |||
476 | /** |
||
477 | * Initialize an empty workflow instance |
||
478 | */ |
||
479 | protected function initialize() |
||
484 | |||
485 | |||
486 | /** |
||
487 | * Is an action available ? |
||
488 | * |
||
489 | * Only actions of current node can be available, and default available. |
||
490 | * |
||
491 | * User should not extend this method directly, instead, user can create |
||
492 | * customize check method for any single $action, named as |
||
493 | * isAction[ActionName]Available(). These method should explicit return |
||
494 | * true to pass available check, other return value will be consider as |
||
495 | * check fail, and will be saved as fail reason/message in property |
||
496 | * $notAvailableActions. This property can be used to show user why these |
||
497 | * action can't execute. |
||
498 | * |
||
499 | * This is more flexible than complicated condition string. |
||
500 | * |
||
501 | * @param string $action |
||
502 | * @return boolean |
||
503 | */ |
||
504 | public function isActionAvailable($action) |
||
530 | |||
531 | |||
532 | /** |
||
533 | * {@inheritdoc} |
||
534 | */ |
||
535 | public function isApproved() |
||
539 | |||
540 | |||
541 | /** |
||
542 | * {@inheritdoc} |
||
543 | */ |
||
544 | public function isEnded() |
||
548 | |||
549 | |||
550 | |||
551 | /** |
||
552 | * {@inheritdoc} |
||
553 | */ |
||
554 | View Code Duplication | public function limitActions(array $actions) |
|
577 | |||
578 | |||
579 | /** |
||
580 | * {@inheritdoc} |
||
581 | */ |
||
582 | public function load($uuid) |
||
593 | |||
594 | |||
595 | /** |
||
596 | * Move workflow to another node |
||
597 | * |
||
598 | * After workflow move to end node and is approved, the method commit() |
||
599 | * will be called, the reverse operate is rollback(), called when node |
||
600 | * leave from end. The end result rejected or canceled has no alike |
||
601 | * mechanism, because in common nothing need to do, although child class |
||
602 | * can extend this method to add that. |
||
603 | * |
||
604 | * @param string $action Moved by action |
||
605 | * @param string $node |
||
606 | * @param int $resultCode Should set when to or from end node. |
||
607 | * @return AbstractManager |
||
608 | * @throws \Exception If 2 user end a workflow at same time. |
||
609 | */ |
||
610 | protected function move( |
||
649 | |||
650 | |||
651 | /** |
||
652 | * Rollback data written by commit() |
||
653 | */ |
||
654 | protected function rollback() |
||
658 | |||
659 | |||
660 | /** |
||
661 | * Save workflow |
||
662 | * |
||
663 | * For new created workflow instance, save() method should generate and |
||
664 | * update $uuid property. |
||
665 | */ |
||
666 | protected function save() |
||
673 | |||
674 | |||
675 | /** |
||
676 | * Save workflow change log |
||
677 | * |
||
678 | * Log is only saved when node change. |
||
679 | * |
||
680 | * @param string $action |
||
681 | * @param string $prevNode |
||
682 | * @param string $nextNode |
||
683 | */ |
||
684 | protected function saveLog($action, $prevNode, $nextNode) |
||
690 | |||
691 | |||
692 | /** |
||
693 | * {@inheritdoc} |
||
694 | */ |
||
695 | public function setModel(ModelInterface $model) |
||
701 | |||
702 | |||
703 | /** |
||
704 | * Change action title |
||
705 | * |
||
706 | * Sometimes action title can't fit both detail and review view mode. For |
||
707 | * example, an action with title 'Save' works fine in review mode. But in |
||
708 | * detail mode, the title 'Go to Edit' is more suitable. |
||
709 | * |
||
710 | * Note: choose a appropriate title is better than do modify like this. |
||
711 | * |
||
712 | * @param string $node |
||
713 | * @param string $action |
||
714 | * @param string $title |
||
715 | * @return AbstractManager |
||
716 | */ |
||
717 | public function setNodeActionTitle($node, $action, $title) |
||
723 | |||
724 | |||
725 | /** |
||
726 | * Update $contents |
||
727 | * |
||
728 | * Eg: Assign filtered $_POST data from View. |
||
729 | * |
||
730 | * @param array $contents |
||
731 | * @return AbstractManager |
||
732 | */ |
||
733 | public function updateContents(array $contents = null) |
||
747 | |||
748 | |||
749 | /** |
||
750 | * Update model information by contents |
||
751 | * |
||
752 | * Model property that not depends on contents should set in initialize() |
||
753 | * or executeAction() method. |
||
754 | */ |
||
755 | protected function updateModelByContents() |
||
759 | } |
||
760 |
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.