Workflows¶
Workflows are part of the entity definition and describe the lifecycle of an entity through a series of statuses and transitions.
When a workflow must be assigned to an entity, a dedicated method, getWorkflow(), must be defined for describing the workflow.
The getWorkflow() method must return an associative array in which all possible statuses are identified by a key of the map.
For a given entity, the workflow reflects all possible values of the special field status (e.g., validated, suspended, confirmed), along with all possible transitions from one status to another (e.g., validate, suspend, confirm).
Example¶
<?php
public static function getWorkflow() {
return [
'created' => [
'transitions' => [
'validate' => [
'watch' => ['validated'],
'domain' => ['validated', '=', true],
'description' => 'Update the user status as validated.',
'status' => 'validated',
]
]
],
'validated' => [
'transitions' => [
'suspend' => [
'description' => 'Set the user status as suspended.',
'status' => 'suspended'
],
'confirm' => [
'domain' => ['validated', '=', true],
'description' => 'Update the user status as confirmed.',
'status' => 'confirmed'
]
]
]
];
}
Key Principles¶
| Principle | Description |
|---|---|
| Default Behavior | By default, no workflow is defined. |
| Status Field Requirement | The workflow assumes the existence of a status field. Only values defined in the selection property of the status field can be assigned to it. The initial value must be specified in the default property. |
| Single Workflow per Class | There can be only one workflow per class. A behavioral change implies a change in logic, justifying the creation of a new class, potentially in a new package. |
| Inheritance | A workflow can be overwritten or overridden by inheritance. |
| Field Constraints | The workflow allows adding constraints to entity fields (readonly, required, visible) based on the status. |
| Dependencies | The status field can have a dependencies property to force recalculation of computed fields when modified. |
| UI Integration | In the UI, possible transitions from the current state are visible in the transition menu, with actions displayed to the right of the current status. |
| Schema Adaptation | If a getWorkflow() method is defined, the schema returned by getSchema() is adapted based on the current status. |
| Initial Status | The initial status is defined in the default property of the status field definition. |
| Final Status | The final status (if any) is the status for which there is no outgoing transition. |
Workflow Graph Structure¶
The workflow is composed of nodes representing statuses (successive logical states through which an entity can pass) connected by vectors representing transitions. Multiple outgoing and incoming transitions can exist on the same status.
{
"status_name_A": {
"readonly": {},
"transitions": {
"transitionX": {
"watch": ["field1", "field3"],
"domain": ["field1", ">", "field3"],
"policies": ["policy1", "policy2"],
"description": "f1 > f3",
"status": "status_name_B"
},
"transitionY": {
"watch": ["has_quotation_sent"],
"description": "has_quotation_sent",
"status": "status_name_C"
},
"transitionZ": {
"description": "manual",
"status": "status_name_D",
"onbefore": "onbeforeTransitionC",
"onafter": "onafterTransitionC"
}
}
}
}
Transition Properties¶
| Property | Description |
|---|---|
watch |
Array of field names that trigger automatic transition evaluation when modified. |
domain |
Condition that must be satisfied for the transition to be allowed. Uses Domain syntax. |
policies |
Array of policy names that must pass for the transition to proceed. |
description |
Human-readable description of the transition. |
status |
Target status after the transition completes. |
onbefore |
Method name to call before the transition executes. |
onafter |
Method name to call after the transition completes. |
Transition Types¶
Manual Transitions¶
Manual transitions are triggered explicitly by controllers or user actions.
Direct status modification: Controllers can directly modify the status of an object, permitted only if consistent with the workflow:
- Retrieve the workflow and find the descriptor for the current status.
- Filter transitions whose
statusproperty matches the requested new status. - If no match exists or no transition has all conditions fulfilled, an error is thrown.
- If a match exists with all conditions fulfilled, the status is updated. If a
functionproperty exists, it is called with the current object.
Signal-based transition: A controller can invoke a transition by emitting a signal using ORM::transition(transition_id):
- Retrieve the workflow and find the descriptor for the current status.
- Search for the transition among those in the descriptor.
- If a match exists and optional conditions are fulfilled, the status is updated.
- Otherwise, an error is thrown.
Automated Transitions¶
Automated transitions occur when modifying an object triggers a status change based on field values:
- Retrieve the workflow and find the descriptor for the current status.
- Filter transitions whose
watchproperty contains any of the updated fields. - If a match exists with all conditions fulfilled, the status is updated based on the
statusproperty. - Otherwise, no action is taken.
Transition Trigger
Transitions can be activated either by complex actions (external) or by modifications of the object's fields.
Object Lifecycle vs Business Workflow¶
In eQual, two distinct concepts govern the evolution of an object: state and status.
state: Technical Lifecycle¶
The state field describes the technical lifecycle of an object, independent of its business logic. It determines whether an object is currently active in the system and what level of interaction is allowed.
| State | Description |
|---|---|
draft |
The object is being created or edited and is not yet considered final. It is excluded from most system operations and may be incomplete. |
instance |
The object is active and usable in the application. This is the default state for normal operations. |
archive |
The object has been deactivated or logically deleted. It is kept for record-keeping but excluded from active workflows. |
This field is managed internally by the framework and is essential for filtering, safety checks, and data lifecycle management.
status: Business Workflow¶
The status field expresses the functional or business logic progression of an object. It models the steps of a business workflow specific to the object type (e.g., orders, publications, approvals).
Recommended generic pattern:
[created] → pending → validated
created(optional): Initial state upon instantiation, before submission.pending: Waiting for review, approval, or further action.validated: Object is confirmed, accepted, or finalized.
Variants like published, approved, rejected, or cancelled can be added depending on the object type and use case.
Important Distinction
Although both state and status may contain a value like draft, their meanings are independent:
state: draft→ Object is not yet usable or finalized at the system level.status: draft→ Object is in the early stage of its business process (e.g., not yet submitted).
Best Practices¶
- Keep use of
statevalues minimal. - Use
statusto implement domain-specific workflows with clear transition rules. - Avoid using the same labels in
statusthat might cause confusion withstatevalues.
Using Policies in Workflows¶
Transitions can reference policies to enforce authorization rules before allowing a status change. Policies are specified in the policies array of a transition definition.
When a transition includes policies, the system verifies that all specified policies pass before executing the transition. If any policy fails, the transition is blocked.
Error Handling¶
Invalid Transition¶
When a transition is not allowed from the current status:
{
"errors": {
"NOT_ALLOWED": {
"invalid_transition": "No transition 'validate' from status 'created' defined in core\\User workflow."
}
}
}
Field-Level Errors¶
NOT_ALLOWED responses can include nested descriptors for each involved field:
{
"errors": {
"NOT_ALLOWED": {
"legal_name": {
"missing": "Legal name cannot be empty for legal person."
}
}
}
}
Broken Policy¶
When a policy check fails:
{
"errors": {
"NOT_ALLOWED": {
"broken_policy": "Collection does not comply with Policy releasable"
}
}
}