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

@@ -19,6 +19,7 @@ use App\Framework\Http\Status;
use App\Framework\Meta\MetaData;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Redis\RedisConnectionPool;
use App\Framework\Router\CompiledRoutes;
use App\Framework\Router\Result\ViewResult;
final readonly class Dashboard
@@ -69,8 +70,8 @@ final readonly class Dashboard
#[Route(path: '/admin/routes', method: Method::GET)]
public function routes(): ViewResult
{
$routeRegistry = $this->container->get('App\Framework\Router\RouteRegistry');
$routes = $routeRegistry->getRoutes();
$compiledRoutes = $this->container->get(CompiledRoutes::class);
$routes = $compiledRoutes->getAllNamedRoutes();
// Sort routes by path for better readability
usort($routes, function ($a, $b) {
@@ -149,7 +150,7 @@ final readonly class Dashboard
}
$env[] = [
'key' => $key,
'value' => is_array($value) ? json_encode($value) : (string)$value,
'value' => is_array($value) ? (json_encode($value) ?: '[encoding failed]') : (string)$value,
];
}

View File

@@ -31,7 +31,7 @@ final readonly class Images
try {
$variantInfo = $image->variants[0]->filename ?? 'no variant';
echo "<small>Variant: " . htmlspecialchars($variantInfo) . "</small>";
} catch (Error $e) {
} catch (\Error $e) {
echo "<small>Variants not loaded yet</small>";
}
}

View File

@@ -14,7 +14,7 @@ class RoutesViewModel
public string $title = 'Routes';
public function __construct(
/** @var array<string, mixed> */
/** @var list<array<string, mixed>> */
public array $routes = []
) {
}

View File

@@ -9,7 +9,7 @@ use App\Framework\Auth\Auth;
use App\Framework\Cache\Cache;
use App\Framework\Discovery\Results\DiscoveryRegistry;
class ShowDiscovery
final readonly class ShowDiscovery
{
public function __construct(
private DiscoveryRegistry $results,
@@ -28,8 +28,8 @@ class ShowDiscovery
echo "<ul>";
$attributeMappings = $this->results->attributes()->get($attributeType);
foreach ($attributeMappings as $attributeMapping) {
$className = $attributeMapping->class->getFullyQualified();
$methodName = $attributeMapping->method?->toString() ?? '';
$className = $attributeMapping->className->getFullyQualified();
$methodName = $attributeMapping->methodName?->toString() ?? '';
echo "<li>" . $className . '::' . $methodName . '()</li>';
}
echo "</ul>";

View File

@@ -13,7 +13,7 @@ use App\Framework\Http\Request;
use App\Framework\Meta\MetaData;
use App\Framework\Router\Result\ViewResult;
class ShowImageSlots
final readonly class ShowImageSlots
{
public function __construct(
private ImageSlotRepository $imageSlotRepository,
@@ -25,6 +25,7 @@ class ShowImageSlots
public function show(): ViewResult
{
$slots = $this->imageSlotRepository->getSlots();
$slotName = '';
/** @var ImageSlot $slot */
foreach ($slots as $slot) {

View File

@@ -75,7 +75,7 @@ final readonly class ImageApiController
'file_size' => $image->fileSize,
'hash' => $image->hash,
'variants' => array_map(fn ($variant) => [
'type' => $variant->type,
'type' => $variant->variantType,
'width' => $variant->width,
'height' => $variant->height,
'path' => $variant->path,

View File

@@ -12,6 +12,7 @@ use App\Framework\Http\Exception\NotFound;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Status;
final readonly class ImageSlotController
{
@@ -47,7 +48,7 @@ final readonly class ImageSlotController
public function getSlot(int $id): JsonResponse
{
try {
$slot = $this->slotRepository->findByIdWithImage($id);
$slot = $this->slotRepository->findByIdWithImage((string) $id);
} catch (\RuntimeException $e) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
@@ -74,7 +75,7 @@ final readonly class ImageSlotController
public function assignImage(int $id, HttpRequest $request): JsonResponse
{
try {
$slot = $this->slotRepository->findById($id);
$slot = $this->slotRepository->findById((string) $id);
} catch (\RuntimeException $e) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
@@ -86,7 +87,7 @@ final readonly class ImageSlotController
$imageUlid = $data['image_ulid'] ?? null;
if (! $imageUlid) {
return new JsonResponse(['error' => 'image_ulid is required'], 400);
return new JsonResponse(['error' => 'image_ulid is required'], Status::BAD_REQUEST);
}
$image = $this->imageRepository->findByUlid($imageUlid);
@@ -99,7 +100,7 @@ final readonly class ImageSlotController
}
// Update slot with new image
$this->slotRepository->updateImageId($id, $imageUlid);
$this->slotRepository->updateImageId((string) $id, $imageUlid);
return new JsonResponse([
'success' => true,
@@ -115,7 +116,7 @@ final readonly class ImageSlotController
public function removeImage(int $id): JsonResponse
{
try {
$slot = $this->slotRepository->findById($id);
$slot = $this->slotRepository->findById((string) $id);
} catch (\RuntimeException $e) {
throw NotFound::create(
ErrorCode::ENTITY_NOT_FOUND,
@@ -124,7 +125,7 @@ final readonly class ImageSlotController
}
// Remove image from slot
$this->slotRepository->updateImageId($id, '');
$this->slotRepository->updateImageId((string) $id, '');
return new JsonResponse([
'success' => true,

View File

@@ -38,7 +38,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($users, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($users, JsonSerializerConfig::pretty())
);
}
@@ -57,7 +57,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($user, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($user, JsonSerializerConfig::pretty())
);
}
@@ -86,7 +86,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::CREATED,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($user, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($user, JsonSerializerConfig::pretty())
);
}
@@ -112,7 +112,7 @@ final readonly class UsersController
'Content-Type' => 'application/json',
'Warning' => '299 - "This endpoint is deprecated and will be removed in v2.0.0"',
]),
body: $this->jsonSerializer->serialize($profile, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($profile, JsonSerializerConfig::pretty())
);
}
}

View File

@@ -75,7 +75,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($response, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($response, JsonSerializerConfig::pretty())
);
}
@@ -116,7 +116,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($response, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($response, JsonSerializerConfig::pretty())
);
}
@@ -174,7 +174,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::CREATED,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($response, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($response, JsonSerializerConfig::pretty())
);
}
@@ -220,7 +220,7 @@ final readonly class UsersController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($response, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($response, JsonSerializerConfig::pretty())
);
}
}

View File

@@ -52,7 +52,7 @@ final readonly class VersionController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($versionInfo, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($versionInfo, JsonSerializerConfig::pretty())
);
}
@@ -108,7 +108,7 @@ final readonly class VersionController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($response, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($response, JsonSerializerConfig::pretty())
);
}
@@ -121,9 +121,9 @@ final readonly class VersionController
return new HttpResponse(
status: Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize([
body: $this->jsonSerializer->serializeWithConfig([
'error' => 'from_version and to_version are required',
])
], JsonSerializerConfig::compact())
);
}
@@ -134,9 +134,9 @@ final readonly class VersionController
return new HttpResponse(
status: Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize([
body: $this->jsonSerializer->serializeWithConfig([
'error' => 'Invalid version format',
])
], JsonSerializerConfig::compact())
);
}
@@ -145,7 +145,7 @@ final readonly class VersionController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($migrationGuide, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($migrationGuide, JsonSerializerConfig::pretty())
);
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Application\Controller;
use App\Framework\Attributes\Route;
use App\Framework\Http\Method;
use App\Framework\Http\Request;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Http\Routing\Route;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Http\Status;

View File

@@ -4,14 +4,16 @@ declare(strict_types=1);
namespace App\Application\Controller;
use App\Framework\Http\Attribute\Route;
use App\Framework\Attributes\Route;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\Method;
use App\Framework\Http\Result\HtmlResult;
use App\Framework\Http\Result\HttpResponse;
use App\Framework\Http\Status;
use App\Framework\QrCode\ErrorCorrectionLevel;
use App\Framework\QrCode\QrCodeGenerator;
use App\Framework\QrCode\QrCodeVersion;
use App\Framework\Router\Result\HtmlResult;
/**
* QR Code Test Controller
@@ -29,7 +31,7 @@ final readonly class QrCodeTestController
* Show QR code test page with multiple examples
*/
#[Route(path: '/test/qr-codes', method: Method::GET)]
public function showTestPage(HttpRequest $request): HttpResponse
public function showTestPage(HttpRequest $request): HtmlResult
{
// Generate various test QR codes
$examples = [
@@ -120,9 +122,9 @@ final readonly class QrCodeTestController
* Generate individual QR code for API testing
*/
#[Route(path: '/test/qr-code', method: Method::GET)]
public function generateTestQrCode(HttpRequest $request): HttpResponse
public function generateTestQrCode(HttpRequest $request): HtmlResult|HttpResponse
{
$data = $request->query->get('data', 'Test QR Code from API');
$data = (string) $request->query->get('data', 'Test QR Code from API');
$format = $request->query->get('format', 'svg'); // svg or datauri
$errorLevel = $request->query->get('error', 'M');
$version = $request->query->get('version');
@@ -147,15 +149,15 @@ final readonly class QrCodeTestController
$svg = $this->qrCodeGenerator->generateSvg($data, $errorCorrectionLevel, $qrVersion);
return new HttpResponse(
body: $svg,
statusCode: 200,
headers: ['Content-Type' => 'image/svg+xml']
Status::OK,
new Headers(['Content-Type' => 'image/svg+xml']),
$svg
);
}
} catch (\Exception $e) {
return new HtmlResult(
"<h1>QR Code Generation Error</h1><p>{$e->getMessage()}</p>",
500
Status::INTERNAL_SERVER_ERROR
);
}
}
@@ -173,7 +175,7 @@ final readonly class QrCodeTestController
$analysis = $qrCode['analysis'];
$analysisHtml = '';
foreach ($analysis as $key => $value) {
$displayValue = is_object($value) ? class_basename($value) : $value;
$displayValue = is_object($value) ? basename(str_replace('\\', '/', get_class($value))) : $value;
$analysisHtml .= "<tr><td>{$key}</td><td>{$displayValue}</td></tr>";
}

View File

@@ -37,7 +37,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode($response, JSON_PRETTY_PRINT)
body: json_encode($response, JSON_PRETTY_PRINT) ?: '{}'
);
}
@@ -51,14 +51,14 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::NOT_FOUND,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['error' => 'Feature flag not found'])
body: json_encode(['error' => 'Feature flag not found']) ?: '{}'
);
}
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode($this->flagToArray($flag), JSON_PRETTY_PRINT)
body: json_encode($this->flagToArray($flag), JSON_PRETTY_PRINT) ?: '{}'
);
}
@@ -72,7 +72,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['message' => "Feature flag '{$name}' enabled"])
body: json_encode(['message' => "Feature flag '{$name}' enabled"]) ?: '{}'
);
}
@@ -86,7 +86,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['message' => "Feature flag '{$name}' disabled"])
body: json_encode(['message' => "Feature flag '{$name}' disabled"]) ?: '{}'
);
}
@@ -102,7 +102,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['error' => 'Percentage must be between 0 and 100'])
body: json_encode(['error' => 'Percentage must be between 0 and 100']) ?: '{}'
);
}
@@ -115,7 +115,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['message' => "Feature flag '{$name}' set to {$percentage}% rollout"])
body: json_encode(['message' => "Feature flag '{$name}' set to {$percentage}% rollout"]) ?: '{}'
);
}
@@ -128,7 +128,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::NOT_FOUND,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['error' => 'Feature flag not found'])
body: json_encode(['error' => 'Feature flag not found']) ?: '{}'
);
}
@@ -137,7 +137,7 @@ final readonly class FeatureFlagController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: json_encode(['message' => "Feature flag '{$name}' deleted"])
body: json_encode(['message' => "Feature flag '{$name}' deleted"]) ?: '{}'
);
}

View File

@@ -57,7 +57,7 @@ final readonly class GraphQLController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize(
body: $this->jsonSerializer->serializeWithConfig(
$result->toArray(),
JsonSerializerConfig::pretty()
)

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Application\GraphQL;
use App\Application\User\UserService;
use App\Framework\Core\ValueObjects\EmailAddress;
use App\Application\GraphQL\ValueObjects\UserStats;
/**
* GraphQL resolvers for User operations
@@ -22,15 +24,20 @@ final readonly class UserResolvers
*/
public function users(mixed $root, array $args, mixed $context): array
{
$filters = [];
$users = $this->userService->findAll();
// Apply filters
if (isset($args['active'])) {
$filters['active'] = $args['active'];
$users = array_filter($users, fn ($user) => $user->active === $args['active']);
}
$limit = isset($args['limit']) ? (int) $args['limit'] : null;
// Apply limit
if (isset($args['limit'])) {
$users = array_slice($users, 0, (int) $args['limit']);
}
return $this->userService->findUsers($filters, $limit);
// Convert to array format for GraphQL
return array_map(fn ($user) => $user->toArray(), $users);
}
/**
@@ -40,8 +47,9 @@ final readonly class UserResolvers
public function user(mixed $root, array $args, mixed $context): ?array
{
$id = (int) $args['id'];
$user = $this->userService->findById($id);
return $this->userService->findById($id);
return $user?->toArray();
}
/**
@@ -50,7 +58,14 @@ final readonly class UserResolvers
*/
public function createUser(mixed $root, array $args, mixed $context): array
{
return $this->userService->createUser($args['input']);
$input = $args['input'];
$user = $this->userService->create(
email: new EmailAddress($input['email']),
name: $input['name']
);
return $user->toArray();
}
/**
@@ -60,8 +75,16 @@ final readonly class UserResolvers
public function updateUser(mixed $root, array $args, mixed $context): ?array
{
$id = (int) $args['id'];
$input = $args['input'];
return $this->userService->updateUser($id, $args['input']);
$email = isset($input['email'])
? new EmailAddress($input['email'])
: null;
$name = $input['name'] ?? null;
$user = $this->userService->update($id, $email, $name);
return $user?->toArray();
}
/**
@@ -71,7 +94,7 @@ final readonly class UserResolvers
{
$id = (int) $args['id'];
return $this->userService->deleteUser($id);
return $this->userService->delete($id);
}
/**
@@ -80,6 +103,7 @@ final readonly class UserResolvers
*/
public function userStats(mixed $root, array $args, mixed $context): array
{
return $this->userService->getUserStats();
$stats = $this->userService->getUserStats();
return $stats->toArray();
}
}

View File

@@ -4,11 +4,19 @@ declare(strict_types=1);
namespace App\Application\GraphQL;
use App\Application\GraphQL\ValueObjects\UserStats;
use App\Framework\DateTime\Clock;
/**
* Service for User operations (demo implementation)
*/
final class UserService
{
public function __construct(
private readonly Clock $clock
) {
}
// In a real application, this would use a repository/database
/** @var array<int, array<string, mixed>> */
private array $users = [
@@ -85,7 +93,7 @@ final class UserService
'email' => $input['email'],
'age' => $input['age'] ?? null,
'active' => $input['active'] ?? true,
'created_at' => date('Y-m-d\TH:i:s\Z'),
'created_at' => $this->clock->now()->format('Y-m-d\TH:i:s\Z'),
];
$this->users[] = $newUser;
@@ -136,24 +144,44 @@ final class UserService
return false;
}
/**
* @return array<string, mixed>
*/
public function getUserStats(): array
public function getUserStats(): UserStats
{
$totalUsers = count($this->users);
$activeUsers = count(array_filter($this->users, fn ($user) => $user['active']));
$inactiveUsers = $totalUsers - $activeUsers;
$ages = array_filter(array_column($this->users, 'age'));
$averageAge = ! empty($ages) ? array_sum($ages) / count($ages) : 0;
$averageAge = ! empty($ages) ? (float) array_sum($ages) / count($ages) : 0.0;
return [
'total' => $totalUsers,
'active' => $activeUsers,
'inactive' => $inactiveUsers,
'average_age' => round($averageAge, 1),
];
// Calculate time-based statistics
$today = $this->clock->now()->format('Y-m-d');
$thisWeekStart = $this->clock->now()->modify('-7 days')->format('Y-m-d');
$thisMonthStart = $this->clock->now()->modify('-30 days')->format('Y-m-d');
$usersCreatedToday = count(array_filter(
$this->users,
fn ($user) => str_starts_with($user['created_at'], $today)
));
$usersCreatedThisWeek = count(array_filter(
$this->users,
fn ($user) => $user['created_at'] >= $thisWeekStart
));
$usersCreatedThisMonth = count(array_filter(
$this->users,
fn ($user) => $user['created_at'] >= $thisMonthStart
));
return new UserStats(
totalUsers: $totalUsers,
activeUsers: $activeUsers,
inactiveUsers: $inactiveUsers,
averageAge: round($averageAge, 1),
usersCreatedToday: $usersCreatedToday,
usersCreatedThisWeek: $usersCreatedThisWeek,
usersCreatedThisMonth: $usersCreatedThisMonth
);
}
private function getNextId(): int

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Application\GraphQL\ValueObjects;
/**
* Value Object für User-Statistiken
*/
final readonly class UserStats
{
public function __construct(
public int $totalUsers,
public int $activeUsers,
public int $inactiveUsers,
public float $averageAge,
public int $usersCreatedToday,
public int $usersCreatedThisWeek,
public int $usersCreatedThisMonth
) {
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'total_users' => $this->totalUsers,
'active_users' => $this->activeUsers,
'inactive_users' => $this->inactiveUsers,
'average_age' => $this->averageAge,
'users_created_today' => $this->usersCreatedToday,
'users_created_this_week' => $this->usersCreatedThisWeek,
'users_created_this_month' => $this->usersCreatedThisMonth,
];
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
totalUsers: (int) ($data['total_users'] ?? 0),
activeUsers: (int) ($data['active_users'] ?? 0),
inactiveUsers: (int) ($data['inactive_users'] ?? 0),
averageAge: (float) ($data['average_age'] ?? 0.0),
usersCreatedToday: (int) ($data['users_created_today'] ?? 0),
usersCreatedThisWeek: (int) ($data['users_created_this_week'] ?? 0),
usersCreatedThisMonth: (int) ($data['users_created_this_month'] ?? 0)
);
}
}

View File

@@ -32,7 +32,7 @@ final readonly class BatchController
{
try {
// Validate content type
$contentType = $request->headers->getFirst('Content-Type', '');
$contentType = $request->headers->getFirst('Content-Type') ?? '';
if (! str_contains($contentType, 'application/json')) {
return $this->errorResponse(
'Content-Type must be application/json',

View File

@@ -123,7 +123,7 @@ final readonly class QrCodeController
return new HttpResponse(
status : Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body : json_encode(['error' => 'TOTP URI parameter is required'])
body : json_encode(['error' => 'TOTP URI parameter is required']) ?: '{"error":"Encoding failed"}'
);
}
@@ -139,7 +139,7 @@ final readonly class QrCodeController
return new HttpResponse(
status : Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body : json_encode(['error' => $e->getMessage()])
body : json_encode(['error' => $e->getMessage()]) ?: '{"error":"Encoding failed"}'
);
}
}

View File

@@ -40,7 +40,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($user, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($user, JsonSerializerConfig::pretty())
);
}
@@ -53,7 +53,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize(['error' => 'Title is required'])
body: $this->jsonSerializer->serializeWithConfig(['error' => 'Title is required'], JsonSerializerConfig::compact())
);
}
@@ -68,7 +68,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::CREATED,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($post, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($post, JsonSerializerConfig::pretty())
);
}
@@ -82,7 +82,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::BAD_REQUEST,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize(['error' => 'Invalid JSON data'])
body: $this->jsonSerializer->serializeWithConfig(['error' => 'Invalid JSON data'], JsonSerializerConfig::compact())
);
}
@@ -96,7 +96,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize($post, JsonSerializerConfig::pretty())
body: $this->jsonSerializer->serializeWithConfig($post, JsonSerializerConfig::pretty())
);
}
@@ -123,7 +123,7 @@ final readonly class BatchExampleController
return new HttpResponse(
status: Status::OK,
headers: new Headers(['Content-Type' => 'application/json']),
body: $this->jsonSerializer->serialize([
body: $this->jsonSerializer->serializeWithConfig([
'message' => 'Slow operation completed',
'delay' => $maxDelay,
'timestamp' => $this->clock->now()->format('Y-m-d H:i:s'),

View File

@@ -81,7 +81,7 @@ final readonly class MetricsController
'Content-Type' => 'application/json',
'Cache-Control' => 'no-cache',
]),
body: json_encode($health, JSON_PRETTY_PRINT)
body: json_encode($health, JSON_PRETTY_PRINT) ?: '{"error":"Encoding failed"}'
);
}
@@ -94,7 +94,7 @@ final readonly class MetricsController
}
// Check Accept header
$acceptHeader = $request->headers->getFirst('Accept', 'text/plain');
$acceptHeader = $request->headers->getFirst('Accept') ?? 'text/plain';
// Map Accept headers to formats
return match(true) {

View File

@@ -7,7 +7,6 @@ namespace App\Application\Newsletter\SignUp;
use App\Framework\CommandBus\CommandHandler;
use App\Framework\EventBus\EventBus;
use App\Infrastructure\Api\RapidMailClient;
use Archive\Config\ApiConfig;
final readonly class NewsletterSignupHandler
{

View File

@@ -504,6 +504,9 @@ final readonly class SearchController
}
}
/**
* @param array<string, mixed> $filterData
*/
private function createSearchFilter(array $filterData): SearchFilter
{
$type = SearchFilterType::from($filterData['type'] ?? 'equals');

View File

@@ -41,7 +41,7 @@ final readonly class SearchRequest
$query = $request->query;
// Parse search query
$searchQuery = $query->get('q', '*');
$searchQuery = $query->get('q', '*') ?? '*';
// Parse filters
$filters = [];
@@ -119,7 +119,7 @@ final readonly class SearchRequest
// Parse sorting
$sortBy = $query->get('sort_by');
$sortDirection = strtolower($query->get('sort_direction', 'asc'));
$sortDirection = strtolower($query->get('sort_direction', 'asc') ?? 'asc');
$sortByRelevance = $query->getBool('sort_by_relevance', ! $sortBy);
// Validate sort direction

View File

@@ -11,11 +11,9 @@ use App\Application\Security\Events\{
System\SystemAnomalyEvent
};
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\Exceptions\{
CryptographicException,
ValidationException
};
use App\Framework\Exception\FrameworkException;
use App\Framework\Logging\Logger;
use App\Framework\Validation\Exceptions\ValidationException;
use Symfony\Component\Finder\Exception\AccessDeniedException;
use Throwable;
@@ -32,7 +30,7 @@ final class SecurityExceptionHandler
match (true) {
$exception instanceof AccessDeniedException => $this->handleAccessDenied($exception),
$exception instanceof ValidationException => $this->handleValidationError($exception),
$exception instanceof CryptographicException => $this->handleCryptographicError($exception),
$exception instanceof FrameworkException && $this->isCryptographicError($exception) => $this->handleCryptographicError($exception),
$exception instanceof \Error => $this->handleSystemError($exception),
default => $this->handleGenericSecurityIssue($exception)
};
@@ -60,11 +58,21 @@ final class SecurityExceptionHandler
}
}
private function handleCryptographicError(CryptographicException $exception): void
private function isCryptographicError(FrameworkException $exception): bool
{
$context = $exception->getContext();
return str_contains(strtolower($exception->getMessage()), 'cryptographic') ||
str_contains(strtolower($context->getOperation() ?? ''), 'crypto') ||
str_contains(strtolower($context->getComponent() ?? ''), 'crypto');
}
private function handleCryptographicError(FrameworkException $exception): void
{
$context = $exception->getContext();
$this->eventDispatcher->dispatch(new CryptographicFailureEvent(
operation: $exception->getOperation(),
algorithm: $exception->getAlgorithm(),
operation: $context->getOperation() ?? 'unknown',
algorithm: $context->getData()['algorithm'] ?? 'unknown',
errorMessage: $exception->getMessage(),
email: $this->getCurrentUserEmail()
));

View File

@@ -66,7 +66,7 @@ final class AuthenticationGuard
session_destroy();
$this->eventDispatcher->dispatch(new \App\Application\Security\Events\Auth\SessionTerminatedEvent(
email: $user->email,
email: $user->email ?? '',
sessionId: $sessionId,
reason: 'manual_logout'
));
@@ -74,17 +74,16 @@ final class AuthenticationGuard
private function handleSuccessfulLogin(User $user): void
{
// Failed attempts zurücksetzen
$user->failed_attempts = 0;
$user->last_login = new \DateTimeImmutable();
$this->userRepository->save($user);
// Failed attempts zurücksetzen - mit immutable object pattern
$updatedUser = $user->withFailedAttemptsReset();
$this->userRepository->save($updatedUser);
// Session regenerieren
session_regenerate_id(true);
$_SESSION['user_id'] = $user->id;
$_SESSION['user_id'] = $updatedUser->id;
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent(
email: $user->email,
email: $updatedUser->email ?? '',
sessionId: session_id(),
method: 'password'
));
@@ -92,17 +91,18 @@ final class AuthenticationGuard
private function handleFailedAttempt(User $user): void
{
$user->failed_attempts++;
$user->last_failed_attempt = new \DateTimeImmutable();
$updatedUser = $user->withIncrementedFailedAttempts();
if ($user->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
$user->locked_until = new \DateTimeImmutable('+' . self::LOCKOUT_DURATION . ' seconds');
$this->userRepository->save($user);
if ($updatedUser->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
$lockedUser = $updatedUser->withLockout(
new \DateTimeImmutable('+' . self::LOCKOUT_DURATION . ' seconds')
);
$this->userRepository->save($lockedUser);
$this->dispatchAccountLocked($user->email, 'max_attempts_exceeded', $user->failed_attempts);
$this->dispatchAccountLocked($lockedUser->email ?? '', 'max_attempts_exceeded', $lockedUser->failed_attempts);
} else {
$this->userRepository->save($user);
$this->dispatchFailedAttempt($user->email, 'invalid_password', $user->failed_attempts);
$this->userRepository->save($updatedUser);
$this->dispatchFailedAttempt($updatedUser->email ?? '', 'invalid_password', $updatedUser->failed_attempts);
}
}

View File

@@ -6,6 +6,7 @@ namespace App\Application\Security;
use App\Framework\Attributes\Route;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\Router\Result\JsonResult;
use App\Framework\Waf\DetectionCategory;
@@ -30,7 +31,7 @@ final readonly class WafFeedbackController
/**
* Submit feedback for a WAF detection
*/
#[Route(path: '/api/security/waf/feedback', method: 'POST')]
#[Route(path: '/api/security/waf/feedback', method: Method::POST)]
public function submitFeedback(HttpRequest $request): JsonResult
{
$data = $request->parsedBody->data ?? [];
@@ -149,7 +150,7 @@ final readonly class WafFeedbackController
/**
* Get feedback for a specific detection
*/
#[Route(path: '/api/security/waf/feedback/{detectionId}', method: 'GET')]
#[Route(path: '/api/security/waf/feedback/{detectionId}', method: Method::GET)]
public function getFeedbackForDetection(HttpRequest $request, string $detectionId): JsonResult
{
$feedback = $this->feedbackService->getFeedbackForDetection($detectionId);
@@ -165,7 +166,7 @@ final readonly class WafFeedbackController
/**
* Get feedback statistics
*/
#[Route(path: '/api/security/waf/feedback/stats', method: 'GET')]
#[Route(path: '/api/security/waf/feedback/stats', method: Method::GET)]
public function getFeedbackStats(HttpRequest $request): JsonResult
{
$stats = $this->feedbackService->getFeedbackStats();
@@ -179,7 +180,7 @@ final readonly class WafFeedbackController
/**
* Get recent feedback
*/
#[Route(path: '/api/security/waf/feedback/recent', method: 'GET')]
#[Route(path: '/api/security/waf/feedback/recent', method: Method::GET)]
public function getRecentFeedback(HttpRequest $request): JsonResult
{
$limit = (int)($request->queryParams['limit'] ?? 10);

View File

@@ -7,6 +7,7 @@ namespace App\Application\Security;
use App\Framework\Attributes\Route;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Waf\DetectionCategory;
use App\Framework\Waf\Feedback\FeedbackRepositoryInterface;
@@ -31,7 +32,7 @@ final readonly class WafFeedbackDashboardController
/**
* Show the WAF feedback dashboard
*/
#[Route(path: '/admin/security/waf/feedback', method: 'GET')]
#[Route(path: '/admin/security/waf/feedback', method: Method::GET)]
public function showDashboard(HttpRequest $request): ViewResult
{
// Get feedback statistics
@@ -104,7 +105,7 @@ final readonly class WafFeedbackDashboardController
/**
* Show detailed feedback for a specific category
*/
#[Route(path: '/admin/security/waf/feedback/category/{category}', method: 'GET')]
#[Route(path: '/admin/security/waf/feedback/category/{category}', method: Method::GET)]
public function showCategoryFeedback(HttpRequest $request, string $category): ViewResult
{
try {
@@ -166,7 +167,7 @@ final readonly class WafFeedbackDashboardController
/**
* Show feedback learning history
*/
#[Route(path: '/admin/security/waf/feedback/learning', method: 'GET')]
#[Route(path: '/admin/security/waf/feedback/learning', method: Method::GET)]
public function showLearningHistory(HttpRequest $request): ViewResult
{
// In a real implementation, this would retrieve learning history from a database

View File

@@ -10,19 +10,12 @@ use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\Router\Result\JsonResult;
use App\Infrastructure\Api\ShopifyClient;
use Archive\Config\ApiConfig;
final class ProductController
final readonly class ProductController
{
private ShopifyClient $client;
public function __construct()
{
$this->client = new ShopifyClient(
ApiConfig::SHOPIFY_SHOP_DOMAIN->value,
ApiConfig::SHOPIFY_ACCESS_TOKEN->value,
ApiConfig::SHOPIFY_API_VERSION->value
);
public function __construct(
private readonly ShopifyClient $client
) {
}
/**

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Application\User;
use App\Framework\Core\ValueObjects\EmailAddress;
/**
* User Data Value Object
* Framework-konformes Value Object statt primitive Arrays
*/
final readonly class UserData
{
public function __construct(
public int $id,
public EmailAddress $email,
public string $name,
public bool $active = true
) {
if (trim($name) === '') {
throw new \InvalidArgumentException('User name cannot be empty');
}
}
/**
* Konvertiert zu Array für GraphQL/API Kompatibilität
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'email' => $this->email->toString(),
'name' => $this->name,
'active' => $this->active,
];
}
/**
* Factory Method für Array-Input (Migration von primitives)
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
id: $data['id'] ?? 0,
email: new EmailAddress($data['email'] ?? ''),
name: $data['name'] ?? '',
active: $data['active'] ?? true
);
}
public function isActive(): bool
{
return $this->active;
}
public function getDisplayName(): string
{
return $this->name;
}
public function getMaskedEmail(): string
{
return $this->email->obfuscate();
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace App\Application\User;
use App\Framework\Core\ValueObjects\EmailAddress;
use App\Application\GraphQL\ValueObjects\UserStats;
/**
* User Service mit framework-konformen Value Objects
* Ersetzt primitive Arrays durch typisierte Value Objects
*/
final readonly class UserService
{
public function __construct()
{
// In einem echten System würden hier Repository und andere Dependencies injiziert
}
/**
* @return array<int, UserData>
*/
public function findAll(): array
{
// Demo-Implementation - würde normalerweise Repository verwenden
return [
new UserData(
id: 1,
email: new EmailAddress('john@example.com'),
name: 'John Doe',
active: true
),
new UserData(
id: 2,
email: new EmailAddress('jane@example.com'),
name: 'Jane Smith',
active: true
),
];
}
public function findById(int $id): ?UserData
{
// Demo-Implementation
$users = $this->findAll();
foreach ($users as $user) {
if ($user->id === $id) {
return $user;
}
}
return null;
}
public function findByEmail(EmailAddress $email): ?UserData
{
$users = $this->findAll();
foreach ($users as $user) {
if ($user->email->equals($email)) {
return $user;
}
}
return null;
}
public function create(EmailAddress $email, string $name): UserData
{
// Demo-Implementation - würde normalerweise Repository verwenden
return new UserData(
id: $this->getNextId(),
email: $email,
name: $name,
active: true
);
}
public function update(int $id, ?EmailAddress $email = null, ?string $name = null): ?UserData
{
$existingUser = $this->findById($id);
if ($existingUser === null) {
return null;
}
// Immutable Update Pattern
return new UserData(
id: $existingUser->id,
email: $email ?? $existingUser->email,
name: $name ?? $existingUser->name,
active: $existingUser->active
);
}
public function deactivate(int $id): ?UserData
{
$existingUser = $this->findById($id);
if ($existingUser === null) {
return null;
}
return new UserData(
id: $existingUser->id,
email: $existingUser->email,
name: $existingUser->name,
active: false
);
}
public function delete(int $id): bool
{
// Demo-Implementation - würde normalerweise Repository verwenden
$user = $this->findById($id);
return $user !== null;
}
/**
* @return array<string, mixed>
*/
public function getStats(): array
{
$stats = $this->getUserStats();
return $stats->toArray();
}
public function getUserStats(): UserStats
{
$users = $this->findAll();
$totalUsers = count($users);
$activeUsers = count(array_filter($users, fn ($user) => $user->active));
return new UserStats(
totalUsers: $totalUsers,
activeUsers: $activeUsers,
inactiveUsers: $totalUsers - $activeUsers,
averageAge: 0.0, // Demo data doesn't have ages
usersCreatedToday: 0,
usersCreatedThisWeek: 0,
usersCreatedThisMonth: 0
);
}
private function getNextId(): int
{
// Demo-Implementation
return count($this->findAll()) + 1;
}
}

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace App\Application\Webhook\Controller;
use App\Framework\Attributes\Route;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\EventBus\Attributes\OnEvent;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\Method;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Routing\Route;
use App\Framework\Webhook\Attributes\WebhookEndpoint;
use App\Framework\Webhook\Events\WebhookReceived;
use App\Framework\Webhook\WebhookService;

View File

@@ -40,4 +40,20 @@ final readonly class Image
public string $altText,
) {
}
public function withFilename(string $filename): self
{
return new self(
ulid: $this->ulid,
filename: $filename,
originalFilename: $this->originalFilename,
mimeType: $this->mimeType,
fileSize: $this->fileSize,
width: $this->width,
height: $this->height,
hash: $this->hash,
path: $this->path,
altText: $this->altText
);
}
}

View File

@@ -40,6 +40,9 @@ final readonly class ImageRepository
return $this->entityManager->findOneBy(Image::class, ['hash' => $hash]);
}
/**
* @return Image[]
*/
public function findAll(int $limit = 50, int $offset = 0, ?string $search = null): array
{
// Simplified version - use EntityManager::findAll for now
@@ -91,11 +94,14 @@ final readonly class ImageRepository
$image = $this->findByUlid($ulid);
if ($image) {
$image->filename = $filename;
$this->entityManager->save($image);
$updatedImage = $image->withFilename($filename);
$this->entityManager->save($updatedImage);
}
}
/**
* @return Image[]
*/
public function search(string $query, ?string $type = null, int $minWidth = 0, int $minHeight = 0): array
{
$allImages = $this->entityManager->findAll(Image::class);

View File

@@ -33,6 +33,9 @@ final readonly class ImageSlotRepository
return $this->entityManager->save($imageSlot);
}
/**
* @return array<int, ImageSlotView>
*/
public function findAllWithImages(): array
{
$slots = $this->entityManager->findAll(ImageSlot::class);

View File

@@ -11,9 +11,9 @@ use App\Domain\Meta\Service\MetaManager;
use App\Domain\Meta\Service\MetaTemplateResolver;
use App\Domain\Meta\ValueObject\MetaData;
use App\Framework\Attributes\Route;
use App\Framework\Http\JsonResult;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\Router\Result\JsonResult;
final class MetaAdminController
{

View File

@@ -15,11 +15,74 @@ final readonly class User
public string $id,
#[Column(name: 'name')]
public string $name,
#public ?string $email = null,
/*#[Column(name: 'email')]
public string $email*/
#[Column(name: 'email')]
public ?string $email = null,
#[Column(name: 'failed_attempts')]
public int $failed_attempts = 0,
#[Column(name: 'last_login')]
public ?\DateTimeImmutable $last_login = null,
#[Column(name: 'password_hash')]
public ?string $password_hash = null,
#[Column(name: 'last_failed_attempt')]
public ?\DateTimeImmutable $last_failed_attempt = null,
#[Column(name: 'locked_until')]
public ?\DateTimeImmutable $locked_until = null
) {
}
public function withFailedAttemptsReset(): self
{
return new self(
id: $this->id,
name: $this->name,
email: $this->email,
failed_attempts: 0,
last_login: new \DateTimeImmutable(),
password_hash: $this->password_hash,
last_failed_attempt: $this->last_failed_attempt,
locked_until: null // Clear lockout on successful login
);
}
public function withIncrementedFailedAttempts(): self
{
return new self(
id: $this->id,
name: $this->name,
email: $this->email,
failed_attempts: $this->failed_attempts + 1,
last_login: $this->last_login,
password_hash: $this->password_hash,
last_failed_attempt: new \DateTimeImmutable(),
locked_until: $this->locked_until
);
}
public function withLockout(\DateTimeImmutable $lockedUntil): self
{
return new self(
id: $this->id,
name: $this->name,
email: $this->email,
failed_attempts: $this->failed_attempts,
last_login: $this->last_login,
password_hash: $this->password_hash,
last_failed_attempt: $this->last_failed_attempt,
locked_until: $lockedUntil
);
}
public function withUpdatedLastLogin(\DateTimeImmutable $lastLogin): self
{
return new self(
id: $this->id,
name: $this->name,
email: $this->email,
failed_attempts: $this->failed_attempts,
last_login: $lastLogin,
password_hash: $this->password_hash,
last_failed_attempt: $this->last_failed_attempt,
locked_until: $this->locked_until
);
}
}

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
{

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