Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
141
src/Framework/FeatureFlags/FeatureFlag.php
Normal file
141
src/Framework/FeatureFlags/FeatureFlag.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
|
||||
/**
|
||||
* Represents a feature flag configuration
|
||||
*/
|
||||
final readonly class FeatureFlag
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $conditions
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public FeatureFlagStatus $status,
|
||||
public ?string $description = null,
|
||||
public array $conditions = [],
|
||||
public ?Timestamp $enabledAt = null,
|
||||
public ?Timestamp $expiresAt = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
// Check if flag is explicitly disabled
|
||||
if ($this->status->isDisabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if ($this->expiresAt !== null && Timestamp::now()->toFloat() > $this->expiresAt->toFloat()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If unconditionally enabled
|
||||
if ($this->status->isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For conditional flags, we'll need context to evaluate
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isEnabledForContext(FeatureFlagContext $context): bool
|
||||
{
|
||||
// Check basic enabled status first
|
||||
if (! $this->isEnabled() && ! $this->status->isConditional()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If unconditionally enabled (and not expired)
|
||||
if ($this->status->isEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Evaluate conditions for conditional flags
|
||||
if ($this->status->isConditional()) {
|
||||
return $this->evaluateConditions($context);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function evaluateConditions(FeatureFlagContext $context): bool
|
||||
{
|
||||
foreach ($this->conditions as $key => $expectedValue) {
|
||||
$contextValue = $context->getValue($key);
|
||||
|
||||
if ($contextValue === null) {
|
||||
return false; // Required context missing
|
||||
}
|
||||
|
||||
// Handle array conditions (e.g., user_ids: [1, 2, 3])
|
||||
if (is_array($expectedValue)) {
|
||||
if (! in_array($contextValue, $expectedValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle percentage rollout (e.g., percentage: 50)
|
||||
if ($key === 'percentage' && is_numeric($expectedValue)) {
|
||||
$hash = crc32($this->name . ':' . (string) $context->getUserId());
|
||||
$userPercentage = abs($hash) % 100;
|
||||
if ($userPercentage >= $expectedValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if ($contextValue !== $expectedValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function withStatus(FeatureFlagStatus $status): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$status,
|
||||
$this->description,
|
||||
$this->conditions,
|
||||
$this->enabledAt,
|
||||
$this->expiresAt
|
||||
);
|
||||
}
|
||||
|
||||
public function withConditions(array $conditions): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$this->status,
|
||||
$this->description,
|
||||
$conditions,
|
||||
$this->enabledAt,
|
||||
$this->expiresAt
|
||||
);
|
||||
}
|
||||
|
||||
public function withExpiration(Timestamp $expiresAt): self
|
||||
{
|
||||
return new self(
|
||||
$this->name,
|
||||
$this->status,
|
||||
$this->description,
|
||||
$this->conditions,
|
||||
$this->enabledAt,
|
||||
$expiresAt
|
||||
);
|
||||
}
|
||||
}
|
||||
117
src/Framework/FeatureFlags/FeatureFlagContext.php
Normal file
117
src/Framework/FeatureFlags/FeatureFlagContext.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags;
|
||||
|
||||
use App\Framework\Http\IpAddress;
|
||||
use App\Framework\UserAgent\UserAgent;
|
||||
|
||||
/**
|
||||
* Context for evaluating feature flags
|
||||
*/
|
||||
final readonly class FeatureFlagContext
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function __construct(
|
||||
private array $data = [],
|
||||
private ?string $userId = null,
|
||||
private ?string $environment = null,
|
||||
private ?UserAgent $userAgent = null,
|
||||
private ?IpAddress $ipAddress = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function getValue(string $key): mixed
|
||||
{
|
||||
return match($key) {
|
||||
'user_id' => $this->userId,
|
||||
'environment' => $this->environment,
|
||||
'user_agent' => $this->userAgent?->value,
|
||||
'ip_address' => $this->ipAddress?->value,
|
||||
default => $this->data[$key] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getEnvironment(): ?string
|
||||
{
|
||||
return $this->environment;
|
||||
}
|
||||
|
||||
public function getUserAgent(): ?UserAgent
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
public function getIpAddress(): ?IpAddress
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
return new self(
|
||||
$this->data,
|
||||
$userId,
|
||||
$this->environment,
|
||||
$this->userAgent,
|
||||
$this->ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function withEnvironment(string $environment): self
|
||||
{
|
||||
return new self(
|
||||
$this->data,
|
||||
$this->userId,
|
||||
$environment,
|
||||
$this->userAgent,
|
||||
$this->ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function withUserAgent(UserAgent $userAgent): self
|
||||
{
|
||||
return new self(
|
||||
$this->data,
|
||||
$this->userId,
|
||||
$this->environment,
|
||||
$userAgent,
|
||||
$this->ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function withIpAddress(IpAddress $ipAddress): self
|
||||
{
|
||||
return new self(
|
||||
$this->data,
|
||||
$this->userId,
|
||||
$this->environment,
|
||||
$this->userAgent,
|
||||
$ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function withData(array $data): self
|
||||
{
|
||||
return new self(
|
||||
array_merge($this->data, $data),
|
||||
$this->userId,
|
||||
$this->environment,
|
||||
$this->userAgent,
|
||||
$this->ipAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function with(string $key, mixed $value): self
|
||||
{
|
||||
return $this->withData([$key => $value]);
|
||||
}
|
||||
}
|
||||
233
src/Framework/FeatureFlags/FeatureFlagManager.php
Normal file
233
src/Framework/FeatureFlags/FeatureFlagManager.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags;
|
||||
|
||||
/**
|
||||
* Main interface for feature flag operations
|
||||
*/
|
||||
final readonly class FeatureFlagManager
|
||||
{
|
||||
public function __construct(
|
||||
private FeatureFlagRepository $repository,
|
||||
private ?FeatureFlagContext $defaultContext = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is enabled
|
||||
*/
|
||||
public function isEnabled(string $flagName, ?FeatureFlagContext $context = null): bool
|
||||
{
|
||||
$flag = $this->repository->find($flagName);
|
||||
if ($flag === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$context = $context ?? $this->defaultContext ?? new FeatureFlagContext();
|
||||
|
||||
return $flag->isEnabledForContext($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is disabled
|
||||
*/
|
||||
public function isDisabled(string $flagName, ?FeatureFlagContext $context = null): bool
|
||||
{
|
||||
return ! $this->isEnabled($flagName, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a feature flag by name
|
||||
*/
|
||||
public function getFlag(string $flagName): ?FeatureFlag
|
||||
{
|
||||
return $this->repository->find($flagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all feature flags
|
||||
* @return FeatureFlag[]
|
||||
*/
|
||||
public function getAllFlags(): array
|
||||
{
|
||||
return $this->repository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a feature flag
|
||||
*/
|
||||
public function enable(string $flagName, ?string $description = null): void
|
||||
{
|
||||
$flag = $this->repository->find($flagName);
|
||||
|
||||
if ($flag === null) {
|
||||
$flag = new FeatureFlag(
|
||||
name: $flagName,
|
||||
status: FeatureFlagStatus::ENABLED,
|
||||
description: $description,
|
||||
enabledAt: \App\Framework\Core\ValueObjects\Timestamp::now()
|
||||
);
|
||||
} else {
|
||||
$flag = $flag->withStatus(FeatureFlagStatus::ENABLED);
|
||||
}
|
||||
|
||||
$this->repository->save($flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a feature flag
|
||||
*/
|
||||
public function disable(string $flagName): void
|
||||
{
|
||||
$flag = $this->repository->find($flagName);
|
||||
|
||||
if ($flag === null) {
|
||||
$flag = new FeatureFlag(
|
||||
name: $flagName,
|
||||
status: FeatureFlagStatus::DISABLED
|
||||
);
|
||||
} else {
|
||||
$flag = $flag->withStatus(FeatureFlagStatus::DISABLED);
|
||||
}
|
||||
|
||||
$this->repository->save($flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set conditional feature flag
|
||||
*/
|
||||
public function setConditional(
|
||||
string $flagName,
|
||||
array $conditions,
|
||||
?string $description = null
|
||||
): void {
|
||||
$flag = $this->repository->find($flagName);
|
||||
|
||||
if ($flag === null) {
|
||||
$flag = new FeatureFlag(
|
||||
name: $flagName,
|
||||
status: FeatureFlagStatus::CONDITIONAL,
|
||||
description: $description,
|
||||
conditions: $conditions
|
||||
);
|
||||
} else {
|
||||
$flag = $flag->withStatus(FeatureFlagStatus::CONDITIONAL)
|
||||
->withConditions($conditions);
|
||||
}
|
||||
|
||||
$this->repository->save($flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set percentage rollout for a feature
|
||||
*/
|
||||
public function setPercentageRollout(
|
||||
string $flagName,
|
||||
int $percentage,
|
||||
?string $description = null
|
||||
): void {
|
||||
if ($percentage < 0 || $percentage > 100) {
|
||||
throw new \InvalidArgumentException('Percentage must be between 0 and 100');
|
||||
}
|
||||
|
||||
$this->setConditional($flagName, ['percentage' => $percentage], $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user-specific feature flag
|
||||
*/
|
||||
public function setForUsers(
|
||||
string $flagName,
|
||||
array $userIds,
|
||||
?string $description = null
|
||||
): void {
|
||||
$this->setConditional($flagName, ['user_id' => $userIds], $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set environment-specific feature flag
|
||||
*/
|
||||
public function setForEnvironment(
|
||||
string $flagName,
|
||||
string $environment,
|
||||
?string $description = null
|
||||
): void {
|
||||
$this->setConditional($flagName, ['environment' => $environment], $description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set expiration for a feature flag
|
||||
*/
|
||||
public function setExpiration(
|
||||
string $flagName,
|
||||
\App\Framework\Core\ValueObjects\Timestamp $expiresAt
|
||||
): void {
|
||||
$flag = $this->repository->find($flagName);
|
||||
if ($flag === null) {
|
||||
throw new \InvalidArgumentException("Feature flag '{$flagName}' not found");
|
||||
}
|
||||
|
||||
$flag = $flag->withExpiration($expiresAt);
|
||||
$this->repository->save($flag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a feature flag
|
||||
*/
|
||||
public function deleteFlag(string $flagName): void
|
||||
{
|
||||
$this->repository->delete($flagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag exists
|
||||
*/
|
||||
public function exists(string $flagName): bool
|
||||
{
|
||||
return $this->repository->exists($flagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feature flag status summary
|
||||
*/
|
||||
public function getStatusSummary(): array
|
||||
{
|
||||
$flags = $this->getAllFlags();
|
||||
$summary = [
|
||||
'total' => count($flags),
|
||||
'enabled' => 0,
|
||||
'disabled' => 0,
|
||||
'conditional' => 0,
|
||||
'expired' => 0,
|
||||
];
|
||||
|
||||
$now = \App\Framework\Core\ValueObjects\Timestamp::now();
|
||||
|
||||
foreach ($flags as $flag) {
|
||||
if ($flag->expiresAt !== null && $now->toFloat() > $flag->expiresAt->toFloat()) {
|
||||
$summary['expired']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($flag->status) {
|
||||
case FeatureFlagStatus::ENABLED:
|
||||
$summary['enabled']++;
|
||||
|
||||
break;
|
||||
case FeatureFlagStatus::DISABLED:
|
||||
$summary['disabled']++;
|
||||
|
||||
break;
|
||||
case FeatureFlagStatus::CONDITIONAL:
|
||||
$summary['conditional']++;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
37
src/Framework/FeatureFlags/FeatureFlagRepository.php
Normal file
37
src/Framework/FeatureFlags/FeatureFlagRepository.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags;
|
||||
|
||||
/**
|
||||
* Repository interface for feature flags
|
||||
*/
|
||||
interface FeatureFlagRepository
|
||||
{
|
||||
/**
|
||||
* Get a feature flag by name
|
||||
*/
|
||||
public function find(string $name): ?FeatureFlag;
|
||||
|
||||
/**
|
||||
* Get all feature flags
|
||||
* @return FeatureFlag[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
|
||||
/**
|
||||
* Save a feature flag
|
||||
*/
|
||||
public function save(FeatureFlag $flag): void;
|
||||
|
||||
/**
|
||||
* Delete a feature flag
|
||||
*/
|
||||
public function delete(string $name): void;
|
||||
|
||||
/**
|
||||
* Check if a feature flag exists
|
||||
*/
|
||||
public function exists(string $name): bool;
|
||||
}
|
||||
30
src/Framework/FeatureFlags/FeatureFlagStatus.php
Normal file
30
src/Framework/FeatureFlags/FeatureFlagStatus.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags;
|
||||
|
||||
/**
|
||||
* Status of a feature flag
|
||||
*/
|
||||
enum FeatureFlagStatus: string
|
||||
{
|
||||
case ENABLED = 'enabled';
|
||||
case DISABLED = 'disabled';
|
||||
case CONDITIONAL = 'conditional';
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this === self::ENABLED;
|
||||
}
|
||||
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this === self::DISABLED;
|
||||
}
|
||||
|
||||
public function isConditional(): bool
|
||||
{
|
||||
return $this === self::CONDITIONAL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags\Storage;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\FeatureFlags\FeatureFlag;
|
||||
use App\Framework\FeatureFlags\FeatureFlagRepository;
|
||||
|
||||
/**
|
||||
* Cached feature flag repository for better performance
|
||||
*/
|
||||
final readonly class CacheFeatureFlagRepository implements FeatureFlagRepository
|
||||
{
|
||||
private string $cachePrefix;
|
||||
|
||||
private Duration $cacheTtl;
|
||||
|
||||
public function __construct(
|
||||
private FeatureFlagRepository $repository,
|
||||
private Cache $cache,
|
||||
string $cachePrefix = 'feature_flags',
|
||||
?Duration $cacheTtl = null
|
||||
) {
|
||||
$this->cachePrefix = $cachePrefix;
|
||||
$this->cacheTtl = $cacheTtl ?? Duration::fromMinutes(15);
|
||||
}
|
||||
|
||||
public function find(string $name): ?FeatureFlag
|
||||
{
|
||||
$cacheKey = $this->getCacheKey($name);
|
||||
|
||||
// Try cache first
|
||||
$cached = $this->cache->get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached instanceof FeatureFlag ? $cached : null;
|
||||
}
|
||||
|
||||
// Load from repository
|
||||
$flag = $this->repository->find($name);
|
||||
|
||||
// Cache the result (including null results)
|
||||
if ($flag !== null) {
|
||||
$this->cache->set(CacheItem::forSet($cacheKey, $flag, $this->cacheTtl));
|
||||
} else {
|
||||
// Cache negative results for shorter time
|
||||
$this->cache->set(CacheItem::forSet($cacheKey, 'NOT_FOUND', Duration::fromMinutes(5)));
|
||||
}
|
||||
|
||||
return $flag;
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey('all');
|
||||
|
||||
// Try cache first
|
||||
$cached = $this->cache->get($cacheKey);
|
||||
if (is_array($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
// Load from repository
|
||||
$flags = $this->repository->findAll();
|
||||
|
||||
// Cache all flags
|
||||
$this->cache->set(CacheItem::forSet($cacheKey, $flags, $this->cacheTtl));
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
public function save(FeatureFlag $flag): void
|
||||
{
|
||||
// Save to repository first
|
||||
$this->repository->save($flag);
|
||||
|
||||
// Invalidate cache
|
||||
$this->invalidateCache($flag->name);
|
||||
}
|
||||
|
||||
public function delete(string $name): void
|
||||
{
|
||||
// Delete from repository first
|
||||
$this->repository->delete($name);
|
||||
|
||||
// Invalidate cache
|
||||
$this->invalidateCache($name);
|
||||
}
|
||||
|
||||
public function exists(string $name): bool
|
||||
{
|
||||
// Use find() method which benefits from caching
|
||||
return $this->find($name) !== null;
|
||||
}
|
||||
|
||||
private function getCacheKey(string $key): CacheKey
|
||||
{
|
||||
return CacheKey::fromString($this->cachePrefix . ':' . $key);
|
||||
}
|
||||
|
||||
private function invalidateCache(string $flagName): void
|
||||
{
|
||||
// Invalidate specific flag cache
|
||||
$this->cache->forget($this->getCacheKey($flagName));
|
||||
|
||||
// Invalidate all flags cache
|
||||
$this->cache->forget($this->getCacheKey('all'));
|
||||
}
|
||||
}
|
||||
149
src/Framework/FeatureFlags/Storage/FileFeatureFlagRepository.php
Normal file
149
src/Framework/FeatureFlags/Storage/FileFeatureFlagRepository.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\FeatureFlags\Storage;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\FeatureFlags\FeatureFlag;
|
||||
use App\Framework\FeatureFlags\FeatureFlagRepository;
|
||||
use App\Framework\FeatureFlags\FeatureFlagStatus;
|
||||
use App\Framework\Filesystem\FilesystemManager;
|
||||
use App\Framework\Filesystem\Serializers\JsonSerializer;
|
||||
|
||||
/**
|
||||
* File-based feature flag storage using Framework's FilesystemManager
|
||||
*/
|
||||
final readonly class FileFeatureFlagRepository implements FeatureFlagRepository
|
||||
{
|
||||
public function __construct(
|
||||
private FilesystemManager $filesystem,
|
||||
private string $filePath = 'feature_flags.json',
|
||||
private string $storageName = 'default',
|
||||
private JsonSerializer $serializer = new JsonSerializer()
|
||||
) {
|
||||
}
|
||||
|
||||
public function find(string $name): ?FeatureFlag
|
||||
{
|
||||
$flags = $this->loadFlags();
|
||||
|
||||
return $flags[$name] ?? null;
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
return array_values($this->loadFlags());
|
||||
}
|
||||
|
||||
public function save(FeatureFlag $flag): void
|
||||
{
|
||||
$flags = $this->loadFlags();
|
||||
$flags[$flag->name] = $flag;
|
||||
$this->saveFlags($flags);
|
||||
}
|
||||
|
||||
public function delete(string $name): void
|
||||
{
|
||||
$flags = $this->loadFlags();
|
||||
unset($flags[$name]);
|
||||
$this->saveFlags($flags);
|
||||
}
|
||||
|
||||
public function exists(string $name): bool
|
||||
{
|
||||
$flags = $this->loadFlags();
|
||||
|
||||
return isset($flags[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, FeatureFlag>
|
||||
*/
|
||||
private function loadFlags(): array
|
||||
{
|
||||
$storage = $this->filesystem->storage($this->storageName);
|
||||
|
||||
if (! $storage->exists($this->filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$content = $storage->get($this->filePath);
|
||||
$data = $this->serializer->deserialize($content);
|
||||
|
||||
if (! is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$flags = [];
|
||||
foreach ($data as $flagData) {
|
||||
if (! is_array($flagData) || ! isset($flagData['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$flag = $this->arrayToFeatureFlag($flagData);
|
||||
if ($flag !== null) {
|
||||
$flags[$flag->name] = $flag;
|
||||
}
|
||||
}
|
||||
|
||||
return $flags;
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, FeatureFlag> $flags
|
||||
*/
|
||||
private function saveFlags(array $flags): void
|
||||
{
|
||||
$data = [];
|
||||
foreach ($flags as $flag) {
|
||||
$data[] = $this->featureFlagToArray($flag);
|
||||
}
|
||||
|
||||
$content = $this->serializer->serialize($data);
|
||||
$storage = $this->filesystem->storage($this->storageName);
|
||||
$storage->put($this->filePath, $content);
|
||||
}
|
||||
|
||||
private function featureFlagToArray(FeatureFlag $flag): array
|
||||
{
|
||||
return [
|
||||
'name' => $flag->name,
|
||||
'status' => $flag->status->value,
|
||||
'description' => $flag->description,
|
||||
'conditions' => $flag->conditions,
|
||||
'enabled_at' => $flag->enabledAt?->toIso8601(),
|
||||
'expires_at' => $flag->expiresAt?->toIso8601(),
|
||||
];
|
||||
}
|
||||
|
||||
private function arrayToFeatureFlag(array $data): ?FeatureFlag
|
||||
{
|
||||
try {
|
||||
$status = FeatureFlagStatus::from($data['status'] ?? 'disabled');
|
||||
|
||||
$enabledAt = isset($data['enabled_at']) && $data['enabled_at'] !== null
|
||||
? Timestamp::fromFloat(strtotime($data['enabled_at']))
|
||||
: null;
|
||||
|
||||
$expiresAt = isset($data['expires_at']) && $data['expires_at'] !== null
|
||||
? Timestamp::fromFloat(strtotime($data['expires_at']))
|
||||
: null;
|
||||
|
||||
return new FeatureFlag(
|
||||
name: $data['name'],
|
||||
status: $status,
|
||||
description: $data['description'] ?? null,
|
||||
conditions: $data['conditions'] ?? [],
|
||||
enabledAt: $enabledAt,
|
||||
expiresAt: $expiresAt
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user