Files
Jonathan Miller e8da1a30ff
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Successful in 10s
Generic: Repo Health / Scripts governance (push) Successful in 9s
Generic: Repo Health / Repository health (push) Successful in 17s
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Successful in 8s
Generic: Repo Health / Release configuration (pull_request) Successful in 6s
Generic: Repo Health / Scripts governance (pull_request) Successful in 8s
Universal: PR Check / Build RC Package (pull_request) Successful in 5s
Generic: Repo Health / Repository health (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (push) Successful in 1m16s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Successful in 1m29s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Successful in 41s
Platform: moko-platform CI / Gate 5: Template Integrity (push) Failing after 6s
Platform: moko-platform CI / Gate 4: Governance (push) Successful in 1m38s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Successful in 1m40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Successful in 1m41s
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Failing after 1m40s
Platform: moko-platform CI / Gate 4: Governance (pull_request) Successful in 1m20s
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Successful in 1m24s
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Failing after 1m24s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Successful in 1m26s
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Successful in 1m30s
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Failing after 12s
fix: PHPStan level 3 → 4 — remove dead code, baseline 41 items
Removed 13 write-only properties and unused code. Remaining 41
baselined items are defensive patterns (null coalesce on API responses,
boolean safety checks) that are intentional.

PHPStan level 4: 0 errors with baseline.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-25 22:31:25 -05:00

344 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoStandards.Enterprise.Transaction
* INGROUP: MokoStandards.Enterprise
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /lib/Enterprise/TransactionManager.php
* BRIEF: Transaction manager for atomic operations
*/
/**
* Transaction Manager for MokoStandards
*
* Provides atomic multi-step operations with automatic rollback:
* - Transaction boundaries for ACID operations
* - Automatic rollback on failure
* - State consistency checks
* - Transaction history tracking
* - Nested transaction support
* - Step-by-step execution with recovery
*
* Example usage:
* ```php
* $txn = new Transaction('user_registration');
* try {
* $txn->execute('create_user', function() {
* // Create user logic
* }, function() {
* // Rollback: delete user
* });
*
* $txn->execute('send_email', function() {
* // Send welcome email
* });
*
* $txn->commit();
* } catch (TransactionError $e) {
* // Automatic rollback on failure
* echo "Transaction failed: " . $e->getMessage();
* }
* ```
*
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @package MokoStandards\Enterprise
* @version 04.00.04
* @author MokoStandards Team
* @license GPL-3.0-or-later
*/
namespace MokoEnterprise;
use DateTime;
use DateTimeZone;
use Exception;
/**
* Exception raised when transaction operations fail
*/
class TransactionError extends Exception
{
}
/**
* Represents a single step in a transaction
*/
class TransactionStep
{
public string $name;
public $executeFunc;
public $rollbackFunc;
public bool $executed = false;
public $result = null;
public ?string $error = null;
public function __construct(string $name, callable $executeFunc, ?callable $rollbackFunc = null)
{
$this->name = $name;
$this->executeFunc = $executeFunc;
$this->rollbackFunc = $rollbackFunc;
}
}
/**
* Transaction manager for atomic multi-step operations
*/
class Transaction
{
private string $name;
/** @var array<int, TransactionStep> */
private array $steps = [];
private bool $committed = false;
private bool $rolledBack = false;
private ?DateTime $startTime = null;
private ?DateTime $endTime = null;
public function __construct(?string $name = null)
{
$this->name = $name ?? 'txn_' . date('Ymd_His');
$this->startTime = new DateTime('now', new DateTimeZone('UTC'));
error_log("Starting transaction: {$this->name}");
}
/**
* Execute a transaction step
*
* @param string $name Step name
* @param callable $func Function to execute
* @param callable|null $rollbackFunc Function to rollback this step
* @param mixed ...$args Arguments for func
* @return mixed Result of func
* @throws TransactionError If step execution fails
*/
public function execute(string $name, callable $func, ?callable $rollbackFunc = null, ...$args)
{
$step = new TransactionStep($name, $func, $rollbackFunc);
try {
error_log("Executing step: {$name}");
$result = $func(...$args);
$step->executed = true;
$step->result = $result;
$this->steps[] = $step;
error_log("Step completed: {$name}");
return $result;
} catch (Exception $e) {
$step->error = $e->getMessage();
error_log("Step failed: {$name} - {$e->getMessage()}");
throw new TransactionError("Transaction step '{$name}' failed: {$e->getMessage()}", 0, $e);
}
}
/**
* Commit the transaction
*
* @throws TransactionError If already committed or rolled back
*/
public function commit(): void
{
if ($this->committed) {
error_log("Transaction already committed");
return;
}
if ($this->rolledBack) {
throw new TransactionError("Cannot commit a rolled-back transaction");
}
$this->committed = true;
$this->endTime = new DateTime('now', new DateTimeZone('UTC'));
$duration = $this->endTime->getTimestamp() - $this->startTime->getTimestamp();
error_log("Transaction committed: {$this->name} (" . count($this->steps) . " steps, {$duration}s)");
}
/**
* Rollback all executed steps in reverse order
*/
public function rollback(): void
{
if ($this->rolledBack) {
error_log("Transaction already rolled back");
return;
}
error_log("Rolling back transaction: {$this->name}");
// Rollback in reverse order
foreach (array_reverse($this->steps) as $step) {
if ($step->executed && $step->rollbackFunc !== null) {
try {
error_log("Rolling back step: {$step->name}");
($step->rollbackFunc)();
} catch (Exception $e) {
error_log("Rollback failed for step {$step->name}: {$e->getMessage()}");
}
}
}
$this->rolledBack = true;
error_log("Transaction rolled back: {$this->name}");
}
/**
* Get transaction status
*
* @return array<string, mixed> Dictionary with transaction status
*/
public function getStatus(): array
{
return [
'name' => $this->name,
'steps_count' => count($this->steps),
'committed' => $this->committed,
'rolled_back' => $this->rolledBack,
'start_time' => $this->startTime?->format('c'),
'end_time' => $this->endTime?->format('c'),
'steps' => array_map(function ($step) {
return [
'name' => $step->name,
'executed' => $step->executed,
'error' => $step->error
];
}, $this->steps)
];
}
/**
* Get transaction name
*/
public function getName(): string
{
return $this->name;
}
/**
* Check if transaction is committed
*/
public function isCommitted(): bool
{
return $this->committed;
}
/**
* Check if transaction is rolled back
*/
public function isRolledBack(): bool
{
return $this->rolledBack;
}
/**
* Destructor - auto rollback if not committed
*/
public function __destruct()
{
if (!$this->committed && !$this->rolledBack && count($this->steps) > 0) {
error_log("Transaction {$this->name} was not committed, auto-rolling back");
$this->rollback();
}
}
}
/**
* High-level transaction management
*/
class TransactionManager
{
private const VERSION = '04.06.00';
/** @var array<int, Transaction> */
private array $transactions = [];
private ?Transaction $activeTransaction = null;
/**
* Begin a new transaction
*
* @param string|null $name Transaction name
* @return Transaction New transaction instance
* @throws TransactionError If another transaction is already active
*/
public function begin(?string $name = null): Transaction
{
if ($this->activeTransaction !== null) {
throw new TransactionError("Another transaction is already active");
}
$txn = new Transaction($name);
$this->activeTransaction = $txn;
$this->transactions[] = $txn;
return $txn;
}
/**
* End active transaction (commit or rollback should be done before this)
*/
public function end(): void
{
$this->activeTransaction = null;
}
/**
* Get transaction history
*
* @return array<int, array<string, mixed>> List of transaction status dictionaries
*/
public function getHistory(): array
{
return array_map(function ($txn) {
return $txn->getStatus();
}, $this->transactions);
}
/**
* Get transaction statistics
*
* @return array<string, int> Dictionary with statistics
*/
public function getStats(): array
{
$committed = 0;
$rolledBack = 0;
foreach ($this->transactions as $txn) {
if ($txn->isCommitted()) {
$committed++;
}
if ($txn->isRolledBack()) {
$rolledBack++;
}
}
return [
'total' => count($this->transactions),
'committed' => $committed,
'rolled_back' => $rolledBack,
'active' => $this->activeTransaction !== null ? 1 : 0
];
}
/**
* Get active transaction
*/
public function getActiveTransaction(): ?Transaction
{
return $this->activeTransaction;
}
public function getVersion(): string
{
return self::VERSION;
}
}