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;