mshule/laravel-pipes
A small Laravel package for building clean, reusable pipelines. Pass data through a sequence of pipe classes, resolve stages from the container, and keep business logic composable, readable, and easy to test.
Start by pulling the package in and checking what it auto-registers:
composer require mshule/laravel-pipes
php artisan vendor:publish --tag=pipes-config
The service provider is auto-discovered, so the first real place to look is config/pipes.php. That file is the map of "incoming source → ordered list of pipe classes." If the config doesn't exist after publishing, inspect the provider's publishes() call (tag names vary) or read the README's quick-start.
A minimal first use case is a single webhook source (e.g. Stripe) flowing through two pipes: verify the signature, then persist the event.
// config/pipes.php
return [
'pipes' => [
'stripe' => [
App\Pipes\VerifyStripeSignature::class,
App\Pipes\StoreStripeEvent::class,
],
],
];
Each pipe is a small invokable/handle() class that receives the payload and the "next" callable — the Laravel Pipeline contract is the mental model to bring with you.
class VerifyStripeSignature
{
public function handle($payload, Closure $next)
{
abort_unless($this->valid($payload), 403);
return $next($payload);
}
}
Point the package's route (or your own controller that calls the pipe manager) at a publicly reachable URL, add the signing secret to .env, and you have an end-to-end webhook handler. Do that once before reading further — the config → pipe class → $next() flow is the whole package.
One pipe per concern. The value of the package is composition, so resist a single giant handler. Typical chain for a payment webhook:
VerifySignature — reject forged payloads before any work.EnsureIdempotent — check event.id against a webhook_events table with a unique index.NormalisePayload — flatten vendor-specific shapes into an internal DTO.DispatchDomainEvent — emit a domain event or job.RecordReceipt — write the audit row only after handlers succeed.Return the payload forward, not a response. Keep pipes focused on transformation and side effects. Let the final pipe (or the controller) build the HTTP response, so a pipe can be reused from a queue worker or an artisan command.
Queue the expensive tail. Verify + idempotency synchronously (fast, must happen before responding 2xx), then push a job that re-enters the remaining pipes. This keeps the vendor's retry window happy while your DB work happens off-request.
Compose from existing event/listener patterns. If you already have EventServiceProvider wiring, a DispatchDomainEvent pipe that calls event(new ...) lets you keep the package as the ingress layer and normal Laravel events as the internal bus — the webhook pipeline becomes thin and testable.
Integration with route model binding / tenancy. If webhooks carry a tenant identifier, add an IdentifyTenant pipe near the top that binds a tenant to the container, so downstream pipes don't repeat the lookup.
Test pipes directly, not through HTTP. Each pipe is a plain class; unit-test it with an array payload and a closure. Reserve HTTP feature tests for one happy path plus a signature-failure case, since the pipeline is exercised implicitly.
The package is stale. Last release was September 2020, which puts it in the Laravel 7/8 era. Check composer.json inside vendor/mshule/laravel-pipes for illuminate/support constraints before assuming it installs on Laravel 10+. On newer versions expect to either --ignore-platform-reqs (risky), vendor the code into app/, or replace it with a hand-rolled Illuminate\Pipeline\Pipeline — the package's core is small enough that this is a realistic fallback.
Config caching bites. Because the chain is defined in config/pipes.php, php artisan config:cache freezes the pipe list. Add a deploy step that clears config after changes, or you will ship a new pipe that never runs — and the symptom is silence, not an error.
Order is semantic, not cosmetic. Signature verification must be first; idempotency must be before any side effect. A reordered array is a security or duplicate-processing bug, so treat the config as code and review changes to it.
Debugging pipelines is opaque. When a webhook "does nothing," the failure is usually (a) source key mismatch in config, (b) a pipe returning null instead of $next($payload), or (c) an exception swallowed upstream. Add a LogWebhook pipe at the head that logs source, event id, and a payload hash, and confirm pipes are actually traversed by logging in each handle().
Payload assumptions. These packages typically hand you a decoded array. If a pipe returns a Request or JsonResponse by accident, later pipes receive the wrong type. Standardise on array or a DTO in one place and enforce it in tests.
Extension points to prefer. Custom pipes, the pipeline config, and (if present) package events are the stable surface. Avoid subclassing the package's manager or controller; those are the most likely to change and the least likely to survive a fork.
Idempotency is your job, not the package's. Most vendors retry on non-2xx and occasionally deliver duplicates on 2xx. A unique constraint on the vendor event id is the only robust guard; do not rely on pipe order or firstOrCreate alone.
No built-in retry/backoff. If a pipe throws, plan the failure path explicitly: log, persist to a failed_webhooks table with the raw body, and provide an artisan command to replay it through the same pipeline. This turns the package from a toy into something you can operate.
How can I help you explore Laravel packages today?