jean-beru/pipeline-bundle
Jean-Beru PipelineBundle brings composable pipeline processing to Symfony apps. Build multi-step workflows from reusable stages, keep business logic organized, and run ordered processing as clean, testable services.
First, the important caveat: jean-beru/pipeline-bundle is a Symfony bundle, not a Laravel package. If you are working in a Laravel codebase, you cannot composer require this and expect a service provider to auto-register. You have three realistic options:
Assuming you are working in Symfony (or evaluating the pattern):
composer require jean-beru/pipeline-bundle:^1.0.1
Register the bundle in config/bundles.php:
return [
// ...
JeanBeru\PipelineBundle\PipelineBundle::class => ['all' => true],
];
Look first at the bundle's DependencyInjection/Configuration.php and Resources/config/services.yaml in the repo — with a package this small (1 star, 0 dependents) the source is the documentation. Read the README, then the two files above, and you'll understand the surface area in under 15 minutes.
Your first use case: a linear chain of Symfony services (stages) that each receive a payload object, transform it, and pass it to the next. Typical fits are request pre-processing (validation → enrichment → normalization), import/ETL steps, or order-processing hooks. Define one pipeline with 2–3 trivial stages to prove the wiring before modeling anything real.
Payload objects over arrays. Pass a dedicated DTO (or at minimum an immutable-ish array contract) through the pipeline. This is the single biggest maintainability win — you can add fields and stages without renegotiating signatures.
final class ImportContext { public function __construct(public array $raw = [], public array $clean = []) {} }
Stage-as-service. Each stage should be a small, single-responsibility Symfony service. Let the bundle resolve them from the container rather than new-ing them.
final class NormalizeStage {
public function __invoke(ImportContext $ctx): ImportContext {
$ctx->clean = array_map('trim', $ctx->raw);
return $ctx;
}
}
Define the pipeline in YAML/config, not in a controller. The whole point of using a bundle here is that the composition is declarative. Keep controllers thin: controller builds the payload, calls the pipeline service, responds.
# conceptual — verify exact key names against the bundle's Configuration.php
pipeline:
import:
stages:
- App\Pipeline\NormalizeStage
- App\Pipeline\ValidateStage
- App\Pipeline\PersistStage
Tagged-service grouping. Symfony's native !tagged_iterator is often all you need for "run every stage tagged import.stage". If the bundle supports a tag, use it; if not, this is a clean escape hatch that keeps stages discoverable.
Symfony Messenger integration. Pipelines pair naturally with Messenger: consume a message, build a payload, run the pipeline, dispatch follow-up messages. This keeps the pipeline itself synchronous and testable while giving you async boundaries at the edges.
Testing. Because stages are plain services, unit-test each one in isolation, then write one integration test per pipeline definition (asserting stage order and final payload). Do not try to test the bundle's orchestration separately — you'll be testing vendor code.
Adoption risk is the headline. 1 star, 0 dependents, a tiny surface area, and a maintenance cadence measured in single patch releases. Treat this as "vendor yourself" territory: read every line before depending on it in production, and consider whether the 30-line pipeline loop is worth the dependency. For most teams, inlining a Pipeline class in app/Support is cheaper than the bundle. Only take the dependency if you specifically want its DI conventions.
Pin ^1.0.1 (or higher), not ^1.0. v1.0.0 emits PHP 8.2 deprecation notices; v1.0.1 (fix: PHP8.2 deprecations) cleans those up. It is a patch release with no API change, so the upgrade is free — but if you're on PHP 8.2+ and your CI fails on deprecations (e.g. Symfony's PHPUnit bridge in strict mode), an unpinned install can resolve to 1.0.0 on a stale lockfile and fail noisily. Run composer update jean-beru/pipeline-bundle if you adopted before v1.0.1.
Symfony/Laravel mismatch. The biggest practical pitfall: if a Laravel developer is evaluating this for a Laravel app, they will waste time hunting for a PipelineServiceProvider. There isn't one. Document this decision in your repo or team wiki so the next person doesn't repeat the search.
Empty-pipeline behavior. Decide what happens when a pipeline has zero stages. Most simple bundles either return the input unchanged or throw — verify which, because a silent pass-through can mask a misconfiguration.
Error handling mid-chain. There is no built-in "rollback" or compensating action. If stage 3 fails after stage 2 mutated shared state, you own the consequences. Prefer returning new payloads (or immutable DTOs) so a failed stage can't leave the pipeline in a half-mutated state. If you need compensation, that's a saga, not this bundle.
Ordering is everything. Declarative config means someone will eventually add a stage in the wrong position and only discover it in staging. Add an integration test that asserts the exact ordered list of stage classes for each named pipeline.
Debugging. Log at the stage boundary (context after each stage) rather than inside stages. When a pipeline misbehaves, the fastest path is dumping the payload before and after each stage — a single decorator around the pipeline service gives you this.
Extension points. Because the bundle is small, resist the urge to fork. Instead, wrap: build your own App\Pipeline\Runner that delegates to the bundle but adds logging, metrics, or tracing. That keeps the upgrade path to future releases trivial (or to removing the bundle entirely).
Config quirks. With bundles this small, configuration key names and defaults are frequently under-documented and can change between minor releases. Pin the version in composer.json with a caret on the current patch (^1.0.1), and read the Configuration.php node tree directly rather than trusting the README.
How can I help you explore Laravel packages today?