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
- PHPStan level 10 strictness:
(string)$data['key']is forbidden onmixed. - Verbose alternatives:
is_string($v) ? $v : (is_numeric($v) ? (string)$v : '')at every call site. - Inconsistent defaults: Different code paths used different fallback values.
- Suppression temptation: Teams resort to
@phpstan-ignoreinstead of proper narrowing.
Decision
Extract a reusable Safe with three static methods that handle
mixed input with sensible defaults and no PHPStan suppressions:
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
nullfor string contexts (concatenation, comparison). - Zero default for int/float -- Neutral value for arithmetic operations.
Complements the Response in Classes/ which
serves a similar purpose for provider API response arrays but with key-based
access (getString($data, 'key')). SafeCastTrait handles standalone values.
Usage in Wizard:
$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-ignoresuppressions. - ● 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/ Safe Cast Trait. php
Modified (consumers):
Classes/-- UsesService/ Wizard Generator Service. php Safefor JSON normalization.Cast Trait Classes/-- UsesController/ Backend/ Task Controller. php Safefor form data casting.Cast Trait