Skip to content

Commit 7d2ace6

Browse files
Merge pull request #67 from dreamfactorysoftware/develop
Develop
2 parents 18a70d6 + f00ec3e commit 7d2ace6

2 files changed

Lines changed: 291 additions & 141 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace DreamFactory\Core\Database\Components;
4+
5+
use Illuminate\Support\Facades\Log;
6+
7+
/**
8+
* Tracks transaction context for virtual relationship writes.
9+
*
10+
* For same-service relations, the existing DB transaction covers both tables.
11+
* For cross-service relations, this tracks compensating actions (saga pattern)
12+
* to reverse operations if a later step fails.
13+
*/
14+
class RelationTransactionContext
15+
{
16+
/** @var bool Whether to rollback all changes on any failure */
17+
protected $rollback = false;
18+
19+
/** @var bool Whether to continue processing after a failure */
20+
protected $continue = false;
21+
22+
/** @var array Compensating actions to reverse cross-service operations */
23+
protected $compensatingActions = [];
24+
25+
/** @var array Errors collected during continue mode */
26+
protected $errors = [];
27+
28+
public function __construct(bool $rollback = false, bool $continue = false)
29+
{
30+
$this->rollback = $rollback;
31+
$this->continue = $continue;
32+
}
33+
34+
/**
35+
* Register a compensating action to reverse a cross-service operation.
36+
* Actions are executed in LIFO order during rollback.
37+
*/
38+
public function addCompensatingAction(
39+
string $service,
40+
string $resource,
41+
string $verb,
42+
$data = null,
43+
$params = null
44+
) {
45+
$this->compensatingActions[] = compact('service', 'resource', 'verb', 'data', 'params');
46+
}
47+
48+
/**
49+
* Record an error for a relation operation (used in continue mode).
50+
*/
51+
public function addError(string $relationName, \Exception $ex)
52+
{
53+
$this->errors[$relationName] = $ex;
54+
}
55+
56+
public function hasErrors(): bool
57+
{
58+
return !empty($this->errors);
59+
}
60+
61+
public function getErrors(): array
62+
{
63+
return $this->errors;
64+
}
65+
66+
/**
67+
* Get error messages as a simple associative array.
68+
*/
69+
public function getErrorMessages(): array
70+
{
71+
$messages = [];
72+
foreach ($this->errors as $name => $ex) {
73+
$messages[$name] = $ex->getMessage();
74+
}
75+
return $messages;
76+
}
77+
78+
/**
79+
* Execute all compensating actions in reverse order (LIFO).
80+
* Compensations are best-effort; failures are logged but not thrown.
81+
*/
82+
public function executeCompensations(callable $handleVirtualRecordsCallback)
83+
{
84+
foreach (array_reverse($this->compensatingActions) as $action) {
85+
try {
86+
$handleVirtualRecordsCallback(
87+
$action['service'],
88+
$action['resource'],
89+
$action['verb'],
90+
$action['data'],
91+
$action['params']
92+
);
93+
} catch (\Exception $e) {
94+
Log::error("Saga compensation failed: " . $e->getMessage(), [
95+
'service' => $action['service'],
96+
'resource' => $action['resource'],
97+
'verb' => $action['verb'],
98+
]);
99+
}
100+
}
101+
102+
$this->compensatingActions = [];
103+
}
104+
105+
public function hasCompensatingActions(): bool
106+
{
107+
return !empty($this->compensatingActions);
108+
}
109+
110+
public function shouldRollback(): bool
111+
{
112+
return $this->rollback;
113+
}
114+
115+
public function shouldContinue(): bool
116+
{
117+
return $this->continue;
118+
}
119+
}

0 commit comments

Comments
 (0)