Spring Boot's cohesion, native to Laravel 13.
Dependency injection with stereotypes, conditional auto-configuration, hexagonal ports & adapters, CQRS, event-driven architecture with a genuine same-transaction outbox, and Spring-Security-shaped method security — wired the moment you install a package, with your own beans always winning.
📘 Book · Quickstart · Why · Architecture · Patterns · Modules · Docs · Changelog
Table of contents
- 📘 The Book — LaraFly by Example
- Why LaraFly?
- Quickstart
- Philosophy
- Architecture — Boot Pipeline · DI & Auto-Configuration · Request Lifecycle · CQRS ⟷ EDA Bridge · Same-Tx Outbox
- Featured Patterns
- Installation
- CLI & Project Scaffolding
- Modules
- Documentation
- Roadmap
- Firefly Framework Ecosystem
- Requirements
- Contributing
- License
LaraFly by Example is the official, project-driven book for the framework — a PHP sibling to
PyFly by Example. It builds Lumen, the
wallet-and-ledger service in samples/lumen/, from an empty directory into a secured,
event-driven, actuator-observed microservice, chapter by chapter — every listing drawn from that real project
(it boots and its tests pass against this framework version, 26.07.18).
The book is complete and bilingual (English + Spanish): a quick start, thirteen chapters across four
parts — Foundations (DI, config, HTTP), Modelling & Persisting the Domain (repositories, DDD), Coordinating &
Securing the App (CQRS, EDA + transactional outbox, #[Transactional], security), and Observability, Testing
& Delivery (actuator, testing, the CLI + zero-reflection cache) — plus a Laravel→LaraFly cheat-sheet and a
glossary. Every fenced PHP listing is php -l-verified against the real sample. The sources live under
book/ (EN manuscript · ES manuscript) and build to PDF + EPUB
in both languages via book/build/run.sh. samples/lumen/ is the fastest way to see the
whole stack fit together end to end, and the Featured Patterns section below walks
through its most important pieces.
Laravel gives you a superb HTTP kernel, ORM, and ecosystem — but it doesn't tell you how to structure a non-trivial application. Where does business logic live? How do controllers stay thin? How do you keep Eloquent out of your domain model? How do you make a wallet transfer and its audit-log write commit or roll back together? How do you stop an unauthenticated request from calling an admin-only command bus handler? Every team answers these questions differently, and every answer has to be re-learned by the next engineer who joins.
Laravel gives you infinite flexibility. What it doesn't give you is a shared architecture.
LaraFly is a cohesive, hexagonal application layer on top of Laravel 13 — the PHP member of the
Firefly Framework family, built to feel exactly like Spring Boot to
anyone who has used it: PHP-8-attribute dependency injection with stereotypes, conditional auto-configuration
that backs off the moment you supply your own bean, a CQRS command/query bus, event-driven architecture with a
genuine same-transaction outbox (not a "publish and hope"), declarative #[Transactional] transaction
demarcation, deny-by-default method and URL security, and a production-ready actuator/observability surface —
all compiled ahead of time into bootstrap/cache/firefly/ for a zero-reflection boot.
// skeleton/app/GreetingService.php — a #[Service] bean, autowired by type
#[Service]
final class GreetingService
{
public function __construct(private readonly GreetingProperties $properties) {}
public function greet(string $name): string
{
return sprintf('%s, %s!', $this->properties->salutation, $name);
}
}
// skeleton/app/Http/GreetingController.php — a #[RestController], routes compiled to a manifest
#[RestController]
final class GreetingController
{
public function __construct(private readonly GreetingService $greetings) {}
#[GetMapping('/greetings/{name}')]
public function show(#[PathVariable] string $name): array
{
return ['message' => $this->greetings->greet($name)];
}
}No service-provider boilerplate, no manual route registration: the component scanner finds GreetingService
and GreetingController, the container autowires GreetingProperties into the service by constructor type,
and the route scanner compiles #[GetMapping('/greetings/{name}')] into the route table — all from one
php artisan firefly:cache run. See Featured Patterns below for the full CQRS, EDA,
outbox, and security tour, drawn from the runnable samples/lumen/ wallet-ledger sample.
LaraFly is not a fork of Laravel and does not hide it — every package layers cleanly on top of
Illuminate\Container, Eloquent, the HTTP kernel, and the queue/cache/scheduler, so everything you already
know about Laravel keeps working underneath.
- Laravel developers who want enterprise-grade architecture — DI stereotypes, hexagonal ports, CQRS, an event bus with a real outbox — without assembling it package by package.
- Teams who want every service in the org to share one convention instead of reinventing structure per project.
- Architects building polyglot platforms who need consistency across Java, Python, and PHP services.
- Anyone coming from Spring Boot who wants the same mental model expressed natively in PHP — see the Laravel ↔ Spring Boot comparison guide.
# 1 · Install the global installer once, then scaffold a new app
composer global require firefly/installer
firefly new my-app
cd my-app
# 2 · Compile the app for a zero-reflection boot (idempotent — create-project already ran this once)
php artisan firefly:cache
# 3 · Run it
php artisan firefly:servefirefly new wraps composer create-project firefly/skeleton (git-init included by default), which already
wires a sample #[RestController]/#[Service] pair, sqlite for storage, and a post-create-project-cmd hook
that ran firefly:cache for you — so the app is already booting reflection-free. Re-run firefly:cache
yourself any time you add or change a #[Component]/#[RestController]/#[CommandHandler]/etc. class, and
firefly:clear to fall back to the in-process scanner. See Installation for the
non-global-installer path and CLI & Project Scaffolding for the full command
reference.
Four principles shape every design decision in LaraFly.
A new capability package should work the moment it's installed. firefly/autoconfigure discovers each
package's #[Configuration] classes, and a #[ConditionalOnMissingBean]-guarded #[Bean] backs off silently
the instant you register your own — "default, with override," the same contract Spring Boot's auto-configuration
gives you.
Business logic should never use Illuminate\Database\Eloquent\Model directly if it can help it. LaraFly
enforces hexagonal architecture — ports and adapters — across every capability that touches storage or
transport:
// samples/lumen/src/Infrastructure/WalletRepository.php — the PORT the domain depends on
interface WalletRepository
{
public function save(Wallet $wallet): Wallet;
public function findById(string $id): ?Wallet;
/** @return list<Wallet> */
public function findByOwnerId(string $ownerId): array;
}// samples/lumen/src/Infrastructure/EloquentWalletRepository.php — the ADAPTER, auto-bound by interface
#[Repository]
final class EloquentWalletRepository extends EloquentRepository implements WalletRepository
{
protected string $model = Wallet::class;
// save()/findById()/findByOwnerId() implement the port over Eloquent — see the file for the full body.
}Application and command-handler code depends only on WalletRepository; the container's nominal
interface-binding wires EloquentWalletRepository in behind it automatically. Deptrac enforces the boundary at
the monorepo level — a domain package that imports Illuminate\Database\* fails the architecture gate.
Every public surface is typed and analysed at PHPStan max strictness — no mixed left un-narrowed, no
@phpstan-ignore as a substitute for a real fix. Framework behaviour is declared with PHP 8 attributes
(#[Service], #[RestController], #[CommandHandler], #[Transactional], #[PreAuthorize], ...), scanned
once and compiled to a cached manifest — never re-reflected on a production request.
firefly/security's HTTP rule chain is deny-by-default: an unmatched URL is denied, not silently allowed.
firefly/actuator ships health/info/metrics endpoints that are unexposed (404) until you opt in. A cached
firefly:cache boot is the supported way to run in production — reflection only ever happens once, at build
time, never per request.
LaraFly is hexagonal end to end: every capability exposes a nominal PHP interface (a port) with one or more adapters, domain/application code depends only on ports, and Deptrac enforces the boundary at the monorepo level.
Every capability package plugs into one shared FireflyKernel through a FireflyServiceProvider: a
provider's register() only buffers its BootPass contributions into a PendingBootPasses collector — it
never resolves the kernel itself, so Laravel's alphabetical package-discovery order can never affect boot
order. The kernel then drains that buffer and drives the real, phased order:
The DI container (firefly/container) resolves dependencies from type hints discovered by a component
scan — no XML, no service locators:
use Firefly\Container\Attributes\{Primary, Qualifier, Service};
interface Greeter { public function greet(): string; }
#[Service] #[Primary]
final class EnglishGreeter implements Greeter { public function greet(): string { return 'Hello'; } }
#[Service('spanish')] #[Qualifier('spanish')]
final class SpanishGreeter implements Greeter { public function greet(): string { return 'Hola'; } }$container->get(Greeter::class); // EnglishGreeter (the #[Primary] one)
$container->getByName('spanish'); // SpanishGreeter
$container->getAll(Greeter::class); // every implementation, sorted by #[Order]firefly/autoconfigure layers Spring-Boot-style starters on top: a candidate #[Configuration] class is
discovered at phase 200, its bean definitions are committed to the registry at phase 500 tagged
DefinitionSource::AutoConfiguration, and a #[ConditionalOnMissingBean(X)]-guarded #[Bean] only survives
back-off at phase 600 if nothing else already registered an X — so your own bean always wins, regardless of
provider load order:
An HTTP request flows through the Spring-style filter chain (firefly/web), into a #[RestController]
action, through argument-resolved, #[Valid]-checked parameters, into the CQRS bus, through a
#[CommandHandler]/#[QueryHandler], into a repository port and its Eloquent adapter, and back — with any
raised domain events drained after commit:
firefly/cqrs's bus wraps every handler call in a bounded pipeline (correlate → validate → authorize → invoke
→ metrics) and re-throws any fault as a category-preserving CommandProcessingException/
QueryProcessingException. After a #[Transactional] handler's unit of work commits, the same bridge
republishes each raised DomainEvent onto the firefly/eda bus as an integration event — closing the
domain → integration-event loop with no glue code in application handlers:
The default memory/queue firefly/eda providers publish only after the aggregate's transaction commits —
never atomically with it. firefly/eda-postgres closes that gap: with firefly.eda.provider=postgres, the
outbox row is written inside the aggregate's own open transaction, on the aggregate's own connection, so it
commits or rolls back atomically with the aggregate — no dual-write, no "commit then hope the publish
succeeds":
See the same-tx outbox showcase below for the config and the seam that makes this possible.
Nine showcases below, each an accurate snippet lifted straight from samples/lumen/ (the wallet-and-ledger
sample) or the framework itself — no invented API. Every attribute and class shown here compiles against the
shipped 26.07.18 release.
use Firefly\Container\Attributes\Service;
#[Service]
final class GreetingService
{
public function __construct(private readonly GreetingProperties $properties) {}
public function greet(string $name): string
{
return sprintf('%s, %s!', $this->properties->salutation, $name);
}
}A #[Service] is a specialisation of #[Component] — registered as a container singleton by a scan that finds
every class carrying it, with its GreetingProperties constructor dependency autowired by type.
Highlights: stereotypes, scopes, #[Primary]/#[Qualifier], #[Order] — see
Dependency Injection.
#[RestController]
#[RequestMapping('/api/v1/wallets')]
final class WalletController
{
public function __construct(
private readonly CommandBus $commands,
private readonly QueryBus $queries,
) {}
#[PostMapping(status: 201)]
public function open(#[Valid] #[RequestBody] OpenWalletRequest $body): array
{
$id = $this->commands->send(new OpenWallet($body->owner_id, Currency::from($body->currency)));
return ['wallet_id' => $id];
}
#[GetMapping('/{id}/balance')]
public function balance(#[PathVariable] string $id): array
{
$balance = $this->queries->ask(new GetBalance($id));
if ($balance === null) {
throw new ResourceNotFoundException("Wallet {$id} not found");
}
return ['wallet_id' => $id, 'balance_minor' => $balance];
}
}A thin HTTP-onto-CQRS mapping: every action builds a command/query, dispatches it through the bus, and shapes
the response array. A domain fault (an unknown wallet, an overdraw, a denied #[PreAuthorize]) surfaces as an
RFC-7807 problem+json response from the framework's global renderer — no local #[ExceptionHandler] needed.
Highlights: #[GetMapping]/#[PostMapping], #[PathVariable]/#[RequestBody], the web filter chain — see
Web Layer.
#[CommandHandler]
class OpenWalletHandler
{
public function __construct(private readonly WalletRepository $wallets) {}
#[Transactional]
public function handle(OpenWallet $command): string
{
$id = 'wlt-'.bin2hex(random_bytes(8));
$wallet = Wallet::open($id, $command->ownerId, $command->currency);
$this->wallets->save($wallet);
return $id;
}
}The handled command type is inferred from the sole handle() parameter — no explicit
#[CommandHandler(OpenWallet::class)] needed. #[CommandHandler]/#[QueryHandler] both specialise
#[Component], so the handler is a constructor-injected DI bean automatically. Highlights: the bounded
command/query pipeline (correlate → validate → authorize → invoke → metrics), category-preserving exception
wrapping — see CQRS.
final class Wallet extends Model implements RecordsDomainEvents
{
use HasDomainEvents;
public static function open(string $id, string $ownerId, Currency $currency): self
{
if (trim($ownerId) === '') {
throw new ConflictException('owner_id is required');
}
$wallet = new self(['id' => $id, 'owner_id' => $ownerId, 'currency' => $currency->value, 'balance_minor' => 0]);
$wallet->raiseEvent(new WalletOpened($id, $ownerId, $currency->value));
return $wallet;
}
}
#[PublishDomainEvent('wallet.events')]
final readonly class WalletOpened extends DomainEvent
{
public function __construct(
public string $walletId,
public string $ownerId,
public string $currency,
) {
parent::__construct();
}
}raiseEvent() (from the HasDomainEvents trait) buffers the event on the aggregate; #[Transactional]'s
generated proxy is the sole caller of DomainEventDispatcher::dispatchAfterCommit(), so the event only
publishes once the enclosing unit of work actually commits — never on a rolled-back write. Highlights:
Entity/ValueObject/AggregateRoot, derived queries, specifications — see
Domain (DDD) and Data & Repositories.
#[Component]
final class LedgerProjector
{
#[EventListener(['WalletOpened', 'FundsDeposited', 'FundsWithdrawn', 'TransferCompleted'])]
public function onWalletEvent(EventEnvelope $envelope): void
{
$walletId = $envelope->payload['walletId'] ?? '';
$amountMinor = $envelope->payload['amountMinor'] ?? 0;
$balanceMinor = $envelope->payload['balanceMinor'] ?? 0;
LedgerEntry::query()->create([
'wallet_id' => is_string($walletId) ? $walletId : '',
'event_type' => $envelope->eventType,
'amount_minor' => is_int($amountMinor) ? $amountMinor : 0,
'balance_minor' => is_int($balanceMinor) ? $balanceMinor : 0,
'occurred_at' => now(),
]);
}
}#[EventListener] enumerates event-type names (matched with fnmatch against $envelope->eventType), not
the #[PublishDomainEvent] destination — a common gotcha the sample's own docblock calls out explicitly. This
projector turns committed wallet events into an append-only ledger_entries read model. Highlights: the
in-memory/queue adapters, retry + DeadLetterStore, and why #[AsEventListener] (in-process) and
#[EventListener] (the broker bus) are two distinct surfaces — see
Event-Driven Architecture.
// config/firefly.php
'eda' => [
'provider' => 'postgres',
'postgres' => [
'channel' => 'firefly_eda_events',
'max_attempts' => 3,
],
],composer require firefly/eda-postgres
php artisan migrate # creates firefly_eda_outbox
php artisan firefly:eda:consume # terminal in-process delivery, run as a long-lived workerNo application code changes — the same #[EventListener] handlers and the same EventPublisher::publish()
call sites now run against a durable, same-transaction outbox instead of memory/queue. The seam is a small
PreCommitEventHook interface firefly/data's DomainEventDispatcher calls (if bound) before the ordinary
after-commit dispatch — null by default, so installing the package without selecting provider=postgres is
fully inert. Highlights: pg_notify/LISTEN low-latency wake, FOR UPDATE SKIP LOCKED claiming, the
optional firefly:outbox:relay to a distinct downstream broker — see EDA Brokers.
#[CommandHandler]
class TransferHandler
{
public function __construct(private readonly WalletRepository $wallets) {}
#[Transactional(propagation: Propagation::REQUIRED)]
public function handle(Transfer $command): void
{
$source = $this->wallets->findById($command->sourceWalletId)
?? throw new ResourceNotFoundException("Wallet [{$command->sourceWalletId}] not found");
$destination = $this->wallets->findById($command->destinationWalletId)
?? throw new ResourceNotFoundException("Wallet [{$command->destinationWalletId}] not found");
$amount = new Money($command->amountMinor, $source->currency());
$source->withdraw($amount); // debit (raises FundsWithdrawn)
$this->wallets->save($source); // persist inside the tx, so it can genuinely roll back
$destination->deposit($amount); // credit — throws on currency mismatch -> whole tx rolls back
$this->wallets->save($destination);
$source->recordTransferTo($command->destinationWalletId, $amount); // both legs succeeded -> raise TransferCompleted
// commit here -> FundsWithdrawn + FundsDeposited + TransferCompleted drain atomically after the unit of work commits.
}
}#[Transactional] is load-bearing, not cosmetic: a proxy generated at scan time drives manual
DB::beginTransaction()/commit()/rollBack() boundaries (never DB::transaction($closure), so a caught
exception can be committed-and-rethrown when it matches noRollbackFor). The debit is persisted inside the
transaction before the credit runs, so a currency mismatch on the credit leg genuinely rolls the debit back too
— there is no path where the source loses funds the destination never receives. Highlights: the seven
Spring propagation modes (including NESTED over Laravel savepoints), rollbackFor/noRollbackFor — see
Transactions.
#[CommandHandler]
class WithdrawHandler
{
public function __construct(private readonly WalletRepository $wallets) {}
#[PreAuthorize("hasRole('ADMIN') or hasRole('WALLET_OWNER')")]
#[Transactional]
public function handle(Withdraw $command): int
{
$wallet = $this->wallets->findById($command->walletId)
?? throw new ResourceNotFoundException("Wallet [{$command->walletId}] not found");
$wallet->withdraw(new Money($command->amountMinor, $wallet->currency()));
$this->wallets->save($wallet);
return $wallet->balanceMoney()->minorUnits;
}
}SecurityCommandAuthorizer enforces this before the handler runs, at the bus — a denied withdraw surfaces
as a CommandProcessingException wrapping an AuthorizationException (403), never a silent no-op. The
expression evaluator is a closed, no-eval whitelist tokenizer (hasRole, hasAnyRole, hasAuthority,
hasPermission, isAuthenticated, permitAll, denyAll, #param references only). Highlights: the
deny-by-default HttpSecurity URL DSL, JwtService/OAuth2 resource server, CSRF + security headers — see
Security.
#[Component]
final class PingHealthIndicator implements HealthIndicator
{
public function health(): Health
{
return Health::up();
}
}Any #[Component] implementing HealthIndicator is discovered by a bean-scan registrar and aggregated by
GET /actuator/health — a thrown health() degrades to DOWN, never an unhandled 500. Sensitive endpoints
(/actuator/env, /beans, /conditions, ...) return 404 until explicitly exposed via
firefly.management.endpoints.web.exposure.include, and /actuator/metrics + /actuator/prometheus light up
the moment firefly/observability is installed. Highlights: liveness/readiness groups, Db/DiskSpace
built-in indicators, firefly:health/firefly:metrics actuator-over-CLI — see
Actuator and Observability.
Requirements: PHP 8.3+ (8.4 recommended), Composer 2, and an existing (or new) Laravel 13 application — LaraFly layers onto Laravel, it is not a standalone runtime. Full details in Installation.
New project — the global installer:
composer global require firefly/installer
firefly new my-appNew project — without the installer (exactly what firefly new wraps):
composer create-project firefly/skeleton my-appAdding LaraFly to an existing Laravel app — firefly/firefly is a type: metapackage (the Maven BOM
analogue) that pulls in the whole runtime family with one line, and firefly/cli adds the developer console:
composer require firefly/firefly
composer require --dev firefly/cliPoint LaraFly at your app's classes and compile it:
// config/firefly.php
'scan' => [
'paths' => [
'App\\' => app_path(),
],
],php artisan firefly:cache
php artisan firefly:servefirefly/cli is the developer-experience console — the Spring Boot Maven/Gradle-plugin analogue.
| Command | What it does |
|---|---|
firefly:cache |
Compiles the app into bootstrap/cache/firefly/ — DI, routes, config properties, CQRS handlers, event/message listeners, scheduled tasks, security methods, and #[Transactional] proxy classes — for a zero-reflection boot. |
firefly:clear |
The inverse — deletes bootstrap/cache/firefly/; the app falls back to in-process scanning. |
firefly:about / :routes / :health / :metrics |
Actuator-over-CLI: render the info/env/beans/conditions/mappings endpoints, the route table, aggregated health, or the metrics snapshot in-process, with no HTTP round-trip. |
make:firefly-controller / -service / -component / -handler / -listener / -entity / -repository / -config-properties |
One generator per stereotype — --query on -handler scaffolds a #[QueryHandler], --message on -listener scaffolds a #[MessageListener]. |
firefly:serve / firefly:db |
Thin passthroughs to artisan serve (or octane:start when Octane is installed) and Laravel's own migrate/db:seed/migrate:fresh. |
php artisan make:firefly-handler RegisterWidget
php artisan make:firefly-handler CountWidgets --query
php artisan make:firefly-listener WidgetEventListenerFull flag reference and generated-file contents: CLI.
25 packages under packages/* (plus firefly/skeleton at the top level — 26 shippable units in total), each
its own installable Composer package with its own tests and its own module guide:
| Group | Module | Package(s) |
|---|---|---|
| Foundation | Error Handling — the product-agnostic error model and RFC-7807 ErrorResponse |
firefly/kernel |
| Foundation | Dependency Injection — stereotypes, scopes, #[Primary]/#[Qualifier]/#[Order] |
firefly/container |
| Foundation | Configuration — profiles, #[ConfigProperties] binding |
firefly/config |
| Foundation | Application Context — the phased boot engine (ApplicationContext port) |
firefly/context |
| Foundation | Auto-Configuration — #[Configuration]/#[Bean] starters, conditions |
firefly/autoconfigure |
| Foundation | Validation — constraint attributes, #[Valid], structured 422s |
firefly/validation |
| Web & API | Web Layer — #[RestController] routing, RouteManifest |
firefly/web |
| Web & API | Web Filters — the ordered filter chain onto Laravel middleware | firefly/web |
| Resilience & Scheduling | Resilience — retry, circuit breaker, bulkhead, timeout, rate limiter, fallback | firefly/resilience |
| Resilience & Scheduling | Scheduling — #[Scheduled] + distributed locks (cache or Postgres advisory) |
firefly/scheduling, firefly/scheduling-postgres |
| Data & Domain | Domain (DDD) — Entity, ValueObject, AggregateRoot, DomainEvent |
firefly/domain |
| Data & Domain | Data & Repositories — CRUD/paging ports, derived queries, specifications | firefly/data |
| Data & Domain | Relational Data — EloquentRepository, soft-delete, auditing, optimistic locking |
firefly/data |
| Data & Domain | Transactions — #[Transactional], propagation, isolation |
firefly/data |
| Eventing & Messaging | Event-Driven Architecture — EventPublisher, #[EventListener], retry/DLQ |
firefly/eda |
| Eventing & Messaging | EDA Brokers — RabbitMQ/Postgres/Kafka adapters, the same-tx outbox | firefly/eda-rabbitmq, firefly/eda-postgres, firefly/eda-kafka |
| Eventing & Messaging | Messaging — the lower-level raw-bytes broker layer | firefly/messaging |
| CQRS | CQRS — CommandBus/QueryBus, the domain→integration-event bridge |
firefly/cqrs |
| Security | Security — principal model, HttpSecurity, #[PreAuthorize], JWT/OAuth2 |
firefly/security |
| Operations | Actuator — health, info, env, beans, conditions, mappings | firefly/actuator |
| Operations | Observability — Prometheus-format metrics, /actuator/prometheus |
firefly/observability |
| Testing | Testing — FireflyTestCase, recording doubles, Pest expectations |
firefly/testing |
| Testing | Integration Testing — @group integration, testcontainers |
firefly/testing |
| Tooling | Installer — the global firefly new scaffolding tool |
firefly/installer |
firefly/firefly (the runtime metapackage) and firefly/cli (the dev-console — see
CLI & Project Scaffolding above) round out the 25 packages; firefly/skeleton
is the 26th unit, a type: project create-project template at the top level.
Start at the documentation table of contents — it groups every guide by topic. Highlights:
- Getting Started — the full quickstart, adding LaraFly to an existing app.
- Tutorial (Español) — build the wallet service end to end in ~12 guided steps.
- Installation — requirements, the installer, what you get out of the box.
- Architecture — the boot pipeline and kernel layer, in more depth.
- CLI Reference — every
firefly:*command andmake:firefly-*generator. - Laravel ↔ Spring Boot Comparison — concept-by-concept mapping for both audiences.
- Versioning · Contributing · Publishing.
- Every module guide above.
- LaraFly by Example — the complete bilingual book (13 chapters + appendices, PDF + EPUB).
samples/lumen/— the wallet-and-ledger sample this README's showcases are drawn from; run its own test suite withvendor/bin/pest samples/lumen/tests.
Between the tutorial, the book, the module guides, and the
runnable samples/lumen/, you have a complete, concrete tour of the framework — from a first
HTTP endpoint to CQRS, the transactional outbox, method security, and the zero-reflection production cache.
LaraFly's Foundation milestones (M1 through M14 — kernel, container, config, context, autoconfigure,
validation, web, resilience, scheduling, data/domain, eda/messaging, cqrs, security, actuator/observability,
testing, and the CLI/metapackage/skeleton) are complete, followed by real message-broker adapters and the
genuine same-transaction outbox (RabbitMQ, Postgres, Kafka) and the global firefly new installer. What's
still ahead, accurately:
- Broker integration spot-runs in CI. Each broker adapter ships a genuine
@group('integration')round-trip test (RabbitMQ, Postgres, Kafka) gated behind Docker + a live connection string — these run on demand, not in the defaultvendor/bin/pest/CI pass; wiring a scheduled/opt-in CI lane for them is still open. - More actuator endpoints.
/httpexchanges,/caches,/configprops,/refresh,/threaddump, and/shutdownare not yet implemented; there is also no second management port (an Octane second-listener is one option under consideration). - Deeper security surfaces. An OAuth2 authorization server, OAuth2 client/login, and real IdP adapters
(Keycloak/Cognito/Entra/internal-db, MFA) are deferred to their own future packages, matching the Java
Firefly Framework's module topology. Generalising
#[PreAuthorize]to any bean method (today it's enforced at the CQRS bus and the controller dispatcher) is a flagged spike, not yet scheduled. - Read models / projections as a first-class concept. The sample's
LedgerProjectorshows the pattern today via a plain#[EventListener]; a dedicatedfirefly/eventsourcing-style package for event sourcing/snapshots/projections is future work, as it is in PyFly. - Documentation. The end-to-end tutorial (EN + ES), the LaraFly by Example book (13 chapters + appendices, EN + ES, PDF + EPUB), and a docs table of contents all shipped with the documentation-parity milestone. Deeper guides (more recipes, more diagrams) continue to grow from here.
See CHANGELOG.md for the complete, dated history of every shipped milestone.
LaraFly is part of the Firefly Framework ecosystem — one programming model across every runtime:
| Platform | Repository | Status |
|---|---|---|
| Java / Spring Boot | fireflyframework-* (40+ modules) |
Production |
| Python | fireflyframework-pyfly |
Beta (CalVer 26.05+) |
| PHP / Laravel | fireflyframework-php (this repo) |
Active development (CalVer 26.07+) |
| .NET 9 | fireflyframework-dotnet |
Beta (CalVer 26.05+) |
| Rust | fireflyframework-rust |
Active development |
| Frontend (Angular) | flyfront |
Active development |
| GenAI | fireflyframework-genai |
Active development |
| CLI (Go) | fireflyframework-cli |
Active development |
| Requirement | Version |
|---|---|
| PHP | 8.3+ (8.4 recommended) |
| Composer | 2.x |
| Laravel | 13 |
| Git | for cloning and for firefly new's optional git init |
fireflyframework-php is a single monorepo housing every LaraFly package as an independent Composer unit
under packages/*, plus firefly/skeleton at the top level. Before opening a PR, the full gate must pass:
composer install
git config core.hooksPath scripts/hooks # activates the committed pre-push safety guard
composer check # pint --test && phpstan analyse && pest && deptrac analyse
composer mono-validate # validates every packages/*/composer.json
.venv-docs/bin/mkdocs build --strict # docs/: link integrity, no orphaned pages
bash scripts/check-no-sensitive-tracked.shPackage boundaries are enforced with Deptrac; packages from an already-shipped milestone are treated as
FROZEN — further edits to their src/ are the exception, not the rule. See
Contributing for the full guide, including the never-commit list
(.superpowers/, .claude, .env, private key material) enforced by the pre-push guard.
Apache-2.0 © Firefly Software Solutions Inc.