skip to content

Your team is adding declare(strict_types=1) file by file to a legacy PHP library-fines calculator; what breaks, and how do you roll it out safely?

level: seniorimportance: should knowfreq 30%

answer

  1. calls from the converted file change
  2. request input is always strings
  3. round() returns float, not int
  4. types first, strict flag second
  5. convert at the boundary, then test

basics

~20 s

Flipping a file makes its own calls and return checks strict, so request strings passed to int parameters, numeric strings passed to built-ins and round()'s float returned as int throw TypeError. Add types first, validate input at the boundary, then flip files under tests.

solid answer

~50 s

Because strict mode follows the **calling file**, flipping a file changes three things: calls it makes to user functions (a controller passing `$_GET['days']`, always a string, to `fineCents(int $days)`), calls it makes to built-ins (`str_repeat('-', $width)` with a string width), and its own **return checks** (`return round($amount);` from a function declared `: int` — `round()` returns `float`). None of this breaks callers of the converted file. So roll out in an order that keeps each failure local: add accurate parameter and return types while still coercive, since coercive mode already rejects non-numeric strings for numeric parameters; convert and validate input once at the boundary; flip the domain files first, then the entry points; run the test suite and static analysis on every flipped file; and treat each `TypeError` as a real bug found, not a reason to widen the type.

code

php · 13 lines
php
<?php
declare(strict_types=1);

// entry point, flipped last: convert request input once, at the boundary
$raw = $_GET['days'] ?? '';

if (!ctype_digit($raw)) {
    http_response_code(400);
    exit('days must be a whole number');
}

$calculator = new FineCalculator();
echo $calculator->fineCents((int) $raw, 0.25);

go deeper

for a junior

Know that request input is always a string and that strict mode will not turn "4" into 4 for you; conversion has to be written explicitly.

for a middle

Explain which calls change when a file is flipped — its own calls and return checks, not its callers — and trace the round()-returns-float trap.

for a senior

Lay out the order: types first, boundary validation, domain files, then entry points, with tests and static analysis gating each flip and no type widening.

for a principal

Decide how to stage and enforce the migration across teams: which modules go first, what CI rule keeps new files strict, and how much test coverage a flip requires.

## What actually changes when a file is flipped `declare(strict_types=1)` affects the calls a file **makes** and the return values its functions **produce**. It does not affect how other files call into it. When a legacy fines calculator is converted one file at a time, each flip can surface three kinds of `TypeError`: 1. **Arguments this file passes to user functions.** An entry script that reads `$_GET['days']` gets a string — request input is always strings — and passes it to `fineCents(int $days)`. Coercive mode turned `"4"` into `4`; strict mode throws. 2. **Arguments this file passes to built-ins.** `str_repeat('-', $width)` with a width read from a config string, or `round($total, $precisionFromIni)`, now throws. 3. **Return values this file's functions produce.** The return check uses the declaring file's mode, so a strict file whose function is declared `: int` can no longer return a float. The third case hides well in money code: ```php <?php declare(strict_types=1); final class FineCalculator { public function fineCents(int $daysLate, float $dailyRate): int { return round($daysLate * $dailyRate * 100); // round() is declared to return float: // TypeError: FineCalculator::fineCents(): Return value must be of type int, float returned } } ``` In coercive mode this returned `150` for `150.0` without complaint, because a float with no fractional part converts to `int`. The fix is explicit: `return (int) round(...);`, with the rounding rule chosen deliberately. ## What does not break Flipping `FineCalculator.php` does **not** break a coercive controller that calls `fineCents("4", 0.25)`. That call is made from the controller's file, so it is still coerced. This is what makes a file-by-file rollout possible: every flip is local to the file flipped. It also means the calculator gains no protection from sloppy callers until those callers are flipped too. ## A rollout order that keeps failures local | Step | What you do | Why | |---|---|---| | 1 | Add accurate parameter and return types, leave files coercive | Coercive mode already rejects arrays and non-numeric strings for numeric parameters, so obvious mismatches appear with little risk | | 2 | Convert and validate input once, at the boundary | Request, CSV and config values arrive as strings; parse them to `int`/`float` where they enter, and reject garbage there | | 3 | Flip domain files (the calculator, rate tables) | Surfaces return-type and built-in-call bugs inside code you own and test well | | 4 | Flip entry points and glue code last | Surfaces every place that passed raw strings inward, now that step 2 gives them typed values to pass | | 5 | Keep it enforced | A coding-standard rule or review check that new files carry the declaration | Along the way: - **Run the tests after every flip.** A `TypeError` only appears when the code path runs, so coverage of the flipped file is what finds them before production does. - **Run a static analyser.** It reports mismatched argument and return types without executing the path, including branches the tests miss. - **Watch callbacks.** A closure passed to `array_map()` or `usort()` in a strict file still receives coerced arguments, because the built-in calls it; do not rely on strict mode there. - **Watch `null`.** Nullable columns reaching string built-ins are deprecated in coercive mode since PHP 8.1 and become a `TypeError` in a strict file. ## Wrong fixes that come up in review - **Widening the type** to make the error go away (`int|string $days`). That records the bug as the contract. - **Casting at every call site** (`fineCents((int) $_GET['days'])`). A bare `(int)` turns `"abc"` into `0` and charges a zero fine; validation belongs at the boundary, once. - **Catching `TypeError` broadly** around calls. It is an `Error`, it signals a programming mistake, and swallowing it hides the next one. - **Removing the declaration** from the file that threw. The throw is the point: it found a place where a string was treated as a number. ## Why the effort pays In a fines calculator, a silent coercion is a wrong charge: a `true` flag passed where a day count belongs becomes `1`, `"1e3"` becomes `1000`, and a fractional amount handed to an `int` parameter is truncated with only a deprecation notice. Strict mode moves those mistakes to a `TypeError` at the line that made them, and once every file is flipped the declared types become facts the rest of the code — and the static analyser — can rely on.

  • In PHP, after FineCalculator.php gains strict_types, why does a coercive controller calling fineCents("4", 0.25) still work?
    Because argument checks use the calling file's mode. The controller has no declaration, so its call is coerced and `"4"` becomes `4`. Flipping the calculator only changes the calls the calculator makes and its own return checks. The controller starts throwing only when it is flipped as well.
  • In PHP, why is fineCents((int) $_GET['days']) a poor fix for a strict-mode TypeError?
    An `(int)` cast never fails: `"abc"` and `""` both become `0`, so bad input turns into a zero fine instead of an error. The `TypeError` was pointing at unvalidated input. Validate the string where it enters (for example with `ctype_digit()` or a filter), reject it with a 400 if invalid, and only then convert it once.
  • In a PHP 8.5 codebase moving to strict_types, what extra risk do nullable database columns carry?
    A `NULL` column value passed to a string built-in such as `trim()` is only a deprecation in a coercive file, but a `TypeError` in a strict one. Flipping the file therefore turns logged deprecations into exceptions. Clear those call sites first by deciding what `null` means, for example `$row['note'] ?? ''`.

saying these in an interview costs you the question

  • Expects flipping the calculator file to break every coercive caller at once.
  • Fixes each TypeError by widening the declaration to int|string.
  • Casts request strings with (int) at each call site instead of validating once.
  • Wraps calls in catch (TypeError) to keep the old behaviour.
  • Assumes return(round(...)) satisfies an int return type because the value is whole.
  • Relies on strict mode to type-check arguments that array_map passes to a callback.