Polaris for PHP · PSR-15 Slim, Mezzio, any PSR-15 host

polaris/psr15

packagist.org/packages/polaris/psr15 examples/slim /docs/php/psr15

Rendered from univeros/polaris-core at v0.11.0 · View source

The PSR-15 face of Polaris for PHP: a router over the endpoint manifest, the ordered middleware stack (client context, rate limits, bearer and MFA-ticket authentication, step-up, denylist, authorization) and the request handler that runs the endpoints. It works with any PSR-15 host: Slim, Mezzio, or a framework through its PSR-7 bridge.

Install

Terminal window
composer require polaris/psr15

This brings polaris/core; you also need a PSR-17 response factory (nyholm/psr7, laminas/laminas-diactoros, slim/psr7, …).

Use

use Polaris\Psr15\Pipeline;
$pipeline = new Pipeline($polaris->graph(), $responseFactory, pathPrefix: '/');
$pipeline->middleware(); // list<MiddlewareInterface>, in execution order
$pipeline->handler(); // RequestHandlerInterface serving every route of the manifest
$pipeline->handle($request); // or run the whole stack yourself

On Slim, for instance (the full demo is examples/slim):

foreach (array_reverse($pipeline->middleware()) as $middleware) { // Slim runs the last-added first
$app->add($middleware);
}
$app->any('/{path:.*}', fn($request) => $pipeline->handler()->handle($request));

The middleware reads each route’s policy (auth, rate_limit, step_up, required permissions) from the manifest, so nothing is configured per route. Your own middleware can read the request attributes named in Polaris\Http\Attributes: TOKEN (the verified token), IP_ADDRESS, USER_AGENT.

License

MIT. Polaris for PHP is created and maintained by 2am.tech.

The Slim bootstrap

The whole integration, examples/slim/src/bootstrap.php:

<?php
declare(strict_types=1);
use Polaris\Admin\AdminPlugin;
use Polaris\Agents\AgentsPlugin;
use Polaris\Agents\Capability;
use Polaris\Audit\AuditPlugin;
use Polaris\Mcp\McpPlugin;
use Polaris\OAuth\OAuthPlugin;
use Polaris\Passkey\PasskeyPlugin;
use Polaris\Config\EnvironmentConfig;
use Polaris\Pdo\PdoAdapter;
use Polaris\Polaris;
use Polaris\Psr15\Pipeline;
use Polaris\Social\SocialPlugin;
use Polaris\Support\CacheRateStore;
use Polaris\Support\InMemoryCache;
use Polaris\Support\SystemClock;
use Polaris\Wiring\Config;
use PolarisDemo\Dispatcher;
use PolarisDemo\Env;
use PolarisDemo\FakeProvider;
use PolarisDemo\FileCache;
use PolarisDemo\LoopbackClient;
use PolarisDemo\FileMailer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\RequestFactory;
use Slim\Psr7\Factory\ResponseFactory;
use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\Psr7\Factory\StreamFactory;
$root = dirname(__DIR__);
require $root . '/vendor/autoload.php';
Env::load($root);
$dsn = Env::require('POLARIS_DSN');
$pdo = new PDO(str_starts_with($dsn, 'sqlite:') ? 'sqlite:' . Env::sqlitePath($root, $dsn) : $dsn);
$pdo->exec('PRAGMA foreign_keys = ON');
$dispatcher = new Dispatcher();
// localhost, not 127.0.0.1: a passkey's relying party id must be a domain.
$baseUrl = rtrim((string) (getenv('POLARIS_BASE_URL') ?: 'http://localhost:8080'), '/');
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
// The demo's own OAuth 2 server and mailbox, outside Polaris's routes.
FakeProvider::register($app, $root);
$polaris = Polaris::create(new Config(
secrets: EnvironmentConfig::secrets(),
auth: EnvironmentConfig::auth(),
database: new PdoAdapter($pdo),
mailer: new FileMailer($root . '/var/mail.log'),
// php -S runs every request in a fresh process: the plugins' short-lived state (OAuth states, passkey
// challenges, hand-off codes) goes to files; the rate limits stay in memory, so the demo never locks itself out.
cache: new FileCache($root . '/var/cache'),
rateStore: new CacheRateStore(new InMemoryCache(), new SystemClock()),
dispatcher: $dispatcher,
plugins: [
new AuditPlugin(),
new AdminPlugin(),
// Social sign-in through the fake provider (add real ones with their client ids and secrets).
new SocialPlugin(
baseUrl: $baseUrl,
providers: ['fake' => ['client_id' => 'demo', 'client_secret' => 'demo', 'definition' => FakeProvider::definition($baseUrl)]],
httpClient: new LoopbackClient($app),
requestFactory: new RequestFactory(),
streamFactory: new StreamFactory(),
redirectUris: [$baseUrl . '/signed-in'],
),
new PasskeyPlugin(origins: [$baseUrl], rpName: 'Polaris demo'),
// The OAuth 2.1 provider: the consent and device pages are the reference HTML in public/; tokens
// minted for the MCP resource (<base>/mcp) are accepted on every route.
new OAuthPlugin(baseUrl: $baseUrl, consentUrl: $baseUrl . '/consent.html', deviceUrl: $baseUrl . '/device.html', audiences: [$baseUrl . '/mcp']),
// Polaris as an MCP server on POST /mcp: a tool per route the token's principal may call.
new McpPlugin($baseUrl, new ServerRequestFactory(), new ResponseFactory(), new StreamFactory(), serverName: 'Polaris demo'),
// Agents: one low-risk capability, auto-approved for an owner who holds members.read, echoing its arguments;
// public/approve.html is where a person decides the approvals the policy does not.
new AgentsPlugin(
baseUrl: $baseUrl,
capabilities: [new Capability(
name: 'demo.echo',
description: 'Echo the arguments back',
input: ['type' => 'object', 'properties' => ['message' => ['type' => 'string']], 'additionalProperties' => false],
permission: 'members.read',
risk: 'low',
handler: static fn(array $arguments): array => ['echo' => $arguments],
)],
approvalUrl: $baseUrl . '/approve.html',
providerName: 'Polaris demo',
description: 'The Slim demo of Polaris for PHP',
responses: new ResponseFactory(),
),
],
));
foreach ($polaris->listeners() as $listener) {
$dispatcher->listen($listener);
}
$pipeline = new Pipeline($polaris->graph(), new ResponseFactory());
// Polaris's stack runs on its own routes only (the fake provider's static routes match first). Slim
// runs the last-added middleware first; Polaris lists its stack in execution order.
$route = $app->any('/{path:.*}', static fn(ServerRequestInterface $request): ResponseInterface => $pipeline->handler()->handle($request));
foreach (array_reverse($pipeline->middleware()) as $middleware) {
$route->add($middleware);
}
return $app;