feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\LiveComponents\Persistence;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\RequestContext;
|
||||
use App\Framework\LiveComponents\Attributes\TrackStateHistory;
|
||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\StateManagement\Database\StateChangeType;
|
||||
use App\Framework\StateManagement\SerializableState;
|
||||
use App\Framework\StateManagement\StateHistoryManager;
|
||||
use App\Framework\StateManagement\StateManager;
|
||||
|
||||
/**
|
||||
* LiveComponent State Persistence Handler
|
||||
*
|
||||
* Handles automatic state persistence to database with optional history tracking.
|
||||
* Integrates DatabaseStateManager and StateHistoryManager.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic state persistence after action execution
|
||||
* - Optional history tracking via #[TrackStateHistory] attribute
|
||||
* - Changed properties detection
|
||||
* - Context capture (userId, sessionId, ipAddress, userAgent)
|
||||
* - Checksum-based integrity verification
|
||||
*/
|
||||
final readonly class LiveComponentStatePersistence
|
||||
{
|
||||
public function __construct(
|
||||
private StateManager $stateManager,
|
||||
private StateHistoryManager $historyManager,
|
||||
private ?RequestContext $requestContext = null,
|
||||
private ?Logger $logger = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Persist component state after action execution
|
||||
*
|
||||
* Steps:
|
||||
* 1. Get previous state for comparison (if exists)
|
||||
* 2. Persist new state via StateManager
|
||||
* 3. If #[TrackStateHistory] present: Add history entry
|
||||
* 4. Calculate changed properties
|
||||
* 5. Capture request context
|
||||
*
|
||||
* @param LiveComponentContract $component Component instance
|
||||
* @param SerializableState $newState New state after action
|
||||
* @param string $action Action name that triggered the change
|
||||
*/
|
||||
public function persistState(
|
||||
LiveComponentContract $component,
|
||||
SerializableState $newState,
|
||||
string $action
|
||||
): void {
|
||||
$componentId = $component->id;
|
||||
$componentClass = get_class($component);
|
||||
|
||||
// 1. Get previous state for comparison (if exists)
|
||||
$previousState = $this->stateManager->getState($componentId->toString());
|
||||
$previousChecksum = $previousState !== null
|
||||
? $this->calculateChecksum($previousState)
|
||||
: null;
|
||||
|
||||
// 2. Persist new state via StateManager
|
||||
$this->stateManager->setState($componentId->toString(), $newState);
|
||||
|
||||
$currentChecksum = $this->calculateChecksum($newState);
|
||||
|
||||
// 3. Check if history tracking is enabled
|
||||
if (!$this->historyManager->isHistoryEnabled($componentClass)) {
|
||||
$this->log('debug', "State persisted for {$componentId->toString()}, history tracking disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Get TrackStateHistory attribute for configuration
|
||||
$historyConfig = $this->getHistoryConfig($componentClass);
|
||||
|
||||
// 5. Calculate changed properties (if enabled)
|
||||
$changedProperties = null;
|
||||
if ($historyConfig?->trackChangedProperties && $previousState !== null) {
|
||||
$changedProperties = $this->calculateChangedProperties($previousState, $newState);
|
||||
}
|
||||
|
||||
// 6. Capture request context
|
||||
$context = $this->captureContext($historyConfig);
|
||||
|
||||
// 7. Determine change type
|
||||
$changeType = $previousState === null
|
||||
? StateChangeType::CREATED->value
|
||||
: StateChangeType::UPDATED->value;
|
||||
|
||||
// 8. Get version from StateManager (assumes DatabaseStateManager tracks versions)
|
||||
$version = $this->getStateVersion($componentId);
|
||||
|
||||
// 9. Add history entry
|
||||
$this->historyManager->addHistoryEntry(
|
||||
componentId: $componentId->toString(),
|
||||
stateData: json_encode($newState->toArray()),
|
||||
stateClass: get_class($newState),
|
||||
version: $version,
|
||||
changeType: $changeType,
|
||||
context: $context,
|
||||
changedProperties: $changedProperties,
|
||||
previousChecksum: $previousChecksum,
|
||||
currentChecksum: $currentChecksum
|
||||
);
|
||||
|
||||
$this->log('info', "State persisted with history for {$componentId->toString()}", [
|
||||
'action' => $action,
|
||||
'change_type' => $changeType,
|
||||
'version' => $version,
|
||||
'changed_properties_count' => $changedProperties ? count($changedProperties) : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate checksum for state integrity verification
|
||||
*
|
||||
* Uses SHA256 hash of serialized state data.
|
||||
*/
|
||||
private function calculateChecksum(SerializableState $state): string
|
||||
{
|
||||
return hash('sha256', json_encode($state->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate changed properties between old and new state
|
||||
*
|
||||
* Compares state arrays and returns list of changed property names.
|
||||
*
|
||||
* @return array<string> List of changed property names
|
||||
*/
|
||||
private function calculateChangedProperties(
|
||||
SerializableState $oldState,
|
||||
SerializableState $newState
|
||||
): array {
|
||||
$oldData = $oldState->toArray();
|
||||
$newData = $newState->toArray();
|
||||
|
||||
$changed = [];
|
||||
|
||||
// Check for modified or removed properties
|
||||
foreach ($oldData as $key => $oldValue) {
|
||||
if (!array_key_exists($key, $newData)) {
|
||||
$changed[] = $key; // Property removed
|
||||
} elseif ($oldValue !== $newData[$key]) {
|
||||
$changed[] = $key; // Property changed
|
||||
}
|
||||
}
|
||||
|
||||
// Check for added properties
|
||||
foreach ($newData as $key => $newValue) {
|
||||
if (!array_key_exists($key, $oldData)) {
|
||||
$changed[] = $key; // Property added
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture request context based on history configuration
|
||||
*
|
||||
* @return array{user_id?: string, session_id?: string, ip_address?: string, user_agent?: string}
|
||||
*/
|
||||
private function captureContext(?TrackStateHistory $config): array
|
||||
{
|
||||
if ($config === null || $this->requestContext === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$context = [];
|
||||
|
||||
// Always capture userId and sessionId if available
|
||||
if ($this->requestContext->getUserId() !== null) {
|
||||
$context['user_id'] = $this->requestContext->getUserId();
|
||||
}
|
||||
|
||||
if ($this->requestContext->getSessionId() !== null) {
|
||||
$context['session_id'] = $this->requestContext->getSessionId();
|
||||
}
|
||||
|
||||
// Capture IP address if configured
|
||||
if ($config->trackIpAddress && $this->requestContext->getIpAddress() !== null) {
|
||||
$context['ip_address'] = $this->requestContext->getIpAddress();
|
||||
}
|
||||
|
||||
// Capture user agent if configured
|
||||
if ($config->trackUserAgent && $this->requestContext->getUserAgent() !== null) {
|
||||
$context['user_agent'] = $this->requestContext->getUserAgent();
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TrackStateHistory attribute configuration
|
||||
*/
|
||||
private function getHistoryConfig(string $componentClass): ?TrackStateHistory
|
||||
{
|
||||
if (!class_exists($componentClass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reflection = new \ReflectionClass($componentClass);
|
||||
$attributes = $reflection->getAttributes(TrackStateHistory::class);
|
||||
|
||||
if (empty($attributes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $attributes[0]->newInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version for component state
|
||||
*
|
||||
* Note: This assumes DatabaseStateManager provides version tracking.
|
||||
* Falls back to 1 if version cannot be determined.
|
||||
*/
|
||||
private function getStateVersion(ComponentId $componentId): int
|
||||
{
|
||||
$currentState = $this->stateManager->getState($componentId->toString());
|
||||
|
||||
// If state has version property, use it
|
||||
if ($currentState !== null && property_exists($currentState, 'version')) {
|
||||
return $currentState->version + 1;
|
||||
}
|
||||
|
||||
// Otherwise, try to get from history
|
||||
$history = $this->historyManager->getHistory($componentId->toString(), limit: 1);
|
||||
|
||||
if (!empty($history)) {
|
||||
return $history[0]->version + 1;
|
||||
}
|
||||
|
||||
// First version
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message if logger available
|
||||
*/
|
||||
private function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if ($this->logger === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
match ($level) {
|
||||
'debug' => $this->logger->debug($message, $context),
|
||||
'info' => $this->logger->info($message, $context),
|
||||
'warning' => $this->logger->warning($message, $context),
|
||||
'error' => $this->logger->error($message, $context),
|
||||
default => $this->logger->info($message, $context),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user