# 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 ```php // 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 ```php #[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) ```json { "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 `JsonResponse` mit definiertem Error-Schema zurückgeben --- ## 3. Fehlerbehandlung & API Error Schema ### Exceptions-Hierarchie Domain Exceptions → ControllerExceptionHandler → JSON Response ```php // 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: ```php $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: 1. **Pulled** (Clientseitig abonnieren): Client ruft `/mercure/hub` Periodisch ab 2. **Pushed** (Serverseitig publish): Processor publiziert Events direkt an Hub 3. **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) 1. **Controller/Route Tests** – Request→Response, Status-Codes, JSON Schema 2. **Provider/Processor Unittests** – isolierte logik-test der einzelnen Domäne-Layer 3. **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 ```php // 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 1. Controller/Routen-Tests schreiben → **failend** (RED) 2. Minimal-Implementierung schreiben → **test bestanden** (GREEN) 3. Refactor (Code-Qualität, Tests verbessern) 4. 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 1. Test schreiben (RED) → failend 2. Minimal-Implementierung als Provider/Processor/Gateway 3. Test bestehen lassen (GREEN) 4. Refaktorieren und Qualität prüfen (PHPStan, Coding Standards) 5. Entity/Migration erstellen wenn nötig **nach** Code-Phase Die Doku wird mit dem Projekt aktualisiert.