Coding Guidelines (PHP / Backend)

Coding Guidelines (PHP / Backend)

General

  • PHP 8.4+ with declare(strict_types=1); in every file
  • PSR-12 coding standard (enforced by PHP CS Fixer)
  • PHPStan level 8 (enforced in CI)
  • Final classes by default
  • Constructor promotion for value objects and DTOs
  • No mixed type — be explicit

Naming

  • Classes: PascalCase
  • Methods/properties: camelCase
  • Constants: UPPER_SNAKE_CASE
  • Database tables: snake_case with module prefix (people_members, groups_groups)
  • Events: {module}.{entity}.{action} (people.member.created)

Domain Layer (Domain/)

  • Pure PHP — no Symfony, no Doctrine, no framework imports
  • Aggregates extend nothing (no base class required)
  • Value objects are immutable, use readonly classes
  • Domain events implement a shared DomainEventInterface
  • Repository interfaces defined here, implementations in Infrastructure
  • Exceptions are domain-specific: MemberNotFoundException, not EntityNotFoundException

Application Layer (Application/)

  • Command/Query separation (CQRS-lite)
  • Commands change state, return void or the new ID
  • Queries return DTOs, never entities
  • Handlers are final classes with __invoke() method
  • One handler per command/query

Infrastructure Layer (Infrastructure/)

  • Implements domain repository interfaces
  • Doctrine ORM mappings (XML or attributes — on infrastructure classes only)
  • External service adapters
  • Framework integration code

Presentation Layer (Presentation/)

  • Thin controllers: validate input, delegate to handler, return response
  • No business logic in controllers — ever
  • Request DTOs for input validation
  • Response DTOs for output shaping
  • OpenAPI attributes for API documentation

Timezone Strategy

  • Store all timestamps in UTC (DateTimeImmutable with DateTimeZone('UTC'))
  • PostgreSQL columns use timestamptz — always store with timezone info
  • Organizations and events carry an IANA timezone (e.g., Europe/Zurich) as a value object
  • Convert to local time at the presentation layer — API responses include both UTC and the entity’s timezone
  • Use DateTimeImmutable exclusively — never DateTime (mutable)
  • Use ext-intl (IntlDateFormatter) for locale-aware date/time formatting
  • Recurring events (RRULE): expand occurrences in the event’s timezone, then convert to UTC for storage
  • Value object Timezone: wraps a validated IANA timezone string, rejects invalid zones
  • PHP config: date.timezone = UTC in php.ini — the server always thinks in UTC

Error Handling

  • Domain exceptions for business rule violations
  • Specific exception classes, not generic \Exception
  • Controllers catch domain exceptions and map to HTTP status codes
  • Never catch and silently swallow exceptions
  • Log at the boundary (infrastructure), not in the domain

Do

  • Use value objects for IDs, emails, phone numbers, etc.
  • Use enums for finite sets of values
  • Use readonly properties and classes where appropriate
  • Validate invariants in constructors and factory methods
  • Emit domain events from aggregate methods

Don’t

  • Don’t use array when a typed collection or DTO would be clearer
  • Don’t use null as a signal — use Option types or explicit empty states
  • Don’t put SQL in domain or application layers
  • Don’t import from another module’s Domain or Infrastructure
  • Don’t use static methods for business logic
  • Don’t use traits for shared domain logic (use composition)