milpa/runtime

The bootable Milpa kernel: composes milpa/container, milpa/events, milpa/http and milpa/plugin into container -> dispatcher -> capability-check -> ordered plugin boot -> route registration, with zero Doctrine and zero legacy Web coupling. The active-plugins list is config/filesystem-driven, never a

Maintainers

Package info

github.com/getmilpa/runtime

pkg:composer/milpa/runtime

Transparency log

Statistics

Installs: 10

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-07-08 23:50 UTC

This package is auto-updated.

Last update: 2026-07-09 00:17:17 UTC


README

Milpa

Milpa Runtime

The bootable Milpa kernel — composes milpa/core, milpa/container, milpa/events, milpa/http and milpa/plugin into a running app with a config-driven plugin registry, capability checks before boot, and lifecycle events. Zero database, zero magic.

CI Packagist PHP License Docs

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.

Install

composer require milpa/runtime

Quick example

A 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 providesrequires 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.

Composes the family

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 providesrequires 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.

Requirements

Documentation

Full API reference: getmilpa.github.io/runtime — generated straight from the source DocBlocks and dressed with the Milpa design system.

Contributing

Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.

License

Apache-2.0 © TeamX Agency.

Milpa is designed, built, and maintained by TeamX Agency.