$this->counter, 'message' => $this->message, 'items' => $this->items, ]; } public static function fromArray(array $data): self { return new self( counter: $data['counter'] ?? 0, message: $data['message'] ?? '', items: $data['items'] ?? [] ); } } // Test Component with History #[TrackStateHistory( trackIpAddress: true, trackUserAgent: true, trackChangedProperties: true, maxHistoryEntries: 10 )] final readonly class IntegrationTestComponent implements LiveComponentContract { public function __construct( public ComponentId $id, public IntegrationTestState $state ) {} public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'components/integration-test', data: ['state' => $this->state] ); } } describe('Database State Integration', function () { beforeEach(function () { // Real dependencies for integration test // Use in-memory SQLite for testing $pdo = new PDO('sqlite::memory:'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection = new class($pdo) implements ConnectionInterface { public function __construct(private PDO $pdo) {} public function getPdo(): PDO { return $this->pdo; } public function query(string $sql, array $params = []): array { $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(PDO::FETCH_ASSOC); } public function execute(string $sql, array $params = []): int { $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->rowCount(); } public function lastInsertId(): string { return $this->pdo->lastInsertId(); } public function beginTransaction(): void { $this->pdo->beginTransaction(); } public function commit(): void { $this->pdo->commit(); } public function rollback(): void { $this->pdo->rollBack(); } public function inTransaction(): bool { return $this->pdo->inTransaction(); } }; $this->entityManager = new EntityManager($this->connection); $this->cache = new SmartCache(); // Create state manager $this->stateManager = new DatabaseStateManager( entityManager: $this->entityManager, cache: $this->cache, stateClass: IntegrationTestState::class, logger: null, cacheTtl: Duration::fromSeconds(60) ); // Create history manager $this->historyManager = new DatabaseStateHistoryManager( entityManager: $this->entityManager, logger: null ); // Create request context $this->requestContext = new RequestContext( userId: 'test-user-123', sessionId: 'test-session-456', ipAddress: '127.0.0.1', userAgent: 'Test-Agent/1.0' ); // Create persistence handler $this->persistence = new LiveComponentStatePersistence( stateManager: $this->stateManager, historyManager: $this->historyManager, requestContext: $this->requestContext, logger: null ); // Setup database tables // Create component_state table $this->connection->execute(" CREATE TABLE component_state ( component_id TEXT PRIMARY KEY, state_data TEXT NOT NULL, state_class TEXT NOT NULL, component_name TEXT NOT NULL, user_id TEXT, session_id TEXT, version INTEGER DEFAULT 1, checksum TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, expires_at TEXT ) "); // Create component_state_history table $this->connection->execute(" CREATE TABLE component_state_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, component_id TEXT NOT NULL, state_data TEXT NOT NULL, state_class TEXT NOT NULL, version INTEGER NOT NULL, change_type TEXT NOT NULL, changed_properties TEXT, user_id TEXT, session_id TEXT, ip_address TEXT, user_agent TEXT, previous_checksum TEXT, current_checksum TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY (component_id) REFERENCES component_state(component_id) ON DELETE CASCADE ) "); }); afterEach(function () { // Cleanup test tables $this->connection->execute("DROP TABLE IF EXISTS component_state_history"); $this->connection->execute("DROP TABLE IF EXISTS component_state"); }); it('persists component state to database', function () { $componentId = new ComponentId('counter', 'test-1'); $state = new IntegrationTestState( counter: 42, message: 'Hello Integration Test', items: ['item1', 'item2'] ); $component = new IntegrationTestComponent($componentId, $state); // Persist state $this->persistence->persistState($component, $state, 'increment'); // Verify state was saved $retrieved = $this->stateManager->getState($componentId->toString()); expect($retrieved)->toBeInstanceOf(IntegrationTestState::class); expect($retrieved->counter)->toBe(42); expect($retrieved->message)->toBe('Hello Integration Test'); expect($retrieved->items)->toBe(['item1', 'item2']); }); it('tracks state changes in history', function () { $componentId = new ComponentId('counter', 'test-2'); $component = new IntegrationTestComponent( $componentId, new IntegrationTestState(counter: 0, message: 'initial') ); // Create initial state $state1 = new IntegrationTestState(counter: 0, message: 'initial'); $this->persistence->persistState($component, $state1, 'init'); // Update state $state2 = new IntegrationTestState(counter: 1, message: 'updated'); $component = new IntegrationTestComponent($componentId, $state2); $this->persistence->persistState($component, $state2, 'increment'); // Update again $state3 = new IntegrationTestState(counter: 2, message: 'updated again'); $component = new IntegrationTestComponent($componentId, $state3); $this->persistence->persistState($component, $state3, 'increment'); // Get history $history = $this->historyManager->getHistory($componentId->toString()); expect($history)->toBeArray(); expect(count($history))->toBeGreaterThanOrEqual(2); // Verify history entries are ordered DESC expect($history[0]->version)->toBeGreaterThan($history[1]->version); // Verify context was captured expect($history[0]->userId)->toBe('test-user-123'); expect($history[0]->sessionId)->toBe('test-session-456'); expect($history[0]->ipAddress)->toBe('127.0.0.1'); expect($history[0]->userAgent)->toBe('Test-Agent/1.0'); }); it('uses cache for fast retrieval after initial load', function () { $componentId = new ComponentId('counter', 'test-3'); $state = new IntegrationTestState(counter: 99, message: 'cached'); $component = new IntegrationTestComponent($componentId, $state); // First persist (cold) $this->persistence->persistState($component, $state, 'test'); // Get state (should populate cache) $retrieved1 = $this->stateManager->getState($componentId->toString()); // Get state again (should hit cache) $retrieved2 = $this->stateManager->getState($componentId->toString()); expect($retrieved1->counter)->toBe(99); expect($retrieved2->counter)->toBe(99); // Verify cache statistics show hits $stats = $this->stateManager->getStatistics(); expect($stats->hitCount)->toBeGreaterThan(0); }); it('tracks changed properties correctly', function () { $componentId = new ComponentId('counter', 'test-4'); // Initial state $state1 = new IntegrationTestState( counter: 10, message: 'first', items: ['a', 'b'] ); $component = new IntegrationTestComponent($componentId, $state1); $this->persistence->persistState($component, $state1, 'init'); // Update only counter $state2 = new IntegrationTestState( counter: 11, message: 'first', // Same items: ['a', 'b'] // Same ); $component = new IntegrationTestComponent($componentId, $state2); $this->persistence->persistState($component, $state2, 'increment'); // Get latest history entry $history = $this->historyManager->getHistory($componentId->toString(), limit: 1); $latestEntry = $history[0]; // Should only track 'counter' as changed expect($latestEntry->changedProperties)->toBeArray(); expect($latestEntry->changedProperties)->toContain('counter'); expect(count($latestEntry->changedProperties))->toBe(1); }); it('supports atomic state updates', function () { $componentId = new ComponentId('counter', 'test-5'); $initialState = new IntegrationTestState(counter: 0, message: 'start'); $component = new IntegrationTestComponent($componentId, $initialState); // Persist initial state $this->persistence->persistState($component, $initialState, 'init'); // Atomic update $updatedState = $this->stateManager->updateState( $componentId->toString(), fn(IntegrationTestState $state) => new IntegrationTestState( counter: $state->counter + 5, message: 'updated', items: $state->items ) ); expect($updatedState)->toBeInstanceOf(IntegrationTestState::class); expect($updatedState->counter)->toBe(5); expect($updatedState->message)->toBe('updated'); }); it('retrieves specific version from history', function () { $componentId = new ComponentId('counter', 'test-6'); // Create multiple versions for ($i = 1; $i <= 5; $i++) { $state = new IntegrationTestState( counter: $i, message: "version {$i}" ); $component = new IntegrationTestComponent($componentId, $state); $this->persistence->persistState($component, $state, 'update'); } // Get version 3 $version3 = $this->historyManager->getHistoryByVersion($componentId->toString(), 3); expect($version3)->not->toBeNull(); expect($version3->version)->toBe(3); $state3 = IntegrationTestState::fromArray(json_decode($version3->stateData, true)); expect($state3->counter)->toBe(3); expect($state3->message)->toBe('version 3'); }); it('cleans up old history entries', function () { $componentId = new ComponentId('counter', 'test-7'); // Create 10 history entries for ($i = 1; $i <= 10; $i++) { $state = new IntegrationTestState(counter: $i); $component = new IntegrationTestComponent($componentId, $state); $this->persistence->persistState($component, $state, 'update'); } // Keep only last 5 $deleted = $this->historyManager->cleanup($componentId->toString(), keepLast: 5); expect($deleted)->toBe(5); // Verify only 5 entries remain $history = $this->historyManager->getHistory($componentId->toString()); expect(count($history))->toBe(5); }); it('provides statistics about state storage', function () { // Create multiple components for ($i = 1; $i <= 3; $i++) { $componentId = new ComponentId('counter', "test-stats-{$i}"); $state = new IntegrationTestState(counter: $i); $component = new IntegrationTestComponent($componentId, $state); $this->persistence->persistState($component, $state, 'create'); } // Get statistics $stats = $this->stateManager->getStatistics(); expect($stats->totalKeys)->toBeGreaterThanOrEqual(3); expect($stats->setCount)->toBeGreaterThanOrEqual(3); }); });