---
title: "ADR-017: Safe Type Casting via SafeCastTrait"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-017@0.35"
source: "Adr/Adr017SafeCastTrait.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-017: Safe Type Casting via SafeCastTrait

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

## Context

Processing untyped data from JSON API responses, form submissions, and
configuration arrays requires casting `mixed` values to specific scalar types.
At PHPStan level 10, direct casts like `(string)$mixed` trigger
"Cannot cast mixed to string" errors. Each usage site would need inline type
guards, leading to repetitive boilerplate.

### Problem statement

1.  **PHPStan level 10 strictness:**
    `(string)$data['key']` is forbidden on `mixed`.
1.  **Verbose alternatives:**
    `is_string($v) ? $v : (is_numeric($v) ? (string)$v : '')`
    at every call site.
1.  **Inconsistent defaults:** Different code paths used
    different fallback values.
1.  **Suppression temptation:** Teams resort to
    `@phpstan-ignore` instead of proper narrowing.

## Decision

Extract a reusable `SafeCastTrait` with three static methods that handle
`mixed` input with sensible defaults and no PHPStan suppressions:

**Classes/Utility/SafeCastTrait.php**

```php
trait SafeCastTrait
{
    private static function toStr(mixed $value): string
    {
        return is_string($value) || is_numeric($value) ? (string)$value : '';
    }

    private static function toInt(mixed $value): int
    {
        return is_numeric($value) ? (int)$value : 0;
    }

    private static function toFloat(mixed $value): float
    {
        return is_numeric($value) ? (float)$value : 0.0;
    }
}
```

Design choices:

-   **Static methods** -- No instance state needed;
    enables `self::toStr()` calls.
-   **Private visibility** -- Implementation detail of
    the using class, not public API.
-   **Numeric passthrough** -- `is_numeric()` covers
    int, float, and numeric strings.
-   **Empty-string default** -- Safer than `null` for
    string contexts (concatenation, comparison).
-   **Zero default** for int/float -- Neutral value for arithmetic operations.

Complements the `ResponseParserTrait` in `Classes/Provider/` which
serves a similar purpose for provider API response arrays but with key-based
access (`getString($data, 'key')`). SafeCastTrait handles standalone values.

Usage in `WizardGeneratorService`:

**Example: Normalizing LLM JSON output**

```php
$result = [
    'identifier' => $this->sanitizeIdentifier(self::toStr($data['identifier'] ?? '')),
    'temperature' => $this->clamp(self::toFloat($data['temperature'] ?? 0.7), 0.0, 2.0),
    'max_tokens' => $this->clampInt(self::toInt($data['max_tokens'] ?? 4096), 1, 128000),
];
```

## Consequences

**Positive:**

-   ●● PHPStan level 10 compliance without any `@phpstan-ignore` suppressions.
-   ● Consistent fallback behavior across all consumers.
-   ● Three-line methods are trivially testable and auditable.
-   ◐ Reduces boilerplate by  5 lines per cast site.

**Negative:**

-   ◑ Trait usage adds an indirect dependency (mitigated
    by being a small utility).
-   ◑ `is_numeric()` accepts numeric strings like `"1e2"` which may surprise.

**Net Score:** +4.5 (Positive)

## Files changed

**Added:**

-   `Classes/Utility/SafeCastTrait.php`

**Modified (consumers):**

-   `Classes/Service/WizardGeneratorService.php` --
    Uses `SafeCastTrait` for JSON normalization.
-   `Classes/Controller/Backend/TaskWizardController.php`
    -- Uses `SafeCastTrait` for form data casting (the monolithic
    `TaskController` was split per [ADR-027](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-027@0.35)).
