- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
34 KiB
TypedString System
Umfassende Dokumentation des TypedString-Systems für type-safe String-Validierung im Custom PHP Framework.
Übersicht
Das TypedString-System bietet eine sichere, typsichere Alternative zu PHP's primitiven ctype_* Funktionen und String-Validierung. Es folgt konsequent den Framework-Prinzipien: Value Objects statt Primitives, readonly classes, und Composition over Inheritance.
Kern-Features:
- Type-safe String-Validierung
- Fluent Validation API mit Chainable Validierung
- Specialized Value Objects für häufige Use Cases
- Security-First: Zentrale Validierung verhindert SQL-Injection und XSS
- Framework-Compliance: Readonly, immutable, keine Primitives
Architektur
WICHTIG: TypedString ist ein Validierungs-Tool, NICHT ein Storage-Layer!
┌─────────────────────────────────────────────────────────────────┐
│ TypedString (Utility Layer) │
│ - Validierungs-Tool für String-Properties │
│ - Wird NICHT in readonly properties gespeichert │
│ - Nutze während Construction, speichere 'string' │
└─────────────────────────────────────────────────────────────────┘
↓ verwendet von
┌─────────────────────────────────────────────────────────────────┐
│ Domain Value Objects (Storage Layer) │
│ - Email, UserName, UserId │
│ - Property: 'string' (NICHT TypedString!) │
│ - Nutzen TypedString zur Constructor-Validierung │
└─────────────────────────────────────────────────────────────────┘
↓ verwendet von
┌─────────────────────────────────────────────────────────────────┐
│ Domain Entities (Business Layer) │
│ - User, Order, Profile │
│ - Properties: Domain VOs oder validierte strings │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Specialized VOs (Temporäre Guarantees) │
│ - AlphanumericString, NumericString, HexadecimalString │
│ - Use Cases: Type Hints, temporäre Validierung │
│ - NICHT in Domain VOs wrappen (Wrapper-Hell!) │
└─────────────────────────────────────────────────────────────────┘
Design-Entscheidungen
TypedString als Validator (✅):
// ✅ TypedString validiert, speichert aber NICHT
final readonly class Email
{
public function __construct(
public string $value // String bleibt String!
) {
TypedString::fromString($value)
->validate()
->matches('/^[\w\.\-]+@[\w\.\-]+\.\w+$/')
->maxLength(254)
->orThrow();
}
}
$email = Email::fromString('test@example.com');
echo $email->value; // ✅ Direkt string-Zugriff
TypedString als Storage (❌ Wrapper-Hell):
// ❌ NICHT: VO in VO führt zu Double Unwrapping
final readonly class Email
{
public function __construct(
public TypedString $value // Wrapper-Hell!
) {}
}
$email = new Email(TypedString::fromString('test@example.com'));
echo $email->value->value; // 🤮 Double unwrapping!
TypedString Core Class
Überblick
TypedString ist der zentrale Wrapper um einen String-Wert mit umfassenden Validierungsmethoden.
Location: src/Framework/String/ValueObjects/TypedString.php
Core Principles:
- Readonly class (immutable)
- Kein direkter Zugriff auf primitive Strings ohne Validierung
- Alle Validierungsmethoden geben false für leere Strings zurück
- Factory Method Pattern für Erzeugung
Character Type Checks
Alternative zu PHP's ctype_* Funktionen mit besseren Namen und konsistentem Verhalten:
use App\Framework\String\ValueObjects\TypedString;
$str = TypedString::fromString('hello123');
// Character Type Checks
$str->isAlphanumeric(); // true - alphanumerische Zeichen nur (a-z, A-Z, 0-9)
$str->isAlphabetic(); // false - nur Buchstaben (a-z, A-Z)
$str->isDigits(); // false - nur Ziffern (0-9)
$str->isLowercase(); // false - nur Kleinbuchstaben
$str->isUppercase(); // false - nur Großbuchstaben
$str->isHexadecimal(); // false - valides Hex (0-9, a-f, A-F)
$str->isWhitespace(); // false - nur Whitespace (\t, \n, \r, space)
$str->isPrintable(); // true - druckbare Zeichen (keine Control Characters)
$str->isVisible(); // false - sichtbare Zeichen (keine Whitespace)
Wichtig: Alle Methoden geben false für leere Strings zurück:
TypedString::fromString('')->isAlphanumeric(); // false
TypedString::fromString('')->isEmpty(); // true
Length Checks
Umfassende Längen-Validierung:
$str = TypedString::fromString('hello');
// Basic Length
$str->length(); // 5
$str->isEmpty(); // false
$str->isNotEmpty(); // true
// Length Constraints
$str->hasMinLength(3); // true - mindestens 3 Zeichen
$str->hasMaxLength(10); // true - maximal 10 Zeichen
$str->hasLengthBetween(3, 10); // true - zwischen 3 und 10 Zeichen
$str->hasExactLength(5); // true - genau 5 Zeichen
Pattern Matching
Regex und Substring-Matching:
$str = TypedString::fromString('hello world');
// Regex Matching
$str->matches('/^hello/'); // true - beginnt mit "hello"
$str->matches('/\d+/'); // false - enthält keine Ziffern
// String Matching
$str->startsWith('hello'); // true - Prefix-Check
$str->endsWith('world'); // true - Suffix-Check
$str->contains('lo wo'); // true - Substring-Check
Comparison & Conversion
$str1 = TypedString::fromString('hello');
$str2 = TypedString::fromString('hello');
// Equality
$str1->equals($str2); // true - value comparison
// Conversion
$str1->toString(); // 'hello' - explizit
(string) $str1; // 'hello' - implicit via __toString()
$str1->value; // 'hello' - direkter property access
TypedStringValidator (Fluent API)
Überblick
TypedStringValidator bietet eine chainable Validation API für komplexe Validierungslogik.
Key Features:
- Chainable validation rules
- Custom validation callbacks
- Error collection mit detaillierten Messages
- Multiple termination strategies (throw, null, default)
Basic Usage
use App\Framework\String\ValueObjects\TypedString;
// Simple validation chain
$result = TypedString::fromString('user123')
->validate()
->alphanumeric()
->minLength(3)
->maxLength(20)
->orNull(); // Returns TypedString or null
if ($result !== null) {
// Validation passed
}
Available Validation Rules
$validator = TypedString::fromString($input)->validate();
// Character Type Rules
$validator->alphanumeric(?string $error = null); // Must be alphanumeric
$validator->alphabetic(?string $error = null); // Must be alphabetic
$validator->digits(?string $error = null); // Must be digits only
$validator->lowercase(?string $error = null); // Must be lowercase
$validator->uppercase(?string $error = null); // Must be uppercase
$validator->hexadecimal(?string $error = null); // Must be valid hex
$validator->printable(?string $error = null); // Must be printable
$validator->visible(?string $error = null); // Must be visible (no whitespace)
// Length Rules
$validator->notEmpty(?string $error = null); // Must not be empty
$validator->minLength(int $min, ?string $error = null);
$validator->maxLength(int $max, ?string $error = null);
$validator->lengthBetween(int $min, int $max, ?string $error = null);
$validator->exactLength(int $length, ?string $error = null);
// Pattern Rules
$validator->matches(string $pattern, ?string $error = null);
$validator->startsWith(string $prefix, ?string $error = null);
$validator->endsWith(string $suffix, ?string $error = null);
$validator->contains(string $substring, ?string $error = null);
$validator->doesNotContain(string $substring, ?string $error = null);
// Custom Rules
$validator->custom(callable $check, string $error);
// Callback signature: fn(TypedString $ts): bool
Termination Methods
Validation chains benötigen eine Termination-Methode:
// 1. orThrow() - Wirft InvalidArgumentException bei Fehler
try {
$validated = TypedString::fromString($input)
->validate()
->alphanumeric()
->minLength(5)
->orThrow('Invalid username');
} catch (\InvalidArgumentException $e) {
// Handle validation error
}
// 2. orNull() - Gibt null bei Fehler zurück
$validated = TypedString::fromString($input)
->validate()
->digits()
->orNull();
if ($validated === null) {
// Validation failed
}
// 3. orDefault() - Verwendet Fallback-Wert bei Fehler
$validated = TypedString::fromString($input)
->validate()
->alphanumeric()
->orDefault('default_value');
// Garantiert niemals null - immer TypedString
Error Handling
$validator = TypedString::fromString('ab')
->validate()
->alphanumeric()
->minLength(5);
// Check validation status
if ($validator->fails()) {
// Get all errors
$errors = $validator->getErrors();
// ['String must be at least 5 characters long']
// Get first error only
$firstError = $validator->getFirstError();
}
// Or check if passed
if ($validator->passes()) {
// All validations succeeded
}
Complex Validation Example
// Username validation
$username = TypedString::fromString($input)
->validate()
->alphanumeric('Username must contain only letters and numbers')
->minLength(3, 'Username must be at least 3 characters')
->maxLength(20, 'Username must not exceed 20 characters')
->orThrow('Invalid username');
// Password validation with custom rules
$password = TypedString::fromString($input)
->validate()
->minLength(8, 'Password must be at least 8 characters')
->custom(
fn($ts) => preg_match('/[A-Z]/', $ts->value) === 1,
'Password must contain at least one uppercase letter'
)
->custom(
fn($ts) => preg_match('/[0-9]/', $ts->value) === 1,
'Password must contain at least one digit'
)
->custom(
fn($ts) => preg_match('/[!@#$%^&*]/', $ts->value) === 1,
'Password must contain at least one special character'
)
->orThrow('Password does not meet security requirements');
// Email format validation
$email = TypedString::fromString($input)
->validate()
->matches('/^[\w\.\-]+@[\w\.\-]+\.\w+$/', 'Invalid email format')
->maxLength(255)
->orNull();
Specialized Value Objects
Überblick
Specialized Value Objects garantieren spezifische String-Formate durch Constructor-Validierung.
Verfügbare VOs:
AlphanumericString: Nur alphanumerische ZeichenNumericString: Nur Ziffern mit Conversion-MethodenHexadecimalString: Valides Hex mit Conversion-MethodenPrintableString: Nur druckbare Zeichen (keine Control Characters)
AlphanumericString
Use Cases: Usernames, Identifiers, API Keys, Codes
use App\Framework\String\ValueObjects\AlphanumericString;
// ✅ Valid
$username = AlphanumericString::fromString('user123');
$apiKey = AlphanumericString::fromString('ABC123XYZ');
// ❌ Invalid - wirft InvalidArgumentException
AlphanumericString::fromString('user-123'); // Enthält '-'
AlphanumericString::fromString('user@mail'); // Enthält '@'
AlphanumericString::fromString(''); // Leer
// Usage
echo $username->value; // 'user123'
echo (string) $username; // 'user123'
$username->equals($apiKey); // false
// Conversion to TypedString für weitere Operationen
$typed = $username->toTypedString();
$length = $typed->length(); // 7
NumericString
Use Cases: Order IDs, Product Codes, Phone Numbers (digits only), ZIP Codes
Wichtig: Verwende NumericString wenn Ziffern als String bleiben sollen (z.B. führende Nullen: "00123")
use App\Framework\String\ValueObjects\NumericString;
// ✅ Valid
$orderId = NumericString::fromString('12345');
$zipCode = NumericString::fromString('00123'); // Leading zeros erhalten!
// ❌ Invalid
NumericString::fromString('12.34'); // Enthält '.'
NumericString::fromString('123abc'); // Enthält Buchstaben
NumericString::fromString(''); // Leer
// Conversion Methods
$orderId->toInt(); // 12345 (int)
$orderId->toFloat(); // 12345.0 (float)
// Leading zeros bleiben im String erhalten
$zipCode->value; // '00123' (string)
$zipCode->toInt(); // 123 (int) - Leading zeros entfernt
Use Case Example:
// ✅ Order ID System - NumericString validiert, speichert aber NICHT
final readonly class OrderId
{
public function __construct(
public string $value // ✅ String property, NICHT NumericString!
) {
// ✅ NumericString validiert Format
$validated = NumericString::fromString($value);
// Weitere Domain-Validierung
if (strlen($value) < 5) {
throw new \InvalidArgumentException('Order ID must be at least 5 digits');
}
}
public static function generate(): self
{
return new self((string) time());
}
public function toInt(): int
{
return (int) $this->value;
}
public function toString(): string
{
return $this->value;
}
}
HexadecimalString
Use Cases: Hashes, Color Codes, Binary Data Representation, Token IDs
use App\Framework\String\ValueObjects\HexadecimalString;
// ✅ Valid
$hash = HexadecimalString::fromString('deadbeef');
$color = HexadecimalString::fromString('FF5733');
$token = HexadecimalString::fromString('0123456789abcdef');
// ❌ Invalid
HexadecimalString::fromString('ghijk'); // Enthält non-hex Zeichen
HexadecimalString::fromString(''); // Leer
// Conversion Methods
$hash->toBinary(); // Binary string
$hash->toInt(); // 3735928559 (decimal)
// ✅ Use Case: Color Codes - HexadecimalString validiert, speichert aber NICHT
final readonly class HexColor
{
public function __construct(
public string $value // ✅ String property, NICHT HexadecimalString!
) {
// ✅ HexadecimalString validiert Hex-Format
$validated = HexadecimalString::fromString($value);
// Domain-spezifische Validierung: RGB benötigt 6 Zeichen
if (strlen($value) !== 6) {
throw new \InvalidArgumentException('Color must be 6 hex digits');
}
}
public static function fromRGB(int $r, int $g, int $b): self
{
$hex = sprintf('%02X%02X%02X', $r, $g, $b);
// Validierung erfolgt im Constructor
return new self($hex);
}
public function toRGB(): array
{
// Direkt mit string arbeiten - kein Wrapper-Unwrapping!
$hex = HexadecimalString::fromString($this->value);
$binary = $hex->toBinary();
return [
'r' => ord($binary[0]),
'g' => ord($binary[1]),
'b' => ord($binary[2])
];
}
}
PrintableString
Use Cases: User-facing Text, Display Strings, Log Messages, Comments
Wichtig: Erlaubt Whitespace (\t, \n, \r, space) aber keine Control Characters
use App\Framework\String\ValueObjects\PrintableString;
// ✅ Valid
$text = PrintableString::fromString('Hello World!');
$message = PrintableString::fromString("Line 1\nLine 2\tTabbed");
$special = PrintableString::fromString('Price: $99.99 (10% off!)');
// ❌ Invalid
PrintableString::fromString("Hello\x00World"); // Null byte (Control Character)
PrintableString::fromString(''); // Leer
// Usage
echo $text->value; // 'Hello World!'
$text->toTypedString()->length(); // 12
Integration Patterns
Mit Database Value Objects
TypedString verbessert Database VOs durch flexible Validierung:
use App\Framework\Database\ValueObjects\TableName;
use App\Framework\String\ValueObjects\TypedString;
// ✅ TypedString validiert, speichert aber NICHT
final readonly class TableName
{
public function __construct(public string $value)
{
TypedString::fromString($value)
->validate()
->alphanumeric('Table name must be alphanumeric')
->notEmpty('Table name cannot be empty')
->maxLength(63, 'Table name too long (PostgreSQL limit: 63)')
->orThrow();
}
public static function fromString(string $value): self
{
return new self($value);
}
}
// Property bleibt string - kein Wrapper!
$table = TableName::fromString('users');
echo $table->value; // ✅ Direkt string-Zugriff
Mit Form Validation
final readonly class CreateUserRequest implements ControllerRequest
{
public function __construct(
public Email $email,
public string $username, // ✅ String, NICHT AlphanumericString!
public ?string $bio = null
) {}
public static function fromHttpRequest(HttpRequest $request): self
{
$data = $request->parsedBody->toArray();
// ✅ Validation beim Erstellen, aber Storage ist string
$username = TypedString::fromString($data['username'] ?? '')
->validate()
->alphanumeric('Username must be alphanumeric')
->minLength(3, 'Username too short')
->maxLength(20, 'Username too long')
->orThrow();
$bio = null;
if (!empty($data['bio'])) {
$bio = TypedString::fromString($data['bio'])
->validate()
->printable('Bio must contain only printable characters')
->maxLength(500, 'Bio too long')
->orThrow();
}
return new self(
email: new Email($data['email'] ?? ''),
username: $username->value, // ✅ Extract string from validation
bio: $bio?->value // ✅ Optional string
);
}
}
Mit Domain Models
final readonly class User
{
public function __construct(
public UserId $id,
public Email $email,
public string $username, // ✅ String, validiert im constructor
public string $displayName // ✅ String, validiert im constructor
) {}
public static function create(
Email $email,
string $username,
string $displayName
): self {
// ✅ Validation happens here, but storage is plain string
$validatedUsername = TypedString::fromString($username)
->validate()
->alphanumeric('Username must be alphanumeric')
->minLength(3)
->maxLength(20)
->orThrow();
$validatedDisplayName = TypedString::fromString($displayName)
->validate()
->printable('Display name must be printable')
->minLength(1)
->maxLength(50)
->orThrow();
return new self(
id: UserId::generate(),
email: $email,
username: $validatedUsername->value, // ✅ Store string
displayName: $validatedDisplayName->value // ✅ Store string
);
}
}
Custom Validation Rules
// Custom VO mit spezieller Validierung
final readonly class StrongPassword
{
public function __construct(public string $value)
{
TypedString::fromString($value)
->validate()
->minLength(12, 'Password must be at least 12 characters')
->custom(
fn($ts) => preg_match('/[A-Z]/', $ts->value) === 1,
'Must contain uppercase letter'
)
->custom(
fn($ts) => preg_match('/[a-z]/', $ts->value) === 1,
'Must contain lowercase letter'
)
->custom(
fn($ts) => preg_match('/[0-9]/', $ts->value) === 1,
'Must contain digit'
)
->custom(
fn($ts) => preg_match('/[!@#$%^&*]/', $ts->value) === 1,
'Must contain special character'
)
->orThrow('Password does not meet security requirements');
}
public static function fromString(string $value): self
{
return new self($value);
}
}
Migration Guide
Von Primitiven Strings zu TypedString
Step 1: Identifiziere Primitive String Validation
// ❌ Before: Primitive validation
public function setUsername(string $username): void
{
if (!ctype_alnum($username)) {
throw new \InvalidArgumentException('Username must be alphanumeric');
}
if (strlen($username) < 3 || strlen($username) > 20) {
throw new \InvalidArgumentException('Username length invalid');
}
$this->username = $username;
}
Step 2: Verwende TypedString Validation
// ✅ After: TypedString validation
public function setUsername(string $username): void
{
$validated = TypedString::fromString($username)
->validate()
->alphanumeric('Username must be alphanumeric')
->lengthBetween(3, 20, 'Username must be 3-20 characters')
->orThrow();
$this->username = $validated->value;
}
Step 3: Oder verwende Specialized VO
// ✅ Best: Specialized Value Object
public function setUsername(AlphanumericString $username): void
{
// Length check wenn benötigt
if (strlen($username->value) < 3 || strlen($username->value) > 20) {
throw new \InvalidArgumentException('Username must be 3-20 characters');
}
$this->username = $username->value;
}
Von ctype_* zu TypedString
Mapping Table:
| PHP ctype_* | TypedString Method | Notes |
|---|---|---|
ctype_alnum($str) |
$ts->isAlphanumeric() |
Konsistent mit leeren Strings |
ctype_alpha($str) |
$ts->isAlphabetic() |
Nur Buchstaben |
ctype_digit($str) |
$ts->isDigits() |
Nur Ziffern |
ctype_lower($str) |
$ts->isLowercase() |
Nur Kleinbuchstaben |
ctype_upper($str) |
$ts->isUppercase() |
Nur Großbuchstaben |
ctype_xdigit($str) |
$ts->isHexadecimal() |
Valides Hex |
ctype_space($str) |
$ts->isWhitespace() |
Nur Whitespace |
ctype_print($str) |
$ts->isPrintable() |
Druckbare Zeichen |
ctype_graph($str) |
$ts->isVisible() |
Keine Whitespace |
Example Migration:
// ❌ Before
if (ctype_alnum($input) && strlen($input) >= 3) {
$this->process($input);
}
// ✅ After
$validated = TypedString::fromString($input)
->validate()
->alphanumeric()
->minLength(3)
->orNull();
if ($validated !== null) {
$this->process($validated->value);
}
Security Considerations
SQL Injection Prevention
TypedString hilft SQL-Injection zu verhindern durch zentrale Validierung:
// ❌ Dangerous: Direct user input
$tableName = $_GET['table'];
$query = "SELECT * FROM {$tableName}"; // SQL Injection möglich!
// ✅ Safe: TypedString validation
$validated = TypedString::fromString($_GET['table'])
->validate()
->alphanumeric('Invalid table name')
->maxLength(63)
->orThrow();
$query = "SELECT * FROM {$validated->value}"; // Safe - validated
Best Practice: Verwende Database VOs (TableName) powered by TypedString:
use App\Framework\Database\ValueObjects\TableName;
// Automatic validation via TableName VO
$tableName = TableName::fromString($_GET['table']); // Throws on invalid input
$query = "SELECT * FROM {$tableName}"; // Safe
XSS Prevention
// ✅ Validate user input
$comment = TypedString::fromString($_POST['comment'])
->validate()
->printable('Comment contains invalid characters')
->maxLength(1000)
->orThrow();
// Still escape for HTML output!
echo htmlspecialchars($comment->value, ENT_QUOTES, 'UTF-8');
Wichtig: TypedString ersetzt NICHT HTML-Escaping - es ergänzt es durch Input-Validierung!
Password Validation
// ✅ Strong password requirements
final readonly class SecurePassword
{
public function __construct(public string $value)
{
TypedString::fromString($value)
->validate()
->minLength(12)
->custom(
fn($ts) => preg_match('/[A-Z]/', $ts->value) === 1,
'Must contain uppercase'
)
->custom(
fn($ts) => preg_match('/[a-z]/', $ts->value) === 1,
'Must contain lowercase'
)
->custom(
fn($ts) => preg_match('/[0-9]/', $ts->value) === 1,
'Must contain digit'
)
->custom(
fn($ts) => preg_match('/[!@#$%^&*(),.?":{}|<>]/', $ts->value) === 1,
'Must contain special character'
)
->orThrow('Password does not meet security requirements');
}
public function hash(): string
{
return password_hash($this->value, PASSWORD_ARGON2ID);
}
}
Performance Considerations
Validation Overhead
TypedString hat minimalen Performance-Overhead:
- Creation: ~0.01ms pro TypedString (einmalige Validierung)
- Validation Chain: ~0.02-0.05ms für 3-5 Rules
- Specialized VO: ~0.01ms (Constructor-Validierung)
- Memory: ~300 bytes pro TypedString Instance
Recommendation: TypedString ist für alle String-Validierungen akzeptabel, auch in Performance-kritischen Pfaden.
Caching Strategies
Für häufig validierte Strings:
final class ValidatedStringCache
{
private static array $cache = [];
public static function getAlphanumeric(string $value): ?AlphanumericString
{
return self::$cache[$value] ??= self::tryValidate($value);
}
private static function tryValidate(string $value): ?AlphanumericString
{
try {
return AlphanumericString::fromString($value);
} catch (\InvalidArgumentException) {
return null;
}
}
}
// Usage
$username = ValidatedStringCache::getAlphanumeric($input);
Wichtig: Nur für Read-Heavy Scenarios - bei Writes lieber direkt validieren!
Testing
Unit Testing TypedString
use App\Framework\String\ValueObjects\TypedString;
it('validates alphanumeric strings', function () {
$str = TypedString::fromString('abc123');
expect($str->isAlphanumeric())->toBeTrue();
expect($str->isDigits())->toBeFalse();
});
it('validates length constraints', function () {
$str = TypedString::fromString('hello');
expect($str->hasMinLength(3))->toBeTrue();
expect($str->hasMaxLength(10))->toBeTrue();
expect($str->hasLengthBetween(3, 10))->toBeTrue();
});
it('throws on invalid validation', function () {
TypedString::fromString('invalid')
->validate()
->digits()
->orThrow();
})->throws(\InvalidArgumentException::class);
Testing Specialized VOs
it('creates alphanumeric string', function () {
$str = AlphanumericString::fromString('user123');
expect($str->value)->toBe('user123');
});
it('rejects non-alphanumeric', function () {
AlphanumericString::fromString('user-123');
})->throws(\InvalidArgumentException::class, 'alphanumeric');
it('converts numeric string to int', function () {
$num = NumericString::fromString('12345');
expect($num->toInt())->toBe(12345);
expect($num->toFloat())->toBe(12345.0);
});
Testing Fluent Validation
it('validates complex password', function () {
$password = TypedString::fromString('Pass123!')
->validate()
->minLength(8)
->custom(fn($ts) => preg_match('/[A-Z]/', $ts->value) === 1, 'uppercase')
->custom(fn($ts) => preg_match('/[0-9]/', $ts->value) === 1, 'digit')
->orThrow();
expect($password->value)->toBe('Pass123!');
});
it('collects validation errors', function () {
$validator = TypedString::fromString('ab')
->validate()
->alphanumeric()
->minLength(5);
expect($validator->fails())->toBeTrue();
$errors = $validator->getErrors();
expect($errors)->toHaveCount(1);
expect($errors[0])->toContain('at least 5 characters');
});
Best Practices
1. Verwende Specialized VOs für Domain-Konzepte
// ✅ Good: Domain-specific VO
final readonly class Username
{
public function __construct(
private AlphanumericString $value
) {
if (strlen($value->value) < 3 || strlen($value->value) > 20) {
throw new \InvalidArgumentException('Username must be 3-20 characters');
}
}
}
// ❌ Bad: Generic TypedString in domain
final readonly class User
{
public function __construct(
public TypedString $username // Zu generisch!
) {}
}
2. Validiere am Boundary
// ✅ Good: Validate at API boundary
final readonly class CreateUserRequest implements ControllerRequest
{
public function __construct(
public AlphanumericString $username,
public Email $email
) {}
public static function fromHttpRequest(HttpRequest $request): self
{
$data = $request->parsedBody->toArray();
// Validation happens here at the boundary
return new self(
username: AlphanumericString::fromString($data['username'] ?? ''),
email: new Email($data['email'] ?? '')
);
}
}
// ❌ Bad: Validate deep in business logic
final readonly class UserService
{
public function createUser(string $username, string $email): User
{
// Too late - validation should be at boundary
$validated = AlphanumericString::fromString($username);
}
}
3. Nutze Custom Validation für Business Rules
// ✅ Good: Custom validation for business rules
$productCode = TypedString::fromString($input)
->validate()
->alphanumeric()
->exactLength(8)
->custom(
fn($ts) => $this->productCodeExists($ts->value),
'Product code does not exist'
)
->orThrow();
// ❌ Bad: Separate validation checks
$code = AlphanumericString::fromString($input);
if (strlen($code->value) !== 8) {
throw new \InvalidArgumentException('Invalid length');
}
if (!$this->productCodeExists($code->value)) {
throw new \InvalidArgumentException('Does not exist');
}
4. Prefer orNull() für Optional Fields
// ✅ Good: orNull() für optional
$bio = TypedString::fromString($input)
->validate()
->printable()
->maxLength(500)
->orNull(); // null wenn validation fails
if ($bio !== null) {
$user->setBio($bio->value);
}
// ❌ Bad: Exception für optional field
try {
$bio = TypedString::fromString($input)
->validate()
->printable()
->maxLength(500)
->orThrow(); // Exception für optional field?
$user->setBio($bio->value);
} catch (\InvalidArgumentException) {
// Optional field - exception nicht nötig
}
5. Cache Validation Results sparsam
// ✅ Good: Cache nur für Read-Heavy Scenarios
final class ConfigValidator
{
private ?AlphanumericString $cachedAppName = null;
public function getAppName(): AlphanumericString
{
return $this->cachedAppName ??=
AlphanumericString::fromString($this->config->get('app.name'));
}
}
// ❌ Bad: Cache für User Input (Security Risk!)
private array $validatedUsernames = [];
public function validateUsername(string $input): AlphanumericString
{
// SECURITY RISK - cache könnte umgangen werden
return $this->validatedUsernames[$input] ??=
AlphanumericString::fromString($input);
}
Framework Compliance
TypedString folgt allen Framework-Prinzipien:
- ✅ Readonly Classes: Alle Klassen sind
final readonly - ✅ Immutability: Keine State-Mutation nach Construction
- ✅ No Inheritance:
finalclasses, composition only - ✅ Value Objects: Keine Primitive Obsession
- ✅ Type Safety: Strikte Type Hints überall
- ✅ Explicit: Factory Methods (
fromString()) für Clarity - ✅ Framework Integration: Fluent API, Chainable Methods
- ✅ Validation: Constructor-basierte oder Fluent Validation
Zusammenfassung
Das TypedString-System bietet:
- ✅ Type-Safe String Validation: Sichere Alternative zu ctype_* Funktionen
- ✅ Fluent Validation API: Chainable Rules mit Error Collection
- ✅ Specialized Value Objects: Domain-spezifische VOs für häufige Use Cases
- ✅ Security: Zentrale Validierung verhindert SQL-Injection und XSS
- ✅ Performance: Minimaler Overhead (<0.05ms pro Validation)
- ✅ Framework Compliance: Readonly, immutable, keine Primitives
- ✅ Testing: Comprehensive Test Coverage mit Pest
- ✅ Migration: Einfacher Wechsel von ctype_* zu TypedString
When to Use:
- Input Validation at API boundaries
- Domain Value Objects requiring format constraints
- Database Identifier Validation
- Security-critical String Validation
- Complex Validation Logic with Custom Rules
When NOT to Use:
- Simple string concatenation
- Performance-ultra-critical inner loops (>10k iterations)
- Strings that don't need validation
- Already validated strings from trusted sources