Skip to content

Commit 0b6d83a

Browse files
authored
Merge pull request #8540 from ProcessMaker/FOUR-26712
FOUR-26712 Implement the Events and Logic in Backed for Conditional Destination
2 parents d74f962 + 4c710ea commit 0b6d83a

7 files changed

Lines changed: 1150 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace ProcessMaker\Contracts;
4+
5+
use ProcessMaker\Models\ProcessRequestToken;
6+
7+
/**
8+
* @see \ProcessMaker\Services\ConditionalRedirectService
9+
* @package ProcessMaker\Contracts
10+
*/
11+
interface ConditionalRedirectServiceInterface
12+
{
13+
/**
14+
* Process a set of conditions and return the first that satisfies for an array of data.
15+
*
16+
* @param array $conditionalRedirect
17+
* @param array $data
18+
*
19+
* @return array|null
20+
*/
21+
public function resolve(array $conditionalRedirect, array $data): ?array;
22+
23+
/**
24+
* Process a set of conditions and return the first that satisfies for a process request token.
25+
*
26+
* @param array $conditionalRedirect
27+
* @param ProcessRequestToken $token
28+
*
29+
* @return array|null
30+
*/
31+
public function resolveForToken(array $conditionalRedirect, ProcessRequestToken $token): ?array;
32+
}

ProcessMaker/Models/ProcessRequestToken.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Laravel\Scout\Searchable;
1414
use Log;
1515
use ProcessMaker\Casts\MillisecondsToDateCast;
16+
use ProcessMaker\Contracts\ConditionalRedirectServiceInterface;
1617
use ProcessMaker\Events\ActivityAssigned;
1718
use ProcessMaker\Events\ActivityReassignment;
1819
use ProcessMaker\Facades\WorkflowUserManager;
@@ -1308,10 +1309,21 @@ public function reassign($toUserId, User $requestingUser)
13081309
*
13091310
* @return array|null Returns the destination URL.
13101311
*/
1311-
private function getElementDestination($elementDestinationType, $elementDestinationProp): ?array
1312+
private function getElementDestination($elementDestinationType, $elementDestinationProp, array $conditionalRedirectProp): ?array
13121313
{
13131314
$elementDestination = null;
13141315

1316+
if (!empty($conditionalRedirectProp['isEnabled']) && !empty($conditionalRedirectProp['conditions'])) {
1317+
$result = $this->evaluateConditionalRedirect(app(ConditionalRedirectServiceInterface::class), $conditionalRedirectProp);
1318+
if ($result) {
1319+
$elementDestinationType = $result['taskDestination']['value'];
1320+
$elementDestinationProp = [
1321+
'value' => [
1322+
'url' => ($result['customDashboard']['url'] ?? null) ?: ($result['externalUrl'] ?? null),
1323+
]
1324+
];
1325+
}
1326+
}
13151327
switch ($elementDestinationType) {
13161328
case 'anotherProcess':
13171329
case 'customDashboard':
@@ -1355,6 +1367,14 @@ private function getElementDestination($elementDestinationType, $elementDestinat
13551367
];
13561368
}
13571369

1370+
private function evaluateConditionalRedirect(ConditionalRedirectServiceInterface $conditionalRedirectService, array $conditionalRedirectProp): ?array
1371+
{
1372+
if (!$conditionalRedirectProp['isEnabled']) {
1373+
return null;
1374+
}
1375+
return $conditionalRedirectService->resolveForToken($conditionalRedirectProp['conditions'], $this);
1376+
}
1377+
13581378
/**
13591379
* Determines the destination URL based on the element destination type specified in the definition.
13601380
*
@@ -1365,6 +1385,8 @@ public function getElementDestinationAttribute(): ?array
13651385
$definition = $this->getDefinition();
13661386
$elementDestinationProp = $definition['elementDestination'] ?? null;
13671387
$elementDestinationType = null;
1388+
$conditionalRedirectProp = $definition['conditionalRedirect'] ?? '[]';
1389+
$conditionalRedirectProp = json_decode($conditionalRedirectProp, true);
13681390

13691391
try {
13701392
$elementDestinationProp = json_decode($elementDestinationProp, true);
@@ -1375,7 +1397,7 @@ public function getElementDestinationAttribute(): ?array
13751397
return null;
13761398
}
13771399

1378-
return $this->getElementDestination($elementDestinationType, $elementDestinationProp);
1400+
return $this->getElementDestination($elementDestinationType, $elementDestinationProp, $conditionalRedirectProp);
13791401
}
13801402

13811403
/**

ProcessMaker/Providers/ProcessMakerServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Lavary\Menu\Menu;
2323
use ProcessMaker\Cache\Settings\SettingCacheManager;
2424
use ProcessMaker\Console\Migration\ExtendedMigrateCommand;
25+
use ProcessMaker\Contracts\ConditionalRedirectServiceInterface;
2526
use ProcessMaker\Events\ActivityAssigned;
2627
use ProcessMaker\Events\ScreenBuilderStarting;
2728
use ProcessMaker\Events\TenantResolved;
@@ -41,6 +42,7 @@
4142
use ProcessMaker\Observers;
4243
use ProcessMaker\PolicyExtension;
4344
use ProcessMaker\Repositories\SettingsConfigRepository;
45+
use ProcessMaker\Services\ConditionalRedirectService;
4446
use RuntimeException;
4547
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
4648
use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent;
@@ -288,6 +290,12 @@ public function register(): void
288290
});
289291

290292
$this->app->instance('tenant-resolved', false);
293+
294+
/**
295+
* Conditional Redirect Service
296+
* This service is used to evaluate the conditional redirect property of a process request token.
297+
*/
298+
$this->app->bind(ConditionalRedirectServiceInterface::class, ConditionalRedirectService::class);
291299
}
292300

293301
/**
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
namespace ProcessMaker\Services;
4+
5+
use InvalidArgumentException;
6+
use ProcessMaker\Contracts\ConditionalRedirectServiceInterface;
7+
use ProcessMaker\Managers\DataManager;
8+
use ProcessMaker\Models\Comment;
9+
use ProcessMaker\Models\FormalExpression;
10+
use ProcessMaker\Models\ProcessRequest;
11+
use ProcessMaker\Models\ProcessRequestToken;
12+
13+
/**
14+
* ConditionalRedirectService
15+
*
16+
* This service handles the evaluation of conditional redirects in ProcessMaker workflows.
17+
* It processes a set of conditions and returns the first condition that evaluates to true,
18+
* along with its associated redirect configuration.
19+
*
20+
* The service uses FEEL (Friendly Enough Expression Language) expressions to evaluate
21+
* conditions against process data, allowing for dynamic routing based on runtime data.
22+
*
23+
* @package ProcessMaker\Services
24+
* @since 4.0.0
25+
*/
26+
class ConditionalRedirectService implements ConditionalRedirectServiceInterface
27+
{
28+
/**
29+
* @var FormalExpression
30+
*/
31+
private FormalExpression $feel;
32+
33+
/**
34+
* @var DataManager
35+
*/
36+
private DataManager $dataManager;
37+
38+
private array $errors = [];
39+
40+
/**
41+
* Constructor
42+
*
43+
* Initializes the service with required dependencies for expression evaluation
44+
* and data management.
45+
*/
46+
public function __construct()
47+
{
48+
$this->feel = new FormalExpression();
49+
$this->dataManager = new DataManager();
50+
}
51+
52+
/**
53+
* Process a set of conditional redirects and return the first condition that evaluates to true
54+
*
55+
* This method iterates through an array of conditional redirect configurations,
56+
* evaluating each condition using FEEL expressions against the provided data.
57+
* Returns the first condition that evaluates to true, or null if no conditions match.
58+
*
59+
* @param array $conditionalRedirect Array of conditional redirect configurations
60+
* Each item must contain a 'condition' key with a FEEL expression
61+
* Example: [
62+
* [
63+
* 'condition' => 'amount > 1000',
64+
* 'type' => 'externalURL',
65+
* 'value' => 'https://example.com/approval'
66+
* ],
67+
* [
68+
* 'condition' => 'status = "urgent"',
69+
* 'type' => 'taskList',
70+
* 'value' => null
71+
* ]
72+
* ]
73+
* @param array $data Process data to evaluate conditions against
74+
* Contains variables from the process instance
75+
* Example: ['amount' => 1500, 'status' => 'urgent', 'user' => 'john']
76+
*
77+
* @return array|null The first matching conditional redirect configuration, or null if none match
78+
*
79+
* @throws InvalidArgumentException When a condition item is missing the required 'condition' key
80+
*
81+
* @example
82+
* ```php
83+
* $service = new ConditionalRedirectService();
84+
*
85+
* $conditionalRedirect = [
86+
* [
87+
* 'condition' => 'amount > 1000',
88+
* 'type' => 'externalURL',
89+
* 'value' => 'https://example.com/approval'
90+
* ],
91+
* [
92+
* 'condition' => 'amount <= 1000',
93+
* 'type' => 'taskList',
94+
* 'value' => null
95+
* ]
96+
* ];
97+
*
98+
* $data = ['amount' => 1500, 'status' => 'pending'];
99+
*
100+
* $result = $service->resolve($conditionalRedirect, $data);
101+
* // Returns: ['condition' => 'amount > 1000', 'type' => 'externalURL', 'value' => 'https://example.com/approval']
102+
* ```
103+
*/
104+
public function resolve(array $conditionalRedirect, array $data): ?array
105+
{
106+
$this->errors = [];
107+
foreach ($conditionalRedirect as $item) {
108+
if (!isset($item['condition'])) {
109+
throw new InvalidArgumentException('Condition is required');
110+
}
111+
112+
$condition = $item['condition'];
113+
114+
$this->feel->setBody($condition);
115+
try {
116+
$result = ($this->feel)($data);
117+
} catch (\Throwable $e) {
118+
$this->errors[] = $e->getMessage();
119+
continue;
120+
}
121+
if ($result) {
122+
return $item;
123+
}
124+
}
125+
126+
return null;
127+
}
128+
129+
/**
130+
* Process conditional redirects for a specific process request token
131+
*
132+
* This method is a convenience wrapper that automatically retrieves process data
133+
* from a ProcessRequestToken and evaluates conditional redirects against that data.
134+
* It's commonly used when you have a token and want to determine the appropriate
135+
* redirect based on the current process state and data, it also considers
136+
* multi-instance tasks.
137+
*
138+
* @param array $conditionalRedirect Array of conditional redirect configurations
139+
* Each item must contain a 'condition' key with a FEEL expression
140+
* Example: [
141+
* [
142+
* 'condition' => 'taskStatus = "completed"',
143+
* 'type' => 'homepageDashboard',
144+
* 'value' => null
145+
* ],
146+
* [
147+
* 'condition' => 'taskStatus = "pending"',
148+
* 'type' => 'taskList',
149+
* 'value' => null
150+
* ]
151+
* ]
152+
* @param ProcessRequestToken $token The process request token to evaluate conditions against
153+
* The token contains the process instance data and context
154+
*
155+
* @return array|null The first matching conditional redirect configuration, or null if none match
156+
*
157+
* @throws InvalidArgumentException When a condition item is missing the required 'condition' key
158+
*
159+
* @example
160+
* ```php
161+
* $service = new ConditionalRedirectService();
162+
* $token = ProcessRequestToken::find(123);
163+
*
164+
* $conditionalRedirect = [
165+
* [
166+
* 'condition' => 'taskStatus = "completed"',
167+
* 'type' => 'homepageDashboard',
168+
* 'value' => null
169+
* ],
170+
* [
171+
* 'condition' => 'taskStatus = "pending"',
172+
* 'type' => 'taskList',
173+
* 'value' => null
174+
* ]
175+
* ];
176+
*
177+
* $result = $service->resolveForToken($conditionalRedirect, $token);
178+
* // Returns the appropriate redirect configuration based on the token's data
179+
* ```
180+
*
181+
* @see resolve() For detailed parameter documentation
182+
*/
183+
public function resolveForToken(array $conditionalRedirect, ProcessRequestToken $token): ?array
184+
{
185+
$data = $this->dataManager->getData($token);
186+
$result = $this->resolve($conditionalRedirect, $data);
187+
if ($this->errors) {
188+
$case_number = $this->getCaseNumber($token);
189+
foreach ($this->errors as $error) {
190+
$this->addLogComment($token, $error, $case_number);
191+
}
192+
}
193+
return $result;
194+
}
195+
196+
private function getCaseNumber(ProcessRequestToken $token): ?int
197+
{
198+
// get process request from relationship if loaded, otherwise get from database
199+
if ($token->relationLoaded('processRequest')) {
200+
$case_number = $token->processRequest->case_number;
201+
} else {
202+
// get case_number only to avoid to hidrate all the process request data
203+
$case_number = ProcessRequest::where('id', $token->process_request_id)->value('case_number');
204+
}
205+
206+
return $case_number;
207+
}
208+
209+
private function addLogComment(ProcessRequestToken $token, string $error, string $case_number)
210+
{
211+
Comment::create([
212+
'body' => $error,
213+
'user_id' => null,
214+
'subject' => $error,
215+
'type' => 'LOG',
216+
'case_number' => $case_number,
217+
'commentable_type' => ProcessRequest::class,
218+
'commentable_id' => $token->process_request_id,
219+
]);
220+
}
221+
}

0 commit comments

Comments
 (0)