The bootable Milpa kernel — composes
milpa/core,milpa/container,milpa/events,milpa/httpandmilpa/plugininto a running app with a config-driven plugin registry, capability checks before boot, and lifecycle events. Zero database, zero magic.
milpa/runtime is where the rest of the family stops being separate packages and becomes an
app. Kernel::boot() wires a DI container, an event dispatcher, a pre-boot capability check
over every configured plugin, an ordered boot loop that emits lifecycle events at each step,
and a route table assembled from whatever plugins contribute one. The active-plugins list is
whatever list<class-string> the caller passes in — a config array, a file required into
that array, or filesystem discovery the caller performs beforehand. No Doctrine, no legacy
Milpa\Web, no database-backed plugin registry — those, if you want them, live in your host
application or a plugin you add on top.
composer require milpa/runtimeA plugin declares itself with #[PluginMetadata] and, optionally, contributes routes by
implementing RouteProviderInterface:
use Milpa\Attributes\PluginMetadata;
use Milpa\Http\HttpMethod;
use Milpa\Http\Routing\HandlerReference;
use Milpa\Http\Routing\Route;
use Milpa\Http\Routing\RouteResult;
use Milpa\Interfaces\Di\DIContainerInterface;
use Milpa\Interfaces\Plugin\PluginInterface;
use Milpa\Runtime\Http\RouteProviderInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class HelloController
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$name = $request->getAttribute(RouteResult::ATTRIBUTE)?->parameter('name', 'world') ?? 'world';
return new \Nyholm\Psr7\Response(200, ['Content-Type' => 'text/plain'], "hello, {$name}");
}
}
#[PluginMetadata(version: '1.0.0', author: 'Acme', site: 'https://example.test', name: 'HelloPlugin', type: 'Web')]
final class HelloPlugin implements PluginInterface, RouteProviderInterface
{
public function __construct(private readonly DIContainerInterface $container)
{
}
public function boot(): void
{
}
public function install(): void
{
}
public function uninstall(): void
{
}
public function enable(): void
{
}
public function disable(): void
{
}
/** @return list<Route> */
public function routes(): array
{
return [
new Route(
path: '/hello/{name}',
methods: HttpMethod::GET,
name: 'hello',
handler: new HandlerReference(HelloController::class, 'handle'),
),
];
}
}Kernel::boot() builds the container, capability-checks and boots the configured plugins in
provides → requires order, and assembles the route table; RequestHandler matches a real
PSR-7 request against it and dispatches to the resolved controller:
use Milpa\Runtime\Http\RequestHandler;
use Milpa\Runtime\Kernel;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\ServerRequest;
$kernel = Kernel::boot(['plugins' => [HelloPlugin::class]]);
$kernel->bootedPluginNames(); // -> ['HelloPlugin']
$handler = new RequestHandler($kernel, new Psr17Factory());
$response = $handler->handle(new ServerRequest('GET', '/hello/milpa'));
$response->getStatusCode(); // -> 200
(string) $response->getBody(); // -> 'hello, milpa'No plugin can leave the boot loop undetected: boot() throws PluginDependencyException
before any plugin boots if a configured plugin requires a capability nothing configured
provides, and every step along the way — capability.resolved, plugin.booting (vetoable
via an InterceptionSlot), plugin.booted, kernel.booted — fires on the wired event
dispatcher for observability or feature-flag plugins to hook into.
milpa/runtime doesn't reimplement anything the family already ships — it wires the pieces
together and adds the boot sequence on top:
| Package | Owns |
|---|---|
milpa/core |
Contracts (PluginInterface, PluginMetadata, events) and the pre-boot capability check (CapabilityGraphChecker). |
milpa/container |
The DI container every plugin and controller is resolved through. |
milpa/events |
The dispatcher every lifecycle event (plugin.booting/plugin.booted, capability.resolved, kernel.booted) fires on. |
milpa/http |
Routing contracts — Route, RouteResult, RouterInterface — the route table is built from. |
milpa/plugin |
ContractResolver, the provides → requires load-order algorithm the boot loop follows. |
milpa/runtime (this package) |
Kernel::boot() itself: the wiring, the pre-boot capability check call, the ordered boot loop with lifecycle events, and Router/RequestHandler — a minimal RouterInterface implementation and PSR-15 entry point over the assembled route table. |
- PHP ≥ 8.3
milpa/core^0.5milpa/container^0.1milpa/events^0.2milpa/http^0.1milpa/plugin^0.1
Full API reference: getmilpa.github.io/runtime — generated straight from the source DocBlocks and dressed with the Milpa design system.
Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.
Apache-2.0 © TeamX Agency.
Milpa is designed, built, and maintained by TeamX Agency.