chore: complete update
This commit is contained in:
43
src/Framework/Exception/ConsoleException.php
Normal file
43
src/Framework/Exception/ConsoleException.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final class ConsoleException extends FrameworkException
|
||||
{
|
||||
public static function commandFailed(string $command, int $exitCode, string $output = ''): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('console.command', 'ConsoleRunner')
|
||||
->withData([
|
||||
'command' => $command,
|
||||
'exit_code' => $exitCode,
|
||||
'output' => $output,
|
||||
'working_directory' => getcwd()
|
||||
]);
|
||||
|
||||
return new self("Console command failed: {$command}", $exitCode, null, $context);
|
||||
}
|
||||
|
||||
public static function commandNotFound(string $command): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('console.command_lookup', 'ConsoleRunner')
|
||||
->withData([
|
||||
'command' => $command
|
||||
]);
|
||||
|
||||
return new self("Console command not found: {$command}", 404, null, $context);
|
||||
}
|
||||
|
||||
public static function invalidArguments(string $command, array $arguments, array $errors): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('console.argument_validation', 'ConsoleRunner')
|
||||
->withData([
|
||||
'command' => $command,
|
||||
'arguments' => $arguments,
|
||||
'validation_errors' => $errors
|
||||
]);
|
||||
|
||||
return new self("Invalid arguments for command: {$command}", 400, null, $context);
|
||||
}
|
||||
}
|
||||
61
src/Framework/Exception/DatabaseException.php
Normal file
61
src/Framework/Exception/DatabaseException.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final class DatabaseException extends FrameworkException
|
||||
{
|
||||
public static function connectionFailed(string $dsn, \Throwable $previous): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('database.connect', 'PDO')
|
||||
->withData([
|
||||
'dsn' => self::sanitizeDsn($dsn),
|
||||
'driver' => explode(':', $dsn)[0] ?? 'unknown'
|
||||
]);
|
||||
|
||||
return new self('Database connection failed', 0, $previous, $context);
|
||||
}
|
||||
|
||||
public static function queryFailed(string $query, array $params, \Throwable $previous): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('database.query', 'PDO')
|
||||
->withData([
|
||||
'query' => $query,
|
||||
'parameters' => $params,
|
||||
'query_type' => self::detectQueryType($query)
|
||||
]);
|
||||
|
||||
return new self('Database query failed', 0, $previous, $context);
|
||||
}
|
||||
|
||||
public static function transactionFailed(string $operation, \Throwable $previous): self
|
||||
{
|
||||
$context = ExceptionContext::forOperation('database.transaction', 'PDO')
|
||||
->withData([
|
||||
'transaction_operation' => $operation
|
||||
]);
|
||||
|
||||
return new self("Database transaction failed: {$operation}", 0, $previous, $context);
|
||||
}
|
||||
|
||||
private static function sanitizeDsn(string $dsn): string
|
||||
{
|
||||
return preg_replace('/password=[^;]+/', 'password=[REDACTED]', $dsn);
|
||||
}
|
||||
|
||||
private static function detectQueryType(string $query): string
|
||||
{
|
||||
$query = trim(strtoupper($query));
|
||||
return match (true) {
|
||||
str_starts_with($query, 'SELECT') => 'SELECT',
|
||||
str_starts_with($query, 'INSERT') => 'INSERT',
|
||||
str_starts_with($query, 'UPDATE') => 'UPDATE',
|
||||
str_starts_with($query, 'DELETE') => 'DELETE',
|
||||
str_starts_with($query, 'CREATE') => 'CREATE',
|
||||
str_starts_with($query, 'DROP') => 'DROP',
|
||||
str_starts_with($query, 'ALTER') => 'ALTER',
|
||||
default => 'UNKNOWN'
|
||||
};
|
||||
}
|
||||
}
|
||||
30
src/Framework/Exception/DirectoryCreateException.php
Normal file
30
src/Framework/Exception/DirectoryCreateException.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final class DirectoryCreateException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $directory,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
$context = ExceptionContext::forOperation('filesystem.create_directory', 'FileSystem')
|
||||
->withData([
|
||||
'directory' => $directory,
|
||||
'permissions' => is_dir(dirname($directory)) ? decoct(fileperms(dirname($directory))) : 'unknown',
|
||||
'parent_exists' => is_dir(dirname($directory)),
|
||||
'parent_writable' => is_writable(dirname($directory)),
|
||||
'disk_free_space' => disk_free_space(dirname($directory))
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: "Ordner '$directory' konnte nicht angelegt werden.",
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: $context
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/Framework/Exception/DirectoryListException.php
Normal file
30
src/Framework/Exception/DirectoryListException.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final class DirectoryListException extends FrameworkException
|
||||
{
|
||||
public function __construct(
|
||||
string $directory,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
$context = ExceptionContext::forOperation('filesystem.list_directory', 'FileSystem')
|
||||
->withData([
|
||||
'directory' => $directory,
|
||||
'exists' => is_dir($directory),
|
||||
'readable' => is_readable($directory),
|
||||
'permissions' => is_dir($directory) ? decoct(fileperms($directory)) : 'unknown',
|
||||
'file_count' => is_readable($directory) ? count(scandir($directory)) - 2 : 'unknown'
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: "Fehler beim Auslesen des Verzeichnisses '$directory'.",
|
||||
code: $code,
|
||||
previous: $previous,
|
||||
context: $context
|
||||
);
|
||||
}
|
||||
}
|
||||
208
src/Framework/Exception/ErrorHandlerContext.php
Normal file
208
src/Framework/Exception/ErrorHandlerContext.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
|
||||
/**
|
||||
* Vollständiger Kontext für Error-Handling - kombiniert alle Kontext-Typen
|
||||
*/
|
||||
final readonly class ErrorHandlerContext
|
||||
{
|
||||
public function __construct(
|
||||
public ExceptionContext $exception,
|
||||
public RequestContext $request,
|
||||
public SystemContext $system,
|
||||
public array $metadata = []
|
||||
) {}
|
||||
|
||||
public static function create(
|
||||
ExceptionContext $exceptionContext,
|
||||
?RequestContext $requestContext = null,
|
||||
?SystemContext $systemContext = null,
|
||||
array $metadata = []
|
||||
): self {
|
||||
return new self(
|
||||
exception: $exceptionContext,
|
||||
request: $requestContext ?? RequestContext::fromGlobals(),
|
||||
system: $systemContext ?? SystemContext::current(),
|
||||
metadata: $metadata
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromException(\Throwable $exception, array $metadata = []): self
|
||||
{
|
||||
$exceptionContext = ExceptionContext::empty();
|
||||
|
||||
if ($exception instanceof FrameworkException) {
|
||||
$exceptionContext = $exception->getContext();
|
||||
}
|
||||
|
||||
return self::create($exceptionContext, metadata: $metadata);
|
||||
}
|
||||
|
||||
public function withMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
exception: $this->exception,
|
||||
request: $this->request,
|
||||
system: $this->system,
|
||||
metadata: array_merge($this->metadata, $metadata)
|
||||
);
|
||||
}
|
||||
|
||||
public function withRequest(RequestContext $request): self
|
||||
{
|
||||
return new self(
|
||||
exception: $this->exception,
|
||||
request: $request,
|
||||
system: $this->system,
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
public function withSystem(SystemContext $system): self
|
||||
{
|
||||
return new self(
|
||||
exception: $this->exception,
|
||||
request: $this->request,
|
||||
system: $system,
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'exception' => $this->exception->toArray(),
|
||||
'request' => $this->request->toArray(),
|
||||
'system' => $this->system->toArray(),
|
||||
'metadata' => $this->metadata
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt eine flache Struktur zurück für einfache Serialisierung
|
||||
*/
|
||||
public function toFlatArray(): array
|
||||
{
|
||||
$contexts = [
|
||||
'exception' => $this->exception->toArray(),
|
||||
'request' => $this->request->toArray(),
|
||||
'system' => $this->system->toArray()
|
||||
];
|
||||
|
||||
$flattened = [];
|
||||
foreach ($contexts as $prefix => $data) {
|
||||
array_walk($data, function($value, $key) use (&$flattened, $prefix) {
|
||||
$flattened[$prefix . '_' . $key] = $value;
|
||||
});
|
||||
}
|
||||
|
||||
return array_merge($flattened, $this->metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert nur relevante Daten für Logging
|
||||
*/
|
||||
public function forLogging(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->exception->operation,
|
||||
'component' => $this->exception->component,
|
||||
'client_ip' => $this->request->clientIp,
|
||||
'request_method' => $this->request->requestMethod,
|
||||
'request_uri' => $this->request->requestUri,
|
||||
'memory_usage' => $this->system->memoryUsage,
|
||||
'execution_time' => $this->system->executionTime,
|
||||
'data' => $this->exception->data,
|
||||
'metadata' => $this->metadata
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Security-Event-Log im OWASP-Format
|
||||
*/
|
||||
public function toSecurityEventFormat(string $appId = 'app'): array
|
||||
{
|
||||
$exceptionData = $this->exception->toArray();
|
||||
|
||||
// Basis-Format
|
||||
$securityLog = [
|
||||
'datetime' => date('c'),
|
||||
'appid' => $appId,
|
||||
'level' => 'INFO',
|
||||
'description' => 'No description',
|
||||
'useragent' => $this->request->userAgent,
|
||||
'source_ip' => $this->request->clientIp,
|
||||
'host_ip' => $this->request->hostIp,
|
||||
'hostname' => $this->request->hostname,
|
||||
'protocol' => $this->request->protocol,
|
||||
'port' => $this->request->port,
|
||||
'request_uri' => $this->request->requestUri,
|
||||
'request_method' => $this->request->requestMethod,
|
||||
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
|
||||
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown'
|
||||
];
|
||||
|
||||
// Security-Event-spezifische Daten falls verfügbar
|
||||
if (isset($exceptionData['metadata']['security_event'])) {
|
||||
$securityLog['event'] = $exceptionData['metadata']['security_event'];
|
||||
$securityLog['level'] = $exceptionData['metadata']['security_level'] ?? 'INFO';
|
||||
$securityLog['description'] = $exceptionData['metadata']['security_description'] ?? 'Security event';
|
||||
} else {
|
||||
// Fallback für normale Exceptions
|
||||
$securityLog['event'] = 'application_error';
|
||||
$securityLog['level'] = 'ERROR';
|
||||
$securityLog['description'] = 'Application error occurred';
|
||||
}
|
||||
|
||||
return $securityLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert JSON für Security-Event-Log
|
||||
*/
|
||||
public function toSecurityEventJson(string $appId = 'app'): string
|
||||
{
|
||||
return json_encode($this->toSecurityEventFormat($appId), JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory-Methode um ErrorHandlerContext aus Exception zu erstellen
|
||||
*/
|
||||
/*public static function fromException(Throwable $exception, ?MiddlewareContext $middlewareContext = null): self
|
||||
{
|
||||
// Request-Context aufbauen
|
||||
$requestContext = RequestContext::create(
|
||||
clientIp: $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
requestMethod: $_SERVER['REQUEST_METHOD'] ?? 'CLI',
|
||||
requestUri: $_SERVER['REQUEST_URI'] ?? '-',
|
||||
hostIp: $_SERVER['SERVER_ADDR'] ?? 'unknown',
|
||||
hostname: $_SERVER['HTTP_HOST'] ?? 'localhost',
|
||||
protocol: $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1',
|
||||
port: $_SERVER['SERVER_PORT'] ?? '80',
|
||||
requestId: $middlewareContext?->requestId ?? bin2hex(random_bytes(8))
|
||||
);
|
||||
|
||||
// Exception-Context
|
||||
$exceptionContext = ExceptionContext::fromThrowable($exception);
|
||||
|
||||
// System-Context
|
||||
$systemContext = SystemContext::create(
|
||||
memoryUsage: memory_get_usage(true),
|
||||
peakMemoryUsage: memory_get_peak_usage(true),
|
||||
executionTime: microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))
|
||||
);
|
||||
|
||||
return new self(
|
||||
exception: $exceptionContext,
|
||||
request: $requestContext,
|
||||
system: $systemContext,
|
||||
metadata: []
|
||||
);
|
||||
}*/
|
||||
}
|
||||
97
src/Framework/Exception/ExceptionContext.php
Normal file
97
src/Framework/Exception/ExceptionContext.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
/**
|
||||
* Fachlicher Kontext für Exceptions - nur domain-spezifische Informationen
|
||||
*/
|
||||
final readonly class ExceptionContext
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $operation = null,
|
||||
public ?string $component = null,
|
||||
public array $data = [],
|
||||
public array $debug = [],
|
||||
public array $metadata = []
|
||||
) {}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function forOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(operation: $operation, component: $component);
|
||||
}
|
||||
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
operation: $operation,
|
||||
component: $component ?? $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
public function withData(array $data): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: array_merge($this->data, $data),
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
public function withDebug(array $debug): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: array_merge($this->debug, $debug),
|
||||
metadata: $this->metadata
|
||||
);
|
||||
}
|
||||
|
||||
public function withMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: array_merge($this->metadata, $metadata)
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->operation,
|
||||
'component' => $this->component,
|
||||
'data' => $this->sanitizeData($this->data),
|
||||
'debug' => $this->debug,
|
||||
'metadata' => $this->metadata
|
||||
];
|
||||
}
|
||||
|
||||
private function sanitizeData(array $data): array
|
||||
{
|
||||
$sensitiveKeys = ['password', 'token', 'api_key', 'secret', 'private_key'];
|
||||
|
||||
array_walk_recursive($data, function (&$value, $key) use ($sensitiveKeys) {
|
||||
if (in_array(strtolower($key), $sensitiveKeys)) {
|
||||
$value = '[REDACTED]';
|
||||
}
|
||||
});
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
73
src/Framework/Exception/FrameworkException.php
Normal file
73
src/Framework/Exception/FrameworkException.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
class FrameworkException extends \RuntimeException
|
||||
{
|
||||
protected ExceptionContext $context;
|
||||
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
?ExceptionContext $context = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->context = $context ?? ExceptionContext::empty();
|
||||
}
|
||||
|
||||
public function getContext(): ExceptionContext
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
public function withContext(ExceptionContext $context): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->context = $context;
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return $this->withContext(
|
||||
$this->context->withOperation($operation, $component)
|
||||
);
|
||||
}
|
||||
|
||||
public function withData(array $data): self
|
||||
{
|
||||
return $this->withContext(
|
||||
$this->context->withData($data)
|
||||
);
|
||||
}
|
||||
|
||||
public function withDebug(array $debug): self
|
||||
{
|
||||
return $this->withContext(
|
||||
$this->context->withDebug($debug)
|
||||
);
|
||||
}
|
||||
|
||||
public function withMetadata(array $metadata): self
|
||||
{
|
||||
return $this->withContext(
|
||||
$this->context->withMetadata($metadata)
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'class' => static::class,
|
||||
'message' => $this->getMessage(),
|
||||
'code' => $this->getCode(),
|
||||
'file' => $this->getFile(),
|
||||
'line' => $this->getLine(),
|
||||
'context' => $this->context->toArray(),
|
||||
'trace' => $this->getTraceAsString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
22
src/Framework/Exception/InErrorHandler.md
Normal file
22
src/Framework/Exception/InErrorHandler.md
Normal file
@@ -0,0 +1,22 @@
|
||||
```` php
|
||||
// Im ErrorHandler wird jetzt ErrorHandlerContext verwendet
|
||||
private function createErrorContext(Throwable $exception, ?MiddlewareContext $context = null): ErrorContext
|
||||
{
|
||||
$handlerContext = ErrorHandlerContext::fromException($exception, [
|
||||
'request_id' => $context?->requestId ?? $this->requestIdGenerator->generate(),
|
||||
'timestamp' => date('c'),
|
||||
'environment' => $_ENV['APP_ENV'] ?? 'production',
|
||||
'debug_mode' => $this->isDebugMode
|
||||
]);
|
||||
|
||||
return new ErrorContext(
|
||||
exception: $exception,
|
||||
level: $this->determineErrorLevel($exception),
|
||||
requestId: $context?->requestId ?? $this->requestIdGenerator->generate(),
|
||||
context: $handlerContext, // Jetzt ErrorHandlerContext statt ExceptionContext
|
||||
additionalData: [
|
||||
'memory_usage' => memory_get_peak_usage(true),
|
||||
]
|
||||
);
|
||||
}
|
||||
````
|
||||
95
src/Framework/Exception/RequestContext.php
Normal file
95
src/Framework/Exception/RequestContext.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final readonly class RequestContext
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $hostIp = null,
|
||||
public ?string $hostname = null,
|
||||
public ?string $protocol = null,
|
||||
public ?string $port = null,
|
||||
public ?string $requestUri = null,
|
||||
public ?string $requestMethod = null,
|
||||
public ?string $userAgent = null,
|
||||
public ?string $clientIp = null,
|
||||
public array $headers = []
|
||||
) {}
|
||||
|
||||
public static function fromGlobals(): self
|
||||
{
|
||||
return new self(
|
||||
hostIp: $_SERVER['SERVER_ADDR'] ?? null,
|
||||
hostname: $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? null,
|
||||
protocol: isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
|
||||
port: $_SERVER['SERVER_PORT'] ?? null,
|
||||
requestUri: $_SERVER['REQUEST_URI'] ?? null,
|
||||
requestMethod: $_SERVER['REQUEST_METHOD'] ?? null,
|
||||
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
clientIp: self::getClientIp(),
|
||||
headers: self::getHeaders()
|
||||
);
|
||||
}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'host_ip' => $this->hostIp,
|
||||
'hostname' => $this->hostname,
|
||||
'protocol' => $this->protocol,
|
||||
'port' => $this->port,
|
||||
'request_uri' => $this->requestUri,
|
||||
'request_method' => $this->requestMethod,
|
||||
'user_agent' => $this->userAgent,
|
||||
'client_ip' => $this->clientIp,
|
||||
'headers' => $this->headers
|
||||
];
|
||||
}
|
||||
|
||||
private static function getClientIp(): ?string
|
||||
{
|
||||
$ipKeys = [
|
||||
'HTTP_CF_CONNECTING_IP', // Cloudflare
|
||||
'HTTP_X_FORWARDED_FOR', // Load balancer/proxy
|
||||
'HTTP_X_FORWARDED', // Proxy
|
||||
'HTTP_X_CLUSTER_CLIENT_IP', // Cluster
|
||||
'HTTP_FORWARDED_FOR', // Proxy
|
||||
'HTTP_FORWARDED', // Proxy
|
||||
'REMOTE_ADDR' // Standard
|
||||
];
|
||||
|
||||
foreach ($ipKeys as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$ips = explode(',', $_SERVER[$key]);
|
||||
$ip = trim($ips[0]);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $_SERVER['REMOTE_ADDR'] ?? null;
|
||||
}
|
||||
|
||||
private static function getHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
|
||||
foreach ($_SERVER as $key => $value) {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$header = str_replace('_', '-', substr($key, 5));
|
||||
$header = ucwords(strtolower($header), '-');
|
||||
$headers[$header] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Account-Sperrung Event
|
||||
*/
|
||||
final readonly class AuthenticationAccountLockedEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public int $attempts
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authn_account_locked:{$this->userId},{$this->attempts}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} account locked after {$this->attempts} failed attempts";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authentication';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// Account-Sperrungen sind immer alert-würdig
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'attempts' => $this->attempts,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Login-Fehler Event
|
||||
*/
|
||||
final readonly class AuthenticationLoginFailedEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public string $reason = 'invalid_credentials'
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authn_login_fail:{$this->userId}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} login failure";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authentication';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// Login-Fehler sind nicht kritisch genug für sofortige Alerts
|
||||
return false;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'reason' => $this->reason,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Login-Erfolg nach Fehlversuchen Event
|
||||
*/
|
||||
final readonly class AuthenticationLoginSuccessAfterFailEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public int $retries
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authn_login_successafterfail:{$this->userId},{$this->retries}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} login successfully after {$this->retries} failures";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authentication';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// Alert wenn viele Fehlversuche vorausgingen
|
||||
return $this->retries >= 5;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'retries' => $this->retries,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Login-Erfolg Event
|
||||
*/
|
||||
final readonly class AuthenticationLoginSuccessEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authn_login_success:{$this->userId}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} login successfully";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::INFO;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authentication';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Zugriff verweigert Event
|
||||
*/
|
||||
final readonly class AuthorizationAccessDeniedEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public string $resource,
|
||||
public string $action = 'access'
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authz_fail:{$this->userId},{$this->resource}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} authorization failure for {$this->resource}";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authorization';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'resource' => $this->resource,
|
||||
'action' => $this->action,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Admin-Aktion Event
|
||||
*/
|
||||
final readonly class AuthorizationAdminActionEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $userId,
|
||||
public string $resource,
|
||||
public string $action = 'admin_action'
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "authz_admin:{$this->userId},{$this->resource}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->userId} administrative action on {$this->resource}";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'authorization';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// Admin-Aktionen sind immer beobachtungswürdig
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'userId' => $this->userId,
|
||||
'resource' => $this->resource,
|
||||
'action' => $this->action,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityException;
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* SQL-Injection-Versuch Event
|
||||
*/
|
||||
final readonly class InputSqlInjectionAttemptEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $field,
|
||||
public string $detectedPattern = 'generic_sql_pattern'
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "input_sql_injection:{$this->field}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "SQL injection attempt detected in field {$this->field}";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'input_validation';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// SQL-Injection-Versuche sind immer kritisch
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'field' => $this->field,
|
||||
'detectedPattern' => $this->detectedPattern,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* XSS-Versuch Event
|
||||
*/
|
||||
final readonly class InputXssAttemptEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $field,
|
||||
public string $detectedPattern = 'generic_xss_pattern'
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "input_xss_attempt:{$this->field}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "XSS attack attempt detected in field {$this->field}";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'input_validation';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'field' => $this->field,
|
||||
'detectedPattern' => $this->detectedPattern,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Interface für alle Security-Event Value Objects
|
||||
*/
|
||||
interface SecurityEventInterface
|
||||
{
|
||||
/**
|
||||
* Gibt den OWASP-Event-Identifier zurück (z.B. "authn_login_fail:user123")
|
||||
*/
|
||||
public function getEventIdentifier(): string;
|
||||
|
||||
/**
|
||||
* Gibt die OWASP-Beschreibung zurück (z.B. "User user123 login failure")
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Gibt das Log-Level zurück
|
||||
*/
|
||||
public function getLogLevel(): SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Prüft ob Event kritisch ist und Alerting erfordert
|
||||
*/
|
||||
public function requiresAlert(): bool;
|
||||
|
||||
/**
|
||||
* Gibt die Event-Daten als Array zurück
|
||||
*/
|
||||
public function toArray(): array;
|
||||
|
||||
/**
|
||||
* Gibt die Event-Kategorie zurück (auth, authz, input, etc.)
|
||||
*/
|
||||
public function getCategory(): string;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\SecurityEvent;
|
||||
|
||||
use App\Framework\Exception\SecurityLogLevel;
|
||||
|
||||
/**
|
||||
* Exzessive Nutzung Event
|
||||
*/
|
||||
final readonly class SystemExcessiveUseEvent implements SecurityEventInterface
|
||||
{
|
||||
public function __construct(
|
||||
public string $identifier,
|
||||
public int $limit,
|
||||
public int $currentUsage
|
||||
) {}
|
||||
|
||||
public function getEventIdentifier(): string
|
||||
{
|
||||
return "excessive_use:{$this->identifier},{$this->limit}";
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Excessive usage detected for {$this->identifier} exceeding limit {$this->limit}";
|
||||
}
|
||||
|
||||
public function getLogLevel(): SecurityLogLevel
|
||||
{
|
||||
return SecurityLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getCategory(): string
|
||||
{
|
||||
return 'system';
|
||||
}
|
||||
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
// Rate-Limiting-Verletzungen sind immer alert-würdig
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'identifier' => $this->identifier,
|
||||
'limit' => $this->limit,
|
||||
'currentUsage' => $this->currentUsage,
|
||||
'event_identifier' => $this->getEventIdentifier(),
|
||||
'category' => $this->getCategory(),
|
||||
'log_level' => $this->getLogLevel()->value,
|
||||
'requires_alert' => $this->requiresAlert()
|
||||
];
|
||||
}
|
||||
}
|
||||
103
src/Framework/Exception/SecurityException.php
Normal file
103
src/Framework/Exception/SecurityException.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
use App\Framework\Exception\SecurityEvent\SecurityEventInterface;
|
||||
|
||||
/**
|
||||
* Security-spezifische Exception mit OWASP-konformen Event-Logging
|
||||
*/
|
||||
class SecurityException extends FrameworkException
|
||||
{
|
||||
protected SecurityEventInterface $securityEvent;
|
||||
|
||||
public function __construct(
|
||||
SecurityEventInterface $securityEvent,
|
||||
string $message = '',
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
?ExceptionContext $additionalContext = null
|
||||
) {
|
||||
$this->securityEvent = $securityEvent;
|
||||
|
||||
// Erstelle Security-Context
|
||||
$context = $this->createSecurityContext($securityEvent, $additionalContext);
|
||||
|
||||
// Verwende Event-Beschreibung als Message falls nicht gesetzt
|
||||
$finalMessage = $message ?: $securityEvent->getDescription();
|
||||
|
||||
parent::__construct($finalMessage, $code, $previous, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory Method für Security-Events
|
||||
*/
|
||||
public static function fromEvent(SecurityEventInterface $event, string $message = '', int $code = 0): self
|
||||
{
|
||||
return new self($event, $message, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt Security-spezifischen Context
|
||||
*/
|
||||
private function createSecurityContext(
|
||||
SecurityEventInterface $securityEvent,
|
||||
?ExceptionContext $additionalContext
|
||||
): ExceptionContext {
|
||||
$baseContext = ExceptionContext::forOperation(
|
||||
'security.' . $securityEvent->getCategory(),
|
||||
'Security'
|
||||
)->withData([
|
||||
'event_type' => $securityEvent->getEventIdentifier(),
|
||||
'event_category' => $securityEvent->getCategory(),
|
||||
'event_data' => $securityEvent->toArray(),
|
||||
'client_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null,
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'] ?? null,
|
||||
'timestamp' => time()
|
||||
])->withMetadata([
|
||||
'security_event' => $securityEvent->getEventIdentifier(),
|
||||
'security_level' => $securityEvent->getLogLevel()->value,
|
||||
'security_description' => $securityEvent->getDescription(),
|
||||
'requires_alert' => $securityEvent->requiresAlert(),
|
||||
'event_category' => $securityEvent->getCategory()
|
||||
]);
|
||||
|
||||
// Merge mit zusätzlichem Context falls vorhanden
|
||||
if ($additionalContext) {
|
||||
$baseContext = $baseContext
|
||||
->withData($additionalContext->data)
|
||||
->withDebug($additionalContext->debug)
|
||||
->withMetadata($additionalContext->metadata);
|
||||
}
|
||||
|
||||
return $baseContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Security-Event zurück
|
||||
*/
|
||||
public function getSecurityEvent(): SecurityEventInterface
|
||||
{
|
||||
return $this->securityEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Security-Level zurück
|
||||
*/
|
||||
public function getSecurityLevel(): SecurityLogLevel
|
||||
{
|
||||
return $this->securityEvent->getLogLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Alert erforderlich ist
|
||||
*/
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return $this->securityEvent->requiresAlert();
|
||||
}
|
||||
}
|
||||
45
src/Framework/Exception/SecurityLogLevel.php
Normal file
45
src/Framework/Exception/SecurityLogLevel.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
/**
|
||||
* Security-spezifische Log-Level nach OWASP-Standard
|
||||
*/
|
||||
enum SecurityLogLevel: string
|
||||
{
|
||||
case DEBUG = 'DEBUG';
|
||||
case INFO = 'INFO';
|
||||
case WARN = 'WARN';
|
||||
case ERROR = 'ERROR';
|
||||
case FATAL = 'FATAL';
|
||||
|
||||
/**
|
||||
* Konvertiert zu Standard-PSR-Log-Level
|
||||
*/
|
||||
public function toPsrLevel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DEBUG => 'debug',
|
||||
self::INFO => 'info',
|
||||
self::WARN => 'warning',
|
||||
self::ERROR => 'error',
|
||||
self::FATAL => 'critical',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Numerischer Wert für Vergleiche
|
||||
*/
|
||||
public function getNumericValue(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::DEBUG => 100,
|
||||
self::INFO => 200,
|
||||
self::WARN => 300,
|
||||
self::ERROR => 400,
|
||||
self::FATAL => 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/Framework/Exception/SystemContext.php
Normal file
63
src/Framework/Exception/SystemContext.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final readonly class SystemContext
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $memoryUsage = null,
|
||||
public ?float $executionTime = null,
|
||||
public ?string $phpVersion = null,
|
||||
public ?string $frameworkVersion = null,
|
||||
public array $environment = []
|
||||
) {}
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
return new self(
|
||||
memoryUsage: self::formatBytes(memory_get_usage(true)),
|
||||
executionTime: isset($_SERVER['REQUEST_TIME_FLOAT'])
|
||||
? microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']
|
||||
: null,
|
||||
phpVersion: PHP_VERSION,
|
||||
frameworkVersion: '1.0.0', // Aus Config laden
|
||||
environment: [
|
||||
'os' => PHP_OS,
|
||||
'sapi' => PHP_SAPI,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'max_execution_time' => ini_get('max_execution_time')
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'memory_usage' => $this->memoryUsage,
|
||||
'execution_time' => $this->executionTime,
|
||||
'php_version' => $this->phpVersion,
|
||||
'framework_version' => $this->frameworkVersion,
|
||||
'environment' => $this->environment
|
||||
];
|
||||
}
|
||||
|
||||
private static function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
123
src/Framework/Exception/ValidationException.php
Normal file
123
src/Framework/Exception/ValidationException.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception;
|
||||
|
||||
final class ValidationException extends FrameworkException
|
||||
{
|
||||
public readonly ValidationResult $validationResult;
|
||||
public readonly array $errors;
|
||||
public readonly string $field;
|
||||
|
||||
/**
|
||||
* @param ValidationResult $validationResult Das Validierungsergebnis mit allen Fehlern
|
||||
* @param string|null $field Optionaler einzelner Feldname für Rückwärtskompatibilität
|
||||
*/
|
||||
public function __construct(
|
||||
ValidationResult $validationResult,
|
||||
?string $field = null
|
||||
) {
|
||||
$this->validationResult = $validationResult;
|
||||
|
||||
// Für Rückwärtskompatibilität: Wenn nur ein Feld angegeben wurde, verwende dessen Fehler
|
||||
if ($field !== null && $validationResult->getFieldErrors($field)) {
|
||||
$this->field = $field;
|
||||
$this->errors = $validationResult->getFieldErrors($field);
|
||||
} else {
|
||||
// Andernfalls verwende das erste Feld oder einen Standard
|
||||
$allErrors = $validationResult->getAll();
|
||||
$firstField = array_key_first($allErrors);
|
||||
$this->field = $firstField ?? 'unknown';
|
||||
$this->errors = $firstField ? $allErrors[$firstField] : [];
|
||||
}
|
||||
|
||||
// Erstelle eine aussagekräftige Fehlernachricht aus allen Fehlern
|
||||
$message = $this->createErrorMessage();
|
||||
|
||||
// Erstelle Exception-Kontext
|
||||
$context = ExceptionContext::forOperation('validation.validate', 'Validator')
|
||||
->withData([
|
||||
'failed_fields' => array_keys($validationResult->getAll()),
|
||||
'error_count' => count($validationResult->getAllErrorMessages()),
|
||||
'primary_field' => $this->field,
|
||||
'validation_errors' => $validationResult->getAll()
|
||||
]);
|
||||
|
||||
parent::__construct(message: $message, context: $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine strukturierte Fehlernachricht aus allen Validierungsfehlern
|
||||
*/
|
||||
private function createErrorMessage(): string
|
||||
{
|
||||
$allErrors = $this->validationResult->getAll();
|
||||
|
||||
if (empty($allErrors)) {
|
||||
return 'Unbekannter Validierungsfehler.';
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
foreach ($allErrors as $field => $fieldErrors) {
|
||||
$fieldMessage = $field . ': ' . implode(', ', $fieldErrors);
|
||||
$messages[] = $fieldMessage;
|
||||
}
|
||||
|
||||
return implode('; ', $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Fehlermeldungen für ein bestimmtes Feld zurück
|
||||
*
|
||||
* @param string $field Feldname
|
||||
* @return array<string> Liste der Fehlermeldungen für das Feld
|
||||
*/
|
||||
public function getFieldErrors(string $field): array
|
||||
{
|
||||
return $this->validationResult->getFieldErrors($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Fehlermeldungen als Array zurück
|
||||
*
|
||||
* @return array<string, string[]> Alle Fehlermeldungen gruppiert nach Feldern
|
||||
*/
|
||||
public function getAllErrors(): array
|
||||
{
|
||||
return $this->validationResult->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt alle Fehlermeldungen als flache Liste zurück
|
||||
*
|
||||
* @return array<string> Liste aller Fehlermeldungen
|
||||
*/
|
||||
public function getAllErrorMessages(): array
|
||||
{
|
||||
return $this->validationResult->getAllErrorMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein bestimmtes Feld Fehler hat
|
||||
*/
|
||||
public function hasFieldErrors(string $field): bool
|
||||
{
|
||||
return !empty($this->validationResult->getFieldErrors($field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Statische Factory-Methode für einfache Einzelfeld-Fehler (Rückwärtskompatibilität)
|
||||
*
|
||||
* @param array<string> $errors Liste der Fehlermeldungen
|
||||
* @param string $field Feldname
|
||||
* @return self
|
||||
*/
|
||||
public static function forField(array $errors, string $field): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
$validationResult->addErrors($field, $errors);
|
||||
|
||||
return new self($validationResult, $field);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user