- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
8.3 KiB
8.3 KiB
Routing Value Objects
Das Framework unterstützt parallele Routing-Ansätze für maximale Flexibilität und Typsicherheit.
Überblick
// ✅ Traditioneller String-Ansatz (schnell & gewohnt)
#[Route(path: '/api/users/{id}')]
// ✅ Value Object-Ansatz (typsicher & framework-konform)
#[Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')))]
Beide Ansätze sind vollständig kompatibel und können parallel verwendet werden.
String-Basierte Routen
Einfach und gewohnt für schnelle Entwicklung:
final readonly class UserController
{
#[Route(path: '/api/users')]
public function index(): JsonResult { /* ... */ }
#[Route(path: '/api/users/{id}')]
public function show(int $id): JsonResult { /* ... */ }
#[Route(path: '/api/users/{id}/posts/{postId}')]
public function showPost(int $id, int $postId): JsonResult { /* ... */ }
#[Route(path: '/files/{path*}', method: Method::GET)]
public function downloadFile(string $path): StreamResult { /* ... */ }
}
Value Object-Basierte Routen
Typsicher und framework-konform für Production-Code:
Basis-Syntax
use App\Framework\Router\ValueObjects\RoutePath;
use App\Framework\Router\ValueObjects\Placeholder;
final readonly class ImageController
{
#[Route(path: RoutePath::fromElements('images', Placeholder::fromString('filename')))]
public function show(string $filename): ImageResult
{
return new ImageResult($this->imageService->getImage($filename));
}
}
Typisierte Parameter
#[Route(path: RoutePath::fromElements(
'api',
'users',
Placeholder::typed('userId', 'uuid'),
'posts',
Placeholder::typed('postId', 'int')
))]
public function getUserPost(string $userId, int $postId): JsonResult
{
// userId wird automatisch als UUID validiert
// postId wird automatisch als Integer validiert
}
Verfügbare Parameter-Typen
| Typ | Regex Pattern | Beispiel |
|---|---|---|
int |
(\d+) |
/users/{id} → 123 |
uuid |
([0-9a-f]{8}-...) |
/users/{id} → 550e8400-e29b-... |
slug |
([a-z0-9\-]+) |
/posts/{slug} → my-blog-post |
alpha |
([a-zA-Z]+) |
/category/{name} → Technology |
alphanumeric |
([a-zA-Z0-9]+) |
/code/{id} → ABC123 |
filename |
([a-zA-Z0-9._\-]+) |
/files/{name} → image.jpg |
Wildcard-Parameter
#[Route(path: RoutePath::fromElements('files', Placeholder::wildcard('path')))]
public function serveFile(string $path): StreamResult
{
// Matched: /files/uploads/2024/image.jpg
// $path = "uploads/2024/image.jpg"
}
Fluent Builder API
Expressiver Builder für komplexe Routen:
final readonly class ApiController
{
#[Route(path: RoutePath::create()
->segment('api')
->segment('v1')
->segment('users')
->typedParameter('userId', 'uuid')
->segment('posts')
->typedParameter('postId', 'int')
->build()
)]
public function getUserPost(string $userId, int $postId): JsonResult { /* ... */ }
// Quick Helper für häufige Patterns
#[Route(path: RoutePath::create()->segments('api', 'users')->uuid())]
public function showUser(string $id): JsonResult { /* ... */ }
}
Praktische Beispiele
RESTful API Routes
final readonly class ProductController
{
// String-Ansatz für einfache Routen
#[Route(path: '/api/products', method: Method::GET)]
public function index(): JsonResult { }
#[Route(path: '/api/products', method: Method::POST)]
public function create(CreateProductRequest $request): JsonResult { }
// Value Object-Ansatz für komplexe Routen
#[Route(path: RoutePath::fromElements(
'api',
'products',
Placeholder::typed('productId', 'uuid'),
'reviews',
Placeholder::typed('reviewId', 'int')
), method: Method::GET)]
public function getProductReview(string $productId, int $reviewId): JsonResult { }
}
File Serving
final readonly class FileController
{
// Static files (string)
#[Route(path: '/assets/{type}/{filename}')]
public function staticAsset(string $type, string $filename): StreamResult { }
// Dynamic file paths (Value Object mit Wildcard)
#[Route(path: RoutePath::fromElements(
'uploads',
Placeholder::wildcard('path')
))]
public function uploadedFile(string $path): StreamResult { }
}
Admin Routes
final readonly class AdminController
{
// String für bekannte Admin-Pfade
#[Route(path: '/admin/dashboard')]
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1'])]
public function dashboard(): ViewResult { }
// Value Objects für dynamische Admin-Actions
#[Route(path: RoutePath::fromElements(
'admin',
'users',
Placeholder::typed('userId', 'uuid'),
'actions',
Placeholder::fromString('action')
))]
#[Auth(strategy: 'session', roles: ['admin'])]
public function userAction(string $userId, string $action): JsonResult { }
}
Migration & Kompatibilität
Bestehende Routen bleiben unverändert
// ✅ Weiterhin gültig und funktional
#[Route(path: '/api/users/{id}')]
public function show(int $id): JsonResult { }
Schrittweise Migration
final readonly class UserController
{
// Phase 1: Strings für einfache Routen
#[Route(path: '/users')]
public function index(): ViewResult { }
// Phase 2: Value Objects für neue komplexe Routen
#[Route(path: RoutePath::fromElements(
'users',
Placeholder::typed('userId', 'uuid'),
'preferences',
Placeholder::fromString('section')
))]
public function userPreferences(string $userId, string $section): JsonResult { }
}
Konsistenz-Check
// Beide Ansätze sind äquivalent
$stringRoute = new Route(path: '/api/users/{id}');
$objectRoute = new Route(path: RoutePath::fromElements('api', 'users', Placeholder::fromString('id')));
$stringRoute->getPathAsString() === $objectRoute->getPathAsString(); // true
Best Practices
Wann String verwenden
- Prototyping: Schnelle Route-Erstellung
- Einfache Routen: Statische oder 1-Parameter-Routen
- Legacy-Kompatibilität: Bestehende Routen beibehalten
// ✅ Gut für einfache Fälle
#[Route(path: '/health')]
#[Route(path: '/api/status')]
#[Route(path: '/users/{id}')]
Wann Value Objects verwenden
- Production-Code: Maximale Typsicherheit
- Komplexe Routen: Mehrere Parameter mit Validierung
- API-Endpoints: Starke Typisierung für externe Schnittstellen
- Framework-Konsistenz: Vollständige Value Object-Nutzung
// ✅ Gut für komplexe Fälle
#[Route(path: RoutePath::fromElements(
'api',
'v2',
'organizations',
Placeholder::typed('orgId', 'uuid'),
'projects',
Placeholder::typed('projectId', 'slug'),
'files',
Placeholder::wildcard('filePath')
))]
Hybrid-Ansatz (Empfohlen)
final readonly class ProjectController
{
// String für einfache Routen
#[Route(path: '/projects')]
public function index(): ViewResult { }
// Value Objects für komplexe/kritische Routen
#[Route(path: RoutePath::fromElements(
'api',
'projects',
Placeholder::typed('projectId', 'uuid'),
'members',
Placeholder::typed('memberId', 'uuid')
))]
public function getProjectMember(string $projectId, string $memberId): JsonResult { }
}
Router-Integration
Das Framework konvertiert automatisch zwischen beiden Ansätzen:
// Route-Attribute bietet einheitliche Interface
public function getPathAsString(): string // Immer String für Router
public function getRoutePath(): RoutePath // Immer RoutePath-Objekt
// RouteCompiler verwendet automatisch getPathAsString()
$path = $routeAttribute->getPathAsString(); // Funktioniert für beide
Fazit
- String-Routen: Schnell, gewohnt, ideal für einfache Fälle
- Value Object-Routen: Typsicher, framework-konform, ideal für komplexe Fälle
- Vollständige Kompatibilität: Beide Ansätze parallel nutzbar
- Keine Breaking Changes: Bestehender Code funktioniert weiterhin
- Schrittweise Adoption: Migration nach Bedarf möglich
Empfehlung: Hybrid-Ansatz mit Strings für einfache und Value Objects für komplexe Routen.