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

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

View File

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

View File

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

View File

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

View File

@@ -75,8 +75,8 @@ final class ETagManager
return false;
}
$ifNoneMatch = $request->headers->get('If-None-Match');
if (! is_string($ifNoneMatch) || empty($ifNoneMatch)) {
$ifNoneMatch = $request->headers->getFirst('If-None-Match');
if (empty($ifNoneMatch)) {
return false;
}
@@ -100,8 +100,8 @@ final class ETagManager
return true; // If disabled, allow all requests
}
$ifMatch = $request->headers->get('If-Match');
if (! is_string($ifMatch) || empty($ifMatch)) {
$ifMatch = $request->headers->getFirst('If-Match');
if (empty($ifMatch)) {
return true; // No If-Match header means proceed
}
@@ -139,9 +139,10 @@ final class ETagManager
*/
public function createNotModifiedResponse(ETag $etag, ?Headers $additionalHeaders = null): HttpResponse
{
$headers = Headers::empty()
->with('ETag', $etag->toHeaderValue())
->with('Cache-Control', 'private, max-age=0');
$headers = new Headers([
'ETag' => $etag->toHeaderValue(),
'Cache-Control' => 'private, max-age=0',
]);
if ($additionalHeaders) {
// Merge additional headers (Date, Last-Modified, etc.)
@@ -157,9 +158,9 @@ final class ETagManager
}
return new HttpResponse(
status: 304,
headers: $headers,
body: '' // 304 responses must not have a body
Status::NOT_MODIFIED,
$headers,
'' // 304 responses must not have a body
);
}
@@ -205,9 +206,9 @@ final class ETagManager
if (in_array($method, ['PUT', 'PATCH', 'DELETE'], true)) {
if (! $this->checkIfMatch($request, $currentETag)) {
return new HttpResponse(
status: 412, // Precondition Failed
headers: Headers::empty()->with('ETag', $currentETag->toHeaderValue()),
body: 'Precondition Failed'
Status::PRECONDITION_FAILED,
new Headers(['ETag' => $currentETag->toHeaderValue()]),
'Precondition Failed'
);
}
}
@@ -216,9 +217,9 @@ final class ETagManager
if (! in_array($method, ['GET', 'HEAD'], true)) {
if ($this->checkIfNoneMatch($request, $currentETag)) {
return new HttpResponse(
status: 412, // Precondition Failed
headers: Headers::empty()->with('ETag', $currentETag->toHeaderValue()),
body: 'Precondition Failed'
Status::PRECONDITION_FAILED,
new Headers(['ETag' => $currentETag->toHeaderValue()]),
'Precondition Failed'
);
}
}
@@ -247,8 +248,8 @@ final class ETagManager
*/
public function extractETagFromResponse(HttpResponse $response): ?ETag
{
$etagHeader = $response->headers->get('ETag');
if (! is_string($etagHeader) || empty($etagHeader)) {
$etagHeader = $response->headers->getFirst('ETag');
if (empty($etagHeader)) {
return null;
}

View File

@@ -5,13 +5,21 @@ declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Router\ValueObjects\RouteParameters;
/**
* HTTP request implementation
*
* @param array<string, string> $queryParams
*/
final readonly class HttpRequest implements Request
{
#public array $parsedBody;
public Query $query;
public RouteParameters $routeParameters;
public function __construct(
public Method $method = Method::GET,
public Headers $headers = new Headers(),
@@ -23,10 +31,12 @@ final readonly class HttpRequest implements Request
public Cookies $cookies = new Cookies(),
public ServerEnvironment $server = new ServerEnvironment(),
public RequestId $id = new RequestId(),
public RequestBody $parsedBody = new RequestBody(Method::GET, new Headers(), '', [])
public RequestBody $parsedBody = new RequestBody(Method::GET, new Headers(), '', []),
?RouteParameters $routeParameters = null
) {
#$this->parsedBody = new RequestBodyParser()->parse($this);
$this->query = new Query($this->queryParams);
$this->routeParameters = $routeParameters ?? RouteParameters::empty();
}
// Hilfsmethode zum Abrufen von Query-Parametern
@@ -36,6 +46,14 @@ final readonly class HttpRequest implements Request
return $this->queryParams[$key] ?? $default;
}
/**
* Get the request path
*/
public function getPath(): string
{
return $this->path;
}
public function getUri(): Uri
{
$scheme = $this->server->isHttps() ? 'https' : 'http';

View File

@@ -34,9 +34,13 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
);
}
/**
* @return array<int, string>
*/
private function buildMiddlewareStack(): array
{
// Explizite Reihenfolge definieren - wichtigste zuerst
/** @var array<int, string> */
$explicitOrder = [
// 0. Exception Handling - MUSS absolut zuerst kommen, um alle Exceptions zu fangen
\App\Framework\Http\Middlewares\ExceptionHandlingMiddleware::class,
@@ -44,50 +48,53 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
// 1. System und Error Handling
\App\Framework\Http\Middlewares\RequestIdMiddleware::class,
// 2. Security und DDoS Protection
// 2. Production Security - MUST come early to block routes
\App\Framework\Http\Middlewares\ProductionSecurityMiddleware::class,
// 3. Security und DDoS Protection
DDoSProtectionMiddleware::class,
WafMiddleware::class,
// 3. Session - MUSS vor Auth und CSRF kommen!
// 4. Session - MUSS vor Auth und CSRF kommen!
\App\Framework\Http\Session\SessionMiddleware::class,
// 4. Security und Rate Limiting
// 5. Security und Rate Limiting
RateLimitMiddleware::class,
#\App\Application\Security\Middleware\SecurityEventMiddleware::class,
// 5. Headers und CORS
// 6. Headers und CORS
\App\Framework\Http\Middlewares\SecurityHeaderMiddleware::class,
\App\Framework\Http\Middlewares\RemovePoweredByMiddleware::class,
\App\Framework\Http\Middlewares\CORSMiddleware::class,
// 6. Authentication und CSRF (brauchen Session)
// 7. Authentication und CSRF (brauchen Session)
\App\Framework\Http\Middlewares\AuthMiddleware::class,
\App\Framework\Http\Middlewares\CsrfMiddleware::class,
\App\Framework\Http\Middlewares\HoneypotMiddleware::class,
// 7. Routing und Request Processing
// 8. Routing und Request Processing
\App\Framework\Http\Middlewares\RoutingMiddleware::class,
\App\Framework\Http\Middlewares\ControllerRequestMiddleware::class,
// 8. Content und Static Files
// 9. Content und Static Files
#\App\Framework\Http\Middlewares\ServeStaticFilesMiddleware::class,
\App\Framework\Http\Middlewares\ResponseGeneratorMiddleware::class,
\App\Framework\Http\Middlewares\FormDataResponseMiddleware::class, // Temporarily disabled
// 9. Monitoring und Analytics
// 10. Monitoring und Analytics
\App\Framework\Analytics\Middleware\AnalyticsMiddleware::class,
\App\Framework\Performance\Middleware\RequestPerformanceMiddleware::class,
\App\Framework\Performance\Middleware\RoutingPerformanceMiddleware::class,
\App\Framework\Performance\Middleware\ControllerPerformanceMiddleware::class,
\App\Framework\Tracing\TracingMiddleware::class,
// 10. Logging (am Ende)
// 11. Logging (am Ende)
\App\Framework\Http\Middlewares\RequestLoggingMiddleware::class,
\App\Framework\Http\Middlewares\LoggingMiddleware::class,
\App\Framework\Performance\Middleware\PerformanceDebugMiddleware::class,
\App\Framework\Security\RequestSigning\RequestSigningMiddleware::class,
// 11. FALLBACK - Absolut letztes Middleware (nur wenn keine Response vorhanden)
// 12. FALLBACK - Absolut letztes Middleware (nur wenn keine Response vorhanden)
\App\Framework\Http\Middlewares\DefaultResponseMiddleware::class,
];
@@ -113,8 +120,8 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
/**
* Resolve middleware dependencies using constructor analysis
* @param array<string> $middlewares
* @return array<string>
* @param array<int, string> $middlewares
* @return array<int, string>
*/
private function resolveDependencies(array $middlewares): array
{
@@ -168,7 +175,7 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
/**
* Get the explicit order array (for testing and debugging)
* @return array<string>
* @return array<int, string>
*/
private function getExplicitOrder(): array
{
@@ -287,8 +294,12 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
return MiddlewarePriority::BUSINESS_LOGIC->value;
}
// Methode zum Sortieren der Middleware nach Priorität
private function sortMiddlewaresByPriority($middlewares): array
/**
* Methode zum Sortieren der Middleware nach Priorität
* @param array<int, object|string> $middlewares
* @return array<int, object|string>
*/
private function sortMiddlewaresByPriority(array $middlewares): array
{
usort($middlewares, function (object|string $a, object|string $b) {
$priorityA = $this->getMiddlewarePriority($a);
@@ -343,6 +354,8 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
/**
* Get cache statistics for debugging
*
* @return array<string, string>
*/
public function getCacheStats(): array
{

View File

@@ -56,9 +56,13 @@ final readonly class ApiVersioningMiddleware implements HttpMiddleware
$context = $next($context);
// Add version headers to response
$response = $this->addVersionHeaders($context->response, $requestedVersion);
if ($context->response instanceof HttpResponse) {
$response = $this->addVersionHeaders($context->response, $requestedVersion);
return $context->withResponse($response);
return $context->withResponse($response);
}
return $context;
}
public function validateRouteVersion(ApiVersionAttribute $versionAttribute, ApiVersion $requestedVersion): bool
@@ -124,7 +128,7 @@ final readonly class ApiVersioningMiddleware implements HttpMiddleware
'Content-Type' => 'application/json',
'API-Version' => $this->config->getLatestVersion()->toString(),
]),
body: $this->jsonSerializer->serialize($error)
body: $this->jsonSerializer->serialize($error) ?: '{"error":"Serialization failed"}'
);
}
}

View File

@@ -14,6 +14,7 @@ use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Next;
use App\Framework\Http\Request;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\StateKey;
use App\Framework\Meta\MetaData;
@@ -25,6 +26,7 @@ use App\Framework\Router\Result\ContentNegotiationResult;
use App\Framework\Router\Result\JsonResult;
use App\Framework\Router\Result\Redirect;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Router\RouteContext;
use App\Framework\Router\RouteDispatcher;
use App\Framework\Router\Router;
@@ -64,6 +66,7 @@ final readonly class RoutingMiddleware implements HttpMiddleware
try {
// Measure route matching
/** @var RouteContext $routeContext */
$routeContext = $this->performanceService->measure(
'route_matching',
fn () => $this->router->match($request),
@@ -150,7 +153,7 @@ final readonly class RoutingMiddleware implements HttpMiddleware
/**
* Perform IP and namespace-based authentication checks
*/
private function performAuthenticationChecks($request, $routeContext): void
private function performAuthenticationChecks(Request $request, RouteContext $routeContext): void
{
$clientIp = $this->getClientIp($request);
$controllerClass = $routeContext->match->route->controller;
@@ -182,7 +185,7 @@ final readonly class RoutingMiddleware implements HttpMiddleware
/**
* Get client IP address
*/
private function getClientIp($request): IpAddress
private function getClientIp(Request $request): IpAddress
{
return $request->server->getClientIp() ?? IpAddress::localhost();
}

View File

@@ -6,6 +6,7 @@ namespace App\Framework\Http;
final class MimeTypeResolver
{
/** @var array<string, MimeType> */
private const EXTENSION_MAP = [
// Text
'txt' => MimeType::TEXT_PLAIN,
@@ -65,6 +66,7 @@ final class MimeTypeResolver
'otf' => MimeType::FONT_OTF,
];
/** @var array<string, array<int, string>> */
private const MIME_TYPE_EXTENSIONS = [
MimeType::TEXT_PLAIN->value => ['txt'],
MimeType::TEXT_HTML->value => ['html', 'htm'],
@@ -127,6 +129,9 @@ final class MimeTypeResolver
return self::fromExtension($extension);
}
/**
* @return array<int, string>
*/
public static function getExtensions(MimeType $mimeType): array
{
return self::MIME_TYPE_EXTENSIONS[$mimeType->value] ?? [];

View File

@@ -25,4 +25,31 @@ final readonly class Query
{
return isset($this->query[$key]);
}
public function getBool(string $key, bool $default = false): bool
{
$value = $this->get($key);
if ($value === null) {
return $default;
}
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
public function getInt(string $key, int $default = 0): int
{
$value = $this->get($key);
if ($value === null) {
return $default;
}
return (int) $value;
}
public function getFloat(string $key, float $default = 0.0): float
{
$value = $this->get($key);
if ($value === null) {
return $default;
}
return (float) $value;
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Router\ValueObjects\RouteParameters;
interface Request
{
@@ -28,4 +29,14 @@ interface Request
public RequestBody $parsedBody {get;}
public UploadedFiles $files {get;}
public RouteParameters $routeParameters {get;}
public Query $query {get;}
/**
* Get query parameter value
* @deprecated use $request->query->get() instead
*/
public function getQuery(string $key, mixed $default = null): mixed;
}

View File

@@ -15,8 +15,7 @@ final readonly class RequestFactory
{
public function __construct(
private HttpRequestParser $parser
) {
}
) {}
/**
* Create a request from PHP globals

View File

@@ -9,7 +9,7 @@ use WeakMap;
final class RequestStateManager
{
/**
* @param WeakMap<object, mixed> $requestStates
* @param WeakMap<object, array<string, mixed>> $requestStates
*/
public function __construct(
private WeakMap $requestStates,

View File

@@ -10,7 +10,7 @@ namespace App\Framework\Http;
final readonly class ResolvedMiddlewareStack
{
/**
* @param array<string> $middlewares
* @param array<int, string> $middlewares
* @param array<string, mixed> $dependencyGraph
*/
public function __construct(
@@ -21,7 +21,7 @@ final readonly class ResolvedMiddlewareStack
/**
* Get middlewares in resolved execution order
* @return array<string>
* @return array<int, string>
*/
public function getMiddlewares(): array
{
@@ -55,7 +55,7 @@ final readonly class ResolvedMiddlewareStack
/**
* Get dependencies for a specific middleware
* @return array<string>
* @return array<int, string>
*/
public function getDependenciesFor(string $middlewareClass): array
{
@@ -64,7 +64,7 @@ final readonly class ResolvedMiddlewareStack
/**
* Get services that a middleware provides
* @return array<string>
* @return array<int, string>
*/
public function getProvidedBy(string $middlewareClass): array
{
@@ -81,10 +81,11 @@ final readonly class ResolvedMiddlewareStack
/**
* Get all critical middlewares
* @return array<string>
* @return array<int, string>
*/
public function getCriticalMiddlewares(): array
{
/** @var array<int, string> */
$critical = [];
foreach ($this->middlewares as $middleware) {
@@ -117,6 +118,7 @@ final readonly class ResolvedMiddlewareStack
*/
private function getDependencyChains(): array
{
/** @var array<string, array<int, string>> */
$chains = [];
foreach ($this->middlewares as $middleware) {
@@ -131,11 +133,13 @@ final readonly class ResolvedMiddlewareStack
/**
* Detect any circular dependencies in the graph
* @return array<string>
* @return array<int, array<int, string>>
*/
private function detectCircularDependencies(): array
{
/** @var array<int, array<int, string>> */
$circular = [];
/** @var array<string, bool> */
$visited = [];
foreach (array_keys($this->dependencyGraph) as $node) {
@@ -161,8 +165,10 @@ final readonly class ResolvedMiddlewareStack
{
$visited[$node] = true;
$stack[$node] = true;
/** @var array<int, array<int, string>> */
$circular = [];
/** @var array<int, string> */
$dependencies = $this->dependencyGraph[$node]['dependencies'] ?? [];
foreach ($dependencies as $dependency) {

View File

@@ -226,6 +226,8 @@ final class FlashManager
/**
* Debug-Informationen für var_dump etc.
*
* @return array<string, mixed>
*/
public function __debugInfo(): array
{

View File

@@ -98,6 +98,11 @@ final class Session implements SessionInterface
);
}
public function getId(): SessionId
{
return $this->id;
}
/**
* @param array<string, mixed> $data
*/

View File

@@ -21,5 +21,13 @@ interface SessionInterface
public function all(): array;
public function getId(): SessionId;
public CsrfProtection $csrf { get; }
public ValidationErrorBag $validation { get; }
public FormDataStorage $form { get; }
public static function fromArray(SessionId $sessionId, Clock $clock, CsrfTokenGenerator $csrfTokenGenerator, array $data): self;
}

View File

@@ -48,7 +48,7 @@ final readonly class UploadedFiles
/**
* Get array of all field names
* @return string[]
* @return array<int, string>
*/
public function keys(): array
{
@@ -87,6 +87,7 @@ final readonly class UploadedFiles
*/
public static function fromFilesArray(array $filesArray): self
{
/** @var array<string, UploadedFile|array<int, UploadedFile>> */
$files = [];
foreach ($filesArray as $key => $fileSpec) {
@@ -100,6 +101,9 @@ final readonly class UploadedFiles
return new self($files);
}
/**
* @param array<string, mixed> $fileSpec
*/
private static function createSingleFile(array $fileSpec): UploadedFile
{
return new UploadedFile(
@@ -111,9 +115,15 @@ final readonly class UploadedFiles
);
}
/**
* @param array<string, mixed> $fileSpec
* @return array<int, UploadedFile>
*/
private static function createMultipleFiles(array $fileSpec): array
{
/** @var array<int, UploadedFile> */
$files = [];
/** @var array<int, string> */
$names = $fileSpec['name'] ?? [];
foreach ($names as $index => $name) {

View File

@@ -28,4 +28,9 @@ final readonly class Uri
{
return $this->value;
}
public function toString(): string
{
return $this->value;
}
}

View File

@@ -90,6 +90,7 @@ final readonly class ETag
/**
* Parse multiple ETags from If-None-Match header
* @return array<int, string|self>
*/
public static function parseMultiple(string $headerValue): array
{