query()` against the WordPress database connection; * the in-memory double records the lifecycle transitions for * test assertions without touching a real database. Both honor * the same nesting rule and the same exception-propagation * contract. * * Failure semantics summary: * * - `$work` returns normally → `COMMIT`, return value forwarded. * - `$work` throws → `ROLLBACK`, original exception re-thrown. * - Nested call → {@see RuntimeException} before any driver call. * - Driver error during `START TRANSACTION` / `COMMIT` / * `ROLLBACK` → {@see RuntimeException} carrying the driver * message verbatim, with the original `$work` exception (when * applicable) chained via `$previous`. * * @package Affilun\Infrastructure\Persistence */ declare(strict_types=1); namespace Affilun\Infrastructure\Persistence; use RuntimeException; use Throwable; /** * Persistence port that brackets a unit of work with database * transaction primitives. * * See the file-level docblock for the boundary's value-transparent * forwarding contract, the deliberate non-support of nesting in Phase * 2, and the exception-propagation rules implementations must honor. */ interface TransactionManager { /** * Execute the given callable inside a database transaction. * * Lifecycle: * * 1. Open a fresh transaction. If a transaction is already * active on this manager instance, raise * {@see RuntimeException} before issuing any driver call — * Phase 2 does not support nested transactions. * 2. Invoke `$work` with no arguments. * 3. If `$work` returns normally, commit and forward the * return value verbatim to the caller. * 4. If `$work` throws any {@see Throwable}, roll back the * transaction and re-throw the original exception verbatim * (no wrapping, no swallowing). A subsequent driver error * during ROLLBACK MAY surface as a {@see RuntimeException} * chained to the original exception via `$previous`. * * The callable is executed exactly once per call. Implementations * MUST NOT retry on failure — retry / backoff policy is the * caller's concern (per the Phase 9 sync slice's retry budget on * `affilun_sync_jobs`). * * @template TReturn * * @param callable():TReturn $work Unit of work to execute. * * @return TReturn The value returned by `$work`, forwarded verbatim. * * @throws RuntimeException When called while a transaction is * already active on this manager * instance, or when the underlying * driver reports an error during * `START TRANSACTION` / `COMMIT` / * `ROLLBACK`. * @throws Throwable Any exception raised by `$work` is * re-thrown verbatim after the * transaction has been rolled back. */ public function transactional( callable $work ); /** * Report whether a transaction is currently active on this * manager instance. * * Exposed so test assertions can check the lifecycle state * without driving a fresh `transactional()` call. Production * code SHOULD NOT branch on this method — the * {@see self::transactional()} boundary is the canonical entry * point and the only way to enter a transactional scope. */ public function inTransaction(): bool; }