mpociot/pipeline
Lightweight PHP pipeline implementation (based on illuminate/pipeline) without requiring the Illuminate container. Send an object through an array of middleware/handlers and finish with a callback—useful for middleware-style request/command processing.
Installation:
composer require mpociot/pipeline
Ensure autoloading is configured in composer.json:
"autoload": {
"psr-4": {
"App\\": "app/",
"Mpociot\\Pipeline\\": "vendor/mpociot/pipeline/src/"
}
}
First Use Case: Process a simple object through middleware:
use Mpociot\Pipeline\Pipeline;
$pipeline = new Pipeline();
$result = $pipeline
->send('initial_data')
->through([
new class {
public function handle($data, \Closure $next) {
return $next($data . '_processed');
}
},
new class {
public function handle($data, \Closure $next) {
return $next($data . '_final');
}
}
])
->then(function ($data) {
return $data; // 'initial_data_processed_final'
});
Where to Look First:
vendor/mpociot/pipeline/src/Pipeline.php (main pipeline logic).vendor/mpociot/pipeline/src/Pipeline/StageInterface.php (defines handle() method).Send Data:
$pipeline->send($data); // Start with initial payload.
Define Middleware:
Middleware must implement Mpociot\Pipeline\Pipeline\StageInterface with a handle() method:
class ValidateMiddleware {
public function handle($data, \Closure $next) {
if (!$this->isValid($data)) {
throw new \InvalidArgumentException('Invalid data');
}
return $next($data); // Pass to next middleware.
}
}
Execute Pipeline:
$pipeline
->send($request)
->through([new ValidateMiddleware(), new LogMiddleware()])
->then(function ($response) {
// Final processing.
return $response;
});
Closure-Based Middleware: Use anonymous functions for one-off logic:
$pipeline->through([
function ($data, \Closure $next) {
return $next($data . '_step1');
},
function ($data, \Closure $next) {
return $next($data . '_step2');
}
]);
Conditional Pipeline Branching: Dynamically add middleware based on runtime conditions:
$middleware = [];
if ($user->hasRole('admin')) {
$middleware[] = new AdminMiddleware();
}
$pipeline->through($middleware);
Pipeline Composition: Chain pipelines for multi-stage workflows:
$firstPipeline = (new Pipeline())->send($data)->through([$middleware1]);
$result = $firstPipeline->then(function ($data) {
return (new Pipeline())->send($data)->through([$middleware2])->then(...);
});
Error Handling:
Use catch() to handle exceptions globally:
$pipeline->then(...)->catch(function (\Exception $e) {
log::error('Pipeline failed: ' . $e->getMessage());
return $defaultResponse;
});
Laravel-Like Middleware: Mimic Laravel’s middleware structure for consistency:
class TransformData {
public function __invoke($data, \Closure $next) {
$data = $this->transform($data);
return $next($data);
}
}
Dependency Injection: Pass dependencies to middleware via constructor:
class LoggerMiddleware {
protected $logger;
public function __construct($logger) {
$this->logger = $logger;
}
public function handle($data, \Closure $next) {
$this->logger->log('Processing data');
return $next($data);
}
}
Testing: Mock pipelines for unit tests:
$mockPipeline = $this->getMockBuilder(Pipeline::class)
->disableOriginalConstructor()
->onlyMethods(['then'])
->getMock();
Middleware Order Matters:
Pipelines execute middleware in the order they’re passed to through(). Reversing order can break logic (e.g., auth before validation).
No Dependency Injection: Unlike Laravel’s pipeline, this package lacks a built-in container. Manually pass dependencies to middleware:
// Bad: Assumes global $logger.
$middleware = new LoggerMiddleware();
// Good: Explicit dependency.
$middleware = new LoggerMiddleware(new MonologLogger());
No Pipeline-Wide Exception Handling:
Exceptions in middleware halt the pipeline unless caught with catch():
$pipeline->then(...)->catch(function (\Exception $e) {
// Handle globally.
});
PHP Version Quirks:
array() syntax or named arguments.$next closures are bound correctly (use Closure::bind() if needed).State Management: Avoid storing state in middleware instances (e.g., static properties) unless thread-safe. Pipelines may be reused across requests.
Log Middleware Execution:
Add logging in handle() to trace pipeline flow:
public function handle($data, \Closure $next) {
\Log::debug('Middleware called with data: ' . json_encode($data));
return $next($data);
}
Use Xdebug: Step through pipeline execution to identify where data transforms unexpectedly.
Validate Middleware:
Ensure all middleware return the $next result or throw exceptions (never silently modify data).
Custom Pipeline Classes:
Extend Mpociot\Pipeline\Pipeline to add features (e.g., async support):
class AsyncPipeline extends Pipeline {
public function asyncThrough(array $middleware) {
// Implement async logic.
}
}
Middleware Adapters: Create adapters for other middleware standards (e.g., PSR-15):
class Psr15ToPipelineAdapter {
public static function adapt(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Server\MiddlewareInterface $middleware) {
return new class($middleware) implements Pipeline\StageInterface {
public function handle($data, \Closure $next) {
return $next($data);
}
};
}
}
Pipeline Decorators: Wrap pipelines to add cross-cutting concerns (e.g., timing, retries):
class TimedPipeline {
public function __construct(Pipeline $pipeline) {
$this->pipeline = $pipeline;
}
public function send($data) {
$start = microtime(true);
$result = $this->pipeline->send($data)->then(...);
\Log::info('Pipeline executed in ' . (microtime(true) - $start) . 's');
return $result;
}
}
No Built-in Config:
The package is stateless; configure pipelines dynamically via code (no config/pipeline.php).
Middleware Caching: Avoid caching pipeline instances with middleware that rely on request-specific state (e.g., session data).
Avoid Closure Overhead: Prefer class-based middleware for complex logic (closures create new scope overhead).
Reuse Pipelines: Instantiate pipelines once (e.g., in a service container) and reuse them:
$pipeline = new Pipeline();
$pipeline->send($data)->through($middleware);
Benchmark: Compare pipeline performance against manual chaining for critical paths:
// Manual (baseline)
$data = $middleware1->handle($data, function ($d) use ($middleware2) {
return $middleware2->handle($d, function ($d) { return $d; });
});
// Pipeline
$pipeline->send($data)->through([$middleware1, $middleware2])->then(...);
How can I help you explore Laravel packages today?