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

Laravel Loop Functions Laravel Package

michael-rubel/laravel-loop-functions

Laravel Loop Functions provides handy helper functions for writing cleaner loops in Laravel, offering concise, readable alternatives to native PHP iteration when processing arrays and collections. A lightweight package that keeps repeated loop logic simple and expressive.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin to the current line. The latest release is 3.4.3, so pin to ^3.4 — not ^1.0. This matters: the package has moved through three majors since 1.x, and installing an old constraint silently gives you an API the rest of this assessment doesn't describe.

composer require michael-rubel/laravel-loop-functions:^3.4

There is nothing to publish and no config file: the package registers itself through Composer's files autoload, so the helpers are available globally as soon as the autoloader is regenerated (composer dump-autoload if you installed with --no-scripts or copied vendor/ around).

Where to look first. Documentation is thin relative to the code, so read in this order:

  1. composer.json — confirms the files autoload entry (that's where the global functions live) and the supported PHP/Laravel range. As of 3.4.3 that range explicitly includes Laravel 10.
  2. CHANGELOG.md — read the 1.x → 3.x entries before upgrading an existing install. The 3.4.3 patch itself is constraint-only, but the majors between it and 1.x are not.
  3. src/ — the helper file plus the small Loop-style class; it's a few hundred lines total, readable in one sitting.
  4. tests/ — for a package this size the test suite is the reference documentation; every supported shape (array, Traversable, generator) has an example there.

First use case. The package earns its keep the moment you have a loop that (a) isn't already a Collection, and (b) needs early exit or composition. The canonical day-one scenario is a chunked job or import command walking a generator-backed source:

use function MichaelRubel\LoopFunctions\loop;

// e.g. inside a queued job or artisan command
loop($this->orderRows())          // generator, not an array
    ->each(function ($row) {
        if (! $row['valid']) {
            return false;         // stop before doing damage
        }
        $this->persist($row);
    });

Reach for it instead of collect() when materialising the whole dataset would defeat the point (cursor reads, fgetcsv streams, API pagination), and instead of raw foreach when you want to compose map/filter/reduce without losing the ability to bail out.

Implementation Patterns

Wrap iterators at the boundary, unwrap at the edge. The most maintainable pattern is to keep the loop object local and convert back to a framework type as soon as the data leaves the method:

public function activeEmails(): array
{
    return loop(User::query()->cursor())      // LazyCollection, chunked reads
        ->filter(fn ($u) => $u->isActive())
        ->map(fn ($u) => $u->email)
        ->toArray();                           // or collect(...) for a Collection
}

This keeps the memory ceiling low through the pipeline and gives callers something they already know how to use.

Use it as the "for each, with control flow" tool inside Actions and Services. In action classes that fan out over DTOs, the fluent form reads better than a foreach with a flag variable:

final class DispatchReminders
{
    public function __invoke(Collection $reminders): int
    {
        $sent = 0;

        loop($reminders)
            ->each(function (Reminder $reminder) use (&$sent) {
                if ($reminder->isSuppressed()) {
                    return;                    // skip, continue
                }
                Mail::to($reminder->user)->queue(new ReminderMail($reminder));
                $sent++;
            });

        return $sent;
    }
}

Counter-style side effects belong in a use (&$sent) closure, not in a sum() chain — the loop helpers are about iteration, not aggregation semantics.

Stream files instead of loading them. For CSV imports, a hand-rolled generator plus the loop helpers is the smallest correct implementation:

private function rows(): Generator
{
    $handle = fopen($this->path, 'rb');
    $header = fgetcsv($handle);

    while (($line = fgetcsv($handle)) !== false) {
        yield array_combine($header, $line);
    }

    fclose($handle);
}

Then in the caller: chunk with array_chunk/->chunk(...) and wrap each chunk in its own DB transaction. Never assume the loop helper buffers for you — it doesn't, and that's the point.

Compose with Laravel's Pipeline when the loop is one stage. Loops compose badly with other loops but fine with pipelines:

$result = app(Pipeline::class)
    ->send($rows)
    ->through([NormaliseRows::class, ValidateRows::class])
    ->then(fn ($rows) => loop($rows)->map(fn ($r) => $r->toArray())->toArray());

Test with Pest datasets, and assert on the collected output. Because these helpers are deterministic and side-effect-driven, the reliable test shape is: feed a small array, capture side effects in a spy array, assert on the spy. Avoid testing against $loop internals — they're implementation detail, and with a single maintainer shipping infrequent releases the API surface you assert against is effectively the API surface you're stuck with.

Integration tip for queued jobs: wrap the loop in a try/finally and use the finally block to flush any batched writes. If a callback throws mid-loop, the generator is already partially consumed and cannot be replayed — the finally is your only guaranteed hook.

Gotchas and Tips

3.4.3 is a compatibility patch, not a feature release — but read the constraint carefully. The only change is Laravel 10 support in the Composer constraint. The helper API is unchanged, so everything above applies verbatim; what changed is which skeleton versions you can install alongside. If you are still pinned to ^1.0 from an older project, treat the 1.x → 3.x jump as a real upgrade: read the changelog for signature changes, run the package's own tests against your app, and re-check the early-exit contract (below) rather than assuming it is identical.

Verify the maintenance signal, not just the version number. A January-2023-era package picking up a Laravel 10 constraint is good news but is a floor, not a promise. Check the repo's archived banner and last-commit date before adopting. Laravel 11/12 and PHP 8.3+ are still outside what any release note has explicitly validated. Because the entire package is a handful of functions, the pragmatic fallback remains: vendor the two files into app/Support/ and own them — that also removes a supply-chain dependency for something trivially reimplementable.

Global functions collide. These are real global PHP functions, not namespaced classes, so they will clash with your own app/helpers.php or other packages that ship a loop()-ish helper. Guard your copies with if (! function_exists('...')) and grep vendor/composer/autoload_files.php when you see "cannot redeclare".

Composer files autoload does not refresh like classes. After adding or removing the package, or when running under a cached autoloader in CI, composer dump-autoload -o is mandatory. A Class not found-style error for a function almost always means a stale autoload cache, not a bad install. This is doubly true after a constraint bump: if Composer resolved a new version but the cached autoload_files.php still points at the old path, you'll get undefined-function errors that look like a broken package.

Generators are single-pass. Any helper call that rewinds (a second each, a toArray() after a map that already consumed it, calling count() then iterating) will either throw "Cannot rewind a generator that was already run" or silently yield nothing. Rule: build the generator once, consume it once. If you need two passes, you needed an array.

Early exit only works if the callback's contract supports it. Returning false from an each-style callback is the documented stop signal; returning 0, null, or a falsy object will not stop the loop in most implementations of this pattern. Verify in src/ before relying on it in a job that charges cards — and re-verify after any major bump, since this is exactly the kind of contract a minor-looking release can carry.

By-reference and key semantics differ from foreach. foreach ($items as &$item) mutation does not carry over. If your existing code mutates by reference, converting it to a loop helper silently breaks it — the closure receives a copy for scalars/arrays. Convert explicitly (return $transformed; inside a map) rather than porting the &.

Do not reuse a loop object. Even where the object is technically reusable, keeping state on it invites double-consumption bugs in long-running workers (Horizon/Octane). Construct it inline at the call site.

Debugging: dd() inside the callback is fine, but dump() on the loop object in a generator context will appear empty. Use iterator_to_array($generator, true) in a scratch test to see what you actually have. Xdebug stepping through generator frames is unpleasant; put a ray()/logger() call at the top of the producer instead and check ordering and counts first — 90% of "the loop didn't run" bugs are an empty or already-consumed source.

Extension points are limited by design. If the loop class is Macroable, register custom behaviour in a service provider's boot() and keep those macros in one file (AppServiceProvider or a dedicated LoopMacrosProvider) so the package's missing methods stay discoverable. If it isn't macroable, prefer thin wrapper functions in your own helpers file over forking the package — especially now, since a fork forfeits future constraint patches like 3.4.3 that cost you nothing to inherit.

Don't use it in Blade. Template loops over large data belong to @foreach (streaming, no allocation) or, for anything substantial, a pre-computed view model. Wrapping the view's data in a loop object just to call each buys nothing and adds a dependency to your templates the next developer won't expect.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo