8.8 KiB
8.8 KiB
Symfony Entwicklungs-Dokumentation
1. Provider/Processor-Pattern
Pattern-Struktur
Alle API-Anfragen durchlaufen zwei Phasen:
- Provider: Daten aus verschiedenen Quellen zusammenführen, Validierung vorbereiten, Domain-Logik entkoppeln
- Processor: Daten persistieren, Events auslösen, Response aufbauen
Beispiel: Anwesenheit registrieren
// src/Provider/AnwesenheitCreateProvider.php
final class AnwesenheitCreateProvider
{
public function __construct(
private VeranstaltungRepository $vr,
private PersonenRepository $pr,
private MandantIdScope $scope
) {}
public function provides(CreateAnwesenheitRequest $request): AnwesenheitCommand
{
$veranstaltung = $this->vr->findScop($request->veranstaltungId, $this->scope);
if (!$veranstaltung) {
throw new VeranstaltungNotFoundException($request->veranstaltungId);
}
return new AnwesenheitCommand(
veranstaltung: $veranstaltung,
personenId: $request->personenId,
status: $request->status
);
}
}
// src/Processor/AnwesenheitCreateProcessor.php
final class AnwesenheitCreateProcessor
{
public function __construct(
private EntityManagerInterface $em,
private EventDispatcherInterface $dispatcher
) {}
public function process(AnwesenheitCommand $command): AnwesenheitCreatedEvent
{
$anwesenheit = new Anwesenheit();
$anwesenheit->setVeranstaltung($command->veranstaltung);
$anwesenheit->setPersonenId($command->personenId);
// ...
$this->em->persist($anwesenheit);
$this->em->flush();
$event = new AnwesenheitCreatedEvent($anwesenheit);
$this->dispatcher->dispatch($event);
return $event;
}
}
Controller-Einbindung
#[Route('/veranstaltungen/{id}/anwesenheit', name: 'erstellen_anwesenheit', methods: ['POST'])]
public function create(CreateAnwesenheitRequestDto $dto, RequestCreator $creator): JsonResponse
{
try {
$command = $this->providerProvider->provides($dto);
$result = $this->processorProvider->process($command);
return new JsonResponse(['data' => $result], 201);
} catch (EntityNotFoundException $e) {
return JsonResponse::createNotFound($e->getMessage());
}
}
Muster-Regeln
- Provider enthalten keine Persistenz-Logik
- Processor enthalten keine Validierung-Logik
- Beide sind stateless und leicht testbar
- Commands und Events als Data Transfer Objekte (POPOs)
2. Coding Standards
PHP Version & Basis
- PHP >= 8.4 (LTS empfohlen)
- PSR-12 als Code-Stil-Richtlinie
- PHPStan Level 6-7
Naming Conventions
| Typ | Konvention | Beispiel |
|---|---|---|
| Klassen | PascalCase + Substantiv | AnwesenheitController |
| Methoden | camelCase + Verb oder Aktonsnamen | registriereAnwesenheit() |
| Variablen | camelCase | $veranstaltungsId |
| Constants | UPPER_SNAKE_CASE | MAX_TEILNEHMER |
Controller-Konventionen
- Keine Domain-Logik – immer delegieren an Provider/Processor
- DTOs für Request/Response (keine Entities direkt)
{
"data": {
"id": 1,
"status": "present"
},
"meta": {
"message": "Anwesenheit erfolgreich registriert"
}
}
Exception Handling (→ siehe Abschnitt 3)
- Domain Exceptions werfen (keine raw Symfony Exceptions nach außen)
- Nur
JsonResponsemit definiertem Error-Schema zurückgeben
3. Fehlerbehandlung & API Error Schema
Exceptions-Hierarchie
Domain Exceptions → ControllerExceptionHandler → JSON Response
// Domain Exception
class VeranstaltungNotFoundException extends RuntimeException implements DomainExceptionInterface {}
// Controller Exception Handler (Kernel-Register)
#[AsController]
class ControllerExceptionHandler
{
public function __invoke(DomainExceptionInterface $e): JsonResponse
{
return new JsonResponse([
'errors' => [[
'status' => 422, // oder je nach Situation
'title' => ucfirst($e->getType()),
'detail' => $e->getMessage(),
]]
], 422);
}
}
HTTP Status Codes
| Code | Verwendigunbg |
|---|---|
200 |
OK (GET, PUT) |
201 |
Created (POST, Entity erstellt) |
400 |
Invalid Request Body / Validation Error |
403 |
Forbidden (Mandant fehlt) |
404 |
Not Found |
500 |
Internal Server Error |
4. Logging & Mercure
Events-Logging-Logik
Jeder Processor löst nach Persistierung Events aus:
$this->dispatcher->dispatch(new AnwesenheitAktualisiertEvent($anwesenheit));
Events enthalten:
- Entity-Daten (keine sensiblen)
- Timestamp
- MandantID scope
- ActionTyp ('created', 'updated')
Mercure Integration - Vorschläge & Nutzungsszenarien
- Echtzeit-Anwesenheitsliste: Websocket oder Server-Sent Events über Mercure Hub
- Live-Benachrichtigungen bei Statusänderungen
Konfigurationsoptionen für Mercure:
- Pulled (Clientseitig abonnieren): Client ruft
/mercure/hubPeriodisch ab - Pushed (Serverseitig publish): Processor publiziert Events direkt an Hub
- Hybrid: Pushed bei User-Aktion (z.b. Anwesenheit registrieren), Pulled bei Statusabfragen
Empfehlung: Mercure als optional, nur wenn Echtzeit-Funktionalität wirklich gebraucht wird. Nicht default aktivieren.
5. Datenbank/Migrationen - Entwicklungskonventionen
Doctrine Entity-Spezifikation
- Entities POPOs (Plain Old PHP Objects) mit Annotationen/Attributes für Mapping
- Repository nur Domain-spezifische Queries, kein raw SQL
- Keine Business-Logik in Entities – stattdessen Services / Provider / Processor
- Idempotenz: Updates per primary key
Migrationen-Konventionen
-
Jede Tabelle hat
mandant_id,created_at,updated_at(Timestamp) -
Fremdschlüssel immer als Index + NOT NULL
-
Migrations in Version kontrolliert:
php bin/console make:migration -
Keine manuellen Schema-Changes ohne Migration
Multi-Tenant-Scope Enforcement
Entity::mandant_id → Unique across Mandanten. Alle Queries scoped per MandantIdContext (z.B. durch Attribute/Listener)
6. TDD Workflow (Red-Green-Refactor)
Test-Schichten Priorität (TDD Red-Green)
- Controller/Route Tests – Request→Response, Status-Codes, JSON Schema
- Provider/Processor Unittests – isolierte logik-test der einzelnen Domäne-Layer
- Integrationstests – wenn nötig (DB-Kontext, Services)
Test-Struktur in PHPUnit
api/tests/Unit/Provider/
├── AnwesenheitCreateProviderTest.php ← Provider ohne DB (mock repository)
└── VeranstaltungFindProviderTest.php
api/tests/Unit/Processor/
└── AnwesenheitCreateProcessorTest.php ← Processor mit mock EM
api/tests/Functional/Controller/
└── AnwesenheitControllerTest.php ← Full HTTP request via HttpBrowser
Test-Beispiel PHPUnit
// tests/Unit/Provider/AnwesenheitCreateProviderTest.php
class AnwesenheitCreateProviderTest extends TestCase
{
public function testProvidesCommandWhenVeranstaltungFound(): void
{
$veranstaltung = new Veranstaltung();
$request = new CreateAnwesenheitRequestDto(123);
$provider = new AnwesenheitCreateProvider( // Mocks injected
VeranstaltungRepositoryMock($veranstaltung),
MandantIdScope('mandant-xyz')
);
$result = $provider->provides($request);
self::assertInstanceOf(AnwesenheitCommand::class, $result);
self::assertSame($veranstaltung, $result->getVeranstaltung());
}
}
Test-Prioritäten für Implementierung
- Controller/Routen-Tests schreiben → failend (RED)
- Minimal-Implementierung schreiben → test bestanden (GREEN)
- Refactor (Code-Qualität, Tests verbessern)
- Nächster Test (höhere Abdeckung im Provider/Processor)
7. Framework-Bundles
Philosophie
- Kern-Symfony nur verwenden (Mime, HttpFoundation, Serializer, Validator, Security)
- Neue Bundles nur bei Bedarf dokumentieren und begründen
| Bundle | Verwendung | Begründung erforderlich |
|---|---|---|
symfony/maker |
Boilerplate | Nur für Initialisierung, nicht produktiv |
api-platform/core |
REST API | Explizit vermieden – manuelle Controller |
sensio/framework-extra-bundle |
Annotations/Cache | Symfony 7.4 native statt Bundles bevorzugen |
symfonycasts/validator |
Custom Validators | Nur bei komplexer Validierung |
mercure/bundle |
Mercure-Integration | Nur wenn Echtzeit benötigt wird |
Zusammenfassung Entwicklungsflow
- Test schreiben (RED) → failend
- Minimal-Implementierung als Provider/Processor/Gateway
- Test bestehen lassen (GREEN)
- Refaktorieren und Qualität prüfen (PHPStan, Coding Standards)
- Entity/Migration erstellen wenn nötig nach Code-Phase
Die Doku wird mit dem Projekt aktualisiert.