Skip to content

Use Case Implementation Guide

Anleitung zum Implementieren von Use Cases in der Appiyon-Plattform.

Übersicht

Use Cases repräsentieren die Anwendungslogik und orchestrieren die Domänen-Objekte. Sie folgen dem CQRS-Pattern mit Command/Handler-Separation.

Struktur

Jeder Use Case besteht aus 3 Komponenten:

  1. Command (DTO) - Eingehende Daten
  2. Handler - Business Logic
  3. Result (Optional) - Rückgabewert

Use Case-Ordnerstruktur

src/Appi/[Layer]/[Module]/UseCase/
├── CreateEntity/
│   ├── CreateEntityCommand.php
│   └── CreateEntityHandler.php
├── AuthenticateEntity/
│   ├── AuthenticateEntityCommand.php
│   └── AuthenticateEntityHandler.php
└── UpdateEntity/
    ├── UpdateEntityCommand.php
    └── UpdateEntityHandler.php

Command (DTO)

Commands sind einfache DTOs, die Eingabedaten kapseln:

php
<?php

declare(strict_types=1);

namespace App\Appi\Infrastructure\Admin\UseCase\CreateAdmin;

final readonly class CreateAdminCommand
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password
    ) {
    }
}

Best Practices für Commands:

  • readonly für Immutability
  • final um Vererbung zu verhindern
  • Public Properties für einfachen Zugriff
  • Keine Validierung (erfolgt im Handler)
  • Nur primitive Typen oder Value Objects

Handler

Handler enthält die eigentliche Business Logic:

php
<?php

declare(strict_types=1);

namespace App\Appi\Infrastructure\Admin\UseCase\CreateAdmin;

use App\Appi\Infrastructure\Admin\Contract\AdminRepositoryInterface;
use App\Appi\Infrastructure\Admin\Entity\Admin;
use App\Appi\Infrastructure\Admin\Event\AdminCreatedEvent;
use App\Appi\Infrastructure\Admin\ValueObject\AdminEmail;
use App\Appi\Infrastructure\Admin\ValueObject\AdminPassword;
use App\Appi\Shared\Exception\Domain\DomainException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

final readonly class CreateAdminHandler
{
    public function __construct(
        private AdminRepositoryInterface $adminRepository,
        private EventDispatcherInterface $eventDispatcher
    ) {
    }

    public function handle(CreateAdminCommand $command): Admin
    {
        // 1. Validierung
        $email = AdminEmail::fromString($command->email);
        $password = AdminPassword::fromPlainText($command->password);

        // 2. Business Rules prüfen
        if ($this->adminRepository->existsByEmail($email->getValue())) {
            throw new DomainException('Admin with this email already exists');
        }

        // 3. Entity erstellen
        $admin = new Admin(
            $command->name,
            $email,
            $password
        );

        // 4. Persistieren
        $this->adminRepository->save($admin);

        // 5. Event dispatchen
        $event = new AdminCreatedEvent($admin);
        $this->eventDispatcher->dispatch($event);

        // 6. Rückgabe
        return $admin;
    }
}

Handler Pattern

Standard-Ablauf:

  1. Validierung - Value Objects nutzen
  2. Business Rules - Domain-Regeln prüfen
  3. Entity-Manipulation - Objekte erstellen/ändern
  4. Persistierung - Repository nutzen
  5. Events - Domain Events dispatchen
  6. Rückgabe - Entity oder DTO zurückgeben

Dependency Injection:

Handler nutzen Constructor Injection für Dependencies:

php
public function __construct(
    private AdminRepositoryInterface $adminRepository,
    private EventDispatcherInterface $eventDispatcher,
    private LoggerInterface $logger
) {
}

Komplexere Use Cases

Mit Rate Limiting

php
final readonly class AuthenticateAdminHandler
{
    private const MAX_ATTEMPTS_PER_EMAIL = 5;
    private const MAX_ATTEMPTS_PER_IP = 10;
    private const LOCKOUT_MINUTES = 15;

    public function __construct(
        private AdminRepositoryInterface $adminRepository,
        private AdminLoginAttemptRepositoryInterface $loginAttemptRepository,
        private EventDispatcherInterface $eventDispatcher
    ) {
    }

    public function handle(AuthenticateAdminCommand $command): Admin
    {
        // Rate Limiting prüfen
        $this->checkRateLimits($command->email, $command->ipAddress);

        // Admin finden
        $admin = $this->adminRepository->findByEmail($command->email);
        if (!$admin) {
            $this->recordFailedAttempt($command->email, $command->ipAddress);
            throw new AuthenticationException('Invalid credentials');
        }

        // Password prüfen
        $password = AdminPassword::fromHash($admin->getPassword());
        if (!$password->verify($command->password)) {
            $this->recordFailedAttempt($command->email, $command->ipAddress);
            throw new AuthenticationException('Invalid credentials');
        }

        // Erfolgreichen Login aufzeichnen
        $this->recordSuccessfulAttempt($admin, $command->ipAddress);

        // Event dispatchen
        $event = new AdminAuthenticatedEvent($admin);
        $this->eventDispatcher->dispatch($event);

        return $admin;
    }

    private function checkRateLimits(string $email, string $ipAddress): void
    {
        $emailAttempts = $this->loginAttemptRepository
            ->countRecentFailedAttemptsByEmail($email, self::LOCKOUT_MINUTES);

        if ($emailAttempts >= self::MAX_ATTEMPTS_PER_EMAIL) {
            throw new TooManyRequestsException('Too many login attempts');
        }

        $ipAttempts = $this->loginAttemptRepository
            ->countRecentFailedAttemptsByIp($ipAddress, self::LOCKOUT_MINUTES);

        if ($ipAttempts >= self::MAX_ATTEMPTS_PER_IP) {
            throw new TooManyRequestsException('Too many login attempts');
        }
    }
}

Mit Transactions

php
use Doctrine\ORM\EntityManagerInterface;

public function handle(ComplexCommand $command): void
{
    $this->entityManager->beginTransaction();

    try {
        // Multiple Operationen
        $entity1 = $this->repository1->save($data1);
        $entity2 = $this->repository2->save($data2);
        $this->repository3->delete($entity3);

        $this->entityManager->commit();
    } catch (\Exception $e) {
        $this->entityManager->rollback();
        throw $e;
    }
}

Events dispatchen

Nach erfolgreichen Operationen sollten Events dispatched werden:

php
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

$event = new AdminCreatedEvent($admin);
$this->eventDispatcher->dispatch($event);

Exception Handling

Use Cases sollten spezifische Exceptions werfen:

php
use App\Appi\Shared\Exception\Domain\DomainException;
use App\Appi\Shared\Exception\Application\NotFoundException;
use App\Appi\Shared\Exception\Application\ValidationException;

// Domain-Regel verletzt
throw new DomainException('Admin with this email already exists');

// Entity nicht gefunden
throw new NotFoundException('Admin not found');

// Validierung fehlgeschlagen
throw new ValidationException('Invalid email format');

Testing

Use Cases sollten umfassend getestet werden:

php
// tests/Unit/Appi/Infrastructure/Admin/UseCase/CreateAdmin/CreateAdminHandlerTest.php

class CreateAdminHandlerTest extends TestCase
{
    public function testHandleCreatesAdminSuccessfully(): void
    {
        $repository = $this->createMock(AdminRepositoryInterface::class);
        $eventDispatcher = $this->createMock(EventDispatcherInterface::class);

        $handler = new CreateAdminHandler($repository, $eventDispatcher);

        $command = new CreateAdminCommand(
            'Test Admin',
            'test@example.com',
            'SecurePassword123!'
        );

        $result = $handler->handle($command);

        $this->assertInstanceOf(Admin::class, $result);
    }
}

Beispiele

CreateAdmin Use Case

Siehe vollständiges Beispiel:

AuthenticateAdmin Use Case

Siehe vollständiges Beispiel:

LogAdminAction Use Case

Siehe vollständiges Beispiel:

Checkliste

  • [ ] Command als readonly DTO erstellt
  • [ ] Handler mit Constructor Injection
  • [ ] Value Objects für Validierung genutzt
  • [ ] Business Rules geprüft
  • [ ] Repository Interface genutzt
  • [ ] Events dispatched
  • [ ] Spezifische Exceptions geworfen
  • [ ] Unit Tests geschrieben
  • [ ] Integration Tests geschrieben
  • [ ] PHPDoc wo nötig

Best Practices

  1. Single Responsibility: Ein Use Case = Eine Aktion
  2. Dependency Injection: Nur Interfaces injizieren
  3. Immutable Commands: readonly für Commands
  4. Events: Immer Events für wichtige Aktionen
  5. Exceptions: Spezifische Exception-Types
  6. Testing: Mindestens Unit-Tests
  7. Transactions: Bei mehreren DB-Operationen
  8. Logging: Kritische Operationen loggen

Built with VitePress