Table of Contents
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.
Leave a Reply