Type Hint Your Closures: Let PHP Catch Data Structure Bugs For You

πŸ“– 3 minutes read

Here’s a debugging scenario: you’re iterating a collection with a typed closure, and PHP throws a TypeError at runtime. Annoying? Yes. But also incredibly useful β€” the type hint just caught a data structure bug that would have silently corrupted your output.

The Bug

Imagine a collection that’s supposed to contain model objects. You write a clean, typed closure:

$tasks = $project->tasks; // Should be Collection of Task objects

$formatted = $tasks->map(fn(Task $task) => [
    'title' => $task->title,
    'status' => $task->status_code,
    'assignee' => $task->assigned_to,
]);

This works perfectly β€” until the day a refactor changes how tasks are loaded and some entries come back as raw arrays from a join query instead of hydrated Eloquent models:

TypeError: App\Services\ReportService::App\Services\{closure}():
Argument #1 ($task) must be of type App\Models\Task,
array given

Without the Type Hint

If you’d written fn($task) => instead, there’s no error. The closure happily processes the array, but $task->title triggers a “trying to access property of non-object” warning (or silently returns null in older PHP). Your output has missing data. You might not notice until a user reports a broken export.

Why This Matters

The type hint acts as a runtime assertion. It doesn’t just document what you expect β€” it enforces it at the exact point where wrong data enters your logic. The error message tells you precisely what went wrong: you expected a Task object but got an array.

This is especially valuable with Laravel collections, where data can come from multiple sources:

// Eloquent relationship β€” returns Task objects βœ…
$project->tasks->map(fn(Task $t) => $t->title);

// Raw query result β€” returns stdClass or array ❌
DB::table('tasks')->where('project_id', $id)->get()
    ->map(fn(Task $t) => $t->title); // TypeError caught immediately

// Cached data β€” might be arrays after serialization ❌
Cache::get("project.{$id}.tasks")
    ->map(fn(Task $t) => $t->title); // TypeError caught immediately

The Practice

Type hint your closure parameters in collection operations. It costs nothing in happy-path performance and saves hours of debugging when data structures change unexpectedly:

// Instead of this:
$items->map(fn($item) => $item->name);

// Do this:
$items->map(fn(Product $item) => $item->name);
$items->filter(fn(Invoice $inv) => $inv->isPaid());
$items->each(fn(User $user) => $user->notify(new WelcomeNotification));

It’s not about being pedantic with types. It’s about turning silent data corruption into loud, immediate, debuggable errors. Let PHP’s type system do the work your unit tests might miss.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *