fix: resolve RedisCache array offset error and improve discovery diagnostics

- Fix RedisCache driver to handle MGET failures gracefully with fallback
- Add comprehensive discovery context comparison debug tools
- Identify root cause: WEB context discovery missing 166 items vs CLI
- WEB context missing RequestFactory class entirely (52 vs 69 commands)
- Improved exception handling with detailed binding diagnostics
This commit is contained in:
2025-09-12 20:05:18 +02:00
parent 8040d3e7a5
commit e30753ba0e
46990 changed files with 10789682 additions and 89639 deletions

View File

@@ -15,15 +15,19 @@ final class AsyncAssetProcessor
public function __construct(
private readonly Storage $storage,
private readonly FiberManager $fiberManager = new FiberManager(),
/** @var array<string, mixed> */
private readonly array $config = []
) {
}
/**
* Verarbeitet alle Assets parallel
*
* @return array<string, mixed>
*/
public function processAll(string $sourceDir, string $outputDir): array
{
/** @var array<string, \Closure> */
$operations = [
'css' => fn () => $this->processCss($sourceDir . '/css', $outputDir . '/css'),
'js' => fn () => $this->processJs($sourceDir . '/js', $outputDir . '/js'),
@@ -35,12 +39,15 @@ final class AsyncAssetProcessor
/**
* Verarbeitet CSS-Dateien
*
* @return array<string, string>
*/
public function processCss(string $sourceDir, string $outputDir): array
{
$cssFiles = $this->findFiles($sourceDir, '*.css');
$scssFiles = $this->findFiles($sourceDir, '*.scss');
/** @var array<string, \Closure> */
$operations = [];
// Verarbeite CSS-Dateien
@@ -58,11 +65,14 @@ final class AsyncAssetProcessor
/**
* Verarbeitet JavaScript-Dateien
*
* @return array<string, string>
*/
public function processJs(string $sourceDir, string $outputDir): array
{
$jsFiles = $this->findFiles($sourceDir, '*.js');
/** @var array<string, \Closure> */
$operations = [];
foreach ($jsFiles as $file) {
$operations["js_{$file}"] = fn () => $this->minifyJs($sourceDir . '/' . $file, $outputDir);
@@ -73,6 +83,8 @@ final class AsyncAssetProcessor
/**
* Verarbeitet Bilder
*
* @return array<string, string>
*/
public function processImages(string $sourceDir, string $outputDir): array
{
@@ -84,6 +96,7 @@ final class AsyncAssetProcessor
$this->findFiles($sourceDir, '*.svg')
);
/** @var array<string, \Closure> */
$operations = [];
foreach ($imageFiles as $file) {
$operations["img_{$file}"] = fn () => $this->optimizeImage($sourceDir . '/' . $file, $outputDir);
@@ -94,9 +107,12 @@ final class AsyncAssetProcessor
/**
* Bündelt JavaScript-Dateien
*
* @param array<int, string> $files
*/
public function bundleJs(array $files, string $outputFile): string
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($files as $file) {
$operations[$file] = fn () => $this->storage->get($file);
@@ -113,9 +129,12 @@ final class AsyncAssetProcessor
/**
* Bündelt CSS-Dateien
*
* @param array<int, string> $files
*/
public function bundleCss(array $files, string $outputFile): string
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($files as $file) {
$operations[$file] = fn () => $this->storage->get($file);
@@ -132,9 +151,13 @@ final class AsyncAssetProcessor
/**
* Generiert verschiedene Bildgrößen parallel
*
* @param array<string, array<string, mixed>> $sizes
* @return array<string, string>
*/
public function generateImageSizes(string $sourceImage, array $sizes, string $outputDir): array
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($sizes as $sizeName => $dimensions) {
$operations[$sizeName] = fn () => $this->resizeImage(
@@ -235,6 +258,7 @@ final class AsyncAssetProcessor
// Vereinfachte Variable-Verarbeitung
preg_match_all('/\$([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/', $content, $matches);
/** @var array<string, string> */
$variables = [];
for ($i = 0; $i < count($matches[0]); $i++) {
$variables['$' . $matches[1][$i]] = $matches[2][$i];
@@ -250,6 +274,9 @@ final class AsyncAssetProcessor
return $content;
}
/**
* @return array<int, string>
*/
private function findFiles(string $directory, string $pattern): array
{
if (! is_dir($directory)) {
@@ -267,9 +294,11 @@ final class AsyncAssetProcessor
public function watch(string $sourceDir, string $outputDir): void
{
$this->fiberManager->async(function () use ($sourceDir, $outputDir) {
/** @var array<string, int|false> */
$lastCheck = [];
while (true) {
/** @var array<int, string> */
$files = array_merge(
glob($sourceDir . '/**/*.css') ?: [],
glob($sourceDir . '/**/*.scss') ?: [],

View File

@@ -11,6 +11,7 @@ use App\Framework\Async\FiberManager;
*/
final class AsyncCache
{
/** @var array<string, array<string, mixed>> */
private array $memoryCache = [];
private string $cacheDir;
@@ -68,6 +69,7 @@ final class AsyncCache
$ttl ??= $this->defaultTtl;
$expires = $ttl > 0 ? time() + $ttl : 0;
/** @var array<string, mixed> */
$item = [
'value' => $value,
'expires' => $expires,
@@ -89,9 +91,13 @@ final class AsyncCache
/**
* Holt mehrere Werte parallel
*
* @param array<int, string> $keys
* @return array<string, mixed>
*/
public function getMultiple(array $keys): array
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($keys as $key) {
$operations[$key] = fn () => $this->get($key);
@@ -102,9 +108,12 @@ final class AsyncCache
/**
* Speichert mehrere Werte parallel
*
* @param array<string, mixed> $items
*/
public function setMultiple(array $items, ?int $ttl = null): bool
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($items as $key => $value) {
$operations[$key] = fn () => $this->set($key, $value, $ttl);
@@ -139,9 +148,12 @@ final class AsyncCache
/**
* Löscht mehrere Cache-Einträge parallel
*
* @param array<int, string> $keys
*/
public function deleteMultiple(array $keys): bool
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($keys as $key) {
$operations[$key] = fn () => $this->delete($key);
@@ -175,6 +187,7 @@ final class AsyncCache
$files = glob($this->cacheDir . '/*.cache');
if ($files) {
/** @var array<int, \Closure> */
$operations = [];
foreach ($files as $file) {
$operations[] = fn () => @unlink($file);
@@ -202,10 +215,15 @@ final class AsyncCache
/**
* Erstellt mehrere Cache-Werte parallel falls sie nicht existieren
*
* @param array<string, callable> $callbacks
* @return array<string, mixed>
*/
public function rememberMultiple(array $callbacks, ?int $ttl = null): array
{
/** @var array<string, mixed> */
$results = [];
/** @var array<string, callable> */
$toCreate = [];
// Prüfe welche Keys bereits existieren
@@ -220,6 +238,7 @@ final class AsyncCache
// Erstelle fehlende Werte parallel
if (! empty($toCreate)) {
/** @var array<string, callable> */
$operations = [];
foreach ($toCreate as $key => $callback) {
$operations[$key] = $callback;
@@ -228,6 +247,7 @@ final class AsyncCache
$newValues = $this->fiberManager->batch($operations);
// Speichere neue Werte im Cache
/** @var array<string, \Closure> */
$setOperations = [];
foreach ($newValues as $key => $value) {
$setOperations[$key] = fn () => $this->set($key, $value, $ttl);
@@ -249,6 +269,8 @@ final class AsyncCache
/**
* Gibt Cache-Statistiken zurück
*
* @return array<string, mixed>
*/
public function getStats(): array
{

View File

@@ -19,9 +19,11 @@ final class AsyncDatabase
string $dsn,
string $username = '',
string $password = '',
/** @var array<int, mixed> */
array $options = [],
private readonly FiberManager $fiberManager = new FiberManager()
) {
/** @var array<int, mixed> */
$defaultOptions = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
@@ -33,6 +35,8 @@ final class AsyncDatabase
/**
* Führt eine Query aus
*
* @param array<string, mixed> $params
*/
public function query(string $sql, array $params = []): DatabaseResult
{
@@ -55,11 +59,12 @@ final class AsyncDatabase
/**
* Führt mehrere Queries parallel aus
*
* @param array<string, array> $queries ['key' => ['sql' => '...', 'params' => []], ...]
* @param array<string, array<string, mixed>> $queries ['key' => ['sql' => '...', 'params' => []], ...]
* @return array<string, DatabaseResult>
*/
public function queryMultiple(array $queries): array
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($queries as $key => $query) {
$operations[$key] = fn () => $this->query($query['sql'], $query['params'] ?? []);
@@ -70,6 +75,9 @@ final class AsyncDatabase
/**
* Führt Queries mit begrenzter Parallelität aus
*
* @param array<string, array<string, mixed>> $queries
* @return array<string, DatabaseResult>
*/
public function queryBatch(array $queries, int $maxConcurrency = 5): array
{
@@ -87,6 +95,9 @@ final class AsyncDatabase
/**
* Führt eine SELECT-Query aus
*
* @param array<string, mixed> $params
* @return array<int, array<string, mixed>>
*/
public function select(string $sql, array $params = []): array
{
@@ -95,10 +106,14 @@ final class AsyncDatabase
/**
* Führt eine INSERT-Query aus
*
* @param array<string, mixed> $data
*/
public function insert(string $table, array $data): int
{
/** @var array<int, string> */
$columns = array_keys($data);
/** @var array<int, string> */
$placeholders = array_map(fn ($col) => ":$col", $columns);
$sql = "INSERT INTO $table (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
@@ -110,9 +125,13 @@ final class AsyncDatabase
/**
* Führt mehrere INSERTs parallel aus
*
* @param array<int, array<string, mixed>> $records
* @return array<string, int>
*/
public function insertMultiple(string $table, array $records): array
{
/** @var array<int|string, \Closure> */
$operations = [];
foreach ($records as $key => $data) {
$operations[$key] = fn () => $this->insert($table, $data);
@@ -123,14 +142,20 @@ final class AsyncDatabase
/**
* Führt eine UPDATE-Query aus
*
* @param array<string, mixed> $data
* @param array<string, mixed> $where
*/
public function update(string $table, array $data, array $where): int
{
/** @var array<int, string> */
$setParts = array_map(fn ($col) => "$col = :set_$col", array_keys($data));
/** @var array<int, string> */
$whereParts = array_map(fn ($col) => "$col = :where_$col", array_keys($where));
$sql = "UPDATE $table SET " . implode(', ', $setParts) . " WHERE " . implode(' AND ', $whereParts);
/** @var array<string, mixed> */
$params = [];
foreach ($data as $key => $value) {
$params["set_$key"] = $value;
@@ -144,9 +169,12 @@ final class AsyncDatabase
/**
* Führt eine DELETE-Query aus
*
* @param array<string, mixed> $where
*/
public function delete(string $table, array $where): int
{
/** @var array<int, string> */
$whereParts = array_map(fn ($col) => "$col = :$col", array_keys($where));
$sql = "DELETE FROM $table WHERE " . implode(' AND ', $whereParts);

View File

@@ -13,6 +13,7 @@ use Fiber;
*/
final class AsyncHttpClient
{
/** @var array<string, mixed> */
private array $defaultOptions = [
'timeout' => 30,
'connect_timeout' => 10,
@@ -23,6 +24,7 @@ final class AsyncHttpClient
public function __construct(
private readonly FiberManager $fiberManager = new FiberManager(),
/** @var array<string, mixed> */
array $defaultOptions = []
) {
$this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
@@ -31,7 +33,7 @@ final class AsyncHttpClient
/**
* Sendet einen GET-Request
*/
public function get(string $url, array $headers = [], array $options = []): HttpResponse
public function get(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('GET', $url, null, $headers, $options);
}
@@ -39,7 +41,7 @@ final class AsyncHttpClient
/**
* Sendet einen POST-Request
*/
public function post(string $url, mixed $data = null, array $headers = [], array $options = []): HttpResponse
public function post(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('POST', $url, $data, $headers, $options);
}
@@ -47,7 +49,7 @@ final class AsyncHttpClient
/**
* Sendet einen PUT-Request
*/
public function put(string $url, mixed $data = null, array $headers = [], array $options = []): HttpResponse
public function put(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('PUT', $url, $data, $headers, $options);
}
@@ -55,7 +57,7 @@ final class AsyncHttpClient
/**
* Sendet einen DELETE-Request
*/
public function delete(string $url, array $headers = [], array $options = []): HttpResponse
public function delete(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
return $this->request('DELETE', $url, null, $headers, $options);
}
@@ -63,11 +65,12 @@ final class AsyncHttpClient
/**
* Sendet mehrere Requests parallel
*
* @param array<string, array> $requests ['key' => ['method' => 'GET', 'url' => '...', ...]]
* @param array<string, array<string, mixed>> $requests ['key' => ['method' => 'GET', 'url' => '...', ...]]
* @return array<string, HttpResponse>
*/
public function requestMultiple(array $requests): array
{
/** @var array<string, \Closure> */
$operations = [];
foreach ($requests as $key => $request) {
$operations[$key] = fn () => $this->request(
@@ -84,6 +87,9 @@ final class AsyncHttpClient
/**
* Sendet Requests mit begrenzter Parallelität
*
* @param array<string, array<string, mixed>> $requests
* @return array<string, HttpResponse>
*/
public function requestBatch(array $requests, int $maxConcurrency = 10): array
{
@@ -108,7 +114,7 @@ final class AsyncHttpClient
/**
* Hauptmethode für HTTP-Requests
*/
private function request(string $method, string $url, mixed $data = null, array $headers = [], array $options = []): HttpResponse
private function request(string $method, string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
{
$options = array_merge($this->defaultOptions, $options);
@@ -143,8 +149,9 @@ final class AsyncHttpClient
/**
* @return resource
*/
private function createContext(string $method, mixed $data, array $headers, array $options)
private function createContext(string $method, mixed $data, /** @var array<string, string> */ array $headers, /** @var array<string, mixed> */ array $options)
{
/** @var array<string, array<string, mixed>> */
$contextOptions = [
'http' => [
'method' => $method,
@@ -166,6 +173,7 @@ final class AsyncHttpClient
}
if (! empty($headers)) {
/** @var array<int, string> */
$headerStrings = [];
foreach ($headers as $key => $value) {
$headerStrings[] = "$key: $value";
@@ -176,8 +184,13 @@ final class AsyncHttpClient
return stream_context_create($contextOptions);
}
/**
* @param array<int, string> $httpResponseHeader
* @return array<string, string>
*/
private function parseHeaders(array $httpResponseHeader): array
{
/** @var array<string, string> */
$headers = [];
foreach ($httpResponseHeader as $header) {
if (strpos($header, ':') !== false) {
@@ -189,6 +202,9 @@ final class AsyncHttpClient
return $headers;
}
/**
* @param array<int, string> $httpResponseHeader
*/
private function extractStatusCode(array $httpResponseHeader): int
{
if (empty($httpResponseHeader)) {

View File

@@ -53,6 +53,8 @@ final readonly class HttpResponse
/**
* Konvertiert zu Array
*
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -12,11 +12,13 @@ use App\Framework\Async\FiberManager;
*/
final class AsyncMailer
{
/** @var array<string, mixed> */
private array $config;
private AsyncQueue $mailQueue;
public function __construct(
/** @var array<string, mixed> */
array $config = [],
private readonly FiberManager $fiberManager = new FiberManager()
) {
@@ -50,9 +52,13 @@ final class AsyncMailer
/**
* Sendet mehrere E-Mails parallel
*
* @param array<int, Email> $emails
* @return array<int, bool>
*/
public function sendMultiple(array $emails): array
{
/** @var array<int, \Closure> */
$operations = [];
foreach ($emails as $key => $email) {
$operations[$key] = fn () => $this->send($email);
@@ -71,6 +77,8 @@ final class AsyncMailer
/**
* Fügt mehrere E-Mails zur Queue hinzu
*
* @param array<int, Email> $emails
*/
public function queueMultiple(array $emails): int
{
@@ -110,6 +118,7 @@ final class AsyncMailer
public function processQueue(): int
{
$processed = 0;
/** @var array<int, Email> */
$batch = [];
// Sammle Batch
@@ -131,9 +140,13 @@ final class AsyncMailer
/**
* Sendet Newsletter an mehrere Empfänger
*
* @param array<int, array<string, mixed>> $recipients
* @return array<int, bool>
*/
public function sendNewsletter(string $subject, string $content, array $recipients): array
{
/** @var array<int, Email> */
$emails = [];
foreach ($recipients as $key => $recipient) {
$email = new Email(
@@ -154,6 +167,7 @@ final class AsyncMailer
// Vereinfachte SMTP-Implementation
// In Produktion würde man eine echte SMTP-Library verwenden
/** @var array<int, string> */
$headers = [
'From: ' . $email->fromName . ' <' . $email->fromEmail . '>',
'Reply-To: ' . $email->fromEmail,
@@ -179,8 +193,12 @@ final class AsyncMailer
return $success !== false;
}
/**
* @param array<string, mixed> $recipient
*/
private function personalizeContent(string $content, array $recipient): string
{
/** @var array<string, string> */
$placeholders = [
'{{name}}' => $recipient['name'] ?? '',
'{{email}}' => $recipient['email'] ?? '',
@@ -201,6 +219,8 @@ final class AsyncMailer
/**
* Gibt Mailer-Statistiken zurück
*
* @return array<string, mixed>
*/
public function getStats(): array
{

View File

@@ -16,6 +16,7 @@ final readonly class Route
public string $path,
public Method $method = Method::GET,
public ?string $name = null,
/** @var array<int, string>|string */
public array|string $subdomain = [],
) {
}

View File

@@ -14,8 +14,8 @@ use Attribute;
final readonly class IpAuth
{
/**
* @param string[] $allowedIps Array von erlaubten IP-Adressen/Patterns/CIDR-Ranges
* @param string[] $deniedIps Array von verbotenen IP-Adressen/Patterns/CIDR-Ranges
* @param array<int, string> $allowedIps Array von erlaubten IP-Adressen/Patterns/CIDR-Ranges
* @param array<int, string> $deniedIps Array von verbotenen IP-Adressen/Patterns/CIDR-Ranges
* @param bool $localOnly Nur lokale IP-Adressen erlauben
* @param bool $adminOnly Nur localhost (127.0.0.1, ::1) erlauben
* @param bool $denyPublic Öffentliche IP-Adressen explizit verbieten
@@ -76,11 +76,17 @@ final readonly class IpAuth
return new self(adminOnly: true);
}
/**
* @param array<int, string> $ips
*/
public static function allowIps(array $ips): self
{
return new self(allowedIps: $ips);
}
/**
* @param array<int, string> $ips
*/
public static function denyIps(array $ips): self
{
return new self(deniedIps: $ips);

View File

@@ -9,6 +9,10 @@ use Attribute;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Auth
{
/**
* @param array<int, string> $allowedIps
* @param array<int, string> $roles
*/
public function __construct(
public readonly string $strategy = 'session',
public readonly array $allowedIps = [],

View File

@@ -382,6 +382,7 @@ final readonly class AuthenticationService
/**
* Record security event for monitoring and auditing
* @param array<string, mixed> $data
*/
private function recordSecurityEvent(string $event, array $data): void
{

View File

@@ -20,6 +20,9 @@ final readonly class HashedPassword
{
private DateTimeImmutable $hashedAt;
/**
* @param array<string, mixed> $parameters
*/
public function __construct(
#[SensitiveParameter]
private DerivedKey $derivedKey,
@@ -81,6 +84,7 @@ final readonly class HashedPassword
/**
* Get the parameters used for hashing
* @return array<string, mixed>
*/
public function getParameters(): array
{
@@ -107,7 +111,12 @@ final readonly class HashedPassword
'hashed_at' => $this->hashedAt->format('c'),
];
return base64_encode(json_encode($data));
$json = json_encode($data);
if ($json === false) {
throw new \RuntimeException('Failed to encode password data');
}
return base64_encode($json);
}
/**
@@ -147,6 +156,7 @@ final readonly class HashedPassword
/**
* Check if rehashing is needed based on current standards
* @param array<string, mixed> $currentParameters
*/
public function needsRehash(string $currentAlgorithm, array $currentParameters): bool
{
@@ -164,6 +174,9 @@ final readonly class HashedPassword
};
}
/**
* @param array<string, mixed> $currentParams
*/
private function needsArgon2Rehash(array $currentParams): bool
{
$memoryCost = $this->parameters['memory_cost'] ?? 0;
@@ -175,6 +188,9 @@ final readonly class HashedPassword
$threads < ($currentParams['threads'] ?? 3);
}
/**
* @param array<string, mixed> $currentParams
*/
private function needsPbkdf2Rehash(array $currentParams): bool
{
$iterations = $this->parameters['iterations'] ?? 0;
@@ -182,6 +198,9 @@ final readonly class HashedPassword
return $iterations < ($currentParams['iterations'] ?? 100000);
}
/**
* @param array<string, mixed> $currentParams
*/
private function needsScryptRehash(array $currentParams): bool
{
$costParameter = $this->parameters['cost_parameter'] ?? 0;
@@ -193,6 +212,7 @@ final readonly class HashedPassword
/**
* Get safe representation for logging (no sensitive data)
* @return array<string, mixed>
*/
public function getSafeSummary(): array
{

View File

@@ -239,6 +239,7 @@ final readonly class PasswordHasher
/**
* Get parameters for security level
* @return array<string, mixed>
*/
private function getParametersForLevel(string $algorithm, string $level): array
{
@@ -272,6 +273,7 @@ final readonly class PasswordHasher
/**
* Get default parameters for algorithm
* @return array<string, mixed>
*/
private function getDefaultParameters(string $algorithm): array
{

View File

@@ -12,6 +12,10 @@ namespace App\Framework\Auth;
*/
final readonly class PasswordValidationResult
{
/**
* @param array<int, string> $errors
* @param array<int, string> $warnings
*/
public function __construct(
public bool $isValid,
public array $errors,
@@ -39,6 +43,7 @@ final readonly class PasswordValidationResult
/**
* Get all issues (errors and warnings combined)
* @return array<int, string>
*/
public function getAllIssues(): array
{
@@ -89,6 +94,7 @@ final readonly class PasswordValidationResult
/**
* Convert to array for API responses
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -120,6 +126,7 @@ final readonly class PasswordValidationResult
/**
* Create an invalid result with errors
* @param array<int, string> $errors
*/
public static function invalid(array $errors, int $strengthScore = 0): self
{

View File

@@ -58,6 +58,7 @@ final readonly class IpAuthDecision
/**
* Convert to array
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -27,6 +27,7 @@ final readonly class IpAuthPolicy
/**
* Create policy from configuration array
* @param array<string, mixed> $config
*/
public static function fromConfig(array $config): self
{
@@ -107,6 +108,7 @@ final readonly class IpAuthPolicy
/**
* Create policy allowing specific IPs
* @param string[] $ips
*/
public static function allowIps(array $ips): self
{
@@ -120,6 +122,7 @@ final readonly class IpAuthPolicy
/**
* Create policy denying specific IPs
* @param string[] $ips
*/
public static function denyIps(array $ips): self
{
@@ -298,6 +301,7 @@ final readonly class IpAuthPolicy
/**
* Convert to array representation
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -39,9 +39,18 @@ final readonly class RedisCache implements CacheDriver, Scannable
$prefixedKeys = array_map(fn ($key) => $this->prefixKey($key), $keys);
$values = $this->redis->mget($prefixedKeys);
// Handle case where Redis MGET fails and returns false
if ($values === false) {
// Fallback to individual GET operations
$values = [];
foreach ($prefixedKeys as $prefixedKey) {
$values[] = $this->redis->get($prefixedKey);
}
}
$items = [];
foreach ($keys as $index => $key) {
$value = $values[$index];
$value = $values[$index] ?? false;
if ($value !== false) {
$items[] = CacheItem::hit($key, $value);
} else {

View File

@@ -55,11 +55,11 @@ final readonly class ExceptionTypeFailurePredicate implements FailurePredicate
{
$included = empty($this->includedExceptions)
? ($this->includeAllByDefault ? 'all' : 'none')
: implode(', ', array_map(fn ($class) => class_basename($class), $this->includedExceptions));
: implode(', ', array_map(fn ($class) => basename(str_replace('\\', '/', $class)), $this->includedExceptions));
$excluded = empty($this->excludedExceptions)
? 'none'
: implode(', ', array_map(fn ($class) => class_basename($class), $this->excludedExceptions));
: implode(', ', array_map(fn ($class) => basename(str_replace('\\', '/', $class)), $this->excludedExceptions));
return "Include: {$included}, Exclude: {$excluded}";
}

View File

@@ -19,12 +19,15 @@ final readonly class ConfigValidator
/**
* Validate selected environment variables and return a list of issues.
* Each issue contains: key, issue, severity, recommendation.
* @return array<int, array<string, string>>
*/
public function validate(): array
{
/** @var array<int, array<string, string>> */
$issues = [];
// APP_ENV validation
/** @var array<int, string> */
$allowedEnvs = ['development', 'testing', 'production'];
$appEnv = $this->env->getString(EnvKey::APP_ENV, 'production');
if (! in_array($appEnv, $allowedEnvs, true)) {
@@ -76,14 +79,16 @@ final readonly class ConfigValidator
}
// Rate limit values if present: must be non-negative
foreach ([
/** @var array<int, string> */
$rateKeys = [
'RATE_LIMIT_DEFAULT',
'RATE_LIMIT_WINDOW',
'RATE_LIMIT_AUTH',
'RATE_LIMIT_AUTH_WINDOW',
'RATE_LIMIT_API',
'RATE_LIMIT_API_WINDOW',
] as $rateKey) {
];
foreach ($rateKeys as $rateKey) {
if ($this->env->has($rateKey)) {
$value = (int) $this->env->get($rateKey);
if ($value < 0) {
@@ -102,6 +107,7 @@ final readonly class ConfigValidator
/**
* Validate and log issues. Returns the list of issues for optional handling.
* @return array<int, array<string, string>>
*/
public function validateAndReport(): array
{

View File

@@ -10,6 +10,9 @@ use BackedEnum;
final readonly class Environment
{
/**
* @param array<string, mixed> $variables
*/
public function __construct(
private array $variables = []
) {
@@ -91,6 +94,9 @@ final readonly class Environment
return $this->get($key) !== null;
}
/**
* @return array<string, mixed>
*/
public function all(): array
{
return array_merge(
@@ -162,6 +168,9 @@ final readonly class Environment
return new self($variables);
}
/**
* @param array<string, mixed> $variables
*/
public function withVariables(array $variables): self
{
return new self(array_merge($this->variables, $variables));

View File

@@ -9,6 +9,7 @@ enum EnvironmentType: string
case DEV = 'development';
case STAGING = 'staging';
case PROD = 'production';
case TESTING = 'testing';
public function isProduction(): bool
{
@@ -25,6 +26,11 @@ enum EnvironmentType: string
return $this === self::STAGING;
}
public function isTesting(): bool
{
return $this === self::TESTING;
}
public function isDebugEnabled(): bool
{
return $this === self::DEV;
@@ -42,6 +48,7 @@ enum EnvironmentType: string
return match(strtolower($envValue)) {
'production', 'prod' => self::PROD,
'staging', 'stage' => self::STAGING,
'testing', 'test' => self::TESTING,
'development', 'dev', 'local' => self::DEV,
default => self::DEV
};

View File

@@ -13,6 +13,7 @@ use Traversable;
/**
* Value Object für die Verwaltung von Console Commands
* @implements IteratorAggregate<string, ConsoleCommand>
*/
final readonly class CommandList implements IteratorAggregate, Countable
{
@@ -74,11 +75,17 @@ final readonly class CommandList implements IteratorAggregate, Countable
return $this->commands[$name];
}
/**
* @return array<int, string>
*/
public function getNames(): array
{
return array_keys($this->commands);
}
/**
* @return array<int, string>
*/
public function findSimilar(string $name, int $maxDistance = 3): array
{
$suggestions = [];
@@ -103,6 +110,9 @@ final readonly class CommandList implements IteratorAggregate, Countable
return new ArrayIterator($this->commands);
}
/**
* @return array<string, ConsoleCommand>
*/
public function toArray(): array
{
return $this->commands;

View File

@@ -47,6 +47,9 @@ final readonly class CommandRegistry
return $this->discoveredAttributes[$commandName];
}
/**
* @param array<int, string> $arguments
*/
public function executeCommand(string $commandName, array $arguments, ConsoleOutputInterface $output): ExitCode
{
$command = $this->commandList->get($commandName);

View File

@@ -11,8 +11,6 @@ use App\Framework\DI\Container;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use ReflectionClass;
use ReflectionMethod;
use Throwable;
final class ConsoleApplication
@@ -110,33 +108,6 @@ final class ConsoleApplication
}
}
/**
* Registriert alle Kommandos aus einer Klasse
*/
public function registerCommands(object $commandClass): void
{
$reflection = new ReflectionClass($commandClass);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attributes = $method->getAttributes(ConsoleCommand::class);
foreach ($attributes as $attribute) {
/** @var ConsoleCommand $command */
$command = $attribute->newInstance();
$this->commands[$command->name] = [
'instance' => $commandClass,
'method' => $method->getName(),
'description' => $command->description,
'reflection' => $method,
];
}
}
}
/**
* Führt ein Kommando aus
*/
/**
* Führt ein Kommando aus
* @param array<int, string> $argv

View File

@@ -100,8 +100,6 @@ final readonly class ExecutionContext
$scriptName = $_SERVER['argv'][0] ?? '';
$commandLine = implode(' ', $_SERVER['argv'] ?? []);
var_dump($scriptName);
$type = match(true) {
str_contains($scriptName, 'worker') => ContextType::WORKER,
// Temporarily treat console.php as CLI_SCRIPT until CONSOLE context discovery is fixed

View File

@@ -101,6 +101,7 @@ enum Base32Alphabet: string
/**
* Get recommended use cases for this alphabet
* @return array<int, string>
*/
public function getUseCases(): array
{

View File

@@ -204,6 +204,7 @@ final readonly class Base32Encoder
/**
* Get available alphabets
* @return array<int, Base32Alphabet>
*/
public static function getAvailableAlphabets(): array
{

View File

@@ -170,6 +170,18 @@ final class EventDispatcher implements EventDispatcherInterface
return $this;
}
/**
* Alias für addHandler - für bessere API Kompatibilität
*/
public function listen(
string $eventClass,
callable $handler,
?int $priority = null,
bool $stopPropagation = false
): self {
return $this->addHandler($eventClass, $handler, $priority, $stopPropagation);
}
/**
* Prüft, ob Handler für den angegebenen Event-Typ registriert sind
*

View File

@@ -17,14 +17,17 @@ use App\Framework\Reflection\WrappedReflectionClass;
*/
final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwareVisitor
{
/** @var array<string, mixed> */
private array $interfaces = [];
/** @var array<string, array<int, string>> */
private array $implementations = [];
/** @var array<string, array<int, string>> */
private array $implementationsByClass = [];
/**
* @param array $targetInterfaces Die zu suchenden Interfaces
* @param array<int, string> $targetInterfaces Die zu suchenden Interfaces
*/
public function __construct(
private readonly array $targetInterfaces = []
@@ -147,6 +150,9 @@ final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwa
return CacheKey::fromString('interface_implementations');
}
/**
* @return array<string, mixed>
*/
public function getCacheableData(): array
{
return [
@@ -157,6 +163,7 @@ final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwa
/**
* Gibt alle Implementierungen eines Interfaces zurück
* @return array<int, string>
*/
public function getImplementations(string $interface): array
{
@@ -174,6 +181,7 @@ final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwa
/**
* Gibt alle Interfaces zurück, die eine Klasse implementiert
* @return array<int, string>
*/
public function getClassInterfaces(string $className): array
{
@@ -182,6 +190,7 @@ final class InterfaceImplementationVisitor implements FileVisitor, ReflectionAwa
/**
* Gibt alle gefundenen Implementierungen zurück
* @return array<string, array<int, string>>
*/
public function getAllImplementations(): array
{

View File

@@ -16,13 +16,20 @@ use App\Framework\Performance\PerformanceCategory;
*/
final class ContainerPerformanceMonitor
{
/**
* @var array<string, float>
*/
private array $thresholds;
/**
* @var array<string, float>
*/
private array $operationStartTimes = [];
public function __construct(
private readonly PerformanceCollectorInterface $collector,
private readonly ?Logger $logger = null,
/** @param array<string, float> $customThresholds */
array $customThresholds = []
) {
$this->thresholds = array_merge([
@@ -102,6 +109,7 @@ final class ContainerPerformanceMonitor
/**
* Get performance recommendations based on collected metrics
* @return array<int, string>
*/
public function getOptimizationRecommendations(): array
{
@@ -174,6 +182,7 @@ final class ContainerPerformanceMonitor
/**
* Create a performance report for debugging
* @return array<string, mixed>
*/
public function generatePerformanceReport(): array
{

View File

@@ -18,6 +18,9 @@ final readonly class ContainerPerformanceResult
) {
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [

View File

@@ -188,6 +188,14 @@ final readonly class ClassName
return $this->shortName;
}
/**
* Explicit toString method
*/
public function toString(): string
{
return $this->fullyQualified;
}
/**
* Get string representation for debugging
*/

View File

@@ -106,6 +106,11 @@ final readonly class Percentage
return $this->value === 0.0;
}
public function isZero(): bool
{
return $this->value === 0.0;
}
public function isFull(): bool
{
return $this->value === 100.0;

View File

@@ -99,6 +99,14 @@ final readonly class Score
return $this->value;
}
/**
* Get score as decimal value (alias for value())
*/
public function toDecimal(): float
{
return $this->value;
}
/**
* Convert to percentage using framework Percentage object
*/

View File

@@ -106,6 +106,22 @@ final readonly class Timestamp
return $this->toDateTime(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s.uP');
}
/**
* Convert to ISO 8601 string format (alias)
*/
public function toIso8601String(): string
{
return $this->toIso8601();
}
/**
* Convert to SQL timestamp string format
*/
public function toSqlString(): string
{
return $this->format('Y-m-d H:i:s');
}
/**
* Format timestamp
*/

View File

@@ -307,16 +307,25 @@ final readonly class WarmupResult
) {
}
/**
* @return array<int, WarmupOperation>
*/
public function getSuccessfulOperations(): array
{
return array_filter($this->operations, fn ($op) => $op->success);
}
/**
* @return array<int, WarmupOperation>
*/
public function getFailedOperations(): array
{
return array_filter($this->operations, fn ($op) => ! $op->success);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
@@ -344,6 +353,9 @@ final readonly class WarmupOperation
) {
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [

View File

@@ -286,6 +286,7 @@ final readonly class AdvancedHash
/**
* Get available hash algorithms
* @return array<int, string>
*/
public function getAvailableAlgorithms(): array
{

View File

@@ -62,6 +62,7 @@ final readonly class ConstantTimeExecutor
/**
* Execute operation and return both result and actual duration
* @return array<string, mixed>
*/
public function executeWithTiming(callable $operation): array
{

View File

@@ -30,6 +30,8 @@ final readonly class CryptographicUtilities
/**
* Timing-safe comparison of arrays
* @param array<mixed> $known
* @param array<mixed> $user
*/
public function timingSafeArrayEquals(array $known, array $user): bool
{

View File

@@ -172,6 +172,7 @@ final readonly class DerivedKey
/**
* Export to array (for serialization)
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -204,6 +205,7 @@ final readonly class DerivedKey
/**
* Create from array (for deserialization)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -275,6 +277,7 @@ final readonly class DerivedKey
/**
* Get summary information (safe for logging)
* @return array<string, mixed>
*/
public function getSummary(): array
{

View File

@@ -171,6 +171,7 @@ final readonly class DigitalSignature
/**
* Sign JSON data (useful for API signatures)
* @param array<string, mixed> $data
*/
public function signJson(
array $data,
@@ -190,6 +191,7 @@ final readonly class DigitalSignature
/**
* Verify JSON signature
* @param array<string, mixed> $data
*/
public function verifyJson(
array $data,

View File

@@ -130,6 +130,7 @@ final readonly class DigitalSignatureResult
/**
* Export to array (for serialization)
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -149,6 +150,7 @@ final readonly class DigitalSignatureResult
/**
* Create from array (for deserialization)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -242,6 +244,7 @@ final readonly class DigitalSignatureResult
/**
* Get summary information (safe for logging)
* @return array<string, mixed>
*/
public function getSummary(): array
{

View File

@@ -186,6 +186,7 @@ final readonly class HashResult
/**
* Export to array (for serialization)
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -213,6 +214,7 @@ final readonly class HashResult
/**
* Create from array (for deserialization)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -292,6 +294,7 @@ final readonly class HashResult
/**
* Get summary information (safe for logging)
* @return array<string, mixed>
*/
public function getSummary(): array
{

View File

@@ -294,6 +294,7 @@ final readonly class KeyDerivationFunction
/**
* Get recommended parameters for different security levels
* @return array<string, mixed>
*/
public function getRecommendedParameters(string $algorithm, string $securityLevel = 'standard'): array
{

View File

@@ -75,6 +75,7 @@ final readonly class KeyPair
/**
* Export both keys to array
* @return array<string, array<string, mixed>>
*/
public function toArray(): array
{
@@ -86,6 +87,7 @@ final readonly class KeyPair
/**
* Get key pair summary (safe for logging)
* @return array<string, mixed>
*/
public function getSummary(): array
{

View File

@@ -95,6 +95,7 @@ final readonly class PrivateKey
/**
* Export key information to array (excludes sensitive key material)
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -91,6 +91,7 @@ final readonly class PublicKey
/**
* Export key to array (includes key material - safe for public keys)
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -109,6 +110,7 @@ final readonly class PublicKey
/**
* Create from array
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -208,6 +210,7 @@ final readonly class PublicKey
/**
* Get key as JWK (JSON Web Key) format
* @return array<string, mixed>
*/
public function toJwk(): array
{

View File

@@ -24,6 +24,7 @@ final readonly class SecureToken
private int $length,
private ?string $prefix,
private string $rawBytes,
/** @var array<string, mixed> */
private array $metadata
) {
if (empty($value)) {
@@ -95,6 +96,7 @@ final readonly class SecureToken
/**
* Get metadata
* @return array<string, mixed>
*/
public function getMetadata(): array
{
@@ -285,6 +287,7 @@ final readonly class SecureToken
/**
* Export to array (for serialization)
* @return array<string, mixed>
*/
public function toArray(): array
{
@@ -302,6 +305,7 @@ final readonly class SecureToken
/**
* Create from array (for deserialization)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
@@ -336,6 +340,7 @@ final readonly class SecureToken
/**
* Get safe summary (excludes sensitive token value)
* @return array<string, mixed>
*/
public function getSafeSummary(): array
{

View File

@@ -39,6 +39,7 @@ final readonly class SecureTokenGenerator
/**
* Generate a secure token
* @param array<string, mixed>|null $metadata
*/
public function generate(
string $type,
@@ -257,6 +258,7 @@ final readonly class SecureTokenGenerator
/**
* Generate batch of tokens
* @return array<int, SecureToken>
*/
public function generateBatch(
string $type,

View File

@@ -61,6 +61,7 @@ final class CuidGenerator
/**
* Generate a batch of Cuids with incrementing counters
* @return array<int, Cuid>
*/
public function generateBatch(int $count): array
{
@@ -87,6 +88,7 @@ final class CuidGenerator
/**
* Generate a sequence of Cuids with incrementing timestamps
* @return array<int, Cuid>
*/
public function generateSequence(int $count, int $intervalMs = 1): array
{

View File

@@ -9,6 +9,8 @@ use App\Framework\Database\Config\PoolConfig;
use App\Framework\Database\Driver\DriverConfig;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\DateTime\Timer;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ExceptionContext;
final class ConnectionPool
{
@@ -67,7 +69,16 @@ final class ConnectionPool
return $this->createNewConnection();
}
throw new DatabaseException('Maximum number of connections reached and no healthy connections available');
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
'Maximum number of connections reached and no healthy connections available',
ExceptionContext::forOperation('get_connection', 'ConnectionPool')
->withData([
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
'connections_in_use' => count($this->inUse),
])
);
}
private function findAvailableHealthyConnection(): ?PooledConnection
@@ -122,7 +133,18 @@ final class ConnectionPool
} catch (\Exception $e) {
$retries++;
if ($retries >= $maxRetries) {
throw new DatabaseException("Failed to create connection after {$maxRetries} retries: " . $e->getMessage());
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
"Failed to create connection after {$maxRetries} retries: " . $e->getMessage(),
ExceptionContext::forOperation('create_connection', 'ConnectionPool')
->withData([
'max_retries' => $maxRetries,
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
])
->withDebug(['original_error' => $e->getMessage()]),
$e
);
}
// Exponential backoff
@@ -131,7 +153,15 @@ final class ConnectionPool
}
}
throw new DatabaseException('Unable to create new connection');
throw DatabaseException::create(
ErrorCode::DB_CONNECTION_FAILED,
'Unable to create new connection',
ExceptionContext::forOperation('create_connection', 'ConnectionPool')
->withData([
'current_connections' => $this->currentConnections,
'max_connections' => $this->poolConfig->maxConnections,
])
);
}
public function releaseConnection(string $id): void

View File

@@ -197,6 +197,32 @@ final class DatabaseManager
}
}
public function hasConnection(): bool
{
return $this->connectionPool !== null || $this->readWriteConnection !== null;
}
/**
* @return array<string>
*/
public function getConnectionNames(): array
{
$names = ['default'];
if ($this->config->readWriteConfig->enabled) {
$names[] = 'read';
$names[] = 'write';
}
if ($this->config->poolConfig->enabled) {
for ($i = 0; $i < $this->config->poolConfig->maxConnections; $i++) {
$names[] = "pool_connection_{$i}";
}
}
return $names;
}
/**
* Get profiling statistics for this database manager
*/

View File

@@ -23,14 +23,24 @@ final readonly class User
#[Column(name: 'email')]
public ?string $email;
#[Column(name: 'failed_attempts')]
public int $failed_attempts;
#[Column(name: 'password_hash')]
public ?string $password_hash;
public function __construct(
string $name,
?string $email = null,
?string $id = null
?string $id = null,
int $failed_attempts = 0,
?string $password_hash = null
) {
$this->id = $id ?? IdGenerator::generate();
$this->name = $name;
$this->email = $email;
$this->failed_attempts = $failed_attempts;
$this->password_hash = $password_hash;
}
/**
@@ -38,7 +48,7 @@ final readonly class User
*/
public function withName(string $name): self
{
return new self($name, $this->email, $this->id);
return new self($name, $this->email, $this->id, $this->failed_attempts, $this->password_hash);
}
/**
@@ -46,6 +56,6 @@ final readonly class User
*/
public function withEmail(?string $email): self
{
return new self($this->name, $email, $this->id);
return new self($this->name, $email, $this->id, $this->failed_attempts, $this->password_hash);
}
}

View File

@@ -152,6 +152,8 @@ final class MigrationDependencyGraph
/**
* Utility function for circular dependency detection using DFS
* @param array<string, bool> $visited
* @param array<string, bool> $recursionStack
*/
private function detectCircularDependenciesUtil(string $version, array &$visited, array &$recursionStack): bool
{
@@ -178,6 +180,8 @@ final class MigrationDependencyGraph
/**
* Perform a topological sort using depth-first search
* @param array<string, bool> $visited
* @param array<int, string> $order
*/
private function topologicalSort(string $version, array &$visited, array &$order): void
{

View File

@@ -48,6 +48,7 @@ final readonly class MigrationStatus
/**
* Convert to array for backward compatibility
* @return array<string, string|bool>
*/
public function toArray(): array
{

View File

@@ -85,6 +85,7 @@ final readonly class MigrationStatusCollection implements Countable, IteratorAgg
/**
* Convert to array for backward compatibility
* @return array<int, array<string, string|bool>>
*/
public function toArray(): array
{

View File

@@ -32,6 +32,11 @@ final readonly class MigrationVersion
return $this->timestamp;
}
public function toString(): string
{
return $this->timestamp;
}
public function compare(MigrationVersion $other): int
{
return $this->timestamp <=> $other->timestamp;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Driver\Optimization\MySQLOptimizer;
use App\Framework\Database\Driver\Optimization\PostgreSQLOptimizer;
@@ -175,6 +176,7 @@ final readonly class DatabaseDashboardController implements Controller
/**
* Get connection statistics
* @return array<string, mixed>
*/
private function getConnectionStats(string $connection): array
{

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Monitoring\Health\DatabaseHealthChecker;
use App\Framework\Http\Controller;

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Database\Monitoring\Dashboard;
use App\Framework\Attributes\Route;
use App\Framework\Database\Monitoring\History\QueryHistoryLogger;
use App\Framework\Http\Controller;
use App\Framework\Http\Response\JsonResponse;

View File

@@ -154,6 +154,7 @@ final class HealthCheckResult
/**
* Get the result as an array
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -72,6 +72,7 @@ final readonly class HealthCheckStatus
/**
* Get the status as an array
* @return array<string, string>
*/
public function toArray(): array
{

View File

@@ -27,7 +27,7 @@ final class PdoConnection implements ConnectionInterface
return $statement->rowCount();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to execute query: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to execute query: {$e->getMessage()}", $e);
}
}
@@ -39,7 +39,7 @@ final class PdoConnection implements ConnectionInterface
return new PdoResult($statement);
} catch (\PDOException $e) {
throw new DatabaseException("Failed to execute query: {$e->getMessage()} --- SQL: {$sql} PARAMETERS: {".implode(", ", $parameters)."}", 0, $e);
throw DatabaseException::simple("Failed to execute query: {$e->getMessage()} --- SQL: {$sql} PARAMETERS: {".implode(", ", $parameters)."}", $e);
}
}
@@ -69,7 +69,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->beginTransaction();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to begin transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to begin transaction: {$e->getMessage()}", $e);
}
}
@@ -78,7 +78,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->commit();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to commit transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to commit transaction: {$e->getMessage()}", $e);
}
}
@@ -87,7 +87,7 @@ final class PdoConnection implements ConnectionInterface
try {
$this->pdo->rollBack();
} catch (\PDOException $e) {
throw new DatabaseException("Failed to rollback transaction: {$e->getMessage()}", 0, $e);
throw DatabaseException::simple("Failed to rollback transaction: {$e->getMessage()}", $e);
}
}

View File

@@ -39,6 +39,7 @@ abstract class EntityRepository
/**
* Findet alle Entities
* @return array<int, object>
*/
public function findAll(): array
{
@@ -47,6 +48,7 @@ abstract class EntityRepository
/**
* Findet alle Entities (eager loading)
* @return array<int, object>
*/
public function findAllEager(): array
{
@@ -55,6 +57,9 @@ abstract class EntityRepository
/**
* Findet Entities nach Kriterien
* @param array<string, mixed> $criteria
* @param array<string, string>|null $orderBy
* @return array<int, object>
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null): array
{
@@ -63,6 +68,7 @@ abstract class EntityRepository
/**
* Findet eine Entity nach Kriterien
* @param array<string, mixed> $criteria
*/
public function findOneBy(array $criteria): ?object
{
@@ -79,6 +85,8 @@ abstract class EntityRepository
/**
* Speichert mehrere Entities
* @param array<int, object> $entities
* @return array<int, object>
*/
public function saveAll(array $entities): array
{

View File

@@ -120,6 +120,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL index usage statistics
* @return array<array<string, mixed>>
*/
private function getMySqlIndexUsageStatistics(?string $table = null): array
{
@@ -183,6 +184,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL unused indexes
* @return array<array<string, mixed>>
*/
private function getMySqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
{
@@ -226,6 +228,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL duplicate indexes
* @return array<array<string, mixed>>
*/
private function getMySqlDuplicateIndexes(?string $table = null): array
{
@@ -280,6 +283,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL oversized indexes
* @return array<array<string, mixed>>
*/
private function getMySqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
{
@@ -318,6 +322,7 @@ final class IndexUsageAnalyzer
/**
* Get MySQL fragmented indexes
* @return array<array<string, mixed>>
*/
private function getMySqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
{
@@ -354,6 +359,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL index usage statistics
* @return array<array<string, mixed>>
*/
private function getPostgreSqlIndexUsageStatistics(?string $table = null): array
{
@@ -389,6 +395,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL unused indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlUnusedIndexes(?string $table = null, int $minDaysSinceCreation = 30): array
{
@@ -427,6 +434,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL duplicate indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlDuplicateIndexes(?string $table = null): array
{
@@ -491,6 +499,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL oversized indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlOversizedIndexes(?string $table = null, int $sizeThresholdMb = 100): array
{
@@ -530,6 +539,7 @@ final class IndexUsageAnalyzer
/**
* Get PostgreSQL fragmented indexes
* @return array<array<string, mixed>>
*/
private function getPostgreSqlFragmentedIndexes(?string $table = null, float $fragmentationThreshold = 0.3): array
{

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Database\StoredProcedure\Exception;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Exception\ExceptionContext;
/**
* Base exception for stored function errors
@@ -19,9 +20,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.error', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error in stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -34,9 +39,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.execution', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error executing stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -49,9 +58,13 @@ class StoredFunctionException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_function.creation', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Error creating stored function '{$functionName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -61,7 +74,14 @@ class StoredFunctionException extends DatabaseException
*/
public static function notFound(string $functionName): self
{
return new self("Stored function '{$functionName}' not found");
$context = ExceptionContext::forOperation('stored_function.not_found', 'StoredFunction')
->withData(['function_name' => $functionName]);
return new self(
"Stored function '{$functionName}' not found",
$context,
404
);
}
/**
@@ -69,9 +89,18 @@ class StoredFunctionException extends DatabaseException
*/
public static function invalidReturnType(string $functionName, string $expectedType, string $actualType): self
{
$context = ExceptionContext::forOperation('stored_function.invalid_return_type', 'StoredFunction')
->withData([
'function_name' => $functionName,
'expected_type' => $expectedType,
'actual_type' => $actualType,
]);
return new self(
"Invalid return type for stored function '{$functionName}': " .
"expected {$expectedType}, got {$actualType}"
"expected {$expectedType}, got {$actualType}",
$context,
400
);
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Database\StoredProcedure\Exception;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Exception\ExceptionContext;
/**
* Base exception for stored procedure errors
@@ -19,9 +20,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.error', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error in stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -34,9 +39,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.execution', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error executing stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -49,9 +58,13 @@ class StoredProcedureException extends DatabaseException
string $message,
?\Throwable $previous = null
): self {
$context = ExceptionContext::forOperation('stored_procedure.creation', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Error creating stored procedure '{$procedureName}': {$message}",
0,
$context,
500,
$previous
);
}
@@ -61,6 +74,13 @@ class StoredProcedureException extends DatabaseException
*/
public static function notFound(string $procedureName): self
{
return new self("Stored procedure '{$procedureName}' not found");
$context = ExceptionContext::forOperation('stored_procedure.not_found', 'StoredProcedure')
->withData(['procedure_name' => $procedureName]);
return new self(
"Stored procedure '{$procedureName}' not found",
$context,
404
);
}
}

View File

@@ -102,6 +102,7 @@ final readonly class ColorAnalysisResult
/**
* Gibt die häufigsten Farbformate zurück
* @return array<string, int>
*/
public function getMostUsedFormats(): array
{
@@ -118,6 +119,7 @@ final readonly class ColorAnalysisResult
/**
* Gibt Farbpaletten-Zusammenfassung zurück
* @return array<string, mixed>
*/
public function getPaletteSummary(): array
{
@@ -134,6 +136,7 @@ final readonly class ColorAnalysisResult
/**
* Gibt die problematischsten Farbkombinationen zurück
* @return array<int, array<string, mixed>>
*/
public function getWorstContrastPairs(): array
{
@@ -150,6 +153,7 @@ final readonly class ColorAnalysisResult
/**
* Gibt priorisierte Verbesserungs-Aktionen zurück
* @return array<int, array<string, string>>
*/
public function getPrioritizedActions(): array
{
@@ -192,6 +196,7 @@ final readonly class ColorAnalysisResult
/**
* Gibt Format-Empfehlungen zurück
* @return array<int, array<string, string>>
*/
public function getFormatRecommendations(): array
{
@@ -230,6 +235,7 @@ final readonly class ColorAnalysisResult
/**
* Konvertiert zu Array für Export
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -394,6 +394,7 @@ final readonly class ColorAnalyzer
/**
* Konvertiert RGB zu HSL
* @return array{h: float, s: float, l: float}
*/
private function rgbToHsl(RGBColor $rgb): array
{

View File

@@ -20,6 +20,7 @@ final readonly class ConventionCheckResult
/**
* Gruppiert Violations nach Schweregrad
* @return array<string, array<int, array<string, mixed>>>
*/
public function getViolationsBySeverity(): array
{
@@ -39,6 +40,7 @@ final readonly class ConventionCheckResult
/**
* Gibt die schlimmsten Problembereiche zurück
* @return array<string, int|float>
*/
public function getWorstAreas(): array
{
@@ -50,6 +52,7 @@ final readonly class ConventionCheckResult
/**
* Gibt die besten Bereiche zurück
* @return array<string, int|float>
*/
public function getBestAreas(): array
{
@@ -61,6 +64,7 @@ final readonly class ConventionCheckResult
/**
* Gibt priorisierte Action Items zurück
* @return array<int, array<string, string>>
*/
public function getPrioritizedActions(): array
{
@@ -95,6 +99,7 @@ final readonly class ConventionCheckResult
/**
* Berechnet Verbesserungs-Potenzial
* @return array<string, array<string, int|float>>
*/
public function getImprovementPotential(): array
{
@@ -120,6 +125,7 @@ final readonly class ConventionCheckResult
/**
* Konvertiert zu Array für Export
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -23,6 +23,7 @@ final readonly class TokenAnalysisResult
/**
* Gibt die häufigsten Token-Typen zurück
* @return array<string, int>
*/
public function getMostUsedTokenTypes(): array
{
@@ -39,6 +40,7 @@ final readonly class TokenAnalysisResult
/**
* Gibt Token-Coverage-Statistiken zurück
* @return array<string, int|float>
*/
public function getTokenCoverage(): array
{
@@ -55,6 +57,7 @@ final readonly class TokenAnalysisResult
/**
* Gibt Empfehlungs-Prioritäten zurück
* @return array<int, array<string, string>>
*/
public function getRecommendationPriorities(): array
{
@@ -75,6 +78,7 @@ final readonly class TokenAnalysisResult
/**
* Konvertiert zu Array für Export
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -16,6 +16,9 @@ use App\Framework\Filesystem\FilePath;
*/
final readonly class ComponentScanner
{
/**
* @param array<string|FilePath> $cssFiles
*/
public function scanComponents(array $cssFiles): ComponentRegistry
{
$components = [];
@@ -41,13 +44,17 @@ final readonly class ComponentScanner
return new ComponentRegistry($components);
}
/**
* @return array<Component>
*/
private function extractComponentsFromCss(string $cssContent, string $filePath): array
{
$components = [];
$processedComponents = [];
// Remove comments
$cssContent = preg_replace('/\/\*.*?\*\//s', '', $cssContent);
$cleanedContent = preg_replace('/\/\*.*?\*\//s', '', $cssContent);
$cssContent = $cleanedContent !== null ? $cleanedContent : $cssContent;
// Find all CSS selectors with improved regex that handles nested braces
preg_match_all('/([^{}]+)\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/s', $cssContent, $matches, PREG_SET_ORDER);
@@ -91,8 +98,16 @@ final readonly class ComponentScanner
private function analyzeSelector(string $selector, string $cssRules, string $filePath): ?Component
{
// Clean up selector - remove :where, :is wrappers
$selector = preg_replace('/:where\s*\((.*?)\)/', '$1', $selector);
$selector = preg_replace('/:is\s*\((.*?)\)/', '$1', $selector);
$cleanedSelector = preg_replace('/:where\s*\((.*?)\)/', '$1', $selector);
$selector = $cleanedSelector !== null ? $cleanedSelector : $selector;
$cleanedSelector = preg_replace('/:is\s*\((.*?)\)/', '$1', $selector);
$selector = $cleanedSelector !== null ? $cleanedSelector : $selector;
// Ensure selector is non-empty string
if (empty($selector)) {
return null;
}
// Skip pseudo-elements and certain pseudo-classes
if (preg_match('/::/', $selector) || preg_match('/:not\(/', $selector)) {

View File

@@ -14,6 +14,7 @@ final readonly class ClassNameParser
{
/**
* Extrahiert CSS-Klassennamen aus Content
* @return array<int, CssClass>
*/
public function extractFromContent(string $content): array
{
@@ -35,6 +36,8 @@ final readonly class ClassNameParser
/**
* Erkennt Component-Patterns in den CSS-Klassen
* @param array<int, CssClass> $classNames
* @return array<int, ComponentPattern>
*/
public function detectPatterns(array $classNames): array
{

View File

@@ -30,6 +30,9 @@ final readonly class CssParser
}
$content = file_get_contents($filePath->toString());
if ($content === false) {
throw new \RuntimeException("Failed to read CSS file: {$filePath->toString()}");
}
return $this->parseContent($content, $filePath);
}
@@ -69,6 +72,8 @@ final readonly class CssParser
/**
* Parst mehrere CSS-Dateien
* @param array<int, FilePath|string> $filePaths
* @return array<int, CssParseResult>
*/
public function parseFiles(array $filePaths): array
{
@@ -84,6 +89,7 @@ final readonly class CssParser
/**
* Parst alle CSS-Dateien in einem Verzeichnis
* @return array<int, CssParseResult>
*/
public function parseDirectory(string $directory, bool $recursive = true): array
{
@@ -99,6 +105,7 @@ final readonly class CssParser
/**
* Extrahiert CSS-Regeln aus dem Content
* @return array<int, CssRule>
*/
private function extractRules(string $content): array
{
@@ -130,6 +137,7 @@ final readonly class CssParser
/**
* Parst Selektoren (durch Komma getrennt)
* @return array<int, CssSelector>
*/
private function parseSelectors(string $selectorText): array
{
@@ -148,6 +156,7 @@ final readonly class CssParser
/**
* Parst CSS Properties
* @return array<int, CssProperty>
*/
private function parseProperties(string $propertiesText): array
{

View File

@@ -15,6 +15,10 @@ use App\Framework\Design\ValueObjects\TokenCategory;
*/
final readonly class TokenAnalyzer
{
/**
* @param array<int, CustomProperty> $customProperties
* @return array<int, DesignToken>
*/
public function categorizeTokens(array $customProperties): array
{
$tokens = [];
@@ -33,6 +37,10 @@ final readonly class TokenAnalyzer
return $tokens;
}
/**
* @param array<int, CustomProperty> $customProperties
* @return array<string, int>
*/
public function analyzeNamingPatterns(array $customProperties): array
{
$patterns = [

View File

@@ -74,6 +74,7 @@ final readonly class ComponentPattern
/**
* Gibt alle Klassennamen als Strings zurück
* @return array<int, string>
*/
public function getClassNames(): array
{
@@ -113,6 +114,7 @@ final readonly class ComponentPattern
/**
* Gibt Pattern-spezifische Informationen zurück
* @return array<string, mixed>
*/
public function getPatternInfo(): array
{
@@ -139,6 +141,7 @@ final readonly class ComponentPattern
/**
* Analysiert BEM-Struktur
* @return array<string, mixed>
*/
private function getBemStructure(): array
{
@@ -154,6 +157,10 @@ final readonly class ComponentPattern
];
}
/**
* @param array<int, mixed> $elements
* @param array<int, mixed> $modifiers
*/
private function getBemCompleteness(array $elements, array $modifiers): string
{
if (empty($elements) && empty($modifiers)) {
@@ -171,6 +178,9 @@ final readonly class ComponentPattern
return 'full_bem';
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [

View File

@@ -21,6 +21,7 @@ final readonly class CssRule
/**
* Gibt alle Selektor-Strings zurück
* @return array<int, string>
*/
public function getSelectorStrings(): array
{
@@ -29,6 +30,7 @@ final readonly class CssRule
/**
* Gibt alle Property-Namen zurück
* @return array<int, string>
*/
public function getPropertyNames(): array
{
@@ -51,6 +53,7 @@ final readonly class CssRule
/**
* Gibt alle Properties einer bestimmten Kategorie zurück
* @return array<int, CssProperty>
*/
public function getPropertiesByCategory(CssPropertyCategory $category): array
{

View File

@@ -78,6 +78,7 @@ final readonly class CssSelector
/**
* Extrahiert alle CSS-Klassennamen aus dem Selektor
* @return array<int, string>
*/
public function extractClasses(): array
{
@@ -88,6 +89,7 @@ final readonly class CssSelector
/**
* Extrahiert alle IDs aus dem Selektor
* @return array<int, string>
*/
public function extractIds(): array
{
@@ -98,6 +100,7 @@ final readonly class CssSelector
/**
* Extrahiert alle Element-Namen aus dem Selektor
* @return array<int, string>
*/
public function extractElements(): array
{

View File

@@ -62,6 +62,7 @@ final readonly class DesignToken
/**
* Konvertiert zu Array für Export/Serialisierung
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -24,6 +24,7 @@ final readonly class DiscoveryCacheIdentifiers
/**
* Create cache key for discovery results based on paths, scan type, and execution context
* @param string[] $paths
*/
public static function discoveryKey(array $paths, ScanType $scanType, ?string $context = null): CacheKey
{
@@ -36,6 +37,7 @@ final readonly class DiscoveryCacheIdentifiers
/**
* Create cache key for full discovery
* @param string[] $paths
*/
public static function fullDiscoveryKey(array $paths, ?string $context = null): CacheKey
{
@@ -44,6 +46,7 @@ final readonly class DiscoveryCacheIdentifiers
/**
* Create cache key for incremental discovery
* @param string[] $paths
*/
public static function incrementalDiscoveryKey(array $paths, ?string $context = null): CacheKey
{

View File

@@ -10,7 +10,7 @@ use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\DiscoveryCache;
use App\Framework\Discovery\Storage\DiscoveryCacheManager;
/**
* Unified console commands for clearing discovery-related caches
@@ -23,9 +23,10 @@ use App\Framework\Discovery\DiscoveryCache;
final readonly class ClearDiscoveryCache
{
public function __construct(
private DiscoveryCacheManager $discoveryCacheManager,
private ConsoleOutput $output,
private Cache $cache,
private Clock $clock,
private ConsoleOutput $output,
private PathProvider $pathProvider
) {
}
@@ -36,14 +37,12 @@ final readonly class ClearDiscoveryCache
$this->output->writeLine('Clearing Discovery cache...');
try {
$discoveryCache = new DiscoveryCache($this->cache, $this->clock);
if ($discoveryCache->flush()) {
if ($this->discoveryCacheManager->clearAll()) {
$this->output->writeLine('<success>Discovery cache cleared successfully!</success>');
return 0;
} else {
$this->output->writeLine('<error>Failed to clear Discovery cache - flush returned false</error>');
$this->output->writeLine('<error>Failed to clear Discovery cache - clearAll returned false</error>');
return 1;
}
@@ -61,9 +60,8 @@ final readonly class ClearDiscoveryCache
$success = true;
// Clear discovery cache
$discoveryCache = new DiscoveryCache($this->cache, $this->clock);
if ($discoveryCache->flush()) {
// Clear discovery cache using the cache manager
if ($this->discoveryCacheManager->clearAll()) {
$this->output->writeLine('✓ Discovery cache cleared');
} else {
$this->output->writeLine('✗ Failed to clear Discovery cache');

View File

@@ -48,6 +48,7 @@ final readonly class CacheEvictionEvent
/**
* Convert to array for serialization
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -47,6 +47,7 @@ final readonly class CacheMissEvent
/**
* Convert to array for serialization
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Discovery\Events;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Discovery\Results\DiscoveryResults;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\ScanType;
use Throwable;
@@ -16,7 +16,7 @@ final readonly class DiscoveryFailedEvent
{
public function __construct(
public Throwable $exception,
public ?DiscoveryResults $partialResults,
public ?DiscoveryRegistry $partialResults,
public ScanType $scanType,
public Timestamp $timestamp
) {
@@ -33,7 +33,7 @@ final readonly class DiscoveryFailedEvent
'error' => $this->exception->getMessage(),
'error_type' => get_class($this->exception),
'has_partial_results' => $this->hasPartialResults(),
'partial_files_count' => $this->partialResults ? count($this->partialResults->toArray()) : 0,
'partial_files_count' => $this->partialResults ? $this->partialResults->count() : 0,
'scan_type' => $this->scanType->value,
'timestamp' => $this->timestamp->toFloat(),
];

View File

@@ -192,4 +192,12 @@ final readonly class DiscoveryRegistry implements Countable
{
return $this->count();
}
/**
* Get the attributes registry
*/
public function attributes(): AttributeRegistry
{
return $this->attributes;
}
}

View File

@@ -27,6 +27,7 @@ final class TemplateRegistry implements Countable
/**
* Convert to array for cache serialization
* @return array<string, mixed>
*/
public function toArray(): array
{

View File

@@ -41,8 +41,10 @@ final class DiscoveryCacheManager
private const string CACHE_PREFIX = 'discovery:';
private const int DEFAULT_TTL_HOURS = 24;
/** @var array<string, array<string, mixed>> */
private array $cacheMetrics = [];
/** @var array<string, array<string, mixed>> */
private array $accessPatterns = [];
private CacheLevel $currentLevel = CacheLevel::NORMAL;
@@ -224,9 +226,12 @@ final class DiscoveryCacheManager
/**
* Get enhanced cache health status with memory awareness
*
* @return array<string, mixed>
*/
public function getHealthStatus(): array
{
/** @var array<string, mixed> */
$baseStatus = [
'cache_driver' => get_class($this->cache),
'ttl_hours' => $this->ttlHours,
@@ -275,6 +280,8 @@ final class DiscoveryCacheManager
/**
* Perform memory pressure management
*
* @return array<string, mixed>
*/
public function performMemoryPressureManagement(): array
{
@@ -283,6 +290,7 @@ final class DiscoveryCacheManager
}
$memoryStatus = $this->memoryManager->getMemoryStatus('pressure_management');
/** @var array<int, string> */
$actions = [];
if ($memoryStatus->status === MemoryStatus::CRITICAL) {
@@ -430,6 +438,7 @@ final class DiscoveryCacheManager
// Emit compression event
$this->emitCompressionEvent($originalSize, $compressedSize, $compressionLevel);
/** @var array<string, mixed> */
return [
'__discovery_compressed__' => true,
'data' => $compressed,
@@ -531,6 +540,7 @@ final class DiscoveryCacheManager
$timeWindow = 3600; // 1 hour
$currentTime = $this->clock->time();
/** @var array<int, int> */
$recentAccesses = array_filter(
$pattern['access_history'],
fn ($timestamp) => ($currentTime - $timestamp) <= $timeWindow

View File

@@ -23,11 +23,11 @@ use App\Framework\Reflection\ReflectionProvider;
* Provides helper methods for testing Discovery components
* with mock data, performance validation, and quality checks.
*/
final class DiscoveryTestHelper
final readonly class DiscoveryTestHelper
{
public function __construct(
private readonly Clock $clock,
private readonly ?Logger $logger = null
private Clock $clock,
private ?Logger $logger = null
) {
}
@@ -67,14 +67,14 @@ final class DiscoveryTestHelper
public function createTestConfiguration(array $overrides = []): DiscoveryConfiguration
{
return new DiscoveryConfiguration(
paths: $overrides['paths'] ?? ['/tmp/test/src'],
useCache: $overrides['use_cache'] ?? true,
cacheTimeout: Duration::fromHours($overrides['cache_timeout_hours'] ?? 1),
memoryLimitMB: $overrides['memory_limit_mb'] ?? 128,
maxFilesPerBatch: $overrides['max_files_per_batch'] ?? 50,
memoryPressureThreshold: $overrides['memory_pressure_threshold'] ?? 0.8,
enableMemoryMonitoring: $overrides['enable_memory_monitoring'] ?? true,
enablePerformanceTracking: $overrides['enable_performance_tracking'] ?? true
paths : $overrides['paths'] ?? ['/tmp/test/src'],
useCache : $overrides['use_cache'] ?? true,
cacheTimeout : Duration::fromHours($overrides['cache_timeout_hours'] ?? 1),
memoryLimitMB : $overrides['memory_limit_mb'] ?? 128,
enableMemoryMonitoring : $overrides['enable_memory_monitoring'] ?? true,
enablePerformanceTracking: $overrides['enable_performance_tracking'] ?? true,
maxFilesPerBatch : $overrides['max_files_per_batch'] ?? 50,
memoryPressureThreshold : $overrides['memory_pressure_threshold'] ?? 0.8
);
}
@@ -386,7 +386,7 @@ final class TestController
{
return \'Test Controller\';
}
#[Route(path: \'/test/{id}\', method: Method::GET)]
public function show(int $id): string
{

View File

@@ -145,6 +145,7 @@ final readonly class TemplateCollection implements Countable, IteratorAggregate
/**
* Get templates grouped by type
* @return array<string, self>
*/
public function groupByType(): array
{
@@ -165,6 +166,7 @@ final readonly class TemplateCollection implements Countable, IteratorAggregate
/**
* Get templates grouped by directory
* @return array<string, self>
*/
public function groupByDirectory(): array
{
@@ -186,6 +188,7 @@ final readonly class TemplateCollection implements Countable, IteratorAggregate
/**
* Get unique template names
* @return array<int, string>
*/
public function getUniqueNames(): array
{
@@ -196,6 +199,7 @@ final readonly class TemplateCollection implements Countable, IteratorAggregate
/**
* Get unique template types
* @return array<int, string>
*/
public function getUniqueTypes(): array
{

View File

@@ -52,7 +52,7 @@ final readonly class CssInliner
$styleTags = $dom->getElementsByTagName('style');
foreach ($styleTags as $styleTag) {
$css = $styleTag->textContent;
$css = $styleTag->textContent ?? '';
$rules = $this->parseCss($css);
// Merge rules, later rules override earlier ones
@@ -77,11 +77,15 @@ final readonly class CssInliner
$rules = [];
// Remove comments
$css = preg_replace('/\/\*.*?\*\//s', '', $css);
$cleaned = preg_replace('/\/\*.*?\*\//s', '', $css);
$css = $cleaned !== null ? $cleaned : $css;
// Remove @media queries and other @ rules (not supported in inline styles)
$css = preg_replace('/@[^{]+\{[^{}]*(\{[^{}]*\}[^{}]*)*\}/s', '', $css);
$css = preg_replace('/@[^;]+;/s', '', $css);
$cleaned = preg_replace('/@[^{]+\{[^{}]*(\{[^{}]*\}[^{}]*)*\}/s', '', $css);
$css = $cleaned !== null ? $cleaned : $css;
$cleaned = preg_replace('/@[^;]+;/s', '', $css);
$css = $cleaned !== null ? $cleaned : $css;
// Match CSS rules: selector { declarations }
preg_match_all('/([^{]+)\{([^}]+)\}/s', $css, $matches, PREG_SET_ORDER);
@@ -146,6 +150,8 @@ final readonly class CssInliner
/**
* Apply CSS rules to matching elements
*
* @param array<string, array<string, string>> $cssRules
*/
private function applyCssRules(HTMLDocument $dom, array $cssRules): void
{
@@ -164,6 +170,8 @@ final readonly class CssInliner
/**
* Query elements by CSS selector
*
* @return array<\Dom\HTMLElement>
*/
private function querySelectorAll(HTMLDocument $dom, string $selector): array
{
@@ -174,6 +182,8 @@ final readonly class CssInliner
/**
* Apply styles to an element
*
* @param array<string, string> $declarations
*/
private function applyStylesToElement(\Dom\HTMLElement $element, array $declarations): void
{
@@ -208,6 +218,8 @@ final readonly class CssInliner
/**
* Build style string from declarations
*
* @param array<string, string> $declarations
*/
private function buildStyleString(array $declarations): string
{
@@ -222,6 +234,9 @@ final readonly class CssInliner
/**
* Sort selectors by specificity (simplified)
* Higher specificity selectors should be applied last
*
* @param array<string> $selectors
* @return array<string>
*/
private function sortBySpecificity(array $selectors): array
{

View File

@@ -26,6 +26,7 @@ final readonly class EmailService
private DomTemplateParser $parser,
private CssInliner $cssInliner,
private Logger $logger,
/** @var array<string, string> */
private array $defaultFrom = []
) {
$this->renderer = new EmailTemplateRenderer($parser, $cssInliner);
@@ -151,6 +152,7 @@ final readonly class EmailService
* Send multiple emails using the same template
*
* @param EmailAddress[] $recipients
* @return array<string, array<string>>
*/
public function sendBulkTemplate(
string $template,
@@ -284,6 +286,10 @@ final readonly class EmailService
subject: new EmailSubject($subject)
);
if ($content->subject === null) {
throw new \RuntimeException('Email subject is required for notifications');
}
$message = $this->createMessage(
$to,
$content->subject,
@@ -383,22 +389,32 @@ final readonly class EmailService
private function generatePlainText(string $html): string
{
// Remove style and script tags
$html = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $html);
$html = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
$cleaned = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
$html = $cleaned !== null ? $cleaned : $html;
// Convert breaks and paragraphs
$html = preg_replace('/<br\s*\/?>/i', "\n", $html);
$html = preg_replace('/<\/p>/i', "\n\n", $html);
$cleaned = preg_replace('/<br\s*\/?>/i', "\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<\/p>/i', "\n\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
// Convert links
$html = preg_replace('/<a[^>]+href="([^"]*)"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $html);
$cleaned = preg_replace('/<a[^>]+href="([^"]*)"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $html);
$html = $cleaned !== null ? $cleaned : $html;
// Strip remaining tags
$text = strip_tags($html);
// Clean up whitespace
$text = preg_replace('/\n\s+\n/', "\n\n", $text);
$text = preg_replace('/\n{3,}/', "\n\n", $text);
$cleaned = preg_replace('/\n\s+\n/', "\n\n", $text);
$text = $cleaned !== null ? $cleaned : $text;
$cleaned = preg_replace('/\n{3,}/', "\n\n", $text);
$text = $cleaned !== null ? $cleaned : $text;
return trim($text);
}

View File

@@ -138,7 +138,12 @@ final readonly class EmailTemplateRenderer
throw new \RuntimeException("Email template not found: {$path}");
}
return file_get_contents($fullPath);
$content = file_get_contents($fullPath);
if ($content === false) {
throw new \RuntimeException("Failed to read email template: {$path}");
}
return $content;
}
/**
@@ -170,7 +175,10 @@ final readonly class EmailTemplateRenderer
$head = $dom->head;
if (! $head) {
$head = $dom->createElement('head');
$dom->documentElement->insertBefore($head, $dom->body);
$documentElement = $dom->documentElement;
if ($documentElement !== null) {
$documentElement->insertBefore($head, $dom->body);
}
}
// Create style element
@@ -202,6 +210,8 @@ final readonly class EmailTemplateRenderer
/**
* Add UTM tracking parameters to links
*
* @param array<string, mixed> $utmParams
*/
private function addTrackingParameters(string $html, array $utmParams): string
{
@@ -231,20 +241,32 @@ final readonly class EmailTemplateRenderer
private function generatePlainText(string $html): string
{
// Remove style and script tags
$html = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $html);
$html = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
$cleaned = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
$html = $cleaned !== null ? $cleaned : $html;
// Convert breaks and paragraphs to newlines
$html = preg_replace('/<br\s*\/?>/i', "\n", $html);
$html = preg_replace('/<\/p>/i', "\n\n", $html);
$html = preg_replace('/<\/h[1-6]>/i', "\n\n", $html);
$cleaned = preg_replace('/<br\s*\/?>/i', "\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<\/p>/i', "\n\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<\/h[1-6]>/i', "\n\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
// Convert links to text with URL
$html = preg_replace('/<a[^>]+href="([^"]*)"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $html);
$cleaned = preg_replace('/<a[^>]+href="([^"]*)"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $html);
$html = $cleaned !== null ? $cleaned : $html;
// Add line breaks for list items
$html = preg_replace('/<li[^>]*>/i', '• ', $html);
$html = preg_replace('/<\/li>/i', "\n", $html);
$cleaned = preg_replace('/<li[^>]*>/i', '• ', $html);
$html = $cleaned !== null ? $cleaned : $html;
$cleaned = preg_replace('/<\/li>/i', "\n", $html);
$html = $cleaned !== null ? $cleaned : $html;
// Strip remaining HTML tags
$text = strip_tags($html);
@@ -253,9 +275,14 @@ final readonly class EmailTemplateRenderer
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Clean up whitespace
$text = preg_replace('/[ \t]+/', ' ', $text); // Multiple spaces to single
$text = preg_replace('/\n\s+\n/', "\n\n", $text); // Remove whitespace between lines
$text = preg_replace('/\n{3,}/', "\n\n", $text); // Max 2 newlines
$cleaned = preg_replace('/[ \t]+/', ' ', $text); // Multiple spaces to single
$text = $cleaned !== null ? $cleaned : $text;
$cleaned = preg_replace('/\n\s+\n/', "\n\n", $text); // Remove whitespace between lines
$text = $cleaned !== null ? $cleaned : $text;
$cleaned = preg_replace('/\n{3,}/', "\n\n", $text); // Max 2 newlines
$text = $cleaned !== null ? $cleaned : $text;
return trim($text);
}

View File

@@ -215,6 +215,9 @@ final readonly class ErrorHandler
return RequestContext::fromGlobals();
}
/**
* @return array<string, mixed>
*/
private function createExceptionMetadata(Throwable $exception): array
{
$metadata = [
@@ -311,6 +314,7 @@ final readonly class ErrorHandler
}
} catch (Throwable $e) {
// Sicherer Fallback für Production
return false;
}
return false;

View File

@@ -28,6 +28,31 @@ final readonly class ExceptionContext
return new self(operation: $operation, component: $component);
}
public function getOperation(): ?string
{
return $this->operation;
}
public function getComponent(): ?string
{
return $this->component;
}
public function getData(): array
{
return $this->data;
}
public function getDebug(): array
{
return $this->debug;
}
public function getMetadata(): array
{
return $this->metadata;
}
public function withOperation(string $operation, ?string $component = null): self
{
return new self(

View File

@@ -246,6 +246,14 @@ class FrameworkException extends \RuntimeException
return $new;
}
/**
* Get exception data from context
*/
public function getData(): array
{
return $this->context->data;
}
/**
* String-Representation für Logging
*/

View File

@@ -19,13 +19,10 @@ final readonly class FilePath
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
];
private string $normalized;
private string $directory;
private string $filename;
private string $extension;
public readonly string $normalized;
public readonly string $directory;
public readonly string $filename;
public readonly string $extension;
public function __construct(string $path)
{

View File

@@ -119,12 +119,17 @@ final class FilesystemManager
/**
* JSON-Operationen
*
* @param array<string, mixed> $data
*/
public function putJson(string $path, array $data, string $storage = ''): void
{
$this->putSerialized($path, $data, $this->getSerializer('json'), $storage);
}
/**
* @return array<string, mixed>
*/
public function getJson(string $path, string $storage = ''): array
{
return $this->getSerialized($path, $this->getSerializer('json'), $storage);
@@ -132,12 +137,17 @@ final class FilesystemManager
/**
* CSV-Operationen (wenn CSV-Serializer registriert ist)
*
* @param array<int, array<string, mixed>> $data
*/
public function putCsv(string $path, array $data, string $storage = ''): void
{
$this->putSerialized($path, $data, $this->getSerializer('csv'), $storage);
}
/**
* @return array<int, array<string, mixed>>
*/
public function getCsv(string $path, string $storage = ''): array
{
return $this->getSerialized($path, $this->getSerializer('csv'), $storage);
@@ -193,9 +203,12 @@ final class FilesystemManager
/**
* Storage-Statistiken
*
* @return array<string, array<string, mixed>>
*/
public function getStorageInfo(): array
{
/** @var array<string, array<string, mixed>> */
$info = [];
foreach ($this->storages as $name => $storage) {
@@ -210,9 +223,12 @@ final class FilesystemManager
/**
* Serializer-Statistiken
*
* @return array<string, array<string, string>>
*/
public function getSerializerInfo(): array
{
/** @var array<string, array<string, string>> */
$info = [];
foreach ($this->serializers as $name => $serializer) {
@@ -228,10 +244,13 @@ final class FilesystemManager
/**
* Factory-Methode für Standard-Setup
*
* @param array<string, mixed> $config
*/
public static function create(array $config = []): self
{
$defaultStorage = 'default';
/** @var array<string, Storage> */
$namedStorages = [];
$manager = new self($defaultStorage, $namedStorages);

View File

@@ -8,6 +8,13 @@ use App\Framework\Http\Method;
/**
* Represents a single operation in a batch request
*
* @param string $id
* @param Method $method
* @param string $path
* @param array<string, string> $headers
* @param string|null $body
* @param array<string, mixed> $queryParams
*/
final readonly class BatchOperation
{
@@ -21,6 +28,9 @@ final readonly class BatchOperation
) {
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
@@ -33,6 +43,9 @@ final readonly class BatchOperation
];
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(

View File

@@ -51,6 +51,9 @@ final readonly class BatchRequest
return count($this->operations);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
@@ -59,6 +62,9 @@ final readonly class BatchRequest
];
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
$operations = [];

View File

@@ -14,12 +14,16 @@ final readonly class BatchResponse
public function __construct(
public string $id,
public Status $status,
/** @var array<string, string> */
public array $headers = [],
public ?string $body = null,
public ?string $error = null
) {
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
@@ -31,6 +35,9 @@ final readonly class BatchResponse
];
}
/**
* @param array<string, string> $headers
*/
public static function success(
string $id,
Status $status,

View File

@@ -58,6 +58,7 @@ final readonly class ETagInitializer
/**
* Parse comma-separated environment variable into array
* @return array<int, string>
*/
private function parseArrayEnvVar(string $value): array
{

Some files were not shown because too many files have changed in this diff Show More