Imported from BusyBeaverSoftware/lava-app (
AGENTS.md). Install upstream withnpx skills add BusyBeaverSoftware/lava-app. Copyright stays with the author.
AGENTS.md
This file is generated by lava map from the app's own registries. It describes
THIS app: its routes, services, flags, commands, and the environment variables it
reads. Do not edit it by hand — change the app and run lava map again.
Every path below is relative to the app root (the directory holding this file).
Verify the app in one command:
lava check # boot, wiring, routes, features, env, commands, tests, and this file
lava check --quick # boot's own findings only — no suite, no sweeps (under 2s)
lava map --check # just this file: is it still an accurate map of the app?
Packs
This app loads no packs. Add one in app/Modules.php to enable its
routes, services, and commands.
Routes (3)
Registration order is match order: the app's own routes are registered before a pack's, so an app can override a pack route by claiming the same path.
| methods | path | name | handler | gated by | middleware |
|---|---|---|---|---|---|
| GET|HEAD | /health | health | 'App\Http\health' | - | - |
| GET | /hello/{name:str} | hello | App\Http\HelloController::show | - | - |
| GET | /beta/hello | beta.hello | App\Http\HelloController::beta | beta_greeting | - |
Services (12)
There is no auto-wiring: every id is built by the code at wired at. A handler can
type-hint any of these ids as a parameter.
| id | kind | class | wired at |
|---|---|---|---|
| app.dir | value | - | core:src/Boot/Steps/RegisterCoreServices.php:44 |
| app.env | value | - | core:src/Boot/Steps/RegisterCoreServices.php:45 |
| Lava\Core\Features\Features | factory | Lava\Core\Features\Features | core:src/Boot/Steps/RegisterCoreServices.php:52 |
| Lava\Core\Features\FeatureScope | singleton | Lava\Core\Features\FeatureScope | core:src/Boot/Steps/RegisterCoreServices.php:57 |
| Lava\Core\Log\LineLogger | singleton | Lava\Core\Log\LineLogger | core:src/Boot/Steps/RegisterCoreServices.php:69 |
| Lava\Core\Boot\RuntimeFacts | singleton | Lava\Core\Boot\RuntimeFacts | core:src/Boot/Steps/RegisterCoreServices.php:74 |
| App\Greeter | singleton | App\Greeter | app/Services.php:24 |
| Psr\Log\LoggerInterface | alias | Lava\Core\Log\LineLogger | core:src/Boot/Steps/RegisterDefaultServices.php:42 |
| Psr\Clock\ClockInterface | singleton | Psr\Clock\ClockInterface | core:src/Boot/Steps/RegisterDefaultServices.php:45 |
| Lava\Core\Routing\Router | singleton | Lava\Core\Routing\Router | core:src/Boot/Steps/BuildRouter.php:94 |
| Lava\Core\Routing\UrlGenerator | singleton | Lava\Core\Routing\UrlGenerator | core:src/Boot/Steps/BuildRouter.php:95 |
| Lava\Core\Console\CommandRegistry | value | - | core:src/Boot/Steps/RegisterCommands.php:68 |
Feature flags (1)
default is the code default, the bottom of the resolution order; the environment
and config/.env override it. lava features resolve <flag> shows which layer
decided. A flag gates a pack at boot and a route per request.
| flag | default | pack | description |
|---|---|---|---|
| beta_greeting | rollout:50 | - | The /beta/hello route |
Commands (13)
Run lava <name> --json for the machine-readable form of any of these.
| command | pack | summary |
|---|---|---|
| about | core | Show runtime facts: PHP, extensions, packs, and any boot problems. |
| check | core | Verify the app in one command: boot, wiring, routes, features, and tests. |
| routes | core | List routes with their injection plans and gating state. |
| services | core | List container registrations with their wiring site and real dependencies. |
| features | core | List feature flags, or resolve one with its full layer trace. |
| config | core | Show every config key with the file that set it. |
| env | core | Show declared environment variables and where their values come from. |
| map | core | Write AGENTS.md from the app; --check verifies it is still current. |
| test | core | Run the app's PHPUnit suite and report structured results. |
| serve | core | Serve the app over HTTP with the PHP built-in server. |
| describe | core | Explain one route, service, flag, env var, or command by name. |
| api | core | Search the framework's own API: classes, methods and signatures. |
| list | core | List every available command, grouped by pack. |
Environment variables (0)
A value in the real environment always beats config/.env. A required variable with
no value is a lava check warning, and a failure under --strict.
None.
Global middleware
None. Add class-strings to app/Middleware.php to wrap every route.
Files
The files this app has that the framework reads, every one optional. Config is
read only from config/app.php, config/features.php, config/logging.php and the
files an enabled pack declares; any other file in config/ is not read.
| file |
|---|
| app/Commands.php |
| app/Middleware.php |
| app/Modules.php |
| app/Routes.php |
| app/Services.php |
| config/.env |
| config/app.php |
| config/features.php |
| config/logging.php |
Framework reference
The canonical minimal form of every artifact you can write. Every file except
public/index.php is optional; lava check verifies the wiring between them, and
lava map regenerates this document when the app changes.
app/Routes.php
Returns a callable that registers routes on the Router. A handler is
[Class::class, 'method'] or a function name. A route with no ->when() is
always active; a gated one is a real 404 when its flag is off.
use Lava\Core\Routing\Router;
return function (Router $r): void {
// A custom param type, registered before it is used.
$r->pattern('word', '[a-z]+');
$r->get('/users/{id:int}', 'users.show')
->handler([App\Http\UserController::class, 'show']);
$r->post('/greet/{name:word}', 'greet')
->handler([App\Http\GreetController::class, 'greet'])
->middleware(App\Http\AuthMiddleware::class)
->when('greeting');
};
app/Http/UserController.php
A handler is a class method (or a function). Its parameters are injected by
type: ServerRequestInterface, RouteArgs for the path parameters, or a
container id. The : ResponseInterface return type is required — boot
checks it.
namespace App\Http;
use Lava\Core\Http\Responses;
use Lava\Core\Routing\RouteArgs;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class UserController
{
public function show(ServerRequestInterface $request, RouteArgs $args): ResponseInterface
{
return Responses::json(['id' => $args->int('id')]);
}
}
app/Http/AuthMiddleware.php
A PSR-15 middleware. It is resolved from the container, so register it in
app/Services.php like any other service, then list it in
app/Middleware.php (global) or on a route with ->middleware().
namespace App\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class AuthMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $handler->handle($request);
}
}
app/Middleware.php
Returns the global middleware class-strings, outermost first. Each one wraps every route — including the requests that end in a 404.
return [
App\Http\TimingMiddleware::class,
];
app/Services.php
Returns a callable that registers services on the Container. There is no
auto-wiring: every id is built by visible code, and registering one twice is
fatal. Use singleton() unless an object must not be shared within one
request: a factory() closure re-runs on every get(), is rebuilt on every
request, and is not covered by the boot wiring proof. Two standard ids are
filled by core only if nothing registered them first —
Psr\Log\LoggerInterface (a stderr LineLogger) and Psr\Clock\ClockInterface
(the system clock) — so an app that wants its own simply registers the id
here. This is also where an app declares the environment variables it reads.
use Lava\Core\Boot\AppContext;
use Lava\Core\Config\EnvVar;
use Lava\Core\Container\Container;
return function (Container $c, AppContext $ctx): void {
$c->singleton(App\Greeter::class, fn (Container $c): App\Greeter => new App\Greeter($ctx->env));
$c->value('greeting.name', 'Lava');
$c->value(EnvVar::CONTAINER_ID, [
EnvVar::required('DATABASE_URL', 'The database to connect to.', secret: true),
]);
};
app/Modules.php
Returns the packs this app loads, each gated by a feature. A module whose
feature is off is absent: its routes 404 and its commands do not exist. A pack
that is enabled but not installed is a boot problem naming the exact composer require to run.
use Lava\Core\Modules\ModuleRef;
return [
ModuleRef::of(Lava\Db\DbModule::class, package: 'lavaphp/db', feature: 'db'),
];
app/Commands.php
Returns a callable that registers the app's own commands. A name a core
command already uses is fatal — lava routes means the same thing in every
app. A command that needs the app's services extends
Lava\Core\Console\Commands\AppCommand, which boots the app and hands
inspect() the booted App. Lava\Core\Testing\TestConsole runs any command
in a test and returns its exit code and envelope.
use Lava\Core\Console\CommandRegistry;
return function (CommandRegistry $commands): void {
$commands->add(new App\Console\ReportCommand());
};
config/features.php
define is the code default — the bottom of the resolution order. set is
a deployment override. A value in the real environment or in config/.env
beats both, which is what makes a flag turnable without a deploy.
use Lava\Core\Features\Feature;
use Lava\Core\Features\Flag;
return [
'define' => [
Feature::define('beta_dashboard', Flag::rollout(50), description: 'The new dashboard'),
],
'set' => [
'beta_dashboard' => Flag::on(),
],
];
config/app.php
Read as app.<key>, so base_url here is app.base_url in code. Core reads
config/app.php and config/logging.php, and a pack reads the config files
it declares; any other file in config/ is not read. Each value carries its
provenance, which is what lava config reports.
return [
'env' => 'dev',
'base_url' => 'http://localhost:8080',
];
config/.env
Read at boot, and promoted into the process environment only for names the real environment does not already define — a real env var always wins, so a deployment never has to edit this file.
DATABASE_URL="sqlite:///var/app.sqlite"
LAVA_ENV=dev
tests/HealthTest.php
The harness boots the app in-process — no server, no network — and
dispatches PSR-7 requests through the same handler the HTTP entry point uses.
A client keeps cookies between requests the way a browser does, so a test can
sign in and stay signed in; each new TestClient is a new visitor. To swap a
service for one boot, pass replace: [Id::class => $fake] — e.g.
ClockInterface::class => new FrozenClock('2026-01-01 09:00') — so a fake
never lives in app/Services.php.
namespace App\Tests;
use Lava\Core\Boot\App;
use Lava\Core\Testing\TestApp;
use Lava\Core\Testing\TestClient;
use PHPUnit\Framework\TestCase;
final class HealthTest extends TestCase
{
public function testHealthIsOk(): void
{
$app = TestApp::boot(dirname(__DIR__));
self::assertInstanceOf(App::class, $app);
$response = (new TestClient($app))->get('/health');
self::assertSame(200, $response->status());
}
}