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)