1
|
|
|
<?php |
|
|
|
|
2
|
|
|
/** |
3
|
|
|
* A central point for interacting with workflows |
4
|
|
|
* |
5
|
|
|
* @author [email protected] |
6
|
|
|
* @license BSD License (http://silverstripe.org/bsd-license/) |
7
|
|
|
* @package advancedworkflow |
8
|
|
|
*/ |
9
|
|
|
class WorkflowService implements PermissionProvider { |
|
|
|
|
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* An array of templates that we can create from |
13
|
|
|
* |
14
|
|
|
* @var array |
15
|
|
|
*/ |
16
|
|
|
protected $templates; |
17
|
|
|
|
18
|
|
|
public function __construct() { |
19
|
|
|
} |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Set the list of templates that can be created |
24
|
|
|
* |
25
|
|
|
* @param type $templates |
26
|
|
|
*/ |
27
|
|
|
public function setTemplates($templates) { |
28
|
|
|
$this->templates = $templates; |
|
|
|
|
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Return the list of available templates |
33
|
|
|
* @return type |
34
|
|
|
*/ |
35
|
|
|
public function getTemplates() { |
36
|
|
|
return $this->templates; |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Get a template by name |
41
|
|
|
* |
42
|
|
|
* @param string $name |
43
|
|
|
* @return WorkflowTemplate |
44
|
|
|
*/ |
45
|
|
|
public function getNamedTemplate($name) { |
46
|
|
|
if($importedTemplate = singleton('WorkflowDefinitionImporter')->getImportedWorkflows($name)) { |
47
|
|
|
return $importedTemplate; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
if (!is_array($this->templates)) { |
51
|
|
|
return; |
52
|
|
|
} |
53
|
|
|
foreach ($this->templates as $template) { |
54
|
|
|
if ($template->getName() == $name) { |
55
|
|
|
return $template; |
56
|
|
|
} |
57
|
|
|
} |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Gets the workflow definition for a given dataobject, if there is one |
62
|
|
|
* |
63
|
|
|
* Will recursively query parent elements until it finds one, if available |
64
|
|
|
* |
65
|
|
|
* @param DataObject $dataObject |
66
|
|
|
*/ |
67
|
|
|
public function getDefinitionFor(DataObject $dataObject) { |
68
|
|
|
if ($dataObject->hasExtension('WorkflowApplicable') || $dataObject->hasExtension('FileWorkflowApplicable')) { |
69
|
|
|
if ($dataObject->WorkflowDefinitionID) { |
70
|
|
|
return DataObject::get_by_id('WorkflowDefinition', $dataObject->WorkflowDefinitionID); |
71
|
|
|
} |
72
|
|
|
if ($dataObject->hasMethod('useInheritedWorkflow') && !$this->useInheritedWorkflow()) { |
|
|
|
|
73
|
|
|
return null; |
74
|
|
|
} |
75
|
|
|
if ($dataObject->ParentID) { |
76
|
|
|
return $this->getDefinitionFor($dataObject->Parent()); |
|
|
|
|
77
|
|
|
} |
78
|
|
|
if ($dataObject->hasMethod('workflowParent')) { |
79
|
|
|
$obj = $dataObject->workflowParent(); |
80
|
|
|
if ($obj) { |
81
|
|
|
return $this->getDefinitionFor($obj); |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
return null; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Retrieves a workflow definition by ID for a data object. |
90
|
|
|
* |
91
|
|
|
* @param data object |
92
|
|
|
* @param integer |
93
|
|
|
* @return workflow definition |
94
|
|
|
*/ |
95
|
|
|
|
96
|
|
|
public function getDefinitionByID($object, $workflowID) { |
97
|
|
|
|
98
|
|
|
// Make sure the correct extensions have been applied to the data object. |
99
|
|
|
|
100
|
|
|
$workflow = null; |
101
|
|
|
if($object->hasExtension('WorkflowApplicable') || $object->hasExtension('FileWorkflowApplicable')) { |
102
|
|
|
|
103
|
|
|
// Validate the workflow ID against the data object. |
104
|
|
|
|
105
|
|
|
if(($object->WorkflowDefinitionID == $workflowID) || ($workflow = $object->AdditionalWorkflowDefinitions()->byID($workflowID))) { |
106
|
|
|
if(is_null($workflow)) { |
107
|
|
|
$workflow = DataObject::get_by_id('WorkflowDefinition', $workflowID); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
return $workflow ? $workflow : null; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Retrieves and collates the workflow definitions for a data object, where the first element will be the main workflow definition. |
116
|
|
|
* |
117
|
|
|
* @param data object |
118
|
|
|
* @return array |
119
|
|
|
*/ |
120
|
|
|
|
121
|
|
|
public function getDefinitionsFor($object) { |
122
|
|
|
|
123
|
|
|
// Retrieve the main workflow definition. |
124
|
|
|
|
125
|
|
|
$default = $this->getDefinitionFor($object); |
126
|
|
|
if($default) { |
127
|
|
|
|
128
|
|
|
// Merge the additional workflow definitions. |
129
|
|
|
|
130
|
|
|
return array_merge(array( |
131
|
|
|
$default |
132
|
|
|
), $object->AdditionalWorkflowDefinitions()->toArray()); |
133
|
|
|
} |
134
|
|
|
return null; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Gets the workflow for the given item |
139
|
|
|
* |
140
|
|
|
* The item can be |
141
|
|
|
* |
142
|
|
|
* a data object in which case the ActiveWorkflow will be returned, |
143
|
|
|
* an action, in which case the Workflow will be returned |
144
|
|
|
* an integer, in which case the workflow with that ID will be returned |
145
|
|
|
* |
146
|
|
|
* @param mixed $item |
147
|
|
|
* |
148
|
|
|
* @return WorkflowInstance |
149
|
|
|
*/ |
150
|
|
|
public function getWorkflowFor($item, $includeComplete = false) { |
151
|
|
|
$id = $item; |
|
|
|
|
152
|
|
|
|
153
|
|
|
if ($item instanceof WorkflowAction) { |
154
|
|
|
$id = $item->WorkflowID; |
|
|
|
|
155
|
|
|
return DataObject::get_by_id('WorkflowInstance', $id); |
156
|
|
|
} else if (is_object($item) && ($item->hasExtension('WorkflowApplicable') || $item->hasExtension('FileWorkflowApplicable'))) { |
157
|
|
|
$filter = sprintf('"TargetClass" = \'%s\' AND "TargetID" = %d', ClassInfo::baseDataClass($item), $item->ID); |
158
|
|
|
$complete = $includeComplete ? 'OR "WorkflowStatus" = \'Complete\' ' : ''; |
159
|
|
|
return DataObject::get_one('WorkflowInstance', $filter . ' AND ("WorkflowStatus" = \'Active\' OR "WorkflowStatus"=\'Paused\' ' . $complete . ')'); |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Get all the workflow action instances for an item |
165
|
|
|
* |
166
|
|
|
* @return DataObjectSet |
167
|
|
|
*/ |
168
|
|
|
public function getWorkflowHistoryFor($item, $limit = null){ |
169
|
|
|
if($active = $this->getWorkflowFor($item, true)){ |
170
|
|
|
$limit = $limit ? "0,$limit" : ''; |
171
|
|
|
return $active->Actions('', 'ID DESC ', null, $limit); |
|
|
|
|
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Get all the available workflow definitions |
177
|
|
|
* |
178
|
|
|
* @return DataList |
179
|
|
|
*/ |
180
|
|
|
public function getDefinitions() { |
181
|
|
|
return DataList::create('WorkflowDefinition'); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Given a transition ID, figure out what should happen to |
186
|
|
|
* the given $subject. |
187
|
|
|
* |
188
|
|
|
* In the normal case, this will load the current workflow instance for the object |
189
|
|
|
* and then transition as expected. However, in some cases (eg to start the workflow) |
190
|
|
|
* it is necessary to instead create a new instance. |
191
|
|
|
* |
192
|
|
|
* @param DataObject $target |
193
|
|
|
* @param int $transitionId |
194
|
|
|
*/ |
195
|
|
|
public function executeTransition(DataObject $target, $transitionId) { |
196
|
|
|
$workflow = $this->getWorkflowFor($target); |
197
|
|
|
$transition = DataObject::get_by_id('WorkflowTransition', $transitionId); |
198
|
|
|
|
199
|
|
|
if(!$transition) { |
200
|
|
|
throw new Exception(_t('WorkflowService.INVALID_TRANSITION_ID', "Invalid transition ID $transitionId")); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if(!$workflow) { |
204
|
|
|
throw new Exception(_t('WorkflowService.INVALID_WORKFLOW_TARGET', "A transition was executed on a target that does not have a workflow.")); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
if($transition->Action()->WorkflowDefID != $workflow->DefinitionID) { |
|
|
|
|
208
|
|
|
throw new Exception(_t('WorkflowService.INVALID_TRANSITION_WORKFLOW', "Transition #$transition->ID is not attached to workflow #$workflow->ID.")); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
$workflow->performTransition($transition); |
|
|
|
|
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Starts the workflow for the given data object, assuming it or a parent has |
216
|
|
|
* a definition specified. |
217
|
|
|
* |
218
|
|
|
* @param DataObject $object |
219
|
|
|
*/ |
220
|
|
|
public function startWorkflow(DataObject $object, $workflowID = null) { |
221
|
|
|
$existing = $this->getWorkflowFor($object); |
222
|
|
|
if ($existing) { |
223
|
|
|
throw new ExistingWorkflowException(_t('WorkflowService.EXISTING_WORKFLOW_ERROR', "That object already has a workflow running")); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
$definition = null; |
227
|
|
|
if($workflowID) { |
228
|
|
|
|
229
|
|
|
// Retrieve the workflow definition that has been triggered. |
230
|
|
|
|
231
|
|
|
$definition = $this->getDefinitionByID($object, $workflowID); |
232
|
|
|
} |
233
|
|
|
if(is_null($definition)) { |
234
|
|
|
|
235
|
|
|
// Fall back to the main workflow definition. |
236
|
|
|
|
237
|
|
|
$definition = $this->getDefinitionFor($object); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
if ($definition) { |
241
|
|
|
$instance = new WorkflowInstance(); |
242
|
|
|
$instance->beginWorkflow($definition, $object); |
243
|
|
|
$instance->execute(); |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Get all the workflows that this user is responsible for |
249
|
|
|
* |
250
|
|
|
* @param Member $user |
251
|
|
|
* The user to get workflows for |
252
|
|
|
* |
253
|
|
|
* @return ArrayList |
254
|
|
|
* The list of workflow instances this user owns |
255
|
|
|
*/ |
256
|
|
|
public function usersWorkflows(Member $user) { |
257
|
|
|
|
258
|
|
|
$groupIds = $user->Groups()->column('ID'); |
259
|
|
|
|
260
|
|
|
$groupInstances = null; |
261
|
|
|
|
262
|
|
|
$filter = array(''); |
|
|
|
|
263
|
|
|
|
264
|
|
|
if (is_array($groupIds)) { |
265
|
|
|
$groupInstances = DataList::create('WorkflowInstance') |
266
|
|
|
->filter(array('Group.ID:ExactMatchMulti' => $groupIds)) |
267
|
|
|
->where('"WorkflowStatus" != \'Complete\''); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$userInstances = DataList::create('WorkflowInstance') |
271
|
|
|
->filter(array('Users.ID:ExactMatch' => $user->ID)) |
272
|
|
|
->where('"WorkflowStatus" != \'Complete\''); |
273
|
|
|
|
274
|
|
|
if ($userInstances) { |
275
|
|
|
$userInstances = $userInstances->toArray(); |
276
|
|
|
} else { |
277
|
|
|
$userInstances = array(); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
if ($groupInstances) { |
281
|
|
|
$groupInstances = $groupInstances->toArray(); |
282
|
|
|
} else { |
283
|
|
|
$groupInstances = array(); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
$all = array_merge($groupInstances, $userInstances); |
287
|
|
|
|
288
|
|
|
return ArrayList::create($all); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Get items that the passed-in user has awaiting for them to action |
293
|
|
|
* |
294
|
|
|
* @param Member $member |
|
|
|
|
295
|
|
|
* @return DataList $userInstances |
296
|
|
|
*/ |
297
|
|
|
public function userPendingItems(Member $user) { |
298
|
|
|
// Don't restrict anything for ADMIN users |
299
|
|
|
$userInstances = DataList::create('WorkflowInstance') |
300
|
|
|
->where('"WorkflowStatus" != \'Complete\'') |
301
|
|
|
->sort('LastEdited DESC'); |
302
|
|
|
|
303
|
|
|
if(Permission::checkMember($user, 'ADMIN')) { |
304
|
|
|
return $userInstances; |
305
|
|
|
} |
306
|
|
|
$instances = new ArrayList(); |
307
|
|
|
foreach($userInstances as $inst) { |
308
|
|
|
$instToArray = $inst->getAssignedMembers(); |
309
|
|
|
if(!count($instToArray)>0 || !in_array($user->ID,$instToArray->column())) { |
310
|
|
|
continue; |
311
|
|
|
} |
312
|
|
|
$instances->push($inst); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return $instances; |
|
|
|
|
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Get items that the passed-in user has submitted for workflow review |
320
|
|
|
* |
321
|
|
|
* @param Member $member |
|
|
|
|
322
|
|
|
* @return DataList $userInstances |
323
|
|
|
*/ |
324
|
|
|
public function userSubmittedItems(Member $user) { |
325
|
|
|
$userInstances = DataList::create('WorkflowInstance') |
326
|
|
|
->where('"WorkflowStatus" != \'Complete\'') |
327
|
|
|
->sort('LastEdited DESC'); |
328
|
|
|
|
329
|
|
|
// Restrict the user if they're not an ADMIN. |
330
|
|
|
if(!Permission::checkMember($user, 'ADMIN')) { |
331
|
|
|
$userInstances = $userInstances->filter('InitiatorID:ExactMatch', $user->ID); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
return $userInstances; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* Generate a workflow definition based on a template |
339
|
|
|
* |
340
|
|
|
* @param WorkflowDefinition $definition |
341
|
|
|
* @param string $templateName |
342
|
|
|
*/ |
343
|
|
|
public function defineFromTemplate(WorkflowDefinition $definition, $templateName) { |
344
|
|
|
$template = null; |
|
|
|
|
345
|
|
|
/* @var $template WorkflowTemplate */ |
346
|
|
|
|
347
|
|
|
if (!is_array($this->templates)) { |
348
|
|
|
return; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
$template = $this->getNamedTemplate($templateName); |
352
|
|
|
|
353
|
|
|
if (!$template) { |
354
|
|
|
return; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
$template->createRelations($definition); |
358
|
|
|
|
359
|
|
|
// Set the version and do the write at the end so that we don't trigger an infinite loop!! |
360
|
|
|
$definition->Description = $template->getDescription(); |
|
|
|
|
361
|
|
|
$definition->TemplateVersion = $template->getVersion(); |
|
|
|
|
362
|
|
|
$definition->RemindDays = $template->getRemindDays(); |
|
|
|
|
363
|
|
|
$definition->Sort = $template->getSort(); |
|
|
|
|
364
|
|
|
$definition->write(); |
365
|
|
|
return $definition; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Reorders actions within a definition |
370
|
|
|
* |
371
|
|
|
* @param WorkflowDefinition|WorkflowAction $objects |
372
|
|
|
* The objects to be reordered |
373
|
|
|
* @param array $newOrder |
374
|
|
|
* An array of IDs of the actions in the order they should be. |
375
|
|
|
*/ |
376
|
|
|
public function reorder($objects, $newOrder) { |
377
|
|
|
$sortVals = array_values($objects->map('ID', 'Sort')->toArray()); |
378
|
|
|
sort($sortVals); |
379
|
|
|
|
380
|
|
|
// save the new ID values - but only use existing sort values to prevent |
381
|
|
|
// conflicts with items not in the table |
382
|
|
|
foreach($newOrder as $key => $id) { |
383
|
|
|
if (!$id) { |
384
|
|
|
continue; |
385
|
|
|
} |
386
|
|
|
$object = $objects->find('ID', $id); |
387
|
|
|
$object->Sort = $sortVals[$key]; |
388
|
|
|
$object->write(); |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* |
394
|
|
|
* @return array |
395
|
|
|
*/ |
396
|
|
|
public function providePermissions() { |
397
|
|
|
return array( |
398
|
|
|
'CREATE_WORKFLOW' => array( |
399
|
|
|
'name' => _t('AdvancedWorkflow.CREATE_WORKFLOW', 'Create workflow'), |
400
|
|
|
'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), |
401
|
|
|
'help' => _t('AdvancedWorkflow.CREATE_WORKFLOW_HELP', 'Users can create workflow definitions'), |
402
|
|
|
'sort' => 0 |
403
|
|
|
), |
404
|
|
|
'DELETE_WORKFLOW' => array( |
405
|
|
|
'name' => _t('AdvancedWorkflow.DELETE_WORKFLOW', 'Delete workflow'), |
406
|
|
|
'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), |
407
|
|
|
'help' => _t('AdvancedWorkflow.DELETE_WORKFLOW_HELP', 'Users can delete workflow definitions and active workflows'), |
408
|
|
|
'sort' => 0 |
409
|
|
|
), |
410
|
|
|
'APPLY_WORKFLOW' => array( |
411
|
|
|
'name' => _t('AdvancedWorkflow.APPLY_WORKFLOW', 'Apply workflow'), |
412
|
|
|
'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), |
413
|
|
|
'help' => _t('AdvancedWorkflow.APPLY_WORKFLOW_HELP', 'Users can apply workflows to items'), |
414
|
|
|
'sort' => 0 |
415
|
|
|
), |
416
|
|
|
'VIEW_ACTIVE_WORKFLOWS' => array( |
417
|
|
|
'name' => _t('AdvancedWorkflow.VIEWACTIVE', 'View active workflows'), |
418
|
|
|
'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), |
419
|
|
|
'help' => _t('AdvancedWorkflow.VIEWACTIVEHELP', 'Users can view active workflows via the workflows admin panel'), |
420
|
|
|
'sort' => 0 |
421
|
|
|
), |
422
|
|
|
'REASSIGN_ACTIVE_WORKFLOWS' => array( |
423
|
|
|
'name' => _t('AdvancedWorkflow.REASSIGNACTIVE', 'Reassign active workflows'), |
424
|
|
|
'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), |
425
|
|
|
'help' => _t('AdvancedWorkflow.REASSIGNACTIVEHELP', 'Users can reassign active workflows to different users and groups'), |
426
|
|
|
'sort' => 0 |
427
|
|
|
) |
428
|
|
|
); |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
class ExistingWorkflowException extends Exception {}; |
|
|
|
|
434
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.