Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -1,24 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Exceptions;
|
||||
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Framework\Validation\ValidationResult;
|
||||
|
||||
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
|
||||
* @param ErrorCode|null $errorCode Spezifischer ErrorCode, sonst automatisch bestimmt
|
||||
*/
|
||||
public function __construct(
|
||||
ValidationResult $validationResult,
|
||||
?string $field = null
|
||||
?string $field = null,
|
||||
?ErrorCode $errorCode = null
|
||||
) {
|
||||
$this->validationResult = $validationResult;
|
||||
|
||||
@@ -37,7 +44,24 @@ final class ValidationException extends FrameworkException
|
||||
// Erstelle eine aussagekräftige Fehlernachricht aus allen Fehlern
|
||||
$message = $this->createErrorMessage();
|
||||
|
||||
parent::__construct(message: $message);
|
||||
// Erstelle Exception-Kontext mit Validation-spezifischen Daten
|
||||
$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(),
|
||||
]);
|
||||
|
||||
// Bestimme ErrorCode basierend auf der Art der Validation Errors
|
||||
$finalErrorCode = $errorCode ?? $this->determineErrorCode($validationResult);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: 400, // Bad Request für Validation Errors
|
||||
errorCode: $finalErrorCode
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,9 +120,51 @@ final class ValidationException extends FrameworkException
|
||||
*/
|
||||
public function hasFieldErrors(string $field): bool
|
||||
{
|
||||
return !empty($this->validationResult->getFieldErrors($field));
|
||||
return ! empty($this->validationResult->getFieldErrors($field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den passenden ErrorCode basierend auf den Validation Errors
|
||||
*/
|
||||
private function determineErrorCode(ValidationResult $validationResult): ErrorCode
|
||||
{
|
||||
$allErrors = $validationResult->getAll();
|
||||
|
||||
// Wenn mehrere Felder betroffen sind
|
||||
if (count($allErrors) > 1) {
|
||||
return ErrorCode::VAL_BUSINESS_RULE_VIOLATION;
|
||||
}
|
||||
|
||||
// Analysiere die erste Fehlermeldung um den Typ zu bestimmen
|
||||
$firstErrors = reset($allErrors);
|
||||
if (empty($firstErrors)) {
|
||||
return ErrorCode::VAL_BUSINESS_RULE_VIOLATION;
|
||||
}
|
||||
|
||||
$firstError = $firstErrors[0];
|
||||
|
||||
// Einfache Heuristik basierend auf Fehlermeldungen
|
||||
if (str_contains(strtolower($firstError), 'required') || str_contains(strtolower($firstError), 'missing')) {
|
||||
return ErrorCode::VAL_REQUIRED_FIELD_MISSING;
|
||||
}
|
||||
|
||||
if (str_contains(strtolower($firstError), 'format') || str_contains(strtolower($firstError), 'invalid')) {
|
||||
return ErrorCode::VAL_INVALID_FORMAT;
|
||||
}
|
||||
|
||||
if (str_contains(strtolower($firstError), 'range') || str_contains(strtolower($firstError), 'length')) {
|
||||
return ErrorCode::VAL_OUT_OF_RANGE;
|
||||
}
|
||||
|
||||
if (str_contains(strtolower($firstError), 'duplicate') || str_contains(strtolower($firstError), 'exists')) {
|
||||
return ErrorCode::VAL_DUPLICATE_VALUE;
|
||||
}
|
||||
|
||||
return ErrorCode::VAL_BUSINESS_RULE_VIOLATION;
|
||||
}
|
||||
|
||||
// === Factory Methods ===
|
||||
|
||||
/**
|
||||
* Statische Factory-Methode für einfache Einzelfeld-Fehler (Rückwärtskompatibilität)
|
||||
*
|
||||
@@ -113,4 +179,76 @@ final class ValidationException extends FrameworkException
|
||||
|
||||
return new self($validationResult, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory für Required Field Fehler
|
||||
*/
|
||||
public static function requiredFieldMissing(string $fieldName): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
$validationResult->addErrors($fieldName, ["Field '$fieldName' is required"]);
|
||||
|
||||
return new self($validationResult, $fieldName, ErrorCode::VAL_REQUIRED_FIELD_MISSING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory für Format Fehler
|
||||
*/
|
||||
public static function invalidFormat(string $fieldName, string $expectedFormat): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
$validationResult->addErrors($fieldName, ["Field '$fieldName' must be in format: $expectedFormat"]);
|
||||
|
||||
return new self($validationResult, $fieldName, ErrorCode::VAL_INVALID_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory für Range Fehler
|
||||
*/
|
||||
public static function outOfRange(string $fieldName, $value, $min = null, $max = null): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
|
||||
$rangeMessage = "Field '$fieldName' value '$value' is out of range";
|
||||
if ($min !== null && $max !== null) {
|
||||
$rangeMessage .= " (allowed: $min - $max)";
|
||||
} elseif ($min !== null) {
|
||||
$rangeMessage .= " (minimum: $min)";
|
||||
} elseif ($max !== null) {
|
||||
$rangeMessage .= " (maximum: $max)";
|
||||
}
|
||||
|
||||
$validationResult->addErrors($fieldName, [$rangeMessage]);
|
||||
|
||||
return new self($validationResult, $fieldName, ErrorCode::VAL_OUT_OF_RANGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory für Duplicate Value Fehler
|
||||
*/
|
||||
public static function duplicateValue(string $fieldName, $value): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
$validationResult->addErrors($fieldName, ["Field '$fieldName' value '$value' already exists"]);
|
||||
|
||||
return new self($validationResult, $fieldName, ErrorCode::VAL_DUPLICATE_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory für Business Rule Violations
|
||||
*/
|
||||
public static function businessRuleViolation(string $rule, ?array $affectedFields = null): self
|
||||
{
|
||||
$validationResult = new ValidationResult();
|
||||
|
||||
if ($affectedFields) {
|
||||
foreach ($affectedFields as $field) {
|
||||
$validationResult->addErrors($field, ["Business rule violation: $rule"]);
|
||||
}
|
||||
} else {
|
||||
$validationResult->addErrors('general', ["Business rule violation: $rule"]);
|
||||
}
|
||||
|
||||
return new self($validationResult, null, ErrorCode::VAL_BUSINESS_RULE_VIOLATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -18,7 +19,7 @@ final readonly class Custom implements ValidationRule
|
||||
private Closure $validator,
|
||||
private array $messages = ['Der angegebene Wert ist ungültig.']
|
||||
) {
|
||||
if (!is_callable($validator)) {
|
||||
if (! is_callable($validator)) {
|
||||
throw new \InvalidArgumentException('Der Validator muss eine aufrufbare Funktion sein.');
|
||||
}
|
||||
}
|
||||
|
||||
84
src/Framework/Validation/Rules/DateFormat.php
Normal file
84
src/Framework/Validation/Rules/DateFormat.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
|
||||
use App\Framework\DateTime\DateTime;
|
||||
use App\Framework\DateTime\Exceptions\InvalidDateTimeException;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
final readonly class DateFormat implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* @param string $format Das erwartete Datumsformat (PHP DateTime Format)
|
||||
* @param bool $strict Ob das Format strikt validiert werden soll (exakte Länge)
|
||||
* @param string|null $timezone Zeitzone für die Validierung
|
||||
* @param string|null $message Benutzerdefinierte Fehlermeldung
|
||||
*/
|
||||
public function __construct(
|
||||
private string $format,
|
||||
private bool $strict = true,
|
||||
private ?string $timezone = null,
|
||||
private ?string $message = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verwende die bestehende DateTime-Klasse für die Validierung
|
||||
$dateTime = DateTime::fromFormat($value, $this->format, $this->timezone);
|
||||
|
||||
// Strikte Validierung: Prüfe ob das geparste Datum dem ursprünglichen String entspricht
|
||||
if ($this->strict) {
|
||||
$formatted = $dateTime->format($this->format);
|
||||
|
||||
return $formatted === $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (InvalidDateTimeException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getErrorMessages(): array
|
||||
{
|
||||
if ($this->message !== null) {
|
||||
return [$this->message];
|
||||
}
|
||||
|
||||
$formatExample = $this->getFormatExample();
|
||||
|
||||
return [
|
||||
"Bitte geben Sie ein gültiges Datum im Format '{$this->format}' ein" .
|
||||
($formatExample ? " (z.B. {$formatExample})" : '') . '.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Beispiel für das angegebene Format
|
||||
*/
|
||||
private function getFormatExample(): ?string
|
||||
{
|
||||
try {
|
||||
// Verwende ein bekanntes Datum für das Beispiel
|
||||
$exampleDate = DateTime::fromString('2024-01-15 14:30:25');
|
||||
|
||||
return $exampleDate->format($this->format);
|
||||
} catch (InvalidDateTimeException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -7,14 +8,15 @@ use App\Framework\Validation\ValidationRule;
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
final class Email implements ValidationRule
|
||||
final readonly class Email implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* @param string|null $message Benutzerdefinierte Fehlermeldung
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $message = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -40,7 +41,8 @@ final class In implements ValidationRule
|
||||
return [$this->message];
|
||||
}
|
||||
|
||||
$valuesList = implode(', ', array_map(fn($v) => "'$v'", $this->values));
|
||||
$valuesList = implode(', ', array_map(fn ($v) => "'$v'", $this->values));
|
||||
|
||||
return ["Dieser Wert muss einer der folgenden sein: $valuesList."];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -9,7 +10,6 @@ use Attribute;
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
final class IsTrue implements ValidationRule
|
||||
{
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -14,7 +15,8 @@ final readonly class Numeric implements ValidationRule
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $message = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -16,7 +17,8 @@ final readonly class Pattern implements ValidationRule
|
||||
public function __construct(
|
||||
private string $pattern,
|
||||
private ?string $message = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
@@ -24,7 +26,7 @@ final readonly class Pattern implements ValidationRule
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
35
src/Framework/Validation/Rules/Phone.php
Normal file
35
src/Framework/Validation/Rules/Phone.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
|
||||
use App\Domain\Common\ValueObject\PhoneNumber;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
final readonly class Phone implements ValidationRule
|
||||
{
|
||||
public function __construct(
|
||||
private string $message = 'The field must be a valid phone number.'
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return true; // Allow empty values (use Required rule for mandatory fields)
|
||||
}
|
||||
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return PhoneNumber::isValid($value);
|
||||
}
|
||||
|
||||
public function getErrorMessages(): array
|
||||
{
|
||||
return [$this->message];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -30,7 +31,7 @@ final readonly class Range implements ValidationRule
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
if (! is_numeric($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -14,7 +15,8 @@ final readonly class Required implements ValidationRule
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $message = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
@@ -30,7 +31,7 @@ final readonly class StringLength implements ValidationRule
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
42
src/Framework/Validation/Rules/Ulid.php
Normal file
42
src/Framework/Validation/Rules/Ulid.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
|
||||
use App\Framework\Ulid\UlidValidator;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
final readonly class Ulid implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* @param string|null $message Benutzerdefinierte Fehlermeldung
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $message = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verwende den bestehenden UlidValidator
|
||||
$validator = new UlidValidator();
|
||||
|
||||
return $validator->isValid($value);
|
||||
}
|
||||
|
||||
public function getErrorMessages(): array
|
||||
{
|
||||
return [$this->message ?? 'Bitte geben Sie eine gültige ULID ein (26 Zeichen, Crockford Base32).'];
|
||||
}
|
||||
}
|
||||
85
src/Framework/Validation/Rules/Url.php
Normal file
85
src/Framework/Validation/Rules/Url.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Url as UrlValueObject;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
use Attribute;
|
||||
use InvalidArgumentException;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
final readonly class Url implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* @param bool $requireSecure Ob HTTPS erforderlich ist
|
||||
* @param bool $allowLocal Ob lokale URLs erlaubt sind (localhost, 192.168.x.x, etc.)
|
||||
* @param bool $autoAddProtocol Ob automatisch https:// hinzugefügt werden soll wenn fehlt
|
||||
* @param string|null $message Benutzerdefinierte Fehlermeldung
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $requireSecure = false,
|
||||
private bool $allowLocal = true,
|
||||
private bool $autoAddProtocol = false,
|
||||
private ?string $message = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(mixed $value): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return true; // Leere Werte werden von Required-Regel behandelt
|
||||
}
|
||||
|
||||
if (! is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verwende das bestehende Url Value Object für die Validierung
|
||||
$url = $this->autoAddProtocol
|
||||
? UrlValueObject::parse($value)
|
||||
: UrlValueObject::from($value);
|
||||
|
||||
// Zusätzliche Validierungen basierend auf den Optionen
|
||||
if ($this->requireSecure && ! $url->isSecure()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->allowLocal && $url->isLocal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stelle sicher, dass es sich um eine HTTP/HTTPS URL handelt
|
||||
if (! $url->isHttp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getErrorMessages(): array
|
||||
{
|
||||
if ($this->message !== null) {
|
||||
return [$this->message];
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
|
||||
if ($this->requireSecure && ! $this->allowLocal) {
|
||||
$messages[] = 'Bitte geben Sie eine gültige HTTPS-URL ein (lokale URLs nicht erlaubt).';
|
||||
} elseif ($this->requireSecure) {
|
||||
$messages[] = 'Bitte geben Sie eine gültige HTTPS-URL ein.';
|
||||
} elseif (! $this->allowLocal) {
|
||||
$messages[] = 'Bitte geben Sie eine gültige URL ein (lokale URLs nicht erlaubt).';
|
||||
} else {
|
||||
$messages[] = 'Bitte geben Sie eine gültige URL ein.';
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation\Rules;
|
||||
|
||||
use App\Framework\Validation\GroupAware;
|
||||
use App\Framework\Validation\ValidationRule;
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpMiddleware;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\MiddlewarePriority;
|
||||
use App\Framework\Http\MiddlewarePriorityAttribute;
|
||||
use App\Framework\Http\RequestStateManager;
|
||||
use App\Framework\Http\Responses\JsonResponse;
|
||||
use App\Framework\Http\Responses\RedirectResponse;
|
||||
use App\Framework\Http\ServerKey;
|
||||
use App\Framework\Http\Session\Session;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Validation\Exceptions\ValidationException;
|
||||
|
||||
#[MiddlewarePriorityAttribute(MiddlewarePriority::SESSION, -50)]
|
||||
final readonly class ValidationErrorMiddleware implements HttpMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
private Session $session
|
||||
) {}
|
||||
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
|
||||
{
|
||||
try {
|
||||
return $next($context);
|
||||
} catch (ValidationException $e) {
|
||||
|
||||
// Speichern des Formulars in der Session
|
||||
$this->session->form->store('form', $context->request->parsedBody->data);
|
||||
|
||||
// Speichern der Validierungsfehler in der Session
|
||||
$this->session->validation->add('form', $e->getAllErrors());;
|
||||
|
||||
return $this->createValidationErrorResponse($context, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Fehlerantwort für Validierungsfehler
|
||||
*/
|
||||
private function createValidationErrorResponse(MiddlewareContext $context, ValidationException $e): MiddlewareContext
|
||||
{
|
||||
$acceptHeader = $context->request->server->get(ServerKey::HTTP_ACCEPT, '');
|
||||
|
||||
// Formatierung der Fehlerantwort je nach Content-Type
|
||||
//$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
|
||||
if (str_contains($acceptHeader, 'application/json')) {
|
||||
// JSON-Antwort für API-Anfragen
|
||||
|
||||
return $context->withResponse(
|
||||
new JsonResponse(
|
||||
body: [
|
||||
'error' => 'Validation Error',
|
||||
'message' => $e->getMessage(),
|
||||
'errors' => $e->getAllErrors(),//$this->formatErrors($e),
|
||||
],
|
||||
status: Status::UNPROCESSABLE_ENTITY,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
||||
$uri = $context->request->server->getRefererUri();
|
||||
|
||||
return $context->withResponse(new RedirectResponse($uri));
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/Framework/Validation/ValidationFormHandler.php
Normal file
213
src/Framework/Validation/ValidationFormHandler.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
|
||||
use App\Framework\Http\MiddlewareContext;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Responses\JsonResponse;
|
||||
use App\Framework\Http\Responses\RedirectResponse;
|
||||
use App\Framework\Http\Session\FormIdGenerator;
|
||||
use App\Framework\Http\Session\Session;
|
||||
use App\Framework\Http\Session\SessionManager;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Validation\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Handles ValidationException by storing form data and errors in session
|
||||
* and creating appropriate responses (redirect for web, JSON for API)
|
||||
*/
|
||||
final readonly class ValidationFormHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Session $session,
|
||||
private FormIdGenerator $formIdGenerator,
|
||||
private SessionManager $sessionManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a ValidationException by storing form data and errors
|
||||
* and creating an appropriate response
|
||||
*/
|
||||
public function handle(ValidationException $exception, ?MiddlewareContext $context = null): Response
|
||||
{
|
||||
$isApiRequest = $this->isApiRequest($context);
|
||||
|
||||
// For API requests, return JSON response directly without storing in session
|
||||
if ($isApiRequest) {
|
||||
error_log("ValidationFormHandler: API request detected, returning direct JSON response");
|
||||
|
||||
return $this->createJsonResponse($exception);
|
||||
}
|
||||
|
||||
// For web requests, store data in session for form repopulation
|
||||
$formId = $this->formIdGenerator->generateFromRequestContext($context);
|
||||
|
||||
// Store old input data for form repopulation
|
||||
$requestData = $this->extractRequestData($context);
|
||||
|
||||
error_log("ValidationFormHandler: Extracted request data: " . json_encode($requestData));
|
||||
if (! empty($requestData)) {
|
||||
$this->session->form->store($formId, $requestData);
|
||||
error_log("ValidationFormHandler: Stored form data in session");
|
||||
}
|
||||
|
||||
// Store validation errors for web form redisplay
|
||||
$this->session->validation->add($formId, $exception->getAllErrors());
|
||||
|
||||
$response = $this->sessionManager->saveSession($this->session, $this->createRedirectResponse($context));
|
||||
|
||||
error_log("ValidationFormHandler: Session data stored: " . json_encode($this->session->all()));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract request data from various sources
|
||||
*/
|
||||
private function extractRequestData(?MiddlewareContext $context): array
|
||||
{
|
||||
if (! $context?->request) {
|
||||
error_log("ValidationFormHandler: No request in context");
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try different data sources
|
||||
$data = [];
|
||||
|
||||
// From parsed body
|
||||
if ($context->request->parsedBody?->data) {
|
||||
error_log("ValidationFormHandler: Found parsedBody->data: " . json_encode($context->request->parsedBody->data));
|
||||
$data = array_merge($data, $context->request->parsedBody->data);
|
||||
} else {
|
||||
error_log("ValidationFormHandler: No parsedBody->data found");
|
||||
}
|
||||
|
||||
// From POST data directly
|
||||
if (! empty($_POST)) {
|
||||
error_log("ValidationFormHandler: Found POST data: " . json_encode($_POST));
|
||||
$data = array_merge($data, $_POST);
|
||||
} else {
|
||||
error_log("ValidationFormHandler: No POST data found");
|
||||
}
|
||||
|
||||
// Remove system fields and potential honeypot fields
|
||||
unset($data['_token'], $data['_form_id'], $data['_honeypot_name'], $data['_form_start_time']);
|
||||
|
||||
// Remove common honeypot field names
|
||||
$honeypotNames = ['email_confirm', 'website_url', 'phone_number', 'user_name', 'company_name'];
|
||||
foreach ($honeypotNames as $honeypotName) {
|
||||
unset($data[$honeypotName]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an API request based on Accept header
|
||||
*/
|
||||
private function isApiRequest(?MiddlewareContext $context): bool
|
||||
{
|
||||
if (! $context?->request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$requestedWith = $context->request->headers->getFirst('X-Requested-With');
|
||||
if ($requestedWith === 'XMLHttpRequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the first Accept header value or empty string if not present
|
||||
$acceptHeader = $context->request->headers->getFirst('Accept', '');
|
||||
|
||||
return str_contains($acceptHeader, 'application/json') ||
|
||||
str_contains($acceptHeader, 'application/api') ||
|
||||
str_starts_with($context->request->path, '/api/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON response for API requests
|
||||
*/
|
||||
private function createJsonResponse(ValidationException $exception): JsonResponse
|
||||
{
|
||||
return new JsonResponse(
|
||||
body: [
|
||||
'error' => 'Validation Error',
|
||||
'message' => $exception->getMessage(),
|
||||
'errors' => $exception->getAllErrors(),
|
||||
],
|
||||
status: Status::UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create redirect response for web requests
|
||||
*/
|
||||
private function createRedirectResponse(?MiddlewareContext $context): RedirectResponse
|
||||
{
|
||||
// Try to get referer URL for redirect
|
||||
$refererUrl = $context?->request?->server?->getRefererUri();
|
||||
|
||||
if (! $refererUrl) {
|
||||
// Fallback to current request URL without query parameters
|
||||
$currentUrl = $context?->request?->path ?? '/';
|
||||
$refererUrl = $currentUrl;
|
||||
}
|
||||
|
||||
return new RedirectResponse($refererUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are validation errors for a specific form
|
||||
*/
|
||||
public function hasErrors(string $formId): bool
|
||||
{
|
||||
return $this->session->validation->has($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation errors for a specific form
|
||||
*/
|
||||
public function getErrors(string $formId): array
|
||||
{
|
||||
return $this->session->validation->get($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is old input data for a specific form
|
||||
*/
|
||||
public function hasOldInput(string $formId): bool
|
||||
{
|
||||
return $this->session->form->has($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old input data for a specific form
|
||||
*/
|
||||
public function getOldInput(string $formId): array
|
||||
{
|
||||
return $this->session->form->get($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear validation errors and old input for a specific form
|
||||
* (typically called after successful form submission)
|
||||
*/
|
||||
public function clearForm(string $formId): void
|
||||
{
|
||||
$this->session->validation->clear($formId);
|
||||
$this->session->form->clear($formId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all validation errors and old input data
|
||||
*/
|
||||
public function clearAll(): void
|
||||
{
|
||||
$this->session->validation->clearAll();
|
||||
$this->session->form->clearAll();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
@@ -34,7 +35,7 @@ final class ValidationResult
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return !empty($this->errors);
|
||||
return ! empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +62,7 @@ final class ValidationResult
|
||||
$messages[] = $error;
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
@@ -74,10 +76,11 @@ final class ValidationResult
|
||||
$this->addError($field, $message);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAll():array
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
|
||||
interface ValidationRule
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Validation;
|
||||
|
||||
use ReflectionClass;
|
||||
use App\Framework\Core\ValueObjects\ClassName;
|
||||
use App\Framework\Reflection\ReflectionProvider;
|
||||
|
||||
final class Validator
|
||||
final readonly class Validator
|
||||
{
|
||||
public function __construct(
|
||||
private ReflectionProvider $reflectionProvider
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert ein Objekt und gibt das Ergebnis zurück
|
||||
*
|
||||
@@ -17,15 +24,20 @@ final class Validator
|
||||
public function validate(object $object, ?string $group = null): ValidationResult
|
||||
{
|
||||
$result = new ValidationResult();
|
||||
$refClass = new ReflectionClass($object);
|
||||
$className = ClassName::create($object::class);
|
||||
$properties = $this->reflectionProvider->getProperties($className);
|
||||
|
||||
// Eigenschaften validieren
|
||||
foreach ($refClass->getProperties() as $property) {
|
||||
foreach ($properties as $property) {
|
||||
$propertyName = $property->getName();
|
||||
$allowsNull = $property->getType()?->allowsNull();
|
||||
|
||||
try {
|
||||
// Versuche zuerst den Wert zu lesen
|
||||
$value = $property->getValue($object);
|
||||
|
||||
if(!$property?->isInitialized($object) && !$property->getType()?->allowsNull()){
|
||||
|
||||
// Prüfe nur den tatsächlichen Wert, nicht isInitialized()
|
||||
if ($value === null && ! $allowsNull) {
|
||||
$result->addError(
|
||||
$property->getName(),
|
||||
sprintf("Feld '%s' darf nicht null sein.", $property->getName())
|
||||
@@ -34,14 +46,39 @@ final class Validator
|
||||
continue;
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
// Wenn wir den Wert nicht lesen können, prüfe isInitialized für non-nullable Properties
|
||||
try {
|
||||
$isInitialized = $property->isInitialized($object);
|
||||
|
||||
$value = $property->getValue($object);
|
||||
} catch (\Throwable) {
|
||||
if (! $isInitialized && ! $allowsNull) {
|
||||
$result->addError(
|
||||
$property->getName(),
|
||||
sprintf("Feld '%s' darf nicht null sein.", $property->getName())
|
||||
);
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Fehler beim Lesen sowohl des Werts als auch der Initialisierung - überspringen
|
||||
}
|
||||
|
||||
//$result->addError($property->getName(), 'Fehler beim Lesen des Werts');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Automatische Required-Validierung für non-nullable Properties
|
||||
if (! $allowsNull) {
|
||||
$propertyType = $property->getType();
|
||||
$isString = $propertyType instanceof \ReflectionNamedType && $propertyType->getName() === 'string';
|
||||
|
||||
if ($isString && (empty($value) && $value !== '0')) {
|
||||
$result->addError(
|
||||
$propertyName,
|
||||
sprintf("Feld '%s' ist erforderlich.", $propertyName)
|
||||
);
|
||||
|
||||
// Keine weiteren Validierungen für dieses Property
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($property->getAttributes() as $attribute) {
|
||||
$attrInstance = $attribute->newInstance();
|
||||
@@ -58,7 +95,7 @@ final class Validator
|
||||
$shouldValidate = true;
|
||||
}
|
||||
|
||||
if ($shouldValidate && !$attrInstance->validate($value)) {
|
||||
if ($shouldValidate && ! $attrInstance->validate($value)) {
|
||||
$result->addErrors($property->getName(), $attrInstance->getErrorMessages());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user