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

Pipeline Laravel Package

zaengle/pipeline

Zaengle Pipeline is a lightweight Laravel package for building and running data pipelines. Chain reusable pipe classes to transform, validate, or filter data with a clean, expressive API, making complex multi-step workflows easier to organize and test.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements check first. v6.0.0 is a compatibility release: it adds Laravel 13 and PHP 8.4 to the support matrix. Before upgrading, confirm your app is actually on Laravel 13 / PHP 8.4. If you're still on Laravel 10–12 or PHP 8.1–8.3, stay on the v5 line — the v5 → v6 major bump is the constraint change, not an API change.

composer require zaengle/pipeline

# publish the config (namespace/path defaults for generated classes)
php artisan vendor:publish --tag=pipeline-config

# scaffold something to run
php artisan make:pipeline ProcessOrder
php artisan make:pipe ValidateOrder

Where to look first:

  1. config/pipeline.php — this is what decides where make:pipeline / make:pipe write files and what namespace they get. Set your app/Pipelines and app/Pipelines/Pipes paths here before generating anything, or you'll be moving files on day one.
  2. A generated pipeline class — it's a small class exposing an ordered list of pipe classes. That array is the whole mental model of the package.
  3. A generated pipe — an invokable class with __invoke($payload, Closure $next). If you've used Laravel's own Illuminate\Pipeline\Pipeline, this is exactly that contract with naming and scaffolding on top.

First use case: a validation/normalisation pipeline on an inbound request payload. Three pipes — trim & cast, validate, enrich — each independently unit-testable, then run them as one unit:

use Zaengle\Pipeline\Facades\Pipeline;

$order = Pipeline::pipe(ProcessOrder::class)
    ->send($request->validated())
    ->thenReturn();

That's the package's core value: named, discoverable, testable pipelines instead of an inline ->through([...]) array buried in a controller.

Implementation Patterns

One pipe, one job. The single biggest determinant of whether this package helps or hurts is pipe granularity. A pipe should take a payload, do one thing, and hand it on. If a pipe needs a comment explaining a "phase", it's two pipes.

Payload as a DTO, not an array. Arrays are fine for a two-pipe pipeline and miserable by pipe six — every pipe has to know the key names, and refactors are untyped and ungreppable. Use a readonly value object or a spatie/laravel-data object and let pipes return modified copies. Type-hint the payload in each __invoke and PHP will catch ordering bugs for you.

Constructor injection is your integration point. Pipes are resolved through the container, so dependencies arrive via the constructor and payload mutation stays the only concern of __invoke:

final class ChargeCard
{
    public function __construct(private PaymentGateway $gateway) {}

    public function __invoke(OrderPayload $payload, Closure $next)
    {
        $payload = $payload->withCharge($this->gateway->charge($payload->total));

        return $next($payload);
    }
}

Compose pipelines rather than growing them. For any pipeline past ~6 pipes, split into sub-pipelines (e.g. ValidateOrder, FulfillOrder) and have the parent pipeline reference them as steps. You keep readable class names and get a per-stage test surface.

Conditional steps belong in the pipeline class, not inside pipes. If a pipe is a no-op in half your flows, gate it in the pipeline definition — a pipe that's sometimes skipped is a pipe whose contract is unclear, and it makes the ordering array lie about what actually runs.

Test at two levels, always:

  • Per pipe: instantiate it directly with a stub $next closure (or fn ($p) => $p) and assert the payload it produces. No container, no pipeline needed. This is where 80% of your tests should live.
  • Per pipeline: send a realistic payload through the named pipeline and assert the end state, plus assert the pipe order once. Order regressions are the failure mode this package makes easy to introduce and easy to test for.

Boundaries — when not to reach for this. Pipelines are in-process and synchronous. If a step needs retries, its own failure queue, or long runtime, that's a queued job, not a pipe. Conversely, don't wrap a queued job in a pipeline just to "chain" — use Laravel's job chaining. The package shines on deterministic, in-request transformation and orchestration.

Side-effect ordering. Pipes that write to the DB, send mail, or hit third-party APIs make the pipeline's order load-bearing. Put validation and pure transformation before any side-effecting pipe, and wrap side-effecting pipelines in DB::transaction() at the call site — the pipeline itself doesn't offer transactional rollback.

Gotchas and Tips

The classic bug: forgetting to return the chain. A pipe that calls $next($payload) but doesn't return it silently terminates the remainder of the pipeline and hands back null. Every pipe should end with return $next($payload);. If a pipeline mysteriously stops after pipe three, this is the first thing to check.

Return-value semantics matter. Laravel's pipeline distinguishes then() (you get a closure over the carried value) from thenReturn() (you get the carried value). Mixing them up produces a Closure where you expected a payload — the error surfaces far from the cause. Pick one style per codebase and stick to it.

Payload type drift. If pipe A returns an array and pipe B is typed for a DTO, you get a TypeError at runtime, not at pipeline definition time. There's no static check across the pipe array. The DTO approach above, plus a typed $next in each pipe, is what keeps this from becoming a whack-a-mole exercise.

Config caching after scaffolding. make:pipeline / make:pipe rely on the published config for target paths and namespaces. If you've run php artisan config:cache and then edit config/pipeline.php, generated classes land in stale locations until you re-cache. Standard Laravel footgun, but it bites here because the config governs codegen, not just runtime behaviour.

Upgrading v5 → v6. The release notes are a support-matrix change only — no API, no deprecations listed. Practical upgrade path:

  1. Bump PHP to 8.4 and Laravel to 13 in the app first, verify the app boots and tests are green without touching the package.
  2. composer require zaengle/pipeline:^6.0.
  3. Re-run the suite. Because pipes are plain container-resolved classes, a PHP or Laravel major can surface in them via framework API changes (container resolution, Illuminate\Support helpers) rather than in the package itself — failures will look like they're in your pipes, which is expected.
  4. Pin ^5.0 in any package or service you maintain that still supports Laravel ≤12; the major bump means Composer will refuse to co-install v6 there.

Extension points worth knowing. Generated classes are plain PHP with no base-class inheritance requirements in the common case — which means you can hand-write a pipeline, or add your own base pipeline class with shared helpers (logging, timing, metrics), without fighting the package. If you find yourself repeating the same three pipes across pipelines, extract a sub-pipeline rather than a base class; it stays greppable and testable.

Debugging a pipeline that "does nothing". Add a temporary pipe at the head of the array that dumps and returns $next($payload), or log at the top of each __invoke. Because pipes are individually resolvable, you can also drop into tinker and invoke a single pipe against a fixture payload — often faster than reasoning about the whole chain.

Watch the CI matrix on upgrade. v6.0.0 added a CI matrix covering Laravel 13 / PHP 8.4. If you consume this in a library, mirror those pairs in your own matrix so a future package release doesn't quietly drop the version you still ship.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums