milpa / skeleton
The composer create-project starting point for a Milpa app: boots milpa/runtime's Kernel, serves a real HTTP response with zero database, and ships a minimal coa CLI (doctor/validate/make) wired to milpa/devtools.
Requires
- php: >=8.3
- milpa/container: ^0.1
- milpa/core: ^0.5
- milpa/devtools: ^0.1
- milpa/events: ^0.2
- milpa/http: ^0.1
- milpa/plugin: ^0.1
- milpa/runtime: ^0.1
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.65
- phpstan/phpdoc-parser: ^2.3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
README
Milpa Skeleton
composer create-project milpa/skeleton myapp→ an app that runs — booted, serving/, answeringcoa— with zero database. This is your starting point, not a demo.
milpa/skeleton is the smallest real host of milpa/runtime: a Kernel::boot() call, one
plugin, one route, one CLI. No Doctrine, no legacy Milpa\Web, nothing to configure before you
see it work.
Quickstart
composer create-project milpa/skeleton myapp
cd myapp
php -S localhost:8000 -t public
Open http://localhost:8000 — you'll see "Milpa is running", served by
App\Plugins\HelloPlugin\Controllers\HomeController through Milpa\Runtime\Http\RequestHandler.
Then check the CLI:
php bin/coa doctor
milpa · coa doctor
root: /path/to/myapp
✔ 1 plugin(s) configured, 1 booted: HelloPlugin
✔ container: Milpa\Container\DIContainer
✔ dispatcher: Milpa\Eventing\EventDispatcher
✔ 1 route(s) declared (RouteProviderInterface plugins)
✔ config: app.greeting = 'Milpa is running.'
✔ kernel booted — zero database queries.
What's in here
| Path | What it is |
|---|---|
public/index.php |
The HTTP entry point: builds a PSR-7 request from globals, boots the kernel, dispatches through Milpa\Runtime\Http\RequestHandler, emits the response. |
bin/coa |
The CLI entry point — doctor, validate, make:controller. See src/Console/Application.php. |
config/plugins.php |
The active-plugins list — a plain list<class-string>. This is the only source of truth for what boots; no database, no filesystem discovery. |
config/app.php |
The app-config bag. Registered by Kernel::boot() as Milpa\Runtime\Config; plugins read it in boot(). See The Config idiom. |
src/Plugins/HelloPlugin/ |
The example plugin: #[PluginMetadata], no provides/requires, one GET / route, and the Config read that drives the homepage greeting. Copy its shape for your own plugins. |
tests/Boot/KernelBootTest.php |
The boot smoke test: the kernel boots from config/plugins.php and GET / returns 200. |
Add a plugin
- Create
src/Plugins/YourPlugin/YourPlugin.phpimplementingMilpa\Interfaces\Plugin\PluginInterfacewith a#[Milpa\Attributes\PluginMetadata(...)]attribute (copyHelloPlugin's shape). - To contribute routes, also implement
Milpa\Runtime\Http\RouteProviderInterface::routes()— return alist<Milpa\Http\Routing\Route>, each bound to aMilpa\Http\Routing\HandlerReference(ControllerClass::class, 'method'). - Write a controller whose method takes a
Psr\Http\Message\ServerRequestInterfaceand returns aPsr\Http\Message\ResponseInterface(seeHomeController— this skeleton shipsnyholm/psr7as its PSR-7 implementation, sincemilpa/httpdeliberately ships contracts only, no concrete request/response classes). - Add the class to
config/plugins.php. php bin/coa doctorto confirm it booted and its routes were counted;php bin/coa validatefor a static pre-boot capability check without runningboot().
Milpa\Runtime\Kernel::boot() capability-checks every configured plugin's #[PluginMetadata]
before anything boots — a requires with no matching provides fails loudly, pre-boot, with
a typed PluginDependencyException, not a runtime surprise three requests later.
The Config idiom
A plugin's constructor is fixed by Milpa\Interfaces\Plugin\PluginInterface to a single argument
— (DIContainerInterface $container). It never receives config values directly. So how does a
plugin get a storage path, an API base URL, or a greeting string? It reads them in boot()
from the app-config bag:
-
Put configuration in
config/app.php, which returns a nested array. Dot-notation indexes it, so['app' => ['greeting' => 'Hi']]is read back asapp.greeting. -
public/index.php(andbin/coa) pass it into the kernel:$kernel = Kernel::boot([ 'plugins' => require $root . '/config/plugins.php', 'config' => require $root . '/config/app.php', ]);
-
Kernel::boot()registers it in the container asMilpa\Runtime\Config. A plugin reads what it needs inboot()— this is exactly whatHelloPlugindoes:public function boot(): void { $greeting = $this->container->get(Config::class)->get('app.greeting', 'Milpa is running.'); $this->container->registerService(HomeController::class, new HomeController($greeting)); }
Edit app.greeting in config/app.php, reload http://localhost:8000, and the heading changes
— no env vars, no constructor plumbing. That is the whole idiom: config lives in a file,
plugins read it in boot(), coa doctor echoes back the value it resolved.
Scaffolding with coa
bin/coa wires milpa/devtools' generate/inspect layer straight into this project — including
for an agent driving the CLI, not just a human:
php bin/coa doctor # boot the kernel, report what came up php bin/coa validate # static capability check, no boot() php bin/coa make:controller HelloPlugin GreetingController --methods=index
make:controller is real milpa/devtools machinery (Milpa\DevTools\Make\Generators\ControllerGenerator
WriteGuard) — but its output targets the legacy Milpa host convention devtools was built for (Milpa\Plugins\*\Controllers, extendsMilpa\app\Providers\BaseController), not this skeleton'sApp\Plugins\*+RouteProviderInterfacepattern. The command prints that mismatch after writing the file; treat the generated code as a reference and adapt the namespace, base class, and route wiring by hand. See this front's SDD report for the full context — closing that gap with a runtime-native generator is a natural next step formilpa/devtools.
What this is NOT
- Not a finished app. One plugin, one route, one page. Everything past that is yours to add.
- Not wired to a database.
milpa/runtime's plugin registry is config-driven, never a Doctrine entity — persistence is something a plugin opts into, never something the kernel requires. Adddoctrine/ormand a storage plugin when (if) you need one. - Not the only PSR-7 choice.
nyholm/psr7is declared here as a real dependency becausemilpa/httpships routing contracts only, no concrete request/response implementation. Swap it for another PSR-7/PSR-17 implementation if you prefer —public/index.phpandRequestHandleronly depend on the PSR interfaces.
The family
This skeleton composes six published Milpa packages, unmodified:
milpa/runtime— the bootable kernel that wires the rest togethermilpa/core— contracts, capability graph, eventsmilpa/container— the DI containermilpa/events— the event dispatchermilpa/http— PSR-15-native routing contractsmilpa/plugin— capability-ordered plugin bootmilpa/devtools— the engine behindbin/coa
Contributing
Contributions are welcome. Please report security issues responsibly, and note that this project follows a standard code of conduct.
License
Apache-2.0 © TeamX Agency.
Milpa is designed, built, and maintained by TeamX Agency.