firefly/cli is LaraFly's developer-experience console — the Spring Boot Maven/Gradle-plugin analogue, built as
a set of Artisan commands. It compiles the app for a zero-reflection boot (firefly:cache), introspects a booted
app in-process at the terminal (firefly:about/:routes/:health/:metrics — actuator-over-CLI, no HTTP
round-trip), scaffolds every framework stereotype (make:firefly-*), and thinly delegates to Laravel's own
serve/database commands (firefly:serve/firefly:db). New Deptrac Cli layer — depends on the rest of the
framework, depended on by nothing.
php artisan firefly:cache
The zero-reflection payoff: it runs every settled package's existing scanner → compiler pair over
config('firefly.scan.paths') (a PSR-4 map, e.g. 'App\\' => app_path()) and writes the compiled app manifests
plus the #[Transactional] proxy classes into bootstrap/cache/firefly/:
bootstrap/cache/firefly/
├── component.php # DI component manifest (container + context + autoconfigure)
├── context.php # application-context manifest
├── config-properties.php # #[ConfigProperties] DTOs
├── routes.php # compiled route table
├── exception-handlers.php # #[ControllerAdvice]/#[ExceptionHandler] manifest
├── constraints.php # validation constraint manifest
├── handlers.php # #[CommandHandler]/#[QueryHandler] manifest
├── event-listeners.php # #[EventListener] manifest
├── message-listeners.php # #[MessageListener] manifest
├── scheduled.php # #[Scheduled] manifest
├── security-methods.php # #[PreAuthorize]/#[Secured]/#[RolesAllowed] manifest
├── transactional.php # #[Transactional] method manifest
├── proxies.php # FQCN => file classmap for the generated proxies
└── proxies/ # one generated proxy class file per #[Transactional] target
A FireflyCacheServiceProvider (auto-discovered with firefly/cli) instance()s these compiled manifests over
whatever the capability packages resolved, registers the #[ConfigProperties] bindings, and installs a
spl_autoload_register classmap loader for the proxy classes — all before any bean resolution runs, giving a fully
cached, reflection-free boot. Three config keys point the boot path at the cache:
'firefly' => [
'cache' => [
'path' => base_path('bootstrap/cache/firefly'),
'component_manifest' => base_path('bootstrap/cache/firefly/component.php'),
'context_manifest' => base_path('bootstrap/cache/firefly/context.php'),
],
],Every manifest above is resolved by the same three-step convention, and Firefly\Context\Scan\AppScan is the
seam each capability package uses to do it:
- the compiled artifact exists under
firefly.cache.path→ load it, zero reflection (production); - otherwise
firefly.scan.pathsis non-empty → scan those PSR-4 roots in-process, on every boot (development); - otherwise → an empty manifest, and boot still succeeds.
FireflyAutoConfigureServiceProvider has always done this for the component and context manifests (via
component_manifest/context_manifest). It is now also what routes, #[ControllerAdvice] handlers, CQRS handlers,
event and message listeners, scheduled tasks, validation constraints, method-security rules, #[ConfigProperties]
DTOs and the #[Transactional] manifest do — so an application that has never run firefly:cache behaves the same
as one that has, and pays a full reflection scan per boot for the privilege.
That is a change, not a restatement: before it, step 2 did not exist. Every capability bound an empty
manifest and only firefly/cli's FireflyCacheServiceProvider ever replaced it, which made a require-dev tool
the sole owner of the loading half of the contract. An app that skipped the compile step — or that installed the
firefly/firefly metapackage, which did not require the CLI — booted with no routes (404 on everything it owned)
and, worse, with an empty method-security manifest: both enforcement sites read "no rule for this method" as ALLOW,
so #[PreAuthorize], #[Secured] and #[RolesAllowed] all failed open. firefly/cli is
now part of the firefly/firefly metapackage, and firefly.security.method.strict (default false) makes the
strict reading available to anyone who wants a build that ships without a compiled manifest to refuse to boot
rather than run unprotected.
Compiling is still worth it — reflection-free boot is the point of firefly:cache — but it is now an optimisation
rather than a correctness requirement.
php artisan firefly:clear
The inverse of firefly:cache — recursively deletes bootstrap/cache/firefly/ (or the configured
firefly.cache.path) and nothing else. Safe to run any time; a subsequent boot falls back to in-process scanning.
These render M12 actuator/observability endpoint data at the terminal, in-process — no HTTP request is made. Each
resolves the compiled ActuatorRegistry, calls the matching endpoint's handle(), and prints its response;
none reimplement actuator logic.
php artisan firefly:about
Prints the version followed by the info, env, beans, conditions, mappings, and scheduledtasks
actuator endpoints in sequence — the full Spring Boot --debug/actuator introspection story at a glance.
php artisan firefly:routes
Renders the compiled route table (the mappings actuator endpoint).
php artisan firefly:health
Renders the aggregated health status (the health actuator endpoint).
php artisan firefly:metrics
Renders the observability metrics snapshot (the metrics actuator endpoint; requires firefly/observability).
Any of these commands prints a warning and exits successfully if the corresponding endpoint is disabled or not
wired (e.g. firefly/observability not installed for firefly:metrics).
Pyfly's generate command family, one Artisan generator per stereotype:
| Command | Generates |
|---|---|
make:firefly-controller |
A #[RestController] with a sample #[GetMapping] action, under app/Http. |
make:firefly-service |
A #[Service] bean. |
make:firefly-component |
A #[Component] bean. |
make:firefly-handler |
Two files: a #[CommandHandler] and the command class its handle() takes (#[QueryHandler] + query with --query). |
make:firefly-listener |
A #[Component] class with an #[EventListener] method, or a #[MessageListener] one with --message. |
make:firefly-entity |
A DDD entity extending Firefly\Domain\Entity (there is no #[Entity] attribute). |
make:firefly-repository |
A concrete #[Repository] class extending Firefly\Data\Repository\EloquentRepository, with a $model to repoint. |
make:firefly-config-properties |
A #[ConfigProperties]-bound configuration DTO. |
Three of those outputs are shaped by what the scanners actually accept, and it is worth knowing why:
- The handler generator emits its message class too.
HandlerScannerinfers a bare#[CommandHandler]'s message type fromhandle()'s sole parameter, and a builtin type (the old stub'sobject $command) cannot be resolved — it threwCqrsConfigurationExceptionout offirefly:cache, aborting the whole compile. So the generatedhandle()takes a concrete class, and that class is written alongside it:RegisterWidgetHandler+RegisterWidget,CountWidgetsHandler+CountWidgets. A message file that already exists is left alone and reported, never overwritten. Nested names stay together (make:firefly-handler Widget/RegisterWidgetHandlerputs both in the same sub-namespace). - The listener generator puts
#[Component]on the class.#[EventListener]/#[MessageListener]mark a method of a bean; without a stereotypeComponentScanner::describe()returns null, the class never reaches the component manifest, and the wiring pass's$container->make()falls through to Illuminate's reflective auto-build — a plain object outside Firefly's lifecycle, with no#[Value]injection, no post-processing and a new instance per delivery. - The repository generator emits a class, not an interface. Nothing synthesises an implementation for a
repository interface (there is no Spring-Data dynamic proxy here), so the old
interface X extends CrudRepositorywas unresolvable by construction. The generated class is deliberately notfinal, becausefirefly:cacheemits a#[Transactional]proxy thatextendsit.
php artisan make:firefly-controller GreetingController
php artisan make:firefly-service GreetingService
php artisan make:firefly-handler RegisterWidget
php artisan make:firefly-handler CountWidgets --query
php artisan make:firefly-listener WidgetEventListener
php artisan make:firefly-listener WidgetMessageListener --message
php artisan make:firefly-entity Widget
php artisan make:firefly-repository WidgetRepository
php artisan make:firefly-config-properties GreetingProperties
php artisan firefly:serve {--host=127.0.0.1} {--port=8000}
Delegates to artisan serve, or to octane:start when laravel/octane is installed (probed via class_exists()
only — Octane stays an optional runtime dependency, never required by firefly/cli's composer.json).
php artisan firefly:db {action=migrate}
Delegates to Laravel's own database commands: migrate (default), db:seed (firefly:db seed), or
migrate:fresh (firefly:db fresh). Neither command reimplements any Laravel behavior — both are thin
$this->call(...) passthroughs.
firefly/cli is not the only package that registers Artisan commands; a capability package ships its own where the
command is part of that capability rather than of the console.
| Command | Package | What it does |
|---|---|---|
firefly:openapi |
firefly/openapi |
Writes the generated OpenAPI 3.1 document to --output=<file> (parent directories are created, and a summary line is printed) or raw to stdout. Stdout is written with Symfony's OUTPUT_RAW so the bytes are exactly the document's — php artisan firefly:openapi | <client-generator> is the intended use — which is also why the confirmation line prints only in --output mode. See OpenAPI. |
firefly:eda:consume |
firefly/eda |
Binds the configured broker destinations and runs the consumer loop. See EDA. |
firefly:outbox:relay |
firefly/eda-postgres |
Forwards committed outbox rows to a second broker. See EDA Brokers. |