A pipeline is middleware that runs before the controller — the natural place for authentication, authorization, or request validation that can short-circuit the response.
App\Pipeline namespace, files in lf-app/Pipeline.
php laika pipeline:make Authenticate
namespace App\Pipeline;
use Laika\Route\Contracts\PipelineInterface;
class Authenticate implements PipelineInterface
{
/**
* @param callable $next
* @param array $params
* @return ?string
*/
public function handle(callable $next, array &$params): ?string
{
// Start Code From Here....
return $next();
}
}
Url::get('/', function () {
// controller logic
})->pipeline(Authenticate::class);
Url::get('/dashboard', function () {
// controller logic
})->pipeline([Authenticate::class, VerifiedEmail::class]);
Type hint what the pipeline needs in its constructor. The container builds it — nothing else changes, and the route API is untouched.
namespace App\Pipeline;
use Laika\Route\Contracts\PipelineInterface;
use App\Service\AuthService;
use Laika\Service\Config;
class Authenticate implements PipelineInterface
{
public function __construct(
private AuthService $auth,
private Config $config,
) {}
public function handle(callable $next, array &$params): ?string
{
if (!$this->auth->check()) {
return redirect($this->config->get('app.login_url'));
}
return $next();
}
}
Three rules:
Interface type hints must be bound in a RelayProvider, because an interface cannot be auto-wired:
$this->registry->singleton(PaymentGateway::class, StripeGateway::class);
An unbound one throws at the boundary naming the parameter, rather than injecting null.
singleton() binding is shared across every pipeline, filter and controller in the request. The pipeline object itself is always built fresh.handle() keeps its fixed signature — the constructor is the injection point. Filters work the same way, see Filters.
Pipelines can be referenced by short class name with inline key=value config, available in $params. These are route params, not constructor arguments — the two channels are independent, so a pipeline can use both:
Url::get('/admin', 'AdminController@index')->pipeline(['Role|role=admin']);
Url::get('/reports', 'ReportController@index')->pipeline(['Throttle|limit=60,window=60']);
namespace App\Pipeline;
use Laika\Route\Contracts\PipelineInterface;
class Role implements PipelineInterface
{
public function handle(callable $next, array &$params): ?string
{
if (($_SESSION['role'] ?? null) !== ($params['role'] ?? null)) {
http_response_code(403);
return 'Forbidden'; // stops the chain, this string is the response
}
return $next();
}
}
Apply a pipeline to every route in the application:
Url::globalPipeline(['CSRF', 'CORS']);
| Return value | Chain continues? | Controller runs? | Output |
|---|---|---|---|
$next() |
Yes | Yes (if last pipeline) | Controller’s return value |
$next(false) |
No | No | Controller’s return value |
'anytext' |
No | No | Ignores the controller, returns the string itself |
Laika\Route\Contracts\PipelineInterface.handle(callable $next, array &$params): ?string$params — route params + pipeline config args, merged and passed by reference through the whole chain (pipeline → controller → filter). Mutate it to pass data forward.| Command | Description |
|---|---|
php laika pipeline:make <name> |
Create a pipeline class |
php laika pipeline:list |
List registered pipeline classes |
php laika pipeline:remove <name> |
Delete a pipeline class |
php laika pipeline:rename <old> <new> |
Rename a pipeline class |
See Filters for post-controller middleware, and Routing for how attachment interacts with route groups.