feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\FailedJobsState;
describe('FailedJobsState', function () {
it('creates empty state with default values', function () {
$state = FailedJobsState::empty();
expect($state->totalFailedJobs)->toBe(0);
expect($state->failedJobs)->toBe([]);
expect($state->statistics)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'totalFailedJobs' => 15,
'failedJobs' => [
[
'id' => 'job-1',
'queue' => 'default',
'error' => 'Connection timeout',
],
],
'statistics' => [
'total_retries' => 42,
'avg_failure_time' => 123.45,
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = FailedJobsState::fromArray($data);
expect($state->totalFailedJobs)->toBe(15);
expect($state->failedJobs)->toHaveCount(1);
expect($state->statistics)->toHaveKey('total_retries');
});
it('converts state to array', function () {
$failedJobs = [
[
'id' => 'job-1',
'queue' => 'default',
'job_type' => 'EmailJob',
'error' => 'SMTP connection failed',
'failed_at' => '2024-01-15 12:00:00',
'attempts' => 3,
],
];
$statistics = [
'total_retries' => 10,
'successful_retries' => 8,
];
$state = new FailedJobsState(
totalFailedJobs: 1,
failedJobs: $failedJobs,
statistics: $statistics,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('totalFailedJobs');
expect($array)->toHaveKey('failedJobs');
expect($array)->toHaveKey('statistics');
expect($array['failedJobs'])->toHaveCount(1);
});
it('creates new state with updated failed jobs', function () {
$state = FailedJobsState::empty();
$failedJobs = [
[
'id' => 'job-1',
'error' => 'Database connection lost',
'attempts' => 5,
],
];
$statistics = [
'most_common_error' => 'Database connection lost',
'total_failures_today' => 10,
];
$updatedState = $state->withFailedJobs(
totalFailedJobs: 1,
failedJobs: $failedJobs,
statistics: $statistics
);
// Original unchanged
expect($state->totalFailedJobs)->toBe(0);
expect($state->failedJobs)->toBe([]);
// New state updated
expect($updatedState->totalFailedJobs)->toBe(1);
expect($updatedState->failedJobs)->toHaveCount(1);
expect($updatedState->statistics)->toHaveKey('most_common_error');
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new FailedJobsState(
totalFailedJobs: 5,
failedJobs: [],
statistics: [],
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withFailedJobs(
totalFailedJobs: 10,
failedJobs: [],
statistics: []
);
// Original unchanged
expect($state->totalFailedJobs)->toBe(5);
// New instance
expect($newState)->not->toBe($state);
expect($newState->totalFailedJobs)->toBe(10);
});
it('handles empty failed jobs list', function () {
$state = FailedJobsState::empty()->withFailedJobs(
totalFailedJobs: 0,
failedJobs: [],
statistics: []
);
expect($state->failedJobs)->toBe([]);
expect($state->totalFailedJobs)->toBe(0);
expect($state->statistics)->toBe([]);
});
});

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\QueueStatsState;
describe('QueueStatsState', function () {
it('creates empty state with default values', function () {
$state = QueueStatsState::empty();
expect($state->currentQueueSize)->toBe(0);
expect($state->totalJobs)->toBe(0);
expect($state->successfulJobs)->toBe(0);
expect($state->failedJobs)->toBe(0);
expect($state->successRate)->toBe(0.0);
expect($state->avgExecutionTimeMs)->toBe(0.0);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'currentQueueSize' => 42,
'totalJobs' => 1000,
'successfulJobs' => 950,
'failedJobs' => 50,
'successRate' => 95.0,
'avgExecutionTimeMs' => 123.45,
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = QueueStatsState::fromArray($data);
expect($state->currentQueueSize)->toBe(42);
expect($state->totalJobs)->toBe(1000);
expect($state->successfulJobs)->toBe(950);
expect($state->failedJobs)->toBe(50);
expect($state->successRate)->toBe(95.0);
expect($state->avgExecutionTimeMs)->toBe(123.45);
expect($state->lastUpdated)->toBe('2024-01-15 12:00:00');
});
it('converts state to array', function () {
$state = new QueueStatsState(
currentQueueSize: 10,
totalJobs: 100,
successfulJobs: 90,
failedJobs: 10,
successRate: 90.0,
avgExecutionTimeMs: 50.5,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toBe([
'currentQueueSize' => 10,
'totalJobs' => 100,
'successfulJobs' => 90,
'failedJobs' => 10,
'successRate' => 90.0,
'avgExecutionTimeMs' => 50.5,
'lastUpdated' => '2024-01-15 12:00:00',
]);
});
it('creates new state with updated stats', function () {
$state = QueueStatsState::empty();
$updatedState = $state->withStats(
currentQueueSize: 5,
totalJobs: 50,
successfulJobs: 45,
failedJobs: 5,
successRate: 90.0,
avgExecutionTimeMs: 75.0
);
// Original state unchanged (immutable)
expect($state->currentQueueSize)->toBe(0);
expect($state->totalJobs)->toBe(0);
// New state has updated values
expect($updatedState->currentQueueSize)->toBe(5);
expect($updatedState->totalJobs)->toBe(50);
expect($updatedState->successfulJobs)->toBe(45);
expect($updatedState->failedJobs)->toBe(5);
expect($updatedState->successRate)->toBe(90.0);
expect($updatedState->avgExecutionTimeMs)->toBe(75.0);
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('handles zero division in success rate gracefully', function () {
$state = new QueueStatsState(
currentQueueSize: 0,
totalJobs: 0,
successfulJobs: 0,
failedJobs: 0,
successRate: 0.0,
avgExecutionTimeMs: 0.0,
lastUpdated: '2024-01-15 12:00:00'
);
expect($state->successRate)->toBe(0.0);
});
it('is immutable', function () {
$state = new QueueStatsState(
currentQueueSize: 10,
totalJobs: 100,
successfulJobs: 90,
failedJobs: 10,
successRate: 90.0,
avgExecutionTimeMs: 50.0,
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withStats(
currentQueueSize: 20,
totalJobs: 200,
successfulJobs: 180,
failedJobs: 20,
successRate: 90.0,
avgExecutionTimeMs: 60.0
);
// Original unchanged
expect($state->currentQueueSize)->toBe(10);
expect($state->totalJobs)->toBe(100);
// New instance created
expect($newState)->not->toBe($state);
expect($newState->currentQueueSize)->toBe(20);
expect($newState->totalJobs)->toBe(200);
});
});

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\SchedulerState;
describe('SchedulerState', function () {
it('creates empty state with default values', function () {
$state = SchedulerState::empty();
expect($state->totalScheduledTasks)->toBe(0);
expect($state->dueTasks)->toBe(0);
expect($state->upcomingTasks)->toBe([]);
expect($state->nextExecution)->toBeNull();
expect($state->statistics)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'totalScheduledTasks' => 10,
'dueTasks' => 2,
'upcomingTasks' => [
[
'id' => 'task-1',
'schedule_type' => 'cron',
'next_run' => '2024-01-15 13:00:00',
],
],
'nextExecution' => '2024-01-15 13:00:00',
'statistics' => [
'total_executions_today' => 50,
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = SchedulerState::fromArray($data);
expect($state->totalScheduledTasks)->toBe(10);
expect($state->dueTasks)->toBe(2);
expect($state->upcomingTasks)->toHaveCount(1);
expect($state->nextExecution)->toBe('2024-01-15 13:00:00');
});
it('converts state to array', function () {
$upcomingTasks = [
[
'id' => 'task-1',
'schedule_type' => 'interval',
'next_run' => '2024-01-15 13:00:00',
'next_run_relative' => 'in 1 hour',
'is_due' => false,
],
];
$statistics = [
'successful_executions' => 45,
'failed_executions' => 5,
];
$state = new SchedulerState(
totalScheduledTasks: 5,
dueTasks: 1,
upcomingTasks: $upcomingTasks,
nextExecution: '2024-01-15 13:00:00',
statistics: $statistics,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('totalScheduledTasks');
expect($array)->toHaveKey('dueTasks');
expect($array)->toHaveKey('upcomingTasks');
expect($array)->toHaveKey('nextExecution');
expect($array)->toHaveKey('statistics');
expect($array['upcomingTasks'])->toHaveCount(1);
});
it('creates new state with updated scheduler data', function () {
$state = SchedulerState::empty();
$upcomingTasks = [
[
'id' => 'backup-task',
'schedule_type' => 'cron',
'next_run' => '2024-01-15 02:00:00',
'is_due' => true,
],
];
$statistics = [
'tasks_executed_today' => 20,
'average_execution_time' => 1.5,
];
$updatedState = $state->withSchedulerData(
totalScheduledTasks: 3,
dueTasks: 1,
upcomingTasks: $upcomingTasks,
nextExecution: '2024-01-15 02:00:00',
statistics: $statistics
);
// Original unchanged
expect($state->totalScheduledTasks)->toBe(0);
expect($state->upcomingTasks)->toBe([]);
expect($state->nextExecution)->toBeNull();
// New state updated
expect($updatedState->totalScheduledTasks)->toBe(3);
expect($updatedState->dueTasks)->toBe(1);
expect($updatedState->upcomingTasks)->toHaveCount(1);
expect($updatedState->nextExecution)->toBe('2024-01-15 02:00:00');
expect($updatedState->statistics)->toHaveKey('tasks_executed_today');
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new SchedulerState(
totalScheduledTasks: 10,
dueTasks: 2,
upcomingTasks: [],
nextExecution: '2024-01-15 12:00:00',
statistics: [],
lastUpdated: '2024-01-15 11:00:00'
);
$newState = $state->withSchedulerData(
totalScheduledTasks: 15,
dueTasks: 3,
upcomingTasks: [],
nextExecution: '2024-01-15 13:00:00',
statistics: []
);
// Original unchanged
expect($state->totalScheduledTasks)->toBe(10);
expect($state->dueTasks)->toBe(2);
// New instance
expect($newState)->not->toBe($state);
expect($newState->totalScheduledTasks)->toBe(15);
expect($newState->dueTasks)->toBe(3);
});
it('handles null next execution', function () {
$state = SchedulerState::empty()->withSchedulerData(
totalScheduledTasks: 0,
dueTasks: 0,
upcomingTasks: [],
nextExecution: null,
statistics: []
);
expect($state->nextExecution)->toBeNull();
expect($state->totalScheduledTasks)->toBe(0);
});
it('handles empty upcoming tasks', function () {
$state = SchedulerState::empty()->withSchedulerData(
totalScheduledTasks: 0,
dueTasks: 0,
upcomingTasks: [],
nextExecution: null,
statistics: []
);
expect($state->upcomingTasks)->toBe([]);
expect($state->statistics)->toBe([]);
});
});

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\WorkerHealthState;
describe('WorkerHealthState', function () {
it('creates empty state with default values', function () {
$state = WorkerHealthState::empty();
expect($state->activeWorkers)->toBe(0);
expect($state->totalWorkers)->toBe(0);
expect($state->jobsInProgress)->toBe(0);
expect($state->workerDetails)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'activeWorkers' => 3,
'totalWorkers' => 5,
'jobsInProgress' => 7,
'workerDetails' => [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'healthy' => true,
],
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = WorkerHealthState::fromArray($data);
expect($state->activeWorkers)->toBe(3);
expect($state->totalWorkers)->toBe(5);
expect($state->jobsInProgress)->toBe(7);
expect($state->workerDetails)->toHaveCount(1);
expect($state->workerDetails[0]['id'])->toBe('worker-1');
});
it('converts state to array', function () {
$workerDetails = [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'process_id' => 12345,
'healthy' => true,
'jobs' => 2,
'max_jobs' => 10,
],
];
$state = new WorkerHealthState(
activeWorkers: 1,
totalWorkers: 3,
jobsInProgress: 2,
workerDetails: $workerDetails,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('activeWorkers');
expect($array)->toHaveKey('totalWorkers');
expect($array)->toHaveKey('jobsInProgress');
expect($array)->toHaveKey('workerDetails');
expect($array['workerDetails'])->toHaveCount(1);
});
it('creates new state with updated worker health', function () {
$state = WorkerHealthState::empty();
$workerDetails = [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'healthy' => true,
'jobs' => 5,
],
[
'id' => 'worker-2',
'hostname' => 'server-02',
'healthy' => false,
'jobs' => 0,
],
];
$updatedState = $state->withWorkerHealth(
activeWorkers: 2,
totalWorkers: 2,
jobsInProgress: 5,
workerDetails: $workerDetails
);
// Original unchanged
expect($state->activeWorkers)->toBe(0);
expect($state->workerDetails)->toBe([]);
// New state updated
expect($updatedState->activeWorkers)->toBe(2);
expect($updatedState->totalWorkers)->toBe(2);
expect($updatedState->jobsInProgress)->toBe(5);
expect($updatedState->workerDetails)->toHaveCount(2);
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new WorkerHealthState(
activeWorkers: 5,
totalWorkers: 10,
jobsInProgress: 15,
workerDetails: [],
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withWorkerHealth(
activeWorkers: 6,
totalWorkers: 10,
jobsInProgress: 20,
workerDetails: []
);
// Original unchanged
expect($state->activeWorkers)->toBe(5);
expect($state->jobsInProgress)->toBe(15);
// New instance
expect($newState)->not->toBe($state);
expect($newState->activeWorkers)->toBe(6);
expect($newState->jobsInProgress)->toBe(20);
});
it('handles empty worker details array', function () {
$state = WorkerHealthState::empty()->withWorkerHealth(
activeWorkers: 0,
totalWorkers: 0,
jobsInProgress: 0,
workerDetails: []
);
expect($state->workerDetails)->toBe([]);
expect($state->activeWorkers)->toBe(0);
});
});

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\LiveComponentStateManager;
use App\Application\LiveComponents\ShoppingCart\ShoppingCartState;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\StateManagement\InMemoryStateManager;
use App\Framework\StateManagement\StateManagerStatistics;
describe('LiveComponentStateManager', function () {
beforeEach(function () {
// Create type-safe StateManager for ShoppingCartState
$this->stateManager = InMemoryStateManager::for(ShoppingCartState::class);
$this->manager = new LiveComponentStateManager($this->stateManager);
$this->componentId = ComponentId::create('shopping-cart', 'user-123');
});
it('stores and retrieves component state', function () {
$cartState = ShoppingCartState::fromArray([
'items' => [
['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 2],
],
'discount_code' => 'SAVE10',
'discount_percentage' => 10
]);
$this->manager->setComponentState($this->componentId, $cartState);
$retrieved = $this->manager->getComponentState($this->componentId);
expect($retrieved)->toBeInstanceOf(ShoppingCartState::class);
expect($retrieved->discountCode)->toBe('SAVE10');
expect($retrieved->discountPercentage)->toBe(10);
expect(count($retrieved->items))->toBe(1);
});
it('preserves type safety throughout', function () {
$cartState = ShoppingCartState::fromArray([
'items' => [
['id' => '1', 'name' => 'Product A', 'price' => 19.99, 'quantity' => 1],
],
'discount_code' => 'TEST',
'discount_percentage' => 5
]);
$this->manager->setComponentState($this->componentId, $cartState);
$retrieved = $this->manager->getComponentState($this->componentId);
// Type is preserved - not mixed or LiveComponentState, but ShoppingCartState
expect($retrieved)->toBeInstanceOf(ShoppingCartState::class);
expect($retrieved->items)->toBeArray();
expect($retrieved->discountCode)->toBeString();
});
it('checks if component state exists', function () {
expect($this->manager->hasComponentState($this->componentId))->toBeFalse();
$cartState = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
$this->manager->setComponentState($this->componentId, $cartState);
expect($this->manager->hasComponentState($this->componentId))->toBeTrue();
});
it('removes component state', function () {
$cartState = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
$this->manager->setComponentState($this->componentId, $cartState);
expect($this->manager->hasComponentState($this->componentId))->toBeTrue();
$this->manager->removeComponentState($this->componentId);
expect($this->manager->hasComponentState($this->componentId))->toBeFalse();
expect($this->manager->getComponentState($this->componentId))->toBeNull();
});
it('atomically updates component state', function () {
$initialCart = ShoppingCartState::fromArray([
'items' => [
['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1],
],
'discount_code' => 'SAVE10',
'discount_percentage' => 10
]);
$this->manager->setComponentState($this->componentId, $initialCart);
$updatedCart = $this->manager->updateComponentState(
$this->componentId,
function ($state) {
return $state ? $state->withDiscountCode('SAVE20', 20) : ShoppingCartState::fromArray([]);
}
);
expect($updatedCart)->toBeInstanceOf(ShoppingCartState::class);
expect($updatedCart->discountCode)->toBe('SAVE20');
expect($updatedCart->discountPercentage)->toBe(20);
expect(count($updatedCart->items))->toBe(1);
});
it('handles null state in atomic update', function () {
$newCart = $this->manager->updateComponentState(
$this->componentId,
function ($state) {
return $state ?? ShoppingCartState::fromArray([
'items' => [
['id' => '2', 'name' => 'New Product', 'price' => 15.50, 'quantity' => 1],
],
'discount_code' => 'FIRST',
'discount_percentage' => 5
]);
}
);
expect($newCart)->toBeInstanceOf(ShoppingCartState::class);
expect($newCart->discountCode)->toBe('FIRST');
expect(count($newCart->items))->toBe(1);
});
it('respects TTL for state expiration', function () {
$cartState = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
// Store with very short TTL
$this->manager->setComponentState(
$this->componentId,
$cartState,
Duration::fromSeconds(1)
);
expect($this->manager->hasComponentState($this->componentId))->toBeTrue();
// Wait for expiration
sleep(2);
// State should be expired (for TTL-aware implementations)
// Note: InMemoryStateManager doesn't enforce TTL, but the interface supports it
});
it('retrieves all component states', function () {
$cart1 = ShoppingCartState::fromArray([
'items' => [
['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1],
],
'discount_code' => 'SAVE10',
'discount_percentage' => 10
]);
$cart2 = ShoppingCartState::fromArray([
'items' => [
['id' => '2', 'name' => 'Product B', 'price' => 15.50, 'quantity' => 2],
],
'discount_code' => 'SAVE20',
'discount_percentage' => 20
]);
$componentId1 = ComponentId::create('shopping-cart', 'user-123');
$componentId2 = ComponentId::create('shopping-cart', 'user-456');
$this->manager->setComponentState($componentId1, $cart1);
$this->manager->setComponentState($componentId2, $cart2);
$allStates = $this->manager->getAllComponentStates();
expect($allStates)->toBeArray();
expect(count($allStates))->toBeGreaterThanOrEqual(2);
});
it('clears all component states', function () {
$cart1 = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
$cart2 = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => 'TEST',
'discount_percentage' => 5
]);
$componentId1 = ComponentId::create('shopping-cart', 'user-123');
$componentId2 = ComponentId::create('shopping-cart', 'user-456');
$this->manager->setComponentState($componentId1, $cart1);
$this->manager->setComponentState($componentId2, $cart2);
$this->manager->clearAllComponentStates();
expect($this->manager->hasComponentState($componentId1))->toBeFalse();
expect($this->manager->hasComponentState($componentId2))->toBeFalse();
});
it('provides statistics about state operations', function () {
$cartState = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
$this->manager->setComponentState($this->componentId, $cartState);
$this->manager->getComponentState($this->componentId);
$this->manager->hasComponentState($this->componentId);
$stats = $this->manager->getStatistics();
expect($stats)->toBeInstanceOf(StateManagerStatistics::class);
expect($stats->totalKeys)->toBeGreaterThanOrEqual(1);
expect($stats->setCount)->toBeGreaterThanOrEqual(1);
expect($stats->hitCount)->toBeGreaterThanOrEqual(1);
});
it('can be created via factory method', function () {
$stateManager = InMemoryStateManager::for(ShoppingCartState::class);
$manager = LiveComponentStateManager::from($stateManager);
expect($manager)->toBeInstanceOf(LiveComponentStateManager::class);
});
it('generates consistent cache keys', function () {
$componentId1 = ComponentId::fromString('shopping-cart:user-123');
$componentId2 = ComponentId::fromString('shopping-cart:user-123');
$componentId3 = ComponentId::fromString('shopping-cart:user-456');
$cartState = ShoppingCartState::fromArray([
'items' => [],
'discount_code' => null,
'discount_percentage' => 0
]);
$this->manager->setComponentState($componentId1, $cartState);
// Same component ID should retrieve same state
expect($this->manager->hasComponentState($componentId2))->toBeTrue();
// Different instance ID should not have state
expect($this->manager->hasComponentState($componentId3))->toBeFalse();
});
it('handles complex state transformations', function () {
$initialCart = ShoppingCartState::fromArray([
'items' => [
['id' => '1', 'name' => 'Product A', 'price' => 29.99, 'quantity' => 1],
],
'discount_code' => null,
'discount_percentage' => 0
]);
$this->manager->setComponentState($this->componentId, $initialCart);
// Multiple transformations
$updatedCart = $this->manager->updateComponentState(
$this->componentId,
fn($state) => $state->withDiscountCode('SAVE10', 10)
);
$updatedCart = $this->manager->updateComponentState(
$this->componentId,
function($state) {
return ShoppingCartState::fromArray([
'items' => array_merge($state->items, [
['id' => '2', 'name' => 'Product B', 'price' => 15.50, 'quantity' => 2]
]),
'discount_code' => $state->discountCode,
'discount_percentage' => $state->discountPercentage
]);
}
);
expect(count($updatedCart->items))->toBe(2);
expect($updatedCart->discountCode)->toBe('SAVE10');
});
});