Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Yield Breakable Caller Laravel Package

tourze/yield-breakable-caller

A small PHP utility that lets callers break out of yield-based iteration cleanly, giving generators controllable break semantics without messy manual loop handling.

View on GitHub
Deep Wiki
Context7

Getting Started

tourze/yield-breakable-caller is a small utility package that addresses a long-standing sharp edge in PHP: when a consumer breaks out of a foreach over a Generator, the generator is not immediately completed or destroyed. Its finally blocks, open handles, and in-flight state linger until garbage collection runs. This package gives the callee a way to detect that the caller abandoned the loop, so cleanup can happen deterministically.

Steps:

composer require tourze/yield-breakable-caller

Given the 0.0.x version line, pin it rather than floating it — ^0.0.2 only allows <0.0.3 in Composer's semantics, which is what you want here, but be deliberate about it in composer.json.

Where to look first:

  1. src/ — this is a micro-package. There is realistically one or two entry classes; read the whole directory in a few minutes. That is the API surface.
  2. tests/ — the test suite is the real documentation for a package this size. It will show you the canonical wrap/iterate/break sequence far faster than the README.
  3. composer.json — confirm the PHP constraint before adopting; generator-related packages often require 8.0+.

First use case — the one you'll almost certainly have in a Laravel app: streaming rows or file lines out of a generator while guaranteeing that a lock, cursor, or temp file is released even when the consumer stops early.

public function stream(): iterable
{
    $handle = fopen($path, 'rb');
    try {
        while (($line = fgets($handle)) !== false) {
            yield $line;
        }
    } finally {
        fclose($handle); // only runs promptly if breakage is signalled
    }
}

Wrap that generator with the package's caller and the finally fires when the consumer breaks — not "eventually, whenever GC feels like it."

Implementation Patterns

Wrap at the producer boundary, consume normally. The package is an adapter around the generator, not a replacement for foreach. The ergonomic pattern is to return the wrapped iterable from a service method so controllers, commands, and jobs all get the guarantee for free:

// Service
public function lines(string $path): iterable
{
    return BreakableCaller::call($this->rawLines($path));
}

// Consumer — plain foreach, plain break
foreach ($service->lines($path) as $line) {
    if (str_contains($line, 'STOP')) {
        break; // cleanup in the producer's finally block runs now
    }
    $this->handle($line);
}

(Verify the exact static method / class name against src/ — on 0.0.x the entry point is the thing most likely to be renamed.)

Use it anywhere you'd otherwise cap a stream. The most common real-world trigger is a --limit flag or a take()-style early exit:

  • Artisan commands: foreach ($this->lines($f) as $line) { if (++$n > $limit) break; ... }
  • HTTP streaming responses: response()->stream(...) where the client disconnects mid-loop — this is where leaked file handles actually bite you.
  • Queued jobs: consume a batch from a generator, break once the batch is full.
  • Chunked importers: break out of a CSV reader on validation failure without leaving the file open.

Pair it with domain cleanup, not just fclose. The valuable pattern is putting business cleanup in the generator's finally: releasing an advisory lock, decrementing a concurrency counter, deleting a Storage:: temp file, marking a JobLog row as cancelled. That turns an unreliable "eventually" into a reliable "immediately."

Keep the wrapper alive through the whole iteration. Compose it once and pass the wrapped object down. If you unwrap (e.g. iterator_to_array(), or yield from a raw inner generator) you lose the signal.

Testing pattern. Assert on cleanup explicitly rather than trusting the sequence:

$cleanedUp = false;
$gen = (function () use (&$cleanedUp) {
    try { while (true) yield 1; }
    finally { $cleanedUp = true; }
})();

foreach (BreakableCaller::call($gen) as $_) { break; }

$this->assertTrue($cleanedUp);

If a test is flaky here, that's a signal your cleanup is still GC-dependent rather than caller-dependent — a good canary.

Integration tip: don't mix this with LazyCollection casually. LazyCollection has its own lazy semantics and internal break usage in take()/first(); wrapping a LazyCollection::getIterator() is fine, wrapping the collection itself is not.

Gotchas and Tips

The detection is not instantaneous at the break line. A break is a control-flow event the generator cannot observe directly. The package works by having the wrapper inspect generator state on the next touch or on destruct — which means cleanup fires when the wrapper is released, not the exact microsecond of break. Don't write code that depends on ordering between the break and subsequent statements reading the cleaned-up resource. Unset or scope the wrapper if you need it released before the next line.

Don't let the wrapper escape into a long-lived container. If the wrapped iterable is stored on a singleton, a cached property, or a Livewire/public property, it never gets released, so the cleanup never fires — you've recreated the original bug with extra steps.

Exceptions surface on the consumer's next()/current(), not on the wrap call. BreakableCaller::call() does not execute the generator body. An exception thrown inside the producer appears at the foreach line, and the stack trace will point at your loop, not the source. Wrap the foreach, not the call().

Generator return values are easy to drop. If your producer returns a value (e.g. an aggregate count), plain foreach discards it; you must call getReturn() after the loop is exhausted. Breaking early means getReturn() throws. Design your API around iterable and a separate result object rather than relying on generator return values.

0.0.2 shipped with an empty changelog. The release notes are nothing but a 0.0.1...0.0.2 compare link, which means the release notes tell you nothing. Before adopting or bumping, read the actual diff at that URL. On a 0.0.x line, patch releases routinely contain renames and signature changes because the author has made no stability promise. Concretely:

  • Pin exactly ("tourze/yield-breakable-caller": "0.0.2") if this is load-bearing for resource cleanup.
  • If you were already on 0.0.1, diff the two tags before composer update, and run your cleanup tests specifically — a silent change here manifests as a resource leak in production, not a fatal error.
  • Vendor the one class you need behind your own thin interface if you want the option to swap it out. The package is small enough that this costs almost nothing.

Debugging leaks: if handlers still leak, drop gc_collect_cycles() into a test after the break. If that "fixes" it, the wrapper isn't actually in the path — you're iterating the raw generator somewhere upstream.

Extension points: the package is thin by design, so the practical extension point is your own wrapper class that composes it plus your cleanup semantics (logging, metrics on abandoned streams). Don't subclass internals on 0.0.x; compose.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk