Search by

rajtik76 / nette-middleware

rajtik76

Laravel-style route middleware for Nette Application

Package info

github.com/rajtik76/nette-middleware

pkg:composer/rajtik76/nette-middleware

Statistics

Installs: 9

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.3.0 2026-11-03 16:22 UTC

This package is auto-updated.

Last update: 2026-11-03 16:25:41 UTC


README

CI

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:middleware generator 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 middleware value 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 another IPresenter, 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