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:
- Command (DTO) - Eingehende Daten
- Handler - Business Logic
- 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.phpCommand (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:
readonlyfür Immutabilityfinalum 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:
- Validierung - Value Objects nutzen
- Business Rules - Domain-Regeln prüfen
- Entity-Manipulation - Objekte erstellen/ändern
- Persistierung - Repository nutzen
- Events - Domain Events dispatchen
- 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
- Single Responsibility: Ein Use Case = Eine Aktion
- Dependency Injection: Nur Interfaces injizieren
- Immutable Commands: readonly für Commands
- Events: Immer Events für wichtige Aktionen
- Exceptions: Spezifische Exception-Types
- Testing: Mindestens Unit-Tests
- Transactions: Bei mehreren DB-Operationen
- Logging: Kritische Operationen loggen