rajtik76 / nette-middleware
Laravel-style route middleware for Nette Application
Requires
- php: >=8.4
- nette/application: ^3.2.3
- nette/di: ^3.2
- rajtik76/nette-console: ^0.1 || ^0.2
Requires (Dev)
- dresscode/dresscode: ^1.3
- nette/bootstrap: ^3.2
- nette/http: ^3.3
- nette/phpstan-rules: ^1.0
- nette/security: ^3.2
- nette/tester: ^2.5
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel-style route middleware for Nette Application.
A route lists its middleware in the router metadata, and the middleware runs at presenter
startup, before the action. Returning a response stops the request, returning null lets it through.
- middleware is a DI service with constructor injection, one service per class
- taken only from route metadata, never from the query string
make:middlewaregenerator through rajtik76/nette-console
Installation
composer require rajtik76/nette-middleware
The extension registers itself via package discovery from rajtik76/nette-console, which comes as a dependency. If you turned discovery off, add it to the config:
extensions: middleware: Rajtik\Middleware\DI\MiddlewareExtension(%consoleMode%)
Usage
Generate a middleware:
php nette make:middleware Auth
It lands in app/Http/Middleware/AuthMiddleware.php with an empty handle() that returns null,
so it lets every request through until you fill it in. An auth check can look like this:
namespace App\Http\Middleware; use Nette\Application\LinkGenerator; use Nette\Application\Request; use Nette\Application\Response; use Nette\Application\Responses\RedirectResponse; use Nette\Security\User; use Rajtik\Middleware\Middleware; final class AuthMiddleware implements Middleware { public function __construct( private readonly User $user, private readonly LinkGenerator $linkGenerator, ) { } public function handle(Request $request): ?Response { return $this->user->isLoggedIn() ? null : new RedirectResponse($this->linkGenerator->link('Sign:in')); } }
Register it as a service, one by one or with search. Each middleware class must be registered exactly once,
autowired: false is fine:
search: - in: %appDir%/Http/Middleware classes: - *Middleware
Attach it to a route:
$router->addRoute('/admin', [ 'presenter' => 'Admin', 'action' => 'default', MiddlewareRunner::Parameter => fn() => [AuthMiddleware::class], ]);
The list is wrapped in a closure because Nette reads an array in route metadata as a parameter definition. Middleware runs in the listed order and the first returned response wins.
Intended URL
Middleware has no presenter, so it cannot call Presenter::storeRequest(). Use IntendedRequest instead.
It writes to the same session section in the same format, so the sign-in presenter calls the usual
restoreRequest():
use Rajtik\Middleware\IntendedRequest; final class Authenticate implements Middleware { public function __construct( private readonly User $user, private readonly LinkGenerator $linkGenerator, private readonly IntendedRequest $intended, ) { } public function handle(Request $request): ?Response { return $this->user->isLoggedIn() ? null : new RedirectResponse($this->linkGenerator->link('Sign:in', [ 'backlink' => $this->intended->store($request), ])); } }
final class SignPresenter extends Presenter { #[Persistent] public string $backlink = ''; public function signInFormSucceeded(Form $form, \stdClass $data): void { $this->getUser()->login($data->username, $data->password); $this->restoreRequest($this->backlink); $this->redirect('Home:'); } }
store() returns null and stores nothing for a POST or an AJAX request. restoreRequest() replays
a stored POST as a forward, so a sign-out or a delete would run again after sign-in. null drops the backlink
from the link and the user lands on the default page.
The request of a signed-in user keeps the user id, and restoreRequest() ignores it for anyone else.
A guest request is restored for whoever signs in. The user id needs nette/security, without it every request
is stored as a guest one. The service is registered only when the container has a Nette\Http\Session.
Configuration
middleware: namespace: App\Http\Middleware # namespace of generated classes directory: %appDir%/Http/Middleware stubs: %appDir%/../stubs # optional, overrides middleware.stub
How it works
The extension registers MiddlewareRunner and adds it to Application::$onPresenter.
The runner hooks into Presenter::$onStartup, reads the closure from the request, resolves
each class from the DI container and calls handle(). The first non-null response is sent with
Presenter::sendResponse(), so Nette finishes the request the usual way.
The closure is then removed from the request, so Presenter::storeRequest() can serialize it.
A request created by forward() or a restored POST request does not pass through the router,
so the runner takes the middleware from the route the request would match.
A returned RedirectResponse goes through Presenter::redirectUrl(). An AJAX request gets
the target in $payload->redirect, which Naja and nette.ajax.js follow. Any other request gets the redirect
with its original code.
- Middleware is read only from a closure in route metadata. A
middlewarevalue from the query string is ignored, so a URL cannot choose what gets called. - A class that does not implement
Middleware, or is not registered as exactly one service, throws instead of being skipped. - Middleware needs a
Nette\Application\UI\Presenter. A route with middleware that leads to anotherIPresenter, such as a callback route, throws instead of running unprotected. - Middleware belongs to a route, not to an action. Do not add a catch-all
<presenter>/<action>route, or a protected action becomes reachable through another URL.
Development
composer ci:check # DressCode, PHPStan level 8, Nette Tester
License
MIT