feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready

This commit is contained in:
2025-10-31 01:39:24 +01:00
parent 55c04e4fd0
commit e26eb2aa12
601 changed files with 44184 additions and 32477 deletions

View File

@@ -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),
};
}
}