Extract Nested Closures Into Named Private Methods

📖 2 minutes read

You’re reading a method and hit something like this:

$results = $categories->map(function ($category) use ($config) {
    return $category->items->filter(function ($item) use ($config) {
        return $item->variants->map(function ($variant) use ($config, $item) {
            $basePrice = $variant->price * $config->multiplier;
            $discount = $item->hasDiscount() ? $basePrice * $item->discountRate() : 0;
            return [
                'sku' => $variant->sku,
                'final_price' => $basePrice - $discount,
                'label' => "{$item->name} — {$variant->size}",
            ];
        })->filter(fn ($v) => $v['final_price'] > 0);
    })->flatten(1);
})->flatten(1);

Three levels deep. Four use imports. By the time you reach the inner logic, you’ve lost the context of the outer layers. This is the closure nesting trap.

Extract and Name

Pull the inner closure into a private method with a name that describes what it does:

$results = $categories->map(function ($category) use ($config) {
    return $category->items
        ->flatMap(fn ($item) => $this->buildVariantPrices($item, $config))
        ->filter(fn ($v) => $v['final_price'] > 0);
})->flatten(1);

private function buildVariantPrices(Item $item, PricingConfig $config): Collection
{
    return $item->variants->map(function ($variant) use ($config, $item) {
        $basePrice = $variant->price * $config->multiplier;
        $discount = $item->hasDiscount() ? $basePrice * $item->discountRate() : 0;

        return [
            'sku' => $variant->sku,
            'final_price' => $basePrice - $discount,
            'label' => "{$item->name} — {$variant->size}",
        ];
    });
}

Why This Works

The parent chain becomes scannable. You can read the top-level flow — map categories, build variant prices, filter positives — without parsing the inner logic. The method name tells you what happens; the method body tells you how.

Type hints become possible. The extracted method can declare its parameter and return types. The closure couldn’t.

Testing gets easier. You can test buildVariantPrices() directly with a mock item and config, without setting up the full category→item→variant hierarchy.

The Rule of Thumb

If a closure:

  • Has more than 2 use imports
  • Is nested inside another closure
  • Contains more than 5 lines of logic

…extract it into a named private method. The method name acts as documentation, and the signature acts as a contract.

The takeaway: Closures are great for simple transformations. But when they nest, they become anonymous complexity. Give them a name, and your code reads like an outline instead of a puzzle.

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 *