Skip to content

Commit dc6a664

Browse files
titustangibleclaude
andcommitted
feat(wordpress): fold process discovery into register_hooks (0.2.2)
boot() now truly is the whole ceremony: register_hooks() reads the ddd.long_process container tag (guarded by findTaggedServiceIds, same style as the handler sweep) and registers every tagged LongProcess — plus resume hooks for its #[Awaits] events — with the ProcessRunner. Previously discovery was a separate manual call consumers had to remember: datastream made it, cred declared a privately-named tag (tgbl.long_process) that nothing ever scanned. The scaffolder now emits the _instanceof rule so fresh consumers are saga-ready, and the wiring guide documents the tag-name and manual-call anti-patterns. Double registration stays safe: ProcessRunner::register_event is idempotent per event class on the shared runner instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7c0b8f3 commit dc6a664

5 files changed

Lines changed: 108 additions & 15 deletions

File tree

ddd-wordpress/cli/class-ddd-command.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,13 @@ private function template_services_yaml( string $prefix, string $namespace, stri
554554
autoconfigure: true
555555
public: true
556556
557+
# Every LongProcess subclass is auto-tagged; boot() discovers the tag at
558+
# init:2 and registers each process (plus the resume hooks for its
559+
# #[Awaits] events) with the ProcessRunner. No per-saga wiring needed.
560+
_instanceof:
561+
TangibleDDD\\Application\\Process\\LongProcess:
562+
tags: ['ddd.long_process']
563+
557564
# Config
558565
# Version is wired via the {$prefix}.version container parameter set in di/index.php
559566
# (sourced from the {$version_const} PHP constant when defined).

ddd-wordpress/hooks.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ function register_hooks(IDDDConfig $config, callable $di_getter): void {
5454
ConsumerRegistry::add($config, $di_getter);
5555
register_event_handlers($di_getter);
5656
register_process_hooks($config, $di_getter);
57+
58+
// Process discovery: register ddd.long_process-tagged classes (and the
59+
// resume hooks for their #[Awaits] events) with the ProcessRunner. Needs
60+
// findTaggedServiceIds — a ContainerBuilder API — so containers that don't
61+
// expose it (dumped/opaque) skip discovery, same guard style as
62+
// register_event_handlers' getServiceIds probe above.
63+
if (processes_enabled($config)) {
64+
$container = $di_getter();
65+
if (method_exists($container, 'findTaggedServiceIds')) {
66+
register_processes_from_container($config, $container);
67+
}
68+
}
69+
5770
register_outbox_hooks($config, $di_getter);
5871
register_migration_hooks($config);
5972
}

docs/wiring-a-consumer.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,38 @@ regardless of which features it uses today:
198198
| `register_event_handlers` | async handlers + `IntegrationListener`s never `add_action` (Symfony DI is lazy) — AS fails with "no callbacks registered" |
199199
| `register_outbox_hooks` | **outbox never drains** — integration events written, never delivered (this exact bug shipped twice) |
200200
| `register_process_hooks` | sagas never resume after the AS hop |
201+
| process discovery | saga classes never registered with the `ProcessRunner` — awaited integration events fire but wake nothing (see below) |
201202
| `register_migration_hooks` | framework schema migrations never run — fresh installs never get their tables (the lane runs on `init` + `admin_init`; it replaces the activation hook entirely), upgraded installs drift |
202203

204+
**Process discovery** (0.2.2): `register_hooks()` reads the
205+
`ddd.long_process` container tag and registers every tagged class — plus
206+
the resume hooks for its `#[Awaits(Event::class)]` events — with the
207+
`ProcessRunner`. The consumer's only job is the `_instanceof` rule the
208+
scaffolder already emits:
209+
210+
```yaml
211+
_instanceof:
212+
TangibleDDD\Application\Process\LongProcess:
213+
tags: ['ddd.long_process']
214+
```
215+
216+
Discovery needs `findTaggedServiceIds()` (a `ContainerBuilder` API) — with a
217+
dumped/opaque container it is skipped silently, same guard as the handler
218+
sweep. Declare awaited events with the `#[Awaits]` class attribute, not tag
219+
parameters.
220+
203221
Anti-patterns seen in the wild:
204222

205223
- **À-la-carte calls** (`register_process_hooks` + `register_outbox_hooks`
206224
only) — silently skips migrations and eager handler boot.
207225
- **Homegrown eager-boot registrars** duplicating `register_event_handlers`
208226
by iterating service ids. Delete them; the framework function covers
209227
`\Application\EventHandlers\` and `\Application\IntegrationListeners\`.
228+
- **Manual `register_processes_from_container()` calls** in consumer
229+
bootstrap — redundant since 0.2.2 (idempotent, but delete on next touch).
230+
- **Consumer-prefixed process tags** (`acme.long_process`) — the framework
231+
scans `ddd.long_process` only; a private tag name means your sagas are
232+
never discovered.
210233

211234
## 7. Events — 0.2.0 taxonomy in four rules
212235

tangible-ddd.php

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: Tangible DDD
44
* Plugin URI: https://tangible.one
55
* Description: Domain-Driven Design framework for WordPress plugins
6-
* Version: 0.2.1
6+
* Version: 0.2.2
77
* Author: Tangible
88
* Author URI: https://tangible.one
99
* License: MIT
@@ -38,7 +38,7 @@
3838
// Guarded: the first copy to load wins the constant (oldest-loads-first is fine;
3939
// the registry, not the constant, determines the winner).
4040
if (!defined('TANGIBLE_DDD_VERSION')) {
41-
define('TANGIBLE_DDD_VERSION', '0.2.1');
41+
define('TANGIBLE_DDD_VERSION', '0.2.2');
4242
}
4343

4444
// ─── Tangible_DDD_Versions registry (defined once, first copy wins the class) ─
@@ -87,7 +87,7 @@ public static function instance(): self
8787
/**
8888
* Register a copy of tangible-ddd.
8989
*
90-
* @param string $version SemVer string (e.g. '0.2.1').
90+
* @param string $version SemVer string (e.g. '0.2.2').
9191
* @param string $path Absolute path to the ddd plugin root.
9292
* @param callable $initialize Callback that boots THIS copy: receives $path.
9393
* @param string|null $min_required Minimum ddd version this consumer needs.
@@ -223,42 +223,42 @@ public function is_initialized(): bool
223223
} // if (!class_exists('Tangible_DDD_Versions'))
224224

225225
// ─── Version-named register function (guarded — safe in N copies) ────────────
226-
// Slug: 0.2.10_2_1 (dots and hyphens → underscores)
227-
if (!function_exists('tangible_ddd_register_0_2_1')) {
226+
// Slug: 0.2.20_2_2 (dots and hyphens → underscores)
227+
if (!function_exists('tangible_ddd_register_0_2_2')) {
228228

229229
/**
230-
* Register this copy (0.2.1) into Tangible_DDD_Versions.
230+
* Register this copy (0.2.2) into Tangible_DDD_Versions.
231231
* Hooked at plugins_loaded priority 0 so all copies register before pri-1 wins.
232232
*/
233-
function tangible_ddd_register_0_2_1(): void
233+
function tangible_ddd_register_0_2_2(): void
234234
{
235235
// Pass a closure so the callable type check passes even when this register
236-
// function is invoked before tangible_ddd_initialize_0_2_1() is defined
236+
// function is invoked before tangible_ddd_initialize_0_2_2() is defined
237237
// (which can happen in unit-test context where add_action is absent and we
238238
// self-register immediately at file-include time).
239239
Tangible_DDD_Versions::instance()->register(
240-
'0.2.1',
240+
'0.2.2',
241241
__DIR__,
242242
static function (string $path): void {
243-
tangible_ddd_initialize_0_2_1($path);
243+
tangible_ddd_initialize_0_2_2($path);
244244
}
245245
);
246246
}
247247

248248
if (function_exists('add_action')) {
249-
add_action('plugins_loaded', 'tangible_ddd_register_0_2_1', 0, 0);
249+
add_action('plugins_loaded', 'tangible_ddd_register_0_2_2', 0, 0);
250250
} else {
251251
// Outside WP (unit tests / direct require with no hook system):
252252
// self-register immediately. initialize_latest() is NOT called here
253253
// so that N copies included during tests can each register first;
254254
// test code can call Tangible_DDD_Versions::instance()->initialize_latest()
255255
// explicitly, or the bootstrap triggers it below.
256-
tangible_ddd_register_0_2_1();
256+
tangible_ddd_register_0_2_2();
257257
}
258258
}
259259

260260
// ─── Version-named initializer (guarded — the winner calls this) ──────────────
261-
if (!function_exists('tangible_ddd_initialize_0_2_1')) {
261+
if (!function_exists('tangible_ddd_initialize_0_2_2')) {
262262

263263
/**
264264
* Boot this copy of tangible-ddd as the site winner.
@@ -269,7 +269,7 @@ static function (string $path): void {
269269
*
270270
* @param string $path Absolute path to the winning ddd plugin root.
271271
*/
272-
function tangible_ddd_initialize_0_2_1(string $path): void
272+
function tangible_ddd_initialize_0_2_2(string $path): void
273273
{
274274
// (a) Prepend autoloader — winner's classes beat consumer psr-4 maps.
275275
spl_autoload_register(
@@ -386,6 +386,6 @@ function_exists('did_action') && did_action('plugins_loaded')
386386
&& function_exists('doing_action') && !doing_action('plugins_loaded')
387387
&& !Tangible_DDD_Versions::instance()->is_initialized()
388388
) {
389-
tangible_ddd_register_0_2_1();
389+
tangible_ddd_register_0_2_2();
390390
Tangible_DDD_Versions::instance()->initialize_latest();
391391
}

tests/Unit/WordPress/BootTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
namespace TangibleDDD\Tests\Unit\WordPress;
44

55
use PHPUnit\Framework\TestCase;
6+
use TangibleDDD\Application\Process\ProcessRunner;
7+
use TangibleDDD\Infra\IDDDConfig;
68
use TangibleDDD\Tests\Fakes\FakeDDDConfig;
9+
use TangibleDDD\Tests\Fakes\FakeGatherProcess;
10+
use TangibleDDD\Tests\Fakes\FakeProcessRepository;
11+
use TangibleDDD\Tests\Fakes\FakeResolvedEvent;
712
use TangibleDDD\WordPress\ConsumerRegistry;
813

914
use function TangibleDDD\WordPress\boot;
@@ -107,4 +112,49 @@ public function test_consumers_pass_through_the_filter(): void {
107112

108113
$this->assertSame([], consumers());
109114
}
115+
116+
public function test_register_hooks_discovers_tagged_long_processes(): void {
117+
global $_test_actions;
118+
119+
// Unique prefix so processes_enabled()'s static cache (keyed by prefix,
120+
// poisoned to false by the other tests' stub wpdb) probes fresh.
121+
$config = new class implements IDDDConfig {
122+
public function prefix(): string { return 'proc'; }
123+
public function table(string $name): string { return 'wp_proc_' . $name; }
124+
public function hook(string $name): string { return 'proc_' . $name; }
125+
public function as_group(string $name): string { return 'proc-' . $name; }
126+
public function option(string $name): string { return 'proc_' . $name; }
127+
public function domain_action(string $event_name): string { return 'proc_domain_' . $event_name; }
128+
public function integration_action(string $event_name): string { return 'proc_integration_' . $event_name; }
129+
public function version(): string { return 'proc'; }
130+
};
131+
132+
// Answer every SHOW TABLES probe with the long_processes table name:
133+
// the process gate opens, the outbox gate (comparing against its own
134+
// table name) stays closed.
135+
$GLOBALS['wpdb'] = new class extends \wpdb {
136+
public function get_var(?string $query = null, int $x = 0, int $y = 0) {
137+
return 'wp_proc_long_processes';
138+
}
139+
};
140+
141+
$runner = new ProcessRunner($config, new FakeProcessRepository());
142+
$container = new class($runner) {
143+
public function __construct(private readonly ProcessRunner $runner) {}
144+
public function getServiceIds(): array { return []; }
145+
public function findTaggedServiceIds(string $tag): array {
146+
return 'ddd.long_process' === $tag
147+
? [FakeGatherProcess::class => [[]]]
148+
: [];
149+
}
150+
public function get(string $id): ProcessRunner { return $this->runner; }
151+
};
152+
153+
\TangibleDDD\WordPress\register_hooks($config, static fn () => $container);
154+
155+
$this->assertNotEmpty(
156+
$_test_actions[FakeResolvedEvent::integration_action()] ?? [],
157+
'register_hooks must wire resume hooks for the #[Awaits] events of ddd.long_process-tagged classes',
158+
);
159+
}
110160
}

0 commit comments

Comments
 (0)