Files
michaelschiemer/docs/claude/routing-value-objects.md
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- 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
2025-10-05 11:05:04 +02:00

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.