docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\Redis\Services\RedisMonitoringService;
final readonly class RedisMonitoringServiceInitializer
{
#[Initializer]
public function __invoke(Container $container): void
{
$connectionPool = $container->get(RedisConnectionPool::class);
$monitoringService = new RedisMonitoringService($connectionPool);
$container->singleton(RedisMonitoringService::class, $monitoringService);
}
}

View File

@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\Services;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Redis\RedisConnectionPool;
use App\Framework\Redis\ValueObjects\RedisCachePattern;
use App\Framework\Redis\ValueObjects\RedisDatabase;
use App\Framework\Redis\ValueObjects\RedisKeyInfo;
use App\Framework\Redis\ValueObjects\RedisSlowLogEntry;
use Redis;
final readonly class RedisMonitoringService
{
public function __construct(
private RedisConnectionPool $connectionPool
) {
}
/**
* @return array<string, mixed>
*/
public function getBasicInfo(): array
{
$redis = $this->getRedisClient();
$info = $redis->info();
return [
'status' => 'Connected',
'version' => $info['redis_version'] ?? 'Unknown',
'uptime' => $this->formatUptime((int)($info['uptime_in_seconds'] ?? 0)),
'memory' => Byte::fromBytes((int)($info['used_memory'] ?? 0)),
'peak_memory' => Byte::fromBytes((int)($info['used_memory_peak'] ?? 0)),
'clients' => $info['connected_clients'] ?? 0,
'keys' => $redis->dbsize(),
'commands_per_sec' => $info['instantaneous_ops_per_sec'] ?? 0,
'hit_rate' => $this->calculateHitRate($info),
'evicted_keys' => $info['evicted_keys'] ?? 0,
'expired_keys' => $info['expired_keys'] ?? 0,
];
}
/**
* @return RedisDatabase[]
*/
public function getDatabases(): array
{
$redis = $this->getRedisClient();
$databases = [];
for ($db = 0; $db <= 15; $db++) {
try {
$redis->select($db);
$size = $redis->dbsize();
if ($size > 0) {
$databases[] = RedisDatabase::create($db, $size);
}
} catch (\Throwable $e) {
break;
}
}
// Reset to default database
$redis->select(0);
return $databases;
}
/**
* @return RedisCachePattern[]
*/
public function analyzeCachePatterns(): array
{
$redis = $this->getRedisClient();
// Switch to cache database
$redis->select(1);
$patterns = [
'page' => ['pattern' => 'cache:page:*', 'description' => 'Cached page content'],
'middleware' => ['pattern' => 'cache:middleware_metrics:*', 'description' => 'Middleware performance metrics'],
'discovery' => ['pattern' => 'cache:discovery:*', 'description' => 'Framework discovery cache'],
'http_parser' => ['pattern' => 'cache:http_parser:*', 'description' => 'HTTP request parser cache'],
'template' => ['pattern' => 'cache:template:*', 'description' => 'Template compilation cache'],
'session' => ['pattern' => 'cache:session:*', 'description' => 'User session data'],
];
$analysis = [];
foreach ($patterns as $type => $config) {
$keys = $redis->keys($config['pattern']);
$totalSize = 0;
$count = count($keys);
// Sample keys to estimate size
$sampleKeys = array_slice($keys, 0, min(10, $count));
foreach ($sampleKeys as $key) {
try {
$totalSize += $redis->strlen($key);
} catch (\Throwable $e) {
// Key might have expired
}
}
$avgSize = $count > 0 && count($sampleKeys) > 0 ? (int)($totalSize / count($sampleKeys)) : 0;
$estimatedTotal = $avgSize * $count;
$analysis[] = RedisCachePattern::create(
name: $type,
pattern: $config['pattern'],
keyCount: $count,
avgSizeBytes: $avgSize,
totalSizeBytes: $estimatedTotal,
description: $config['description']
);
}
// Reset to default database
$redis->select(0);
return $analysis;
}
/**
* @return RedisKeyInfo[]
*/
public function getKeyDetails(int $limit = 20): array
{
$redis = $this->getRedisClient();
$keys = [];
$allKeys = $redis->keys('*');
$sampleKeys = array_slice($allKeys, 0, $limit);
foreach ($sampleKeys as $key) {
try {
$ttl = $redis->ttl($key);
$type = $redis->type($key);
$size = $redis->strlen($key);
$preview = $this->getKeyPreview($redis, $key, $type);
$keys[] = RedisKeyInfo::create(
key: $key,
redisType: $type,
sizeBytes: $size,
ttlSeconds: $ttl,
preview: $preview
);
} catch (\Throwable $e) {
// Key might have expired
}
}
return $keys;
}
/**
* @return RedisSlowLogEntry[]
*/
public function getSlowLog(int $limit = 10): array
{
$redis = $this->getRedisClient();
$entries = [];
try {
$slowLog = $redis->slowlog('get', $limit);
foreach ($slowLog as $entry) {
$entries[] = RedisSlowLogEntry::create(
id: $entry[0],
timestamp: $entry[1],
durationMicros: $entry[2],
command: $entry[3]
);
}
} catch (\Throwable $e) {
// Slow log might not be available
}
return $entries;
}
/**
* @return array<string, mixed>
*/
public function getConnectionPoolInfo(): array
{
return [
'pool_status' => 'Connected',
'health_check' => 'OK',
'connection_type' => 'Pool',
];
}
private function getRedisClient(): Redis
{
return $this->connectionPool->getConnection()->getClient();
}
private function calculateHitRate(array $info): Percentage
{
$hits = $info['keyspace_hits'] ?? 0;
$misses = $info['keyspace_misses'] ?? 0;
$total = $hits + $misses;
if ($total === 0) {
return Percentage::zero();
}
return Percentage::fromRatio($hits, $total);
}
private function formatUptime(int $seconds): string
{
$duration = Duration::fromSeconds($seconds);
return $duration->toHumanReadable();
}
private function getKeyPreview(Redis $redis, string $key, int $type): string
{
try {
switch ($type) {
case Redis::REDIS_STRING:
$value = $redis->get($key);
if (is_string($value)) {
return substr($value, 0, 100) . (strlen($value) > 100 ? '...' : '');
}
return 'Binary data';
case Redis::REDIS_LIST:
$count = $redis->llen($key);
return "List with $count items";
case Redis::REDIS_SET:
$count = $redis->scard($key);
return "Set with $count members";
case Redis::REDIS_ZSET:
$count = $redis->zcard($key);
return "Sorted Set with $count members";
case Redis::REDIS_HASH:
$count = $redis->hlen($key);
return "Hash with $count fields";
default:
return 'Unknown type';
}
} catch (\Throwable $e) {
return 'Error reading value';
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
final readonly class RedisCachePattern
{
public function __construct(
public string $name,
public string $pattern,
public int $keyCount,
public Byte $averageSize,
public Byte $totalSize,
public string $description = ''
) {
}
public static function create(
string $name,
string $pattern,
int $keyCount,
int $avgSizeBytes,
int $totalSizeBytes,
string $description = ''
): self {
return new self(
name: $name,
pattern: $pattern,
keyCount: $keyCount,
averageSize: Byte::fromBytes($avgSizeBytes),
totalSize: Byte::fromBytes($totalSizeBytes),
description: $description
);
}
public function isEmpty(): bool
{
return $this->keyCount === 0;
}
public function getIcon(): string
{
return match ($this->name) {
'page' => '📄',
'middleware' => '⚙️',
'discovery' => '🔍',
'http_parser' => '🌐',
'template' => '📝',
'session' => '👤',
'queue' => '📋',
'metrics' => '📊',
default => '📦'
};
}
public function getDisplayName(): string
{
return match ($this->name) {
'page' => 'Page Cache',
'middleware' => 'Middleware Metrics',
'discovery' => 'Discovery Cache',
'http_parser' => 'HTTP Parser',
'template' => 'Template Cache',
'session' => 'Session Data',
'queue' => 'Queue Cache',
'metrics' => 'Performance Metrics',
default => ucfirst($this->name)
};
}
public function getEfficiencyRating(): string
{
if ($this->keyCount === 0) {
return 'Empty';
}
// Rate based on average key size (larger = less efficient)
$avgKb = $this->averageSize->toKilobytes();
return match (true) {
$avgKb < 1 => 'Excellent',
$avgKb < 5 => 'Good',
$avgKb < 20 => 'Fair',
default => 'Poor'
};
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
final readonly class RedisDatabase
{
public function __construct(
public int $id,
public string $name,
public string $description,
public int $keyCount,
public string $memoryUsage = '0 B'
) {
}
public static function create(int $id, int $keyCount, string $memoryUsage = '0 B'): self
{
$type = RedisDatabaseType::fromId($id);
return new self(
id: $id,
name: $type?->getName() ?? "Database $id",
description: $type?->getDescription() ?? "Unknown database purpose",
keyCount: $keyCount,
memoryUsage: $memoryUsage
);
}
public function isEmpty(): bool
{
return $this->keyCount === 0;
}
public function getType(): ?RedisDatabaseType
{
return RedisDatabaseType::fromId($this->id);
}
public function isKnownType(): bool
{
return $this->getType() !== null;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
enum RedisDatabaseType: int
{
case DEFAULT_SESSIONS = 0;
case CACHE = 1;
case QUEUE = 2;
case METRICS = 3;
case TEMP_DATA = 4;
case RATE_LIMITING = 5;
case BACKGROUND_JOBS = 6;
case ANALYTICS = 7;
public function getName(): string
{
return match ($this) {
self::DEFAULT_SESSIONS => 'Default/Sessions',
self::CACHE => 'Cache',
self::QUEUE => 'Queue',
self::METRICS => 'Metrics',
self::TEMP_DATA => 'Temporary Data',
self::RATE_LIMITING => 'Rate Limiting',
self::BACKGROUND_JOBS => 'Background Jobs',
self::ANALYTICS => 'Analytics'
};
}
public function getDescription(): string
{
return match ($this) {
self::DEFAULT_SESSIONS => 'User sessions and general data',
self::CACHE => 'Application cache (templates, data)',
self::QUEUE => 'Job queue and task processing',
self::METRICS => 'Performance and analytics metrics',
self::TEMP_DATA => 'Temporary application data',
self::RATE_LIMITING => 'Rate limiting counters',
self::BACKGROUND_JOBS => 'Background job processing',
self::ANALYTICS => 'Analytics and tracking data'
};
}
public static function fromId(int $id): ?self
{
return self::tryFrom($id);
}
public static function getNameForId(int $id): string
{
$type = self::fromId($id);
return $type?->getName() ?? "Database $id";
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
use App\Framework\Core\ValueObjects\Byte;
final readonly class RedisKeyInfo
{
public function __construct(
public string $key,
public RedisKeyType $type,
public Byte $size,
public RedisTtl $ttl,
public string $preview
) {
}
public static function create(
string $key,
int $redisType,
int $sizeBytes,
int $ttlSeconds,
string $preview
): self {
return new self(
key: $key,
type: RedisKeyType::fromRedisType($redisType),
size: Byte::fromBytes($sizeBytes),
ttl: RedisTtl::fromSeconds($ttlSeconds),
preview: $preview
);
}
public function getKeyPattern(): string
{
$parts = explode(':', $this->key);
return count($parts) > 1 ? $parts[0] . ':*' : 'misc';
}
public function isLargeKey(?Byte $threshold = null): bool
{
$threshold ??= Byte::fromKilobytes(10); // Default 10KB threshold
return $this->size->greaterThan($threshold);
}
public function isExpiringSoon(int $thresholdSeconds = 300): bool
{
return $this->ttl->isExpiringSoon($thresholdSeconds);
}
public function getCategoryIcon(): string
{
$pattern = $this->getKeyPattern();
return match (true) {
str_contains($pattern, 'cache:') => '💾',
str_contains($pattern, 'session:') => '👤',
str_contains($pattern, 'queue:') => '📋',
str_contains($pattern, 'lock:') => '🔒',
str_contains($pattern, 'metrics:') => '📊',
str_contains($pattern, 'temp:') => '⏰',
default => $this->type->getIcon()
};
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
enum RedisKeyType: int
{
case STRING = 1;
case LIST = 3;
case SET = 2;
case ZSET = 4;
case HASH = 5;
case STREAM = 6;
case UNKNOWN = 0;
public static function fromRedisType(int $redisType): self
{
return self::tryFrom($redisType) ?? self::UNKNOWN;
}
public function getDisplayName(): string
{
return match ($this) {
self::STRING => 'String',
self::LIST => 'List',
self::SET => 'Set',
self::ZSET => 'Sorted Set',
self::HASH => 'Hash',
self::STREAM => 'Stream',
self::UNKNOWN => 'Unknown'
};
}
public function getIcon(): string
{
return match ($this) {
self::STRING => '📝',
self::LIST => '📋',
self::SET => '🗂️',
self::ZSET => '📊',
self::HASH => '🗃️',
self::STREAM => '🌊',
self::UNKNOWN => '❓'
};
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
use App\Framework\Core\ValueObjects\Duration;
use DateTimeImmutable;
final readonly class RedisSlowLogEntry
{
public function __construct(
public int $id,
public DateTimeImmutable $timestamp,
public Duration $duration,
public string $command,
public array $fullCommand
) {
}
public static function create(int $id, int $timestamp, int $durationMicros, array $command): self
{
return new self(
id: $id,
timestamp: (new DateTimeImmutable())->setTimestamp($timestamp),
duration: Duration::fromMicroseconds($durationMicros),
command: self::formatCommand($command),
fullCommand: $command
);
}
private static function formatCommand(array $command): string
{
// Limit command display to first 3 parts for readability
$displayParts = array_slice($command, 0, 3);
$hasMore = count($command) > 3;
return implode(' ', $displayParts) . ($hasMore ? '...' : '');
}
public function isSlow(?Duration $threshold = null): bool
{
$threshold ??= Duration::fromMilliseconds(100); // Default 100ms
return $this->duration->isGreaterThan($threshold);
}
public function getCommandType(): string
{
return strtoupper($this->fullCommand[0] ?? 'UNKNOWN');
}
public function getSeverityLevel(): string
{
$ms = $this->duration->toMilliseconds();
return match (true) {
$ms >= 1000 => 'Critical',
$ms >= 500 => 'High',
$ms >= 100 => 'Medium',
default => 'Low'
};
}
public function getSeverityIcon(): string
{
return match ($this->getSeverityLevel()) {
'Critical' => '🔴',
'High' => '🟠',
'Medium' => '🟡',
'Low' => '🟢',
default => '⚪'
};
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Framework\Redis\ValueObjects;
use App\Framework\Core\ValueObjects\Duration;
final readonly class RedisTtl
{
public function __construct(
public int $seconds,
public bool $isPersistent,
public bool $isExpired
) {
}
public static function fromSeconds(int $seconds): self
{
return new self(
seconds: $seconds,
isPersistent: $seconds === -1,
isExpired: $seconds === -2 || $seconds < 0
);
}
public static function persistent(): self
{
return new self(
seconds: -1,
isPersistent: true,
isExpired: false
);
}
public static function expired(): self
{
return new self(
seconds: -2,
isPersistent: false,
isExpired: true
);
}
public function getDisplayValue(): string
{
if ($this->isExpired) {
return 'Expired';
}
if ($this->isPersistent) {
return 'Never';
}
$duration = $this->toDuration();
return $duration ? $duration->toHumanReadable() : 'Unknown';
}
public function toDuration(): ?Duration
{
if ($this->isPersistent || $this->isExpired) {
return null;
}
return Duration::fromSeconds($this->seconds);
}
public function isExpiringSoon(int $thresholdSeconds = 300): bool
{
return ! $this->isPersistent && ! $this->isExpired && $this->seconds <= $thresholdSeconds;
}
}