Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Middleman Laravel Package

mindplay/middleman

PSR-15 middleware dispatcher for PHP that composes middleware into a single request handler. Supports lazy middleware resolution from a PSR-11 container, class-name based middleware, and PSR-7/PSR-15 stacks, making it easy to build clean, layered HTTP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

mindplay/middleman is not a Laravel package — it is a ~3-class PSR-15 dispatcher. Install it alongside a PSR-7 implementation and a PSR-17 factory:

composer require mindplay/middleman nyholm/psr7 nyholm/psr7-server

Read the whole thing before using it; there is no more to read:

  • src/Dispatcher.php — implements RequestHandlerInterface, flattens [middleware…, terminal] into a delegate chain.
  • src/Delegate.php — the RequestHandlerInterface wrapper that calls one middleware with the next delegate.
  • src/MiddlewareResolver.php — the extension point for resolving string/array middleware specifications (used for lazy, container-backed middleware).

Mental model:

$dispatcher = new Dispatcher([
    $outermostMiddleware,   // runs first, sees the response last
    $anotherMiddleware,
    $terminalHandler,       // RequestHandlerInterface (or the last callable)
]);

$psr7Response = $dispatcher->handle($psr7Request);

First use case: run a single third-party PSR-15 middleware (webhook signature check, request ID, CORS) inside a Laravel route without dragging in Slim/Mezzio.

use mindplay\middleman\Dispatcher;
use Nyholm\Psr7\{ServerRequest, Response};
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

Route::post('/webhooks/github', function (Illuminate\Http\Request $request) {
    $psr7 = new ServerRequest(
        $request->method(),
        $request->fullUrl(),
        $request->headers->all(),
        $request->getContent(),
        $request->getProtocolVersion()
    );

    $dispatcher = new Dispatcher(
        [new App\Http\Psr15\GithubSignatureMiddleware(config('services.github.secret'))],
        new class implements RequestHandlerInterface {
            public function handle(ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface
            {
                return new Response(204);
            }
        }
    );

    $psr7Response = $dispatcher->handle($psr7);

    return response((string) $psr7Response->getBody(), $psr7Response->getStatusCode())
        ->withHeaders($psr7Response->getHeaders());
});

Implementation Patterns

1. A single Laravel middleware as the PSR-15 bridge. Register one class globally or per route group, keep the PSR-15 list in config, and let the terminal handler call $next($request) so the rest of the Laravel stack still runs:

class Psr15Bridge
{
    public function __construct(private array $middleware) {}

    public function handle($request, Closure $next)
    {
        $psr7 = $this->toPsr7($request);

        $dispatcher = new Dispatcher(
            $this->middleware,
            new class($next, $request) implements RequestHandlerInterface {
                public function __construct(private Closure $next, private $original) {}

                public function handle(ServerRequestInterface $request): ResponseInterface
                {
                    $response = ($this->next)($this->original);
                    return new Nyholm\Psr7\Response(
                        $response->getStatusCode(),
                        $response->headers->all(),
                        $response->getContent()
                    );
                }
            }
        );

        $response = $dispatcher->handle($psr7);

        return response((string) $response->getBody(), $response->getStatusCode())
            ->withHeaders($response->getHeaders());
    }
}

2. Config-driven, container-resolved stacks. Middleman accepts class strings and [Class::class, 'method'] specs, resolving them lazily through a PSR-11 container plus a MiddlewareResolver. Put the order in config/http.php so ops can reorder without a deploy of code logic:

// AppServiceProvider::boot()
$this->app->bind(Dispatcher::class, fn ($app) => new Dispatcher(
    config('http.psr15'),                 // ['App\Psr15\RequestId', ...]
    $app->make(App\Http\Psr15\TerminalHandler::class),
    $app                                // Laravel's container is PSR-11
));

Because resolution is lazy, an expensive middleware (one that opens a DB connection in its constructor) is only constructed if an earlier layer didn't short-circuit.

3. Nested dispatchers as route groups. A Dispatcher is a RequestHandlerInterface, so you can nest one as the terminal of another: outer stack = global concerns (request ID, CORS, timing), inner stack = route-group concerns (webhook auth, payload validation).

4. Standalone sub-app behind a catch-all route. For a Slack/Discord bot, mount a Route::any('/bot/{any?}', ...)->where('any', '.*') and let a terminal handler do the routing inside the dispatcher. This keeps the PSR-15 stack portable — you can lift it into Mezzio later without touching middleware.

5. Keep the boundary honest. PSR-15 side: request IDs, CORS, signature verification, body parsing, rate limiting, timing/logging, third-party league/* middleware. Laravel side: StartSession, VerifyCsrfToken, SubstituteBindings, auth, anything that needs Auth/Session facades. The PSR-15 stack executes inside Laravel's chain, so Laravel's framework-coupled middleware must already have run.

6. Test middleware without booting Laravel. The package's biggest win day-to-day is testability — new Dispatcher([$mw], $terminal) with a Nyholm ServerRequest gives you a fast, framework-free unit test for each middleware.

Gotchas and Tips

  • Ordering is array order, full stop. First element is outermost. Laravel's $middlewarePriority and named aliases (->middleware('auth')) do not apply — you get no priority negotiation.
  • Always terminate the chain. Pass an explicit RequestHandlerInterface, or make the last element a callable that returns a response. Every middleware must return $handler->handle($request) (or return a response to short-circuit) — returning null or forgetting the return produces a TypeError deep in Delegate, not a helpful message.
  • Call the handler exactly once. Holding $handler and calling it twice will re-run downstream middleware; there is no guard against it.
  • No error handling is built in. Wrap $dispatcher->handle() in try/catch if you want Laravel's exception handler to see framework-native exceptions, or add an outermost PSR-15 error middleware. Also note: a PSR-15 middleware that short-circuits with a response bypasses any Laravel middleware below the bridge (CSRF, auth) — a real security footgun if you mount the bridge above VerifyCsrfToken.
  • Response bodies are streams. (string) $response->getBody() uses __toString, which reads from the current pointer — call $response->getBody()->rewind() first, and use response()->stream() for large or binary payloads rather than buffering.
  • Header conversion is lossy. Symfony's HeaderBag collapses multiple Set-Cookie headers into one comma-joined value; map cookies explicitly with withCookie() if the PSR-15 middleware sets them.
  • Request conversion is lossy too. getParsedBody() and uploaded files don't survive a hand-rolled ServerRequest constructor. For faithful conversion use Nyholm\Psr7Server\ServerRequestCreator::fromGlobals(). For JSON webhooks, manual construction is fine and cheaper.
  • Laravel's container is PSR-11, but has() is bound(). If the resolver checks has() before get(), unbound-but-autowirable middleware classes silently fail to resolve. Bind your PSR-15 middleware explicitly in a service provider. Conversely, Laravel's get() auto-wires anything — typos are caught, but accidental construction of a heavy dependency isn't.
  • Singleton state leak. If you bind Dispatcher as a singleton and its middleware store per-request state (parsed payload, auth user, timing), that state bleeds across requests under Octane and even under FPM. Bind transient/scoped, or construct per request.
  • Immutability traps. $response->withHeader(...) and $request->withAttribute(...) return new instances. Reassign or you'll silently drop data — a classic PSR-7 bug that middleman won't catch for you.
  • PSR-15 v1 vs v2. They differ only in the return type on handle(). If you implement MiddlewareInterface yourself, mind the : ResponseInterface declaration or you'll get a fatal signature error.
  • Session/auth are not reachable. The PSR-15 side has no Auth::user(), session(), or csrf_token(). Injecting them makes that middleware non-portable — keep framework-coupled checks on the Laravel side of the bridge.
  • Debugging. There is no logging, tracing, or profiling. Drop a temporary dump() in Dispatcher::handle(), or add a tiny timing middleware at the top of the stack and log to Log::debug() — you have full visibility because the codebase is three files.
  • Extension point: MiddlewareResolver. Implement it to change how string specs map to instances — tagged container bindings, attribute-based resolution, per-route groups, or cached reflection. This is the only place to add "magic."
  • Maintenance risk. Last release was July 2021 (92 stars, 0 dependents tracked). It's stable and tiny — vendor the three files or pin the exact version; if you ever need to fork, it is a 30-minute read.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource