Type Safety in PHP (strict_types & Static Analysis)
debt(d5/e7/b7/t7)
Closest to 'specialist tool catches it' (d5). The detection_hints.tools list is phpstan, psalm, and rector — all specialist static analysis tools, not default linters. The code_pattern confirms the problem is invisible without these tools: 'mixed types everywhere; type errors only caught at runtime.' Nothing in the default PHP runtime or a basic linter surfaces these issues.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix is 'Work towards PHPStan level 8 incrementally — fix one level per sprint, starting from level 3.' This is explicitly a multi-sprint, iterative effort that touches return types, parameter types, and internal methods across the entire codebase. The common_mistakes confirm the problem decays from the inside out, affecting every method and file without return types. This is well beyond a single-file fix.
Closest to 'strong gravitational pull' (b7). The term applies_to all three contexts (web, cli, queue-worker) and is tagged as a quality principle for php8. Absent type safety, every future change is shaped by untrusted types — callers cannot rely on contracts, internal methods silently degrade, and static analysis is blocked. This exerts a persistent pull on all work streams, not just one component.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception is precise: developers assume declare(strict_types=1) makes the whole application type-safe, but it only affects call boundaries in that one file and has no effect on calls from other files without strict mode. This directly contradicts the intuitive expectation that a single declaration enforces a global guarantee, and it contradicts how strict typing works in languages like TypeScript or Java where type enforcement is global.
Also Known As
TL;DR
Explanation
PHP's type system has evolved substantially: scalar type declarations (PHP 7.0), return types (7.0), nullable types (7.1), typed class properties (7.4), union types (8.0), intersection types (8.1), and DNF types (8.2). Declaring strict_types=1 at the top of a file makes PHP enforce declared types strictly — no silent coercions. But strict_types only applies to function calls within that file. The real force multiplier is static analysis: PHPStan and Psalm infer types through the code graph, catching: passing null where not nullable, incorrect return types, impossible conditions, and missing match arms. Aim for PHPStan level 6+ and full property/return type coverage. Use /** @var Type */ docblocks only where inference is insufficient.
Common Misconception
Why It Matters
Common Mistakes
- Not declaring return types — the method contract is implicit and callers cannot trust the type.
- Using mixed or omitting types on internal methods — type safety degrades from the inside out.
- Not enabling strict_types=1 — PHP silently coerces types without it, hiding mismatches.
- Relying on PHPDoc @return annotations for type safety — they are not enforced at runtime.
Code Examples
// No type declarations — silent type coercion hides bugs:
function calculateDiscount($price, $percent) {
return $price * ($percent / 100); // '10' * (0.1) = 1.0 — works, but by accident
}
calculateDiscount('ten', '20%'); // No error, returns 0
// With types:
function calculateDiscount(float $price, float $percent): float { /* ... */ }
<?php declare(strict_types=1);
class Money {
public function __construct(
public readonly int $amount,
public readonly string $currency,
) {}
public function add(Money $other): Money {
if ($this->currency !== $other->currency) {
throw new CurrencyMismatchException();
}
return new Money($this->amount + $other->amount, $this->currency);
}
}