-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path09_model_lifecycle.php
More file actions
191 lines (156 loc) · 6.92 KB
/
09_model_lifecycle.php
File metadata and controls
191 lines (156 loc) · 6.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<?php
declare(strict_types=1);
/**
* Example 09 — Real-World Pattern: Domain Model with Event-Driven Lifecycle
*
* This example shows the intended usage pattern: a domain model whose
* getters and setters go through event chains, allowing external code to
* hook into every property change and read.
*
* This is the same pattern used in production (e.g. finance library) where
* traits like StatusTrait, CostTrait, NamedTrait wrap every property
* access in EventExecutor::run() + CommonEvents helpers.
*/
require __DIR__ . '/../vendor/autoload.php';
use Minfra\Chains\EventExecutor;
use Minfra\Chains\EventInterface;
use Minfra\Chains\EventSourceHooksInterface;
use Minfra\Chains\CallbackInterface;
use Minfra\Chains\CallbackTrait;
use Minfra\Chains\CallbackEntry;
use Minfra\Chains\CommonEvents;
use Minfra\Chains\Result;
use Minfra\Chains\BasicResultInterface;
use Minfra\Chains\Meta;
echo "=== 09: Domain Model Lifecycle ===\n\n";
$b = fn(bool $v) => $v ? 'true' : 'false';
// ---------------------------------------------------------------------------
// Define event name constants (like interfaces do in a real project)
// ---------------------------------------------------------------------------
const EVENT_SET_NAME = 'on_set_name';
const EVENT_GET_NAME = 'on_get_name';
const EVENT_SET_PRICE = 'on_set_price';
const EVENT_GET_PRICE = 'on_get_price';
const EVENT_SET_STATUS = 'on_set_status';
const EVENT_GET_STATUS = 'on_get_status';
// ---------------------------------------------------------------------------
// The domain model — uses CommonEvents for getter/setter patterns
// ---------------------------------------------------------------------------
class Product implements CallbackInterface, EventSourceHooksInterface {
use CallbackTrait;
protected string $name = '';
protected float $price = 0.0;
protected string $status = 'draft';
function __construct() {
// Register a PRE callback that validates price on every set_price event
$this->appendCallback(
(new CallbackEntry(EVENT_SET_PRICE, 'pre'))
->setCallback(function (Meta $d) {
if ($d->price < 0) {
return Result::false('price_cannot_be_negative', $d);
}
return true;
}),
);
// Register a POST callback that auto-activates when price is set above 0
$this->appendCallback(
(new CallbackEntry(EVENT_SET_PRICE, 'post'))
->setCallback(function (Meta $d) {
if ($d->price > 0 && CommonEvents::safeAttributeGet($d->this, 'status') === 'draft') {
CommonEvents::safeAttributeSet($d->this, 'status', 'active');
}
return true;
}),
);
}
// -- Event source: auto-inject hooks for all events --
function getEventHooks(string $name, array $hooks): array {
// Automatically add an audit log to ON_AFTER for every setter
if (str_starts_with($name, 'on_set_')) {
$hooks[EventInterface::ON_AFTER][] = function (Meta $d) use ($name) {
echo " [audit] {$name} completed\n";
return null;
};
}
return $hooks;
}
// -- Getters / Setters via event chains --
function setName(string $name): BasicResultInterface {
return EventExecutor::run($this, EVENT_SET_NAME, ...CommonEvents::setter(['name' => $name]));
}
function getName(): BasicResultInterface {
return EventExecutor::run($this, EVENT_GET_NAME, ...CommonEvents::getter('name', $this->name ?? null));
}
function setPrice(float $price): BasicResultInterface {
return EventExecutor::run($this, EVENT_SET_PRICE, ...CommonEvents::setter(['price' => $price]));
}
function getPrice(): BasicResultInterface {
return EventExecutor::run($this, EVENT_GET_PRICE, ...CommonEvents::getter('price', $this->price ?? null));
}
function setStatus(string $status): BasicResultInterface {
return EventExecutor::run($this, EVENT_SET_STATUS, ...CommonEvents::setter(['status' => $status]));
}
function getStatus(): BasicResultInterface {
return EventExecutor::run($this, EVENT_GET_STATUS, ...CommonEvents::getter('status', $this->status ?? null));
}
}
// ---------------------------------------------------------------------------
// Usage
// ---------------------------------------------------------------------------
$product = new Product();
// Set name
echo "9a) Set name:\n";
$r = $product->setName('Widget');
echo " ok={$b($r->ok())}\n";
echo " name={$product->getName()->data()->name}\n\n";
// Set price (valid)
echo "9b) Set price = 29.99:\n";
$r = $product->setPrice(29.99);
echo " ok={$b($r->ok())}\n";
echo " price={$product->getPrice()->data()->price}\n";
echo " status={$product->getStatus()->data()->status} (auto-activated from draft)\n\n";
// Set price (invalid — negative)
echo "9c) Set price = -5 (should fail):\n";
$r = $product->setPrice(-5);
echo " ok={$b($r->ok())}, status={$r->status()}\n";
echo " price={$product->getPrice()->data()->price} (unchanged)\n\n";
// ---------------------------------------------------------------------------
// External hook — a consumer adds validation without modifying Product
// ---------------------------------------------------------------------------
echo "9d) External hooks from outside the class:\n";
$product2 = new Product();
// Add an external PRE callback that enforces max price
$product2->appendCallback(
(new CallbackEntry(EVENT_SET_PRICE, 'pre', priority: 10))
->setCallback(function (Meta $d) {
if ($d->price > 1000) {
return Result::false('price_exceeds_max', $d);
}
return true;
}),
);
$r = $product2->setPrice(500);
echo " price=500 -> ok={$b($r->ok())}\n";
$r = $product2->setPrice(1500);
echo " price=1500 -> ok={$b($r->ok())}, status={$r->status()}\n\n";
// ---------------------------------------------------------------------------
// Using the done flag for early termination from hooks
// ---------------------------------------------------------------------------
echo "9e) ON_BEFORE hook with early termination (done):\n";
$result = EventExecutor::run(
instance: $product,
name: EVENT_SET_STATUS,
data: Meta::make(status: 'archived', params: ['status']),
hooks: [
EventInterface::ON_BEFORE => function (Meta $d) {
// Don't allow changing status of active products to archived
if (CommonEvents::safeAttributeGet($d->this, 'status') === 'active') {
echo " [guard] Cannot archive active product\n";
return Result::true($d, completed: true)->setDone();
}
return null;
},
],
);
echo " status={$product->getStatus()->data()->status} (unchanged, hook terminated early)\n";
echo "\n--- 09 done ---\n";