chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Exceptions;
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
*/
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();
parent::__construct(message: $message);
}
/**
* 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);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation;
/**
* Interface für Validierungsregeln, die zu bestimmten Gruppen gehören können
*/
interface GroupAware
{
/**
* Prüft, ob das Attribut zu einer bestimmten Validierungsgruppe gehört
*/
public function belongsToGroup(string $group): bool;
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
use Closure;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class Custom implements ValidationRule
{
/**
* @param callable $validator Funktion zur Validierung (erhält den Wert und gibt bool zurück)
* @param array<string> $messages Fehlermeldungen
*/
public function __construct(
private Closure $validator,
private array $messages = ['Der angegebene Wert ist ungültig.']
) {
if (!is_callable($validator)) {
throw new \InvalidArgumentException('Der Validator muss eine aufrufbare Funktion sein.');
}
}
public function validate(mixed $value): bool
{
return ($this->validator)($value);
}
public function getErrorMessages(): array
{
return $this->messages;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class Email 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
}
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
public function getErrorMessages(): array
{
return [$this->message ?? 'Bitte geben Sie eine gültige E-Mail-Adresse ein.'];
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class In implements ValidationRule
{
private array $values;
/**
* @param array $values Erlaubte Werte
* @param string|null $message Benutzerdefinierte Fehlermeldung
*/
public function __construct(
array $values,
private ?string $message = null
) {
if (empty($values)) {
throw new \InvalidArgumentException('Die Liste der erlaubten Werte darf nicht leer sein.');
}
$this->values = $values;
}
public function validate(mixed $value): bool
{
if ($value === null || $value === '') {
return true; // Leere Werte werden von Required-Regel behandelt
}
return in_array($value, $this->values, true);
}
public function getErrorMessages(): array
{
if ($this->message !== null) {
return [$this->message];
}
$valuesList = implode(', ', array_map(fn($v) => "'$v'", $this->values));
return ["Dieser Wert muss einer der folgenden sein: $valuesList."];
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class IsTrue implements ValidationRule
{
public function validate(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
return false;
}
public function getErrorMessages(): array
{
return ['Dieser Wert muss "true" sein.'];
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class Numeric 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
}
return is_numeric($value);
}
public function getErrorMessages(): array
{
return [$this->message ?? 'Dieser Wert muss numerisch sein.'];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class Pattern implements ValidationRule
{
/**
* @param string $pattern Regulärer Ausdruck
* @param string|null $message Benutzerdefinierte Fehlermeldung
*/
public function __construct(
private string $pattern,
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;
}
return preg_match($this->pattern, $value) === 1;
}
public function getErrorMessages(): array
{
return [$this->message ?? 'Dieser Wert entspricht nicht dem erforderlichen Format.'];
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class Range implements ValidationRule
{
/**
* @param float|null $min Minimaler Wert
* @param float|null $max Maximaler Wert
* @param string|null $message Benutzerdefinierte Fehlermeldung
*/
public function __construct(
private ?float $min = null,
private ?float $max = null,
private ?string $message = null
) {
if ($min === null && $max === null) {
throw new \InvalidArgumentException('Mindestens einer der Parameter min oder max muss gesetzt sein.');
}
}
public function validate(mixed $value): bool
{
if ($value === null || $value === '') {
return true; // Leere Werte werden von Required-Regel behandelt
}
if (!is_numeric($value)) {
return false;
}
if ($this->min !== null && $value < $this->min) {
return false;
}
if ($this->max !== null && $value > $this->max) {
return false;
}
return true;
}
public function getErrorMessages(): array
{
if ($this->message !== null) {
return [$this->message];
}
if ($this->min !== null && $this->max !== null) {
return ["Dieser Wert muss zwischen {$this->min} und {$this->max} liegen."];
} elseif ($this->min !== null) {
return ["Dieser Wert muss mindestens {$this->min} sein."];
} else {
return ["Dieser Wert darf maximal {$this->max} sein."];
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class Required implements ValidationRule
{
/**
* @param string|null $message Benutzerdefinierte Fehlermeldung
*/
public function __construct(
private ?string $message = null
) {}
public function validate(mixed $value): bool
{
return $value !== null && $value !== '';
}
public function getErrorMessages(): array
{
return [$this->message ?? 'Dieser Wert ist erforderlich.'];
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation\Rules;
use App\Framework\Validation\ValidationRule;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final readonly class StringLength implements ValidationRule
{
/**
* @param int|null $min Minimale Länge des Strings
* @param int|null $max Maximale Länge des Strings
* @param string|null $message Benutzerdefinierte Fehlermeldung
*/
public function __construct(
private ?int $min = null,
private ?int $max = null,
private ?string $message = null
) {
if ($min === null && $max === null) {
throw new \InvalidArgumentException('Mindestens einer der Parameter min oder max muss gesetzt sein.');
}
}
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;
}
$length = mb_strlen($value);
if ($this->min !== null && $length < $this->min) {
return false;
}
if ($this->max !== null && $length > $this->max) {
return false;
}
return true;
}
public function getErrorMessages(): array
{
if ($this->message !== null) {
return [$this->message];
}
if ($this->min !== null && $this->max !== null) {
return ["Dieser Wert muss zwischen {$this->min} und {$this->max} Zeichen lang sein."];
} elseif ($this->min !== null) {
return ["Dieser Wert muss mindestens {$this->min} Zeichen lang sein."];
} else {
return ["Dieser Wert darf maximal {$this->max} Zeichen lang sein."];
}
}
}

View File

@@ -0,0 +1,31 @@
<?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)]
final class ValidationGroup implements GroupAware
{
/** @var array<string> */
private array $groups;
/**
* @param string|array<string> $groups Name der Gruppe oder Gruppen
*/
public function __construct(string|array $groups)
{
$this->groups = is_array($groups) ? $groups : [$groups];
}
/**
* Prüft, ob das Attribut zu einer bestimmten Gruppe gehört
*/
public function belongsToGroup(string $group): bool
{
return in_array($group, $this->groups, true);
}
}

View File

@@ -0,0 +1,72 @@
<?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,84 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation;
final class ValidationResult
{
/** @var array<string, string[]> */
private array $errors = [];
/**
* Fügt eine Fehlermeldung für ein Feld hinzu
*/
public function addError(string $field, string $message): void
{
$this->errors[$field][] = $message;
}
/**
* Fügt mehrere Fehlermeldungen für ein Feld hinzu
*
* @param string $field Feldname
* @param array<string> $messages Liste von Fehlermeldungen
*/
public function addErrors(string $field, array $messages): void
{
foreach ($messages as $message) {
$this->addError($field, $message);
}
}
/**
* Prüft, ob Fehler vorhanden sind
*/
public function hasErrors(): bool
{
return !empty($this->errors);
}
/**
* 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->errors[$field] ?? [];
}
/**
* Gibt alle Fehlermeldungen als flache Liste zurück
*
* @return array<string> Liste aller Fehlermeldungen
*/
public function getAllErrorMessages(): array
{
$messages = [];
foreach ($this->errors as $fieldErrors) {
foreach ($fieldErrors as $error) {
$messages[] = $error;
}
}
return $messages;
}
/**
* Kombiniert zwei Validierungsergebnisse
*/
public function merge(ValidationResult $other): self
{
foreach ($other->errors as $field => $messages) {
foreach ($messages as $message) {
$this->addError($field, $message);
}
}
return $this;
}
public function getAll():array
{
return $this->errors;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Framework\Validation;
interface ValidationRule
{
/**
* Validiert einen Wert anhand der Regel
*/
public function validate(mixed $value): bool;
/**
* Gibt alle Fehlermeldungen für diese Regel zurück
*
* @return array<string> Liste der Fehlermeldungen
*/
public function getErrorMessages(): array;
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\Validation;
use ReflectionClass;
final class Validator
{
/**
* Validiert ein Objekt und gibt das Ergebnis zurück
*
* @param object $object Das zu validierende Objekt
* @param string|null $group Optionale Validierungsgruppe
* @return ValidationResult
*/
public function validate(object $object, ?string $group = null): ValidationResult
{
$result = new ValidationResult();
$refClass = new ReflectionClass($object);
// Eigenschaften validieren
foreach ($refClass->getProperties() as $property) {
try {
if(!$property?->isInitialized($object) && !$property->getType()?->allowsNull()){
$result->addError(
$property->getName(),
sprintf("Feld '%s' darf nicht null sein.", $property->getName())
);
continue;
}
$value = $property->getValue($object);
} catch (\Throwable) {
//$result->addError($property->getName(), 'Fehler beim Lesen des Werts');
continue;
}
foreach ($property->getAttributes() as $attribute) {
$attrInstance = $attribute->newInstance();
if ($attrInstance instanceof ValidationRule) {
$shouldValidate = false;
if ($group === null) {
$shouldValidate = true;
} elseif ($attrInstance instanceof GroupAware) {
$shouldValidate = $attrInstance->belongsToGroup($group);
} else {
$shouldValidate = true;
}
if ($shouldValidate && !$attrInstance->validate($value)) {
$result->addErrors($property->getName(), $attrInstance->getErrorMessages());
}
}
}
}
return $result;
}
}