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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -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);
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation;

View File

@@ -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.');
}
}

View 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;
}
}
}

View File

@@ -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
{

View File

@@ -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."];
}
}

View File

@@ -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)) {

View File

@@ -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
{

View File

@@ -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;
}

View 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];
}
}

View File

@@ -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;
}

View File

@@ -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
{

View File

@@ -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;
}

View 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).'];
}
}

View 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;
}
}

View File

@@ -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)]

View File

@@ -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));
}
}
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation;
interface ValidationRule

View File

@@ -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());
}
}