Skip to content

Commit 4ec773a

Browse files
authored
Merge branch 'develop' into task/FOUR-28803
2 parents ea10d72 + e9bcdc9 commit 4ec773a

13 files changed

Lines changed: 1239 additions & 48 deletions

File tree

ProcessMaker/Console/Commands/Install.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class Install extends Command
5252
{--data-username= : The data database username}
5353
{--data-password= : The data database password}
5454
{--data-schema= : The data database schema (if pgsql)}
55-
{--redis-client=predis : The Redis client (predis or phpredis)}
55+
{--redis-client=phpredis : The Redis client (predis or phpredis)}
5656
{--redis-host= : The Redis host, default is 127.0.0.1}
5757
{--redis-prefix= : The prefix to be appended to Redis entries}
5858
{--horizon-prefix=horizon: : The prefix to be appended to Horizon queue entries}
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/Events/ProcessUpdated.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class ProcessUpdated implements ShouldBroadcastNow
2626

2727
public $activeTokens;
2828

29+
public $elementDestination;
30+
2931
/**
3032
* Create a new event instance.
3133
*
@@ -41,6 +43,7 @@ public function __construct(ProcessRequest $processRequest, $event, TokenInterfa
4143
if ($token) {
4244
$this->tokenId = $token->getId();
4345
$this->elementType = $token->element_type;
46+
$this->elementDestination = $token->elementDestination;
4447
}
4548
}
4649

ProcessMaker/Models/ProcessRequestToken.php

Lines changed: 32 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;
@@ -1448,10 +1449,28 @@ public function reassign($toUserId, User $requestingUser, $comments = '')
14481449
*
14491450
* @return array|null Returns the destination URL.
14501451
*/
1451-
private function getElementDestination($elementDestinationType, $elementDestinationProp): ?array
1452+
private function getElementDestination($elementDestinationType, $elementDestinationProp, array $conditionalRedirectProp): ?array
14521453
{
14531454
$elementDestination = null;
14541455

1456+
if (!empty($conditionalRedirectProp['isEnabled']) && !empty($conditionalRedirectProp['conditions'])) {
1457+
$result = $this->evaluateConditionalRedirect(app(ConditionalRedirectServiceInterface::class), $conditionalRedirectProp);
1458+
if ($result) {
1459+
$elementDestinationType = $result['taskDestination']['value'];
1460+
1461+
$url = match ($elementDestinationType) {
1462+
'customDashboard' => $result['customDashboard']['url'] ?? null,
1463+
'externalURL' => $result['externalUrl'] ?? null,
1464+
default => null,
1465+
};
1466+
1467+
$elementDestinationProp = [
1468+
'value' => [
1469+
'url' => $url,
1470+
],
1471+
];
1472+
}
1473+
}
14551474
switch ($elementDestinationType) {
14561475
case 'anotherProcess':
14571476
case 'customDashboard':
@@ -1495,6 +1514,15 @@ private function getElementDestination($elementDestinationType, $elementDestinat
14951514
];
14961515
}
14971516

1517+
private function evaluateConditionalRedirect(ConditionalRedirectServiceInterface $conditionalRedirectService, array $conditionalRedirectProp): ?array
1518+
{
1519+
if (!$conditionalRedirectProp['isEnabled']) {
1520+
return null;
1521+
}
1522+
1523+
return $conditionalRedirectService->resolveForToken($conditionalRedirectProp['conditions'], $this);
1524+
}
1525+
14981526
/**
14991527
* Determines the destination URL based on the element destination type specified in the definition.
15001528
*
@@ -1505,6 +1533,8 @@ public function getElementDestinationAttribute(): ?array
15051533
$definition = $this->getDefinition();
15061534
$elementDestinationProp = $definition['elementDestination'] ?? null;
15071535
$elementDestinationType = null;
1536+
$conditionalRedirectProp = $definition['conditionalRedirect'] ?? '[]';
1537+
$conditionalRedirectProp = json_decode($conditionalRedirectProp, true);
15081538

15091539
try {
15101540
$elementDestinationProp = json_decode($elementDestinationProp, true);
@@ -1515,7 +1545,7 @@ public function getElementDestinationAttribute(): ?array
15151545
return null;
15161546
}
15171547

1518-
return $this->getElementDestination($elementDestinationType, $elementDestinationProp);
1548+
return $this->getElementDestination($elementDestinationType, $elementDestinationProp, $conditionalRedirectProp);
15191549
}
15201550

15211551
/**

ProcessMaker/Providers/ProcessMakerServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use ProcessMaker\Cache\Settings\SettingCacheManager;
3131
use ProcessMaker\Console\Commands\HorizonListen;
3232
use ProcessMaker\Console\Migration\ExtendedMigrateCommand;
33+
use ProcessMaker\Contracts\ConditionalRedirectServiceInterface;
3334
use ProcessMaker\Events\ActivityAssigned;
3435
use ProcessMaker\Events\ScreenBuilderStarting;
3536
use ProcessMaker\Events\TenantResolved;
@@ -50,6 +51,7 @@
5051
use ProcessMaker\PolicyExtension;
5152
use ProcessMaker\Providers\PermissionServiceProvider;
5253
use ProcessMaker\Repositories\SettingsConfigRepository;
54+
use ProcessMaker\Services\ConditionalRedirectService;
5355
use RuntimeException;
5456
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
5557
use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent;
@@ -245,6 +247,12 @@ public function register(): void
245247
});
246248

247249
$this->app->instance('tenant-resolved', false);
250+
251+
/**
252+
* Conditional Redirect Service
253+
* This service is used to evaluate the conditional redirect property of a process request token.
254+
*/
255+
$this->app->bind(ConditionalRedirectServiceInterface::class, ConditionalRedirectService::class);
248256
}
249257

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

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "processmaker/processmaker",
3-
"version": "2026.2.3",
3+
"version": "2026.2.4",
44
"description": "BPM PHP Software",
55
"keywords": [
66
"php bpm processmaker"

0 commit comments

Comments
 (0)