descom/pipeline
A Laravel package implementing the pipeline pattern, letting you pass data through a sequence of reusable stages for cleaner, composable business logic. Define pipes with a simple API and keep request processing modular and testable.
Before writing any feature code, treat this package as a source-audit exercise: 0 stars, 0 dependents, and a single 2025-04-09 release mean the README is likely your only documentation and the tests/ directory is your real spec.
Install and inspect.
composer require descom/pipeline
composer show descom/pipeline # confirm version + autoloaded provider
Laravel will auto-discover the service provider via extra.laravel.providers in its composer.json. If it does not, register it manually in bootstrap/providers.php.
Read in this order: README.md → src/ (find the plugin/stage contract and the pipeline entry point) → tests/ (copy a working example verbatim before improvising). The contract interface is the single most important file — it defines the handle()-style signature every plugin must satisfy.
Publish config if offered.
php artisan vendor:publish --provider="Descom\Pipeline\PipelineServiceProvider"
Verify the provider FQCN from composer.json first; there is no guarantee a config file ships.
First use case: a checkout flow where each "plugin" is one isolated stage — ValidateStock → ApplyDiscount → ChargePayment → SendReceipt. Instead of one 200-line service method, each stage is a class you can test, reorder, or swap per tenant. The package's value is the injection layer: stages are resolved from a registry/config rather than hardcoded in an array.
Skeleton (confirm exact method names against src/ — the shape below is the standard Laravel pipeline contract this package almost certainly wraps):
$result = app(Pipeline::class)
->send(new CheckoutPayload($order))
->through(config('pipeline.checkout')) // injected plugin classes
->thenReturn();
One class per plugin, single responsibility. Each plugin implements the package's contract with a handle($payload, Closure $next) signature. Keep plugins small and pure where possible; return the payload or a new one, never null.
Register via config, not hardcoded arrays. The whole point of "inject plugins" is that the stage list is data. Put ordered arrays in config/pipeline.php keyed by flow name (checkout, onboarding, webhooks) and resolve them at call sites. This lets you reorder stages without touching application code and lets ops toggle a stage via env/feature flag.
Per-tenant / per-plan injection. Bind a resolver that returns a different stage list based on the authenticated tenant:
$this->app->bind('pipeline.checkout', fn () => match (tenant()->plan) {
'enterprise' => [ValidateStock::class, ApplyDiscount::class, ChargePayment::class, FraudCheck::class],
default => [ValidateStock::class, ChargePayment::class],
});
Async wrapping. When stages do I/O (payment gateways, webhooks), wrap the pipeline call in a queued job and pass a DTO, not an Eloquent model — serialization of models across queue boundaries is a classic source of stale-state bugs.
Testing pattern. The fastest way to gain confidence in an unproven package is to test your stages in isolation and test one integration path end-to-end:
$mock = Mockery::mock(ChargePayment::class);
$mock->shouldReceive('handle')->once()->andReturnUsing(fn ($p, $n) => $n($p));
$this->app->instance(ChargePayment::class, $mock);
If the package resolves plugins from the container (most do), $this->app->instance() swaps them cleanly. If it instantiates with new, you'll need to refactor registration to go through the container — a red flag worth noting.
Integration tip: expose the pipeline behind your own thin CheckoutService wrapper. That gives you one seam to swap the package out later without touching controllers.
src/.$next($payload) early — don't assume the package adds conditional logic it doesn't document.thenReturn() vs then(). thenReturn() discards the final return value; then(fn ($p) => ...) lets you post-process. Pick deliberately, and be consistent across call sites.plugins(), through(), or a custom name. Grep src/ for function before writing code — don't cargo-cult Laravel's Illuminate\Pipeline API.php artisan config:clear after edits in dev. In production, config:cache is fine — but ensure your per-tenant resolver doesn't read env() directly at runtime.Log::info(class_basename($this), ['payload' => $payload]) at the top of each handle(). For a package with no docs, this is faster than reasoning about the container.src/: (a) the plugin contract binding — can you rebind it? (b) the registry/config source — can you add a custom resolver? (c) exception handling — can you intercept failures? If any of these are hardcoded final classes, the package will fight you the moment requirements change."descom/pipeline": "1.0.0" or whatever the tag is) and commit composer.lock. A surprise patch could change the contract with no changelog to warn you.How can I help you explore Laravel packages today?