---
title: "ADR-015: Type-Safe Domain Models via PHP 8.1+ Enums & Value Objects"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-015@0.35"
source: "Adr/Adr015TypeSafeDomainModels.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-015: Type-Safe Domain Models via PHP 8.1+ Enums & Value Objects

-   *Status:* Accepted
-   *Date:* 2025-12
-   *Authors:* Netresearch DTT GmbH

## Context

Domain constants were stringly-typed throughout the codebase. Adapter types were
plain strings (`'openai'`, `'anthropic'`), capabilities were CSV strings in
database columns, task categories and output formats were validated ad-hoc. This
caused subtle bugs and PHPStan violations at higher analysis levels.

### Problem statement

1.  **No compile-time safety:** Typos like `'opanai'` pass silently at runtime.
1.  **Scattered validation:** Each usage site re-validated allowed values.
1.  **Missing behavior:** Constants carried no associated
    logic (labels, icons, defaults).
1.  **PHPStan violations:** Stringly-typed comparisons defeated type narrowing.

## Decision

Use PHP 8.1+ backed enums for all domain constants. Each enum provides:

-   A `string`-backed value for database/API compatibility.
-   Static helpers: `values()`, `isValid()`, `tryFromString()`.
-   Domain-specific methods: `label()`,
    `getIconIdentifier()`, `getContentType()`.

**Example: AdapterType enum with behavior**

```php
enum AdapterType: string
{
    case OpenAI = 'openai';
    case Anthropic = 'anthropic';
    case Gemini = 'gemini';
    case Ollama = 'ollama';
    // ...

    public function label(): string { /* ... */ }
    public function defaultEndpoint(): string { /* ... */ }
    public function requiresApiKey(): bool { /* ... */ }
    public static function toSelectArray(): array { /* ... */ }
}
```

Enums implemented:

| Enum | Purpose | Cases |
| --- | --- | --- |
| AdapterType | LLM provider protocol type | 9 cases (OpenAI through Custom) |
| ModelCapability | Model feature flags | 11 cases (chat, completion, embeddings, vision, streaming, tools, json_mode, audio, image, text_to_speech, transcription) |
| TaskCategory | Task organization | 5 cases (content, log_analysis...) |
| TaskInputType | Task input source | 5 cases (manual, syslog, file...) |
| TaskOutputFormat | Response rendering format | 4 cases (markdown, json...) |
| ModelSelectionMode | Model selection strategy | 2 cases (fixed, criteria) |

Immutable readonly DTOs for composite data transfer:

-   `DetectedProvider` -- Provider detection result with confidence score.
-   `DiscoveredModel` -- Model metadata from API discovery.
-   `SuggestedConfiguration` -- AI-generated configuration preset.
-   `CompletionResponse` -- Immutable
    `final readonly class` for LLM responses.

## Consequences

**Positive:**

-   ●● Invalid values caught at instantiation (`BackedEnum::from()` throws).
-   ●● PHPStan level 10 compliance without `@phpstan-ignore` suppressions.
-   ● Self-documenting:
    `AdapterType::OpenAI->defaultEndpoint()` vs string
    lookup.
-   ● IDE auto-completion and refactoring support.
-   ◐ `match` expressions enforce exhaustive handling of all cases.

**Negative:**

-   ◑ Requires PHP 8.1+ (already the minimum for TYPO3 v13).
-   ◑ Enum `#[CoversNothing]` needed for PHPUnit 12 coverage.

**Net Score:** +6.0 (Strong positive)

## Files changed

**Added:**

-   `Classes/Domain/Model/AdapterType.php`
-   `Classes/Domain/Enum/ModelCapability.php`
-   `Classes/Domain/Enum/ModelSelectionMode.php`
-   `Classes/Domain/Enum/TaskCategory.php`
-   `Classes/Domain/Enum/TaskInputType.php`
-   `Classes/Domain/Enum/TaskOutputFormat.php`

**Modified:**

-   `Classes/Domain/Model/Provider.php` -- Uses `AdapterType` enum.
-   `Classes/Domain/Model/Model.php` -- Uses `ModelCapability` enum.
-   `Classes/Domain/Model/Task.php` -- Uses
    `TaskCategory`, `TaskInputType`,
    `TaskOutputFormat`.
-   `Classes/Provider/AbstractProvider.php` --
    Adapter type matching via enum.
