Plugins overview
Quiote is built as an unopinionated kernel plus opinionated drop-ins. The core gives you the request pipeline, container, config system, routing, validation, and rendering — and almost nothing else. Everything with a strong opinion or a heavy dependency lives outside the kernel, in a separately-installable package you add when you want it.
This is the design philosophy taken to its conclusion: a minimal app pulls only the kernel, and you opt into the rest with a composer require and a line of config.
Core, plugins, packages
Section titled “Core, plugins, packages”Three words that get used together here, kept straight:
- The core (kernel) —
quioteframework/quiote. The skeleton. It ships the seams a plugin plugs into (config, DI, middleware, events, routes, commands, database drivers, HTTP clients) and stays neutral about persistence, auth, front-end, observability, and so on. - A plugin — a class implementing
Quiote\Plugin\PluginInterface. Its oneregister()call contributes to the core’s seams. See Plugins and extensibility for the mechanism and how to write your own. - A package — a Composer package (
quioteframework/*for the official ones) that contains a plugin (and its dependency). Installing the package puts the code on disk; enabling its plugin wires it in.
The framework itself dogfoods this: a whole family of subsystems that used to live in the core have been extracted into their own packages. None of them is part of the kernel any more; each is a drop-in you choose (all MIT-licensed, atop the LGPL kernel). They fall into a few groups:
- Security & web — CSRF, rate limiting, and the auth packages.
- Developer experience — the Whoops error page and the MCP server.
- Observability — OpenTelemetry export and the telemetry dashboard.
- Persistence — the Eloquent / Doctrine / Cycle / Propulsion database adapters and the PDO / Azure / S3 / GCS session backends.
- Rendering — the PHPTAL / XSLT / Twig template renderers.
Each one is catalogued, with its install and enable steps, in Official packages.
Plugin vs. middleware
Section titled “Plugin vs. middleware”These two get confused because a plugin so often adds middleware — but they are different kinds of thing, operating at different times.
- A plugin is a boot-time wiring mechanism. Its
register()method runs once, when the framework boots, and contributes things to the core’s seams — config defaults, DI services, event listeners, routes, console commands, database drivers, HTTP clients, and middleware. Think of it as the setup step that installs capabilities. - A middleware is a per-request pipeline stage. It’s a PSR-15 object that runs on every request, getting a turn to act on the request on the way in and the response on the way out. Think of it as a worker on the assembly line each request passes down.
| Plugin | Middleware | |
|---|---|---|
| When it runs | Once, at boot | On every request |
| What it is | A registration hook (PluginInterface::register()) | A request/response processing stage (PSR-15) |
| Its job | Wire capabilities into the framework’s seams | Do one thing to each request as it flows through |
| How you activate it | List its class in Config/plugins.* | It’s in the pipeline via a #[Middleware] attribute or a Config/middleware.* entry |
| Runs your code per request? | No — only its contributions do | Yes |
The relationship is one-directional: a plugin can register middleware (among many other contributions), but middleware is not a plugin. You can add middleware with no plugin at all — a Config/middleware.* entry or the #[Middleware] attribute is enough (see Writing custom middleware). And a plugin usually does much more than add middleware — the db-doctrine plugin, for instance, registers a database driver and no middleware at all.
So reach for a plugin when you’re packaging a capability to drop into an app (often a whole subsystem); reach for middleware when you specifically need to see or change every request as it passes through the pipeline. The middleware pipeline covers the request side; Plugins and extensibility covers the boot-time side.
Why extract them
Section titled “Why extract them”Two payoffs, both downstream of the philosophy:
- A slim default install. Code that runs on zero requests unless you opted in shouldn’t be forced onto every deployment. Moving Whoops, CSRF, the rate limiter, OpenTelemetry, and the ORM adapters out of the kernel’s dependencies keeps a bare app small — you pull
doctrine/ormonly if you installquioteframework/db-doctrine, and so on. - Honest opt-in. When a feature is a package you install, “is this app using an ORM / telemetry / the MCP server?” is answered by looking at
composer.jsonand thepluginsconfig key — not by reading kernel internals. Nothing important happens because it happened to ship in core.
Enabling a package
Section titled “Enabling a package”The pattern is always the same two steps — install, then enable:
composer require quioteframework/db-doctrineThen register its plugin in Config/plugins.php (or .xml/.yaml/.yml):
return [ ['class' => \Quiote\Database\Adapter\Doctrine\DoctrinePlugin::class, 'enabled' => true],];- class: Quiote\Database\Adapter\Doctrine\DoctrinePlugin enabled: true<ae:configurations xmlns:ae="http://quiote.dev/quiote/config/global/envelope/1.1" xmlns="http://quiote.dev/quiote/config/parts/plugins/1.1"> <ae:configuration> <plugin class="Quiote\Database\Adapter\Doctrine\DoctrinePlugin" /> </ae:configuration></ae:configurations>register() runs once at boot, after your settings load and before contexts are created, and it can only add — your settings.* and container bindings always win. Two packages skip the plugins step: rate limiting is a plain library you call from your own code, and the telemetry dashboard contributes a standalone console command that’s available as soon as the package is installed. Official packages spells out the exact “enable” step for each one.
Every official package’s plugin class already carries the required #[Quiote\Plugin\Attribute\Plugin] attribute — see Plugins and extensibility: Writing a plugin for why that attribute exists and what it means for your own plugins.
CSRF is the deliberate exception
Section titled “CSRF is the deliberate exception”One package is not opt-in: quioteframework/csrf. It’s a required dependency of the kernel and registers itself automatically at boot, so every app is CSRF-protected out of the box without a plugins entry. It’s a security default, not a packaging convenience — so you turn it off consciously (set core.csrf.enabled => false, which logs a warning), rather than turning it on. See Official packages: csrf.
How a plugin reaches a request
Section titled “How a plugin reaches a request”Enabling a plugin doesn’t run any request code by itself — there are two distinct moments. First the framework boots and lets each plugin contribute to the core’s seams; then, per request, those contributions are simply there in the pipeline the kernel already assembled. The path from a plugins entry to a live request is:
- Boot & discover.
Quiote::bootstrap()loads yoursettings.*, thenPluginManagerreads thepluginsconfig key, instantiates each enabled plugin (refusing any class-string that lacks the#[Plugin]attribute), and calls itsregister(PluginRegistrar $r)once, in declared order. - Contribute. Inside
register(), eachPluginRegistrarmethod routes a contribution to an existing seam — config defaults toConfig(set-if-absent), services to the DIContainer, middleware toMiddlewareCatalog, listeners toEvents, and routes / commands / HTTP clients to their registries. - Assemble. The kernel builds the pipeline once: a scanner reads the
#[Middleware]attributes, a topological resolver orders them by phase →before/after→ priority, and any middleware added viaMiddlewareCatalog::register()is spliced in at its requested position. - Serve. Every request then flows through that assembled pipeline:
Kernel boots and builds the pipeline → request enters
ErrorHandlingMiddleware→SessionMiddleware→RoutingMiddlewareresolves the action →SecurityMiddlewarechecks access →DispatchMiddlewareruns the action and renders the view → response is emitted.
A plugin that contributes middleware shows up as a station in that chain. A plugin that only contributes a config default or a service doesn’t add a station — it shows up as a value that an action or an existing middleware reads while the request runs. For the complete picture see The request lifecycle and The middleware pipeline.
See also
Section titled “See also”- Official packages — the catalogue of the nine extracted subsystems, with install + enable steps for each.
- Plugins and extensibility — the plugin mechanism, the
PluginRegistrarseams, and writing your own. - Design philosophy — why the core is deliberately unopinionated.