A pipeline is middleware that runs before the controller — the place for authentication, authorization, rate limiting, or any check that may stop the request.
php laika pipeline:make Authenticate
Pipelines live in lf-app/Pipeline/, namespace App\Pipeline.
namespace App\Pipeline;
use Laika\Engine\Route\Contracts\PipelineInterface;
class Authenticate implements PipelineInterface
{
public function handle(callable $next, array &$params): ?string
{
// ... your check ...
return $next(); // continue to the next pipeline, then the controller
}
}
$next — call it to continue. Whatever it returns is the response so far.$params — route parameters plus pipeline args, passed by reference through the whole chain: pipeline → controller → filters. Write to it to hand data forward.use Laika\Engine\Route\Url;
use App\Pipeline\Authenticate;
Url::get('/dashboard', 'DashboardController@index')->pipeline(Authenticate::class);
Url::get('/dashboard', 'DashboardController@index')->pipeline(['Authenticate', 'VerifiedEmail']);
Url::get('/dashboard', 'DashboardController@index', ['Authenticate']); // third argument
// Every route in the application
Url::globalPipeline(['Authenticate']);
For groups, see Routing → Middleware on Groups.
Name resolution: a short name ('Authenticate', 'Admin\Role') resolves to App\Pipeline\.... A fully qualified class name (Authenticate::class, \Laika\Engine\Shield\Pipeline\ShieldPipeline::class) is used as-is.
In handle() you… |
Remaining pipelines | Controller | Filters | Response |
|---|---|---|---|---|
return $next(); |
run | runs | run | the controller’s |
return $next(false); |
skipped | runs | run | the controller’s |
return 'text'; |
skipped | skipped | run | 'text' |
return null; (no $next) |
skipped | skipped | run | nothing is sent |
Code after $next() runs once everything later in the chain has finished, so a pipeline can also inspect or wrap the response:
public function handle(callable $next, array &$params): ?string
{
$start = microtime(true);
$response = $next();
error_log('took ' . round(microtime(true) - $start, 3) . 's');
return $response;
}
Set the status on the Response relay — a bare http_response_code() is overwritten when the response is sent:
use Laika\Engine\Services\{Response, Redirect};
use Laika\Engine\Session\Session;
class Authenticate implements PipelineInterface
{
public function handle(callable $next, array &$params): ?string
{
if (!Session::has('user_id')) {
Redirect::to('login'); // named route; sends Location and exits
}
return $next();
}
}
class AdminOnly implements PipelineInterface
{
public function handle(callable $next, array &$params): ?string
{
if (Session::get('role') !== 'admin') {
Response::setStatus(403);
return 'Forbidden'; // stops here; filters still run
}
return $next();
}
}
For APIs, set the content type too: Response::setStatus(401)->setContentType('application/json'); return json_encode(['error' => 'Unauthenticated']);
Append |key=value,key2=value2 to a pipeline name. The values arrive in $params:
Url::get('/admin', 'AdminController@index')->pipeline(['Role|role=admin']);
Url::get('/reports', 'ReportController@index')->pipeline(['Throttle|limit=60,window=60']);
Url::get('/beta', 'BetaController@index')->pipeline(['Feature|beta']); // bare key = true
class Role implements PipelineInterface
{
public function handle(callable $next, array &$params): ?string
{
if (Session::get('role') !== ($params['role'] ?? null)) {
Response::setStatus(403);
return 'Forbidden';
}
return $next();
}
}
true for a bare key). Cast numbers yourself.'A|x=1, y=2' produces the key " y".=.$params when the pipeline runs, so later pipelines and the controller see them too — and they overwrite a route parameter with the same name.Because $params is shared by reference, a pipeline can load something once and hand it to the controller by name:
// Pipeline
public function handle(callable $next, array &$params): ?string
{
$params['user'] = (new UsersModel())->find((int) Session::get('user_id'));
return $next();
}
// Controller — $user is filled from $params by name
public function dashboard($user): string { /* ... */ }
Pipelines are built through the service container, so type-hint what you need in the constructor:
namespace App\Pipeline;
use Laika\Engine\Auth\AuthManager;
use Laika\Engine\Route\Contracts\PipelineInterface;
use Laika\Engine\Services\Redirect;
class Authenticate implements PipelineInterface
{
public function __construct(private AuthManager $auth) {}
public function handle(callable $next, array &$params): ?string
{
if ($this->auth->guard('web')->user() === null) {
Redirect::to('login'); // a named route; sends Location and exits
}
return $next();
}
}
Redirect::to()takes a route name or an absolute URL — not a path. For a path, build the URL first:Redirect::to(\Laika\Engine\Services\Url::build('login')).
The rules:
Interfaces must be bound in a relay provider:
$this->registry->singleton(PaymentGateway::class, StripeGateway::class);
An unbound interface throws, naming the parameter — unless the parameter is nullable or has a default, in which case you silently get null or the default.
singleton() bound under a class name is shared by every pipeline, filter and controller in the request.Laika\Engine\Services\Config, Laika\Engine\Http\Response). Core services are bound under keys, not class names, so you’d get a fresh object or a proxy with no instance methods. Call relays statically, as above.handle() keeps its fixed signature — the constructor is the injection point. A pipeline is instantiated only when the chain reaches it.
Laika\Engine\Route\Contracts\PipelineInterface (any class with a matching handle() method is accepted).handle(callable $next, array &$params): ?stringPipelineException (status 500) when a request reaches it.| Command | Description |
|---|---|
php laika pipeline:make <name> |
Create a pipeline class |
php laika pipeline:list |
List pipeline classes |
php laika pipeline:remove <name> |
Delete a pipeline class |
php laika pipeline:rename --old=<name> --new=<name> |
Rename a pipeline class |