*/ private array $events = array(); /** * Execute the unit of work inside a recorded transactional scope. * * See {@see TransactionManager::transactional()} for the full * contract. This implementation: * * - Refuses re-entry: if a transaction is already active on * this instance, raises {@see \RuntimeException} before * recording any event, matching the contract's * "Phase 2 does not support nesting" rule. * * - Records {@see self::EVENT_BEGIN} immediately before * invoking `$work`. * * - Records {@see self::EVENT_COMMIT} when `$work` returns * normally and forwards its return value verbatim. * * - Records {@see self::EVENT_ROLLBACK} when `$work` throws * any {@see \Throwable}, then re-throws the original * exception verbatim (no wrapping, no swallowing). * * @template TReturn * * @param callable():TReturn $work Unit of work to execute. * * @return TReturn The value returned by `$work`. * * @throws RuntimeException When called while a transaction is * already active on this instance. * @throws Throwable Any exception raised by `$work` is * re-thrown verbatim after a * rollback event has been recorded. */ public function transactional( callable $work ) { if ( $this->inTransaction ) { $message = 'InMemoryTransactionManager::transactional() does not support nested calls; ' . 'a transaction is already active on this instance.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new RuntimeException( $message ); } $this->inTransaction = true; $this->events[] = self::EVENT_BEGIN; try { $result = $work(); } catch ( Throwable $error ) { $this->events[] = self::EVENT_ROLLBACK; $this->inTransaction = false; throw $error; } $this->events[] = self::EVENT_COMMIT; $this->inTransaction = false; return $result; } /** * Report whether a transaction is currently active on this * instance. * * Mirrors the {@see TransactionManager::inTransaction()} surface; * see the contract for the rule that production code SHOULD NOT * branch on this method. */ public function inTransaction(): bool { return $this->inTransaction; } /** * Ordered list of every lifecycle event this manager has * recorded across its lifetime, in the order they occurred. * * Each entry is one of {@see self::EVENT_BEGIN}, * {@see self::EVENT_COMMIT}, or {@see self::EVENT_ROLLBACK}. * Exposed primarily for test assertions; production code MUST * NOT rely on this method because it has no counterpart on the * {@see TransactionManager} contract and the Wpdb-backed * implementation does not offer it. * * @return list */ public function events(): array { return $this->events; } /** * Number of {@see self::EVENT_BEGIN} events recorded so far. * * Convenience accessor for tests that only care about the * aggregate count, not the full sequence in {@see self::events()}. */ public function beginCount(): int { return $this->countEvent( self::EVENT_BEGIN ); } /** * Number of {@see self::EVENT_COMMIT} events recorded so far. */ public function commitCount(): int { return $this->countEvent( self::EVENT_COMMIT ); } /** * Number of {@see self::EVENT_ROLLBACK} events recorded so far. */ public function rollbackCount(): int { return $this->countEvent( self::EVENT_ROLLBACK ); } /** * Count occurrences of the given lifecycle event marker in * {@see self::$events}. * * Centralizes the per-event counter logic so the public * accessors above stay terse. * * @param string $event One of the {@see self::EVENT_*} constants. */ private function countEvent( string $event ): int { $count = 0; foreach ( $this->events as $recorded ) { if ( $recorded === $event ) { ++$count; } } return $count; } }