WorkflowApplicable   F
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22
Metric Value
wmc 56
lcom 1
cbo 22
dl 0
loc 345
rs 1.7098

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setIsPublishJobRunning() 0 3 1
A getIsPublishJobRunning() 0 3 1
A isPublishJobRunning() 0 4 3
A updateSettingsFields() 0 3 1
A AbsoluteEditLink() 0 15 4
A getWorkflowHistory() 0 3 1
A RecentWorkflowComment() 0 9 4
A canPublish() 0 16 4
A updateCMSFields() 0 9 2
C updateFields() 0 50 7
C updateCMSActions() 0 75 15
A createActionMenu() 0 5 1
A LinkToPendingItems() 0 6 1
A onAfterWrite() 0 6 3
A WorkflowInstances() 0 6 1
A getWorkflowInstance() 0 7 2
A canEdit() 0 10 3
A canEditWorkflow() 0 7 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
/**
3
 * DataObjects that have the WorkflowApplicable extension can have a
4
 * workflow definition applied to them. At some point, the workflow definition is then
5
 * triggered.
6
 *
7
 * @author  [email protected]
8
 * @license BSD License (http://silverstripe.org/bsd-license/)
9
 * @package advancedworkflow
10
 */
11
class WorkflowApplicable extends DataExtension {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
12
13
	private static $has_one = array(
0 ignored issues
show
Unused Code introduced by
The property $has_one is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
14
		'WorkflowDefinition' => 'WorkflowDefinition',
15
	);
16
17
	private static $many_many = array(
0 ignored issues
show
Unused Code introduced by
The property $many_many is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
18
		'AdditionalWorkflowDefinitions' => 'WorkflowDefinition'
19
	);
20
21
	private static $dependencies = array(
0 ignored issues
show
Unused Code introduced by
The property $dependencies is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
22
		'workflowService'		=> '%$WorkflowService',
23
	);
24
25
	/**
26
	 *
27
	 * Used to flag to this extension if there's a WorkflowPublishTargetJob running.
28
	 * @var boolean
29
	 */
30
	public $isPublishJobRunning = false;
31
32
	/**
33
	 *
34
	 * @param boolean $truth
35
	 */
36
	public function setIsPublishJobRunning($truth) {
37
		$this->isPublishJobRunning = $truth;
38
	}
39
40
	/**
41
	 *
42
	 * @return boolean
43
	 */
44
	public function getIsPublishJobRunning() {
45
		return $this->isPublishJobRunning;
46
	}
47
48
	/**
49
	 *
50
	 * @see {@link $this->isPublishJobRunning}
51
	 * @return boolean
52
	 */
53
	public function isPublishJobRunning() {
54
		$propIsSet = $this->getIsPublishJobRunning() ? true : false;
55
		return class_exists('AbstractQueuedJob') && $propIsSet;
56
	}
57
58
	/**
59
	 * @var WorkflowService
60
	 */
61
	public $workflowService;
62
	
63
	/**
64
	 * 
65
	 * A cache var for the current workflow instance
66
	 *
67
	 * @var WorkflowInstance
68
	 */
69
	protected $currentInstance;
70
	
71
	public function updateSettingsFields(FieldList $fields) {
72
		$this->updateFields($fields);
73
	}
74
75
	public function updateCMSFields(FieldList $fields) {
76
		if(!$this->owner->hasMethod('getSettingsFields')) $this->updateFields($fields);
77
78
		// Instantiate a hidden form field to pass the triggered workflow definition through, allowing a dynamic form action.
79
80
		$fields->push(HiddenField::create(
81
			'TriggeredWorkflowID'
82
		));
83
	}
84
85
	public function updateFields(FieldList $fields) {
86
		if (!$this->owner->ID) {
87
			return $fields;
88
		}
89
		
90
		$tab       = $fields->fieldByName('Root') ? $fields->findOrMakeTab('Root.Workflow') : $fields;
91
92
		if(Permission::check('APPLY_WORKFLOW')) {
93
			$definition = new DropdownField('WorkflowDefinitionID', _t('WorkflowApplicable.DEFINITION', 'Applied Workflow'));
94
			$definitions = $this->workflowService->getDefinitions()->map()->toArray();
95
			$definition->setSource($definitions);
96
			$definition->setEmptyString(_t('WorkflowApplicable.INHERIT', 'Inherit from parent'));
97
			$tab->push($definition);
98
99
			// Allow an optional selection of additional workflow definitions.
100
101
			if($this->owner->WorkflowDefinitionID) {
102
				$fields->removeByName('AdditionalWorkflowDefinitions');
103
				unset($definitions[$this->owner->WorkflowDefinitionID]);
104
				$tab->push($additional = ListboxField::create(
105
					'AdditionalWorkflowDefinitions',
106
					_t('WorkflowApplicable.ADDITIONAL_WORKFLOW_DEFINITIONS', 'Additional Workflows')
107
				));
108
				$additional->setSource($definitions);
109
				$additional->setMultiple(true);
110
			}
111
		}
112
113
		// Display the effective workflow definition.
114
115
		if($effective = $this->getWorkflowInstance()) {
116
			$title = $effective->Definition()->Title;
0 ignored issues
show
Documentation introduced by
The property Title does not exist on object<WorkflowDefinition>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
117
			$tab->push(ReadonlyField::create(
118
				'EffectiveWorkflow',
119
				_t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'),
120
				$title
121
			));
122
		}
123
124
		if($this->owner->ID) {
125
			$config = new GridFieldConfig_Base();
126
			$config->addComponent(new GridFieldEditButton());
127
			$config->addComponent(new GridFieldDetailForm());
128
			
129
			$insts = $this->owner->WorkflowInstances();
130
			$log   = new GridField('WorkflowLog', _t('WorkflowApplicable.WORKFLOWLOG', 'Workflow Log'), $insts, $config);
131
132
			$tab->push($log);
133
		}
134
	}
135
136
	public function updateCMSActions(FieldList $actions) {
137
		$active = $this->workflowService->getWorkflowFor($this->owner);
138
		$c = Controller::curr();
139
		if ($c && $c->hasExtension('AdvancedWorkflowExtension')) {
140
			if ($active) {
141
				if ($this->canEditWorkflow()) {
142
					$workflowOptions = new Tab(
143
						'WorkflowOptions', 
144
						_t('SiteTree.WorkflowOptions', 'Workflow options', 'Expands a view for workflow specific buttons')
145
					);
146
147
					$menu = $actions->fieldByName('ActionMenus');
148
					if (!$menu) {
149
						// create the menu for adding to any arbitrary non-sitetree object
150
						$menu = $this->createActionMenu();
151
						$actions->push($menu);
152
					}
153
154
					$menu->push($workflowOptions);
155
					
156
					$transitions = $active->CurrentAction()->getValidTransitions();
157
					
158
					foreach ($transitions as $transition) {
159
						if ($transition->canExecute($active)) {
160
							$action = FormAction::create('updateworkflow-' . $transition->ID, $transition->Title)
161
								->setAttribute('data-transitionid', $transition->ID);
162
							$workflowOptions->push($action);
163
						}
164
					}
165
166
//					$action = FormAction::create('updateworkflow', $active->CurrentAction() ? $active->CurrentAction()->Title : _t('WorkflowApplicable.UPDATE_WORKFLOW', 'Update Workflow'))
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
167
//						->setAttribute('data-icon', 'navigation');
168
//					$actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action);
169
				}
170
			} else {
171
				// Instantiate the workflow definition initial actions.
172
				$definitions = $this->workflowService->getDefinitionsFor($this->owner);
173
				if($definitions) {
174
					$menu = $actions->fieldByName('ActionMenus');
175
					if(is_null($menu)) {
176
177
						// Instantiate a new action menu for any data objects.
178
179
						$menu = $this->createActionMenu();
180
						$actions->push($menu);
181
					}
182
					$tab = Tab::create(
183
						'AdditionalWorkflows'
184
					);
185
					$menu->insertBefore($tab, 'MoreOptions');
186
					$addedFirst = false;
187
					foreach($definitions as $definition) {
188
						if($definition->getInitialAction()) {
189
							$action = FormAction::create(
190
								"startworkflow-{$definition->ID}",
191
								$definition->InitialActionButtonText ? $definition->InitialActionButtonText : $definition->getInitialAction()->Title
192
							)->addExtraClass('start-workflow')->setAttribute('data-workflow', $definition->ID);
193
194
							// The first element is the main workflow definition, and will be displayed as a major action.
195
196
							if(!$addedFirst) {
197
								$addedFirst = true;
198
								$action->setAttribute('data-icon', 'navigation');
199
								$majorActions = $actions->fieldByName('MajorActions');
200
								$majorActions ? $majorActions->push($action) : $actions->push($action);
201
							} else {
202
								$tab->push($action);
203
							}
204
						}
205
					}
206
				}
207
				
208
			}
209
		}
210
	}
211
	
212
	protected function createActionMenu() {
213
		$rootTabSet = new TabSet('ActionMenus');
214
		$rootTabSet->addExtraClass('ss-ui-action-tabset action-menus');
215
		return $rootTabSet;
216
	}
217
	
218
	/**
219
	 * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. 
220
	 * Returns an absolute link to the CMS UI for a Page object
221
	 * 
222
	 * @return string | null
223
	 */	
224
	public function AbsoluteEditLink() {
225
		$CMSEditLink = null;
226
227
		if($this->owner instanceof CMSPreviewable) {
228
			$CMSEditLink = $this->owner->CMSEditLink();
229
		} else if ($this->owner->hasMethod('WorkflowLink')) {
230
			$CMSEditLink = $this->owner->WorkflowLink();
231
		}
232
233
		if ($CMSEditLink === null) {
234
			return null;
235
		}
236
237
		return Controller::join_links(Director::absoluteBaseURL(), $CMSEditLink);
238
	}
239
	
240
	/**
241
	 * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. 
242
	 * Allows users to select a link in an email for direct access to the transition-selection dropdown in the CMS UI.
243
	 * 
244
	 * @return string
245
	 */
246
	public function LinkToPendingItems() {
247
		$urlBase = Director::absoluteBaseURL();
248
		$urlFrag = 'admin/workflows/WorkflowDefinition/EditForm/field';
249
		$urlInst = $this->getWorkflowInstance();
250
		return Controller::join_links($urlBase, $urlFrag, 'PendingObjects', 'item', $urlInst->ID, 'edit');
251
	}
252
	
253
	/**
254
	 * After a workflow item is written, we notify the
255
	 * workflow so that it can take action if needbe
256
	 */
257
	public function onAfterWrite() {
258
		$instance = $this->getWorkflowInstance();
259
		if ($instance && $instance->CurrentActionID) {
0 ignored issues
show
Documentation introduced by
The property CurrentActionID does not exist on object<WorkflowInstance>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
260
			$action = $instance->CurrentAction()->BaseAction()->targetUpdated($instance);
0 ignored issues
show
Documentation Bug introduced by
The method BaseAction does not exist on object<WorkflowActionInstance>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Unused Code introduced by
$action is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

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.

Loading history...
261
		}
262
	}
263
264
	public function WorkflowInstances() {
265
		return WorkflowInstance::get()->filter(array(
266
			'TargetClass' => $this->ownerBaseClass,
267
			'TargetID'    => $this->owner->ID
268
		));
269
	}
270
271
	/**
272
	 * Gets the current instance of workflow
273
	 *
274
	 * @return WorkflowInstance
275
	 */
276
	public function getWorkflowInstance() {
277
		if (!$this->currentInstance) {
278
			$this->currentInstance = $this->workflowService->getWorkflowFor($this->owner);
279
		}
280
281
		return $this->currentInstance;
282
	}
283
284
285
	/**
286
	 * Gets the history of a workflow instance
287
	 *
288
	 * @return DataObjectSet
289
	 */
290
	public function getWorkflowHistory($limit = null) {
291
		return $this->workflowService->getWorkflowHistoryFor($this->owner, $limit);
292
	}
293
294
	/**
295
	 * Check all recent WorkflowActionIntances and return the most recent one with a Comment
296
	 *
297
	 * @return WorkflowActionInstance
298
	 */
299
	public function RecentWorkflowComment($limit = 10){
300
		if($actions = $this->getWorkflowHistory($limit)){
301
			foreach ($actions as $action) {
302
				if ($action->Comment != '') {
303
					return $action;
304
				}
305
			}
306
		}
307
	}
308
309
	/**
310
	 * Content can never be directly publishable if there's a workflow applied.
311
	 *
312
	 * If there's an active instance, then it 'might' be publishable
313
	 */
314
	public function canPublish() {
315
		// Override any default behaviour, to allow queuedjobs to complete
316
		if($this->isPublishJobRunning()) {
317
			return true;
318
		}
319
320
		if ($active = $this->getWorkflowInstance()) {
321
			return $active->canPublishTarget($this->owner);
0 ignored issues
show
Unused Code introduced by
The call to WorkflowInstance::canPublishTarget() has too many arguments starting with $this->owner.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
322
		}
323
324
		// otherwise, see if there's any workflows applied. If there are, then we shouldn't be able
325
		// to directly publish
326
		if ($effective = $this->workflowService->getDefinitionFor($this->owner)) {
0 ignored issues
show
Unused Code introduced by
$effective is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

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.

Loading history...
327
			return false;
328
		}
329
	}
330
331
	/**
332
	 * Can only edit content that's NOT in another person's content changeset
333
	 */
334
	public function canEdit($member) {
335
		// Override any default behaviour, to allow queuedjobs to complete
336
		if($this->isPublishJobRunning()) {
337
			return true;
338
		}
339
340
		if ($active = $this->getWorkflowInstance()) {
341
			return $active->canEditTarget($this->owner);
0 ignored issues
show
Unused Code introduced by
The call to WorkflowInstance::canEditTarget() has too many arguments starting with $this->owner.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
342
		}
343
	}
344
345
	/**
346
	 * Can a user edit the current workflow attached to this item?
347
	 */
348
	public function canEditWorkflow() {
349
		$active = $this->getWorkflowInstance();
350
		if ($active) {
351
			return $active->canEdit();
352
		}
353
		return false;
354
	}
355
}