Files
vollversammlung/Doku/symfony_entwicklung.md

8.8 KiB
Raw Permalink Blame History

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 JsonResponse mit 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:

  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

// 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.