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 WorkflowDefinition 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 WorkflowDefinition, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 18 | class WorkflowDefinition extends DataObject { |
||
|
|
|||
| 19 | |||
| 20 | private static $db = array( |
||
| 21 | 'Title' => 'Varchar(128)', |
||
| 22 | 'Description' => 'Text', |
||
| 23 | 'Template' => 'Varchar', |
||
| 24 | 'TemplateVersion' => 'Varchar', |
||
| 25 | 'RemindDays' => 'Int', |
||
| 26 | 'Sort' => 'Int', |
||
| 27 | 'InitialActionButtonText' => 'Varchar' |
||
| 28 | ); |
||
| 29 | |||
| 30 | private static $default_sort = 'Sort'; |
||
| 31 | |||
| 32 | private static $has_many = array( |
||
| 33 | 'Actions' => 'WorkflowAction', |
||
| 34 | 'Instances' => 'WorkflowInstance' |
||
| 35 | ); |
||
| 36 | |||
| 37 | /** |
||
| 38 | * By default, a workflow definition is bound to a particular set of users or groups. |
||
| 39 | * |
||
| 40 | * This is covered across to the workflow instance - it is up to subsequent |
||
| 41 | * workflow actions to change this if needbe. |
||
| 42 | * |
||
| 43 | * @var array |
||
| 44 | */ |
||
| 45 | private static $many_many = array( |
||
| 46 | 'Users' => 'Member', |
||
| 47 | 'Groups' => 'Group' |
||
| 48 | ); |
||
| 49 | |||
| 50 | public static $icon = 'advancedworkflow/images/definition.png'; |
||
| 51 | |||
| 52 | public static $default_workflow_title_base = 'My Workflow'; |
||
| 53 | |||
| 54 | public static $workflow_defs = array(); |
||
| 55 | |||
| 56 | private static $dependencies = array( |
||
| 57 | 'workflowService' => '%$WorkflowService', |
||
| 58 | ); |
||
| 59 | |||
| 60 | /** |
||
| 61 | * @var WorkflowService |
||
| 62 | */ |
||
| 63 | public $workflowService; |
||
| 64 | |||
| 65 | /** |
||
| 66 | * Gets the action that first triggers off the workflow |
||
| 67 | * |
||
| 68 | * @return WorkflowAction |
||
| 69 | */ |
||
| 70 | public function getInitialAction() { |
||
| 73 | |||
| 74 | /** |
||
| 75 | * Ensure a sort value is set and we get a useable initial workflow title. |
||
| 76 | */ |
||
| 77 | public function onBeforeWrite() { |
||
| 78 | if(!$this->Sort) { |
||
| 79 | $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowDefinition"')->value(); |
||
| 80 | } |
||
| 81 | if(!$this->ID) { |
||
| 82 | $this->Title = $this->getDefaultWorkflowTitle(); |
||
| 83 | } |
||
| 84 | parent::onBeforeWrite(); |
||
| 85 | } |
||
| 86 | |||
| 87 | /** |
||
| 88 | * After we've been written, check whether we've got a template and to then |
||
| 89 | * create the relevant actions etc. |
||
| 90 | */ |
||
| 91 | public function onAfterWrite() { |
||
| 92 | parent::onAfterWrite(); |
||
| 93 | |||
| 94 | // Request via ImportForm where TemplateVersion is already set, so unset it |
||
| 95 | $posted = Controller::curr()->getRequest()->postVars(); |
||
| 96 | if(isset($posted['_CsvFile']) && $this->TemplateVersion) { |
||
| 97 | $this->TemplateVersion = null; |
||
| 98 | } |
||
| 99 | if($this->numChildren() == 0 && $this->Template && !$this->TemplateVersion) { |
||
| 100 | $this->workflowService->defineFromTemplate($this, $this->Template); |
||
| 101 | } |
||
| 102 | } |
||
| 103 | |||
| 104 | /** |
||
| 105 | * Ensure all WorkflowDefinition relations are removed on delete. If we don't do this, |
||
| 106 | * we see issues with targets previously under the control of a now-deleted workflow, |
||
| 107 | * becoming stuck, even if a new workflow is subsequently assigned to it. |
||
| 108 | * |
||
| 109 | * @return null |
||
| 110 | */ |
||
| 111 | public function onBeforeDelete() { |
||
| 112 | parent::onBeforeDelete(); |
||
| 113 | |||
| 114 | // Delete related import |
||
| 115 | $this->deleteRelatedImport(); |
||
| 116 | |||
| 117 | // Reset/unlink related HasMany|ManyMany relations and their orphaned objects |
||
| 118 | $this->removeRelatedHasLists(); |
||
| 119 | } |
||
| 120 | |||
| 121 | /** |
||
| 122 | * Removes User+Group relations from this object as well as WorkflowAction relations. |
||
| 123 | * When a WorkflowAction is deleted, its own relations are also removed: |
||
| 124 | * - WorkflowInstance |
||
| 125 | * - WorkflowTransition |
||
| 126 | * @see WorkflowAction::onAfterDelete() |
||
| 127 | * |
||
| 128 | * @return void |
||
| 129 | */ |
||
| 130 | private function removeRelatedHasLists() { |
||
| 131 | $this->Users()->removeAll(); |
||
| 132 | $this->Groups()->removeAll(); |
||
| 133 | $this->Actions()->each(function($action) { |
||
| 134 | if($orphan = DataObject::get_by_id('WorkflowAction', $action->ID)) { |
||
| 135 | $orphan->delete(); |
||
| 136 | } |
||
| 137 | }); |
||
| 138 | } |
||
| 139 | |||
| 140 | /** |
||
| 141 | * |
||
| 142 | * Deletes related ImportedWorkflowTemplate objects. |
||
| 143 | * |
||
| 144 | * @return void |
||
| 145 | */ |
||
| 146 | private function deleteRelatedImport() { |
||
| 147 | if($import = DataObject::get('ImportedWorkflowTemplate')->filter('DefinitionID', $this->ID)->first()) { |
||
| 148 | $import->delete(); |
||
| 149 | } |
||
| 150 | } |
||
| 151 | |||
| 152 | /** |
||
| 153 | * @return int |
||
| 154 | */ |
||
| 155 | public function numChildren() { |
||
| 158 | |||
| 159 | public function fieldLabels($includerelations = true) { |
||
| 160 | $labels = parent::fieldLabels($includerelations); |
||
| 161 | $labels['Title'] = _t('WorkflowDefinition.TITLE', 'Title'); |
||
| 162 | $labels['Description'] = _t('WorkflowDefinition.DESCRIPTION', 'Description'); |
||
| 163 | $labels['Template'] = _t('WorkflowDefinition.TEMPLATE_NAME', 'Source Template'); |
||
| 164 | $labels['TemplateVersion'] = _t('WorkflowDefinition.TEMPLATE_VERSION', 'Template Version'); |
||
| 165 | |||
| 166 | return $labels; |
||
| 167 | } |
||
| 168 | |||
| 169 | public function getCMSFields() { |
||
| 170 | |||
| 171 | $cmsUsers = Member::mapInCMSGroups(); |
||
| 172 | |||
| 173 | $fields = new FieldList(new TabSet('Root')); |
||
| 174 | |||
| 175 | $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); |
||
| 176 | $fields->addFieldToTab('Root.Main', new TextareaField('Description', $this->fieldLabel('Description'))); |
||
| 177 | $fields->addFieldToTab('Root.Main', TextField::create( |
||
| 178 | 'InitialActionButtonText', |
||
| 179 | _t('WorkflowDefinition.INITIAL_ACTION_BUTTON_TEXT', 'Initial Action Button Text') |
||
| 180 | )); |
||
| 181 | if($this->ID) { |
||
| 182 | $fields->addFieldToTab('Root.Main', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers)); |
||
| 183 | $fields->addFieldToTab('Root.Main', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), 'Group')); |
||
| 184 | } |
||
| 185 | |||
| 186 | if (class_exists('AbstractQueuedJob')) { |
||
| 187 | $before = _t('WorkflowDefinition.SENDREMINDERDAYSBEFORE', 'Send reminder email after '); |
||
| 188 | $after = _t('WorkflowDefinition.SENDREMINDERDAYSAFTER', ' days without action.'); |
||
| 189 | |||
| 190 | $fields->addFieldToTab('Root.Main', new FieldGroup( |
||
| 191 | _t('WorkflowDefinition.REMINDEREMAIL', 'Reminder Email'), |
||
| 192 | new LabelField('ReminderEmailBefore', $before), |
||
| 193 | new NumericField('RemindDays', ''), |
||
| 194 | new LabelField('ReminderEmailAfter', $after) |
||
| 195 | )); |
||
| 196 | } |
||
| 197 | |||
| 198 | if($this->ID) { |
||
| 199 | if ($this->Template) { |
||
| 200 | $template = $this->workflowService->getNamedTemplate($this->Template); |
||
| 201 | $fields->addFieldToTab('Root.Main', new ReadonlyField('Template', $this->fieldLabel('Template'), $this->Template)); |
||
| 202 | $fields->addFieldToTab('Root.Main', new ReadonlyField('TemplateDesc', _t('WorkflowDefinition.TEMPLATE_INFO', 'Template Info'), $template ? $template->getDescription() : '')); |
||
| 203 | $fields->addFieldToTab('Root.Main', $tv = new ReadonlyField('TemplateVersion', $this->fieldLabel('TemplateVersion'))); |
||
| 204 | $tv->setRightTitle(sprintf(_t('WorkflowDefinition.LATEST_VERSION', 'Latest version is %s'), $template ? $template->getVersion() : '')); |
||
| 205 | |||
| 206 | } |
||
| 207 | |||
| 208 | $fields->addFieldToTab('Root.Main', new WorkflowField( |
||
| 209 | 'Workflow', _t('WorkflowDefinition.WORKFLOW', 'Workflow'), $this |
||
| 210 | )); |
||
| 211 | } else { |
||
| 212 | // add in the 'template' info |
||
| 213 | $templates = $this->workflowService->getTemplates(); |
||
| 214 | |||
| 215 | if (is_array($templates)) { |
||
| 216 | $items = array('' => ''); |
||
| 217 | foreach ($templates as $template) { |
||
| 218 | $items[$template->getName()] = $template->getName(); |
||
| 219 | } |
||
| 220 | $templates = array_combine(array_keys($templates), array_keys($templates)); |
||
| 221 | |||
| 222 | $fields->addFieldToTab('Root.Main', $dd = new DropdownField('Template', _t('WorkflowDefinition.CHOOSE_TEMPLATE', 'Choose template (optional)'), $items)); |
||
| 223 | $dd->setRightTitle(_t('WorkflowDefinition.CHOOSE_TEMPLATE_RIGHT', 'If set, this workflow definition will be automatically updated if the template is changed')); |
||
| 224 | } |
||
| 225 | |||
| 226 | /* |
||
| 227 | * Uncomment to allow pre-uploaded exports to appear in a new DropdownField. |
||
| 228 | * |
||
| 229 | * $import = singleton('WorkflowDefinitionImporter')->getImportedWorkflows(); |
||
| 230 | * if (is_array($import)) { |
||
| 231 | * $_imports = array('' => ''); |
||
| 232 | * foreach ($imports as $import) { |
||
| 233 | * $_imports[$import->getName()] = $import->getName(); |
||
| 234 | * } |
||
| 235 | * $imports = array_combine(array_keys($_imports), array_keys($_imports)); |
||
| 236 | * $fields->addFieldToTab('Root.Main', new DropdownField('Import', _t('WorkflowDefinition.CHOOSE_IMPORT', 'Choose import (optional)'), $imports)); |
||
| 237 | * } |
||
| 238 | */ |
||
| 239 | |||
| 240 | $message = _t( |
||
| 241 | 'WorkflowDefinition.ADDAFTERSAVING', |
||
| 242 | 'You can add workflow steps after you save for the first time.' |
||
| 243 | ); |
||
| 244 | $fields->addFieldToTab('Root.Main', new LiteralField( |
||
| 245 | 'AddAfterSaving', "<p class='message notice'>$message</p>" |
||
| 246 | )); |
||
| 247 | } |
||
| 248 | |||
| 249 | if($this->ID && Permission::check('VIEW_ACTIVE_WORKFLOWS')) { |
||
| 250 | $active = $this->Instances()->filter(array( |
||
| 251 | 'WorkflowStatus' => array('Active', 'Paused') |
||
| 252 | )); |
||
| 253 | |||
| 254 | $active = new GridField( |
||
| 255 | 'Active', |
||
| 256 | _t('WorkflowDefinition.WORKFLOWACTIVEIINSTANCES', 'Active Workflow Instances'), |
||
| 257 | $active, |
||
| 258 | new GridFieldConfig_RecordEditor()); |
||
| 259 | |||
| 260 | $active->getConfig()->removeComponentsByType('GridFieldAddNewButton'); |
||
| 261 | $active->getConfig()->removeComponentsByType('GridFieldDeleteAction'); |
||
| 262 | |||
| 263 | if(!Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { |
||
| 264 | $active->getConfig()->removeComponentsByType('GridFieldEditButton'); |
||
| 265 | $active->getConfig()->addComponent(new GridFieldViewButton()); |
||
| 266 | $active->getConfig()->addComponent(new GridFieldDetailForm()); |
||
| 267 | } |
||
| 268 | |||
| 269 | $completed = $this->Instances()->filter(array( |
||
| 270 | 'WorkflowStatus' => array('Complete', 'Cancelled') |
||
| 271 | )); |
||
| 272 | |||
| 273 | $config = new GridFieldConfig_Base(); |
||
| 274 | $config->addComponent(new GridFieldEditButton()); |
||
| 275 | $config->addComponent(new GridFieldDetailForm()); |
||
| 276 | |||
| 277 | $completed = new GridField( |
||
| 278 | 'Completed', |
||
| 279 | _t('WorkflowDefinition.WORKFLOWCOMPLETEDIINSTANCES', 'Completed Workflow Instances'), |
||
| 280 | $completed, |
||
| 281 | $config); |
||
| 282 | |||
| 283 | $fields->findOrMakeTab( |
||
| 284 | 'Root.Active', |
||
| 285 | _t('WorkflowEmbargoExpiryExtension.ActiveWorkflowStateTitle', 'Active') |
||
| 286 | ); |
||
| 287 | $fields->addFieldToTab('Root.Active', $active); |
||
| 288 | |||
| 289 | $fields->findOrMakeTab( |
||
| 290 | 'Root.Completed', |
||
| 291 | _t('WorkflowEmbargoExpiryExtension.CompletedWorkflowStateTitle', 'Completed') |
||
| 292 | ); |
||
| 293 | $fields->addFieldToTab('Root.Completed', $completed); |
||
| 294 | } |
||
| 295 | |||
| 296 | $this->extend('updateCMSFields', $fields); |
||
| 297 | |||
| 298 | return $fields; |
||
| 299 | } |
||
| 300 | |||
| 301 | public function updateAdminActions($actions) { |
||
| 310 | |||
| 311 | public function updateFromTemplate() { |
||
| 312 | if ($this->Template) { |
||
| 313 | $template = $this->workflowService->getNamedTemplate($this->Template); |
||
| 314 | $template->updateDefinition($this); |
||
| 315 | } |
||
| 316 | } |
||
| 317 | |||
| 318 | /** |
||
| 319 | * If a workflow-title doesn't already exist, we automatically create a suitable default title |
||
| 320 | * when users attempt to create title-less workflow definitions or upload/create Workflows that would |
||
| 321 | * otherwise have the same name. |
||
| 322 | * |
||
| 323 | * @return string |
||
| 324 | * @todo Filter query on current-user's workflows. Avoids confusion when other users may already have 'My Workflow 1' |
||
| 325 | * and user sees 'My Workflow 2' |
||
| 326 | */ |
||
| 327 | public function getDefaultWorkflowTitle() { |
||
| 328 | // Where is the title coming from that we wish to test? |
||
| 329 | $incomingTitle = $this->incomingTitle(); |
||
| 330 | $defs = DataObject::get('WorkflowDefinition')->map()->toArray(); |
||
| 331 | $tmp = array(); |
||
| 332 | |||
| 333 | foreach($defs as $def) { |
||
| 334 | $parts = preg_split("#\s#", $def, -1, PREG_SPLIT_NO_EMPTY); |
||
| 335 | $lastPart = array_pop($parts); |
||
| 336 | $match = implode(' ', $parts); |
||
| 337 | // @todo do all this in one preg_match_all() call |
||
| 338 | if(preg_match("#$match#", $incomingTitle)) { |
||
| 339 | // @todo use a simple incrementer?? |
||
| 340 | if($incomingTitle.' '.$lastPart == $def) { |
||
| 341 | array_push($tmp, $lastPart); |
||
| 342 | } |
||
| 343 | } |
||
| 344 | } |
||
| 345 | |||
| 346 | $incr = 1; |
||
| 347 | if(count($tmp)) { |
||
| 348 | sort($tmp,SORT_NUMERIC); |
||
| 349 | $incr = (int)end($tmp)+1; |
||
| 350 | } |
||
| 351 | return $incomingTitle.' '.$incr; |
||
| 352 | } |
||
| 353 | |||
| 354 | /** |
||
| 355 | * Return the workflow definition title according to the source |
||
| 356 | * |
||
| 357 | * @return string |
||
| 358 | */ |
||
| 359 | public function incomingTitle() { |
||
| 360 | $req = Controller::curr()->getRequest(); |
||
| 361 | if(isset($req['_CsvFile']['name']) && !empty($req['_CsvFile']['name'])) { |
||
| 362 | $import = DataObject::get('ImportedWorkflowTemplate')->filter('Filename', $req['_CsvFile']['name'])->first(); |
||
| 363 | $incomingTitle = $import->Name; |
||
| 364 | } |
||
| 365 | else if(isset($req['Template']) && !empty($req['Template'])) { |
||
| 366 | $incomingTitle = $req['Template']; |
||
| 367 | } |
||
| 368 | else if(isset($req['Title']) && !empty($req['Title'])) { |
||
| 369 | $incomingTitle = $req['Title']; |
||
| 370 | } |
||
| 371 | else { |
||
| 372 | $incomingTitle = self::$default_workflow_title_base; |
||
| 373 | } |
||
| 374 | return $incomingTitle; |
||
| 375 | } |
||
| 376 | |||
| 377 | /** |
||
| 378 | * |
||
| 379 | * @param Member $member |
||
| 380 | * @return boolean |
||
| 381 | */ |
||
| 382 | View Code Duplication | public function canCreate($member=null) { |
|
| 383 | if (is_null($member)) { |
||
| 384 | if (!Member::currentUserID()) { |
||
| 385 | return false; |
||
| 386 | } |
||
| 387 | $member = Member::currentUser(); |
||
| 388 | } |
||
| 389 | return Permission::checkMember($member, 'CREATE_WORKFLOW'); |
||
| 390 | } |
||
| 391 | |||
| 392 | /** |
||
| 393 | * |
||
| 394 | * @param Member $member |
||
| 395 | * @return boolean |
||
| 396 | */ |
||
| 397 | public function canView($member=null) { |
||
| 400 | |||
| 401 | /** |
||
| 402 | * |
||
| 403 | * @param Member $member |
||
| 404 | * @return boolean |
||
| 405 | */ |
||
| 406 | public function canEdit($member=null) { |
||
| 409 | |||
| 410 | /** |
||
| 411 | * |
||
| 412 | * @param Member $member |
||
| 413 | * @return boolean |
||
| 414 | * @see {@link $this->onBeforeDelete()} |
||
| 415 | */ |
||
| 416 | View Code Duplication | public function canDelete($member = null) { |
|
| 435 | |||
| 436 | /** |
||
| 437 | * Checks whether the passed user is able to view this ModelAdmin |
||
| 438 | * |
||
| 439 | * @param $memberID |
||
| 440 | */ |
||
| 441 | View Code Duplication | protected function userHasAccess($member) { |
|
| 453 | } |
||
| 454 |
You can fix this by adding a namespace to your class:
When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.