Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e5c161e
feat(cqrs): compile #[CachedQuery] and #[InvalidatesQuery] into the h…
Oct 13, 2026
d045be2
fix(cqrs): refuse to key queries with non-public state and validate #…
Oct 13, 2026
19227f5
fix(cqrs): refuse an #[InvalidatesQuery] key template on a query that…
Oct 13, 2026
6f2f262
fix(cqrs): refuse #[CachedQuery] on a value-keyed query with non-publ…
Oct 13, 2026
7a7ebe7
test(cqrs): type the Traversable collection fixture so PHPStan max pa…
Oct 13, 2026
913ee85
feat(cqrs): a Laravel-cache query cache with generation-based group e…
Oct 13, 2026
c08e43a
fix(cqrs): drop a query cache store that lands after its group's evic…
Oct 13, 2026
e64e3fa
fix(cqrs): bound a no-TTL query cache entry by its generation TTL so …
Oct 13, 2026
a46fd68
feat(cqrs): cache #[CachedQuery] results with per-query TTLs behind f…
Oct 13, 2026
6cd92a1
docs(config): document firefly.cqrs.query.cache provider, store and p…
Oct 13, 2026
21b8e0b
feat(cqrs): evict cached queries on committed domain events and on ED…
Oct 13, 2026
67642f8
docs(config): document firefly.cqrs.query.cache.invalidation in the s…
Oct 13, 2026
18b146f
fix(cqrs): evict the group when an EDA envelope cannot rebuild a type…
Oct 13, 2026
02c0d9a
fix(resilience): select the store with firefly.resilience.store.provi…
Oct 13, 2026
695c9f1
docs(config): document firefly.resilience.store provider and allow-lo…
Oct 13, 2026
b0dbf15
feat(actuator): opt-in health indicators for cache stores, queue conn…
Oct 13, 2026
d99a034
docs(config): document the cache, queue and redis health indicator ke…
Oct 13, 2026
1e2ee76
fix(config): wire the cache, queue and redis health gates to their FI…
Oct 13, 2026
d76e4c9
feat(messaging): a listener container contract with retry and back-of…
Oct 13, 2026
c34acae
docs(config): document firefly.messaging.listener.retry and firefly:m…
Oct 13, 2026
683d8af
feat(eda-kafka): a Kafka listener container for firefly/messaging wit…
Oct 13, 2026
74acc43
docs(config): document firefly.messaging.kafka and firefly.messaging.…
Oct 13, 2026
13addbb
fix(eda-kafka): fail a Kafka flush on a failed delivery report and ro…
Oct 13, 2026
2caa970
test(eda-kafka): prove the messaging listener container against a rea…
Oct 13, 2026
b06db57
feat(eda-rabbitmq): a RabbitMQ listener container for firefly/messagi…
Oct 13, 2026
c390521
docs(config): document firefly.messaging.rabbitmq exchange, dead-lett…
Oct 13, 2026
4fee709
test(admin): opt the paged conditions harness into the default-off he…
Oct 13, 2026
1d08116
fix(eda-rabbitmq): declare and bind a parking queue for every listene…
Oct 13, 2026
bb7deb5
test(eda-rabbitmq): prove the messaging listener container against a …
Oct 13, 2026
4606443
feat(webhooks): register the firefly/webhooks package with validated …
Oct 13, 2026
abcf364
feat(webhooks): HMAC, Stripe, GitHub, Twilio and NoOp signature valid…
Oct 13, 2026
1b627d3
fix(webhooks): choose the Twilio signed string by the URL, so a repla…
Oct 13, 2026
26ee8fe
fix(webhooks): parse Twilio form parameters from the raw signed body,…
Oct 13, 2026
8c061a8
feat(webhooks): atomic idempotency stores on memory, the cache or a p…
Oct 13, 2026
64ccb8b
feat(webhooks): an endpoint per provider that verifies, deduplicates …
Oct 13, 2026
b3357b0
feat(callbacks): register the firefly/callbacks package with a subscr…
Oct 13, 2026
478327a
fix(callbacks): refuse at boot a seeded subscription id wider than th…
Oct 13, 2026
2064bb2
feat(callbacks): an execution log of every delivery on memory or any …
Oct 13, 2026
162afe3
feat(callbacks): deliver events to subscriptions as signed POSTs with…
Oct 13, 2026
eff3e9a
fix(callbacks): dispatch each event once when firefly.callbacks.event…
Oct 13, 2026
d8c8901
feat(callbacks): firefly:callbacks:list and firefly:callbacks:replay …
Oct 13, 2026
759cea4
docs: document the W4 ports — query cache, messaging brokers, resilie…
Oct 13, 2026
509b11e
fix(lumen): boot EdaConsumerServiceProvider in the Postgres outbox ha…
Oct 13, 2026
8f2109b
fix(callbacks): refuse delivery to an inactive subscription in the de…
Oct 13, 2026
67d3ff5
fix(callbacks): deliver and replay to the subscription's current URL …
Oct 13, 2026
af31188
docs(callbacks): document that replay refuses inactive subscriptions …
Oct 13, 2026
91e47ea
Merge origin/main (W1: eda-database, scheduling-database, Integration…
Oct 13, 2026
7642631
Merge origin/main (W2: orchestration, SQL Server LOCK_TIMEOUT hygiene…
Oct 13, 2026
1578f12
docs(publishing): count the 35 component names the root package replaces
Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

### Added

- **CQRS query cache (`firefly.cqrs.query.cache.provider=cache`).** `CacheQueryCache` on any Laravel cache store:
`#[CachedQuery(ttl:, group:)]` on a query class caches its results under a key derived from the query value
object; `#[InvalidatesQuery]` on an event class evicts one entry or a whole group when the event commits in
process or arrives over firefly/eda (`firefly.cqrs.query.cache.invalidation` covers events with no local class).
Groups are generations, so eviction works on file and database stores too. The default stays `none`.
- **Kafka and RabbitMQ for `firefly/messaging` (`firefly.messaging.provider=kafka|rabbitmq`).** Listener
containers in `firefly/eda-kafka` and `firefly/eda-rabbitmq`, built on their client factories, with retries and
exponential back-off (`firefly.messaging.listener.retry.*`), dead-letter topics / a dead-letter exchange with
diagnostic headers (`firefly.messaging.listener.dead-letter.enabled`), at-least-once commits, real consumer
groups, and `php artisan firefly:messaging:consume`.
- **Health indicators for cache stores, queue connections and Redis** (`firefly.management.endpoint.health.
{cache,queue,redis}.enabled`, all off by default).
- **`firefly/webhooks`.** `POST /webhooks/{provider}` per configured provider: HMAC, Stripe, GitHub, Twilio or
no signature, a replay window, atomic idempotency on memory, the cache or a portable `firefly_webhook_receipts`
table, publishing to firefly/eda as `webhook.{provider}.{type}`, problem+json refusals, `firefly:webhooks:prune`.
- **`firefly/callbacks`.** Subscriptions on memory or `firefly_callback_subscriptions`; deliveries of matching
firefly/eda events as HMAC-SHA256-signed POSTs (`X-Firefly-Timestamp`, `X-Firefly-Signature`), retries with
back-off, a circuit breaker per endpoint through firefly/resilience, an execution log on memory or
`firefly_callback_executions`, `firefly:callbacks:list` and `firefly:callbacks:replay`.
- **`firefly.resilience.store.provider`** — `cache` (default) or `memory`.
- **Orchestration (`firefly/orchestration`):** sagas with five compensation policies, compensation error
handlers, a builder DSL and saga composition; TCC with ordered participants, per-phase timeouts and
retries and TCC composition; and durable workflows with dependencies, conditions, signals, timers,
Expand Down Expand Up @@ -60,6 +80,11 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

### Changed

- **A resilience store without atomic locks now refuses to boot** (`ConfigurationException`
`RESILIENCE_STORE_LOCKLESS`) instead of silently running breaker, rate-limiter and bulkhead transitions without
a critical section. Set `firefly.resilience.store.allow-lockless=true` to accept the race explicitly. A
`CacheResilienceStore` constructed by hand over such a store throws from `withLock()` unless built with
`allowLockless: true`.
- **`firefly/eda-postgres` is the database outbox plus `pg_notify`/`LISTEN`.** `PostgresEventPublisher`,
`PostgresEventConsumer` and its `OutboxPreCommitHook` now extend the `firefly/eda-database` classes;
`provider=postgres` reads the `firefly.eda.database.*` keys, with every `firefly.eda.postgres.*` key kept as an
Expand All @@ -68,6 +93,10 @@ All notable changes to LaraFly are documented here. This project uses CalVer (`Y

### Fixed

- **`firefly:callbacks:replay` honors deactivation and endpoint changes.** The deliverer refuses an inactive
subscription on every path (replay needs `--include-inactive`, which the output reports), and every delivery goes
to the subscription's current URL — checked against `http.require-https` / `http.allowed-hosts` — instead of the
URL stored on the execution, recording the change.
- **Concurrent consumers could deliver an outbox row twice.** The Postgres consumer's `FOR UPDATE SKIP LOCKED` ran
outside any transaction, so its lock ended with the statement; on other drivers the claim was an unlocked read.
Every claim is now a lease committed before delivery.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ Full flag reference and generated-file contents: [CLI](docs/cli.md).

One published library, `fireflyframework/larafly`, contains the components under `packages/*` and the bundled
application skeleton. Its `replace` entries satisfy component requirements at the framework's version;
component names remain useful module boundaries, with their own test suites. The 36
component names remain useful module boundaries, with their own test suites. The 38
[module guides](docs/modules/) below group them by concern:

| Group | Module | Package(s) |
Expand All @@ -1096,7 +1096,9 @@ component names remain useful module boundaries, with their own test suites. The
| Eventing & Messaging | [Event-Driven Architecture](docs/modules/eda.md) — `EventPublisher`, `#[EventListener]`, retry/DLQ | `firefly/eda` |
| Eventing & Messaging | [EDA Brokers](docs/modules/eda-brokers.md) — RabbitMQ/Postgres/Kafka adapters; Postgres adds `LISTEN`/`NOTIFY` to the database outbox | `firefly/eda-rabbitmq`, `firefly/eda-postgres`, `firefly/eda-kafka` |
| Eventing & Messaging | [EDA Database Outbox](docs/modules/eda-database.md) — the portable same-transaction outbox on every database, concurrent consumers | `firefly/eda-database` |
| Eventing & Messaging | [Messaging](docs/modules/messaging.md) — the lower-level raw-bytes broker layer | `firefly/messaging` |
| Eventing & Messaging | [Messaging](docs/modules/messaging.md) — the lower-level raw-bytes broker layer, in memory, on the Laravel queue, on Kafka or on RabbitMQ | `firefly/messaging` |
| Eventing & Messaging | [Webhooks](docs/modules/webhooks.md) — inbound webhooks: an endpoint per provider, HMAC/Stripe/GitHub/Twilio signatures, a replay window, idempotency, published to EDA | `firefly/webhooks` |
| Eventing & Messaging | [Callbacks](docs/modules/callbacks.md) — outbound callbacks: subscriptions, signed deliveries driven by events, retries, a circuit breaker per endpoint, a replayable execution log | `firefly/callbacks` |
| Eventing & Messaging | [Orchestration](docs/modules/orchestration.md) — sagas, TCC, durable workflows, recovery, DLQ | `firefly/orchestration` |
| CQRS | [CQRS](docs/modules/cqrs.md) — `CommandBus`/`QueryBus`, the domain→integration-event bridge | `firefly/cqrs` |
| Security | [Security](docs/modules/security.md) — principal model, `HttpSecurity`, `#[PreAuthorize]`, JWT/OAuth2 | `firefly/security` |
Expand Down
21 changes: 17 additions & 4 deletions book/src-es/07-cqrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,19 @@ final class DefaultQueryBus implements QueryBus
$this->validator->validate($query);
$this->authorizer->authorize($query);

$key = $query instanceof Cacheable ? $query->cacheKey() : null;
$policy = $this->policies?->policyFor($query::class);
$key = match (true) {
$query instanceof Cacheable => $query->cacheKey(),
$policy !== null => QueryCacheKey::of($query),
default => null,
};
$group = $policy !== null ? $policy->group : '';
$ttl = $policy->ttl ?? $this->cacheTtl;

if ($key !== null) {
$cached = $this->cache->get($key);
$cached = $this->cache instanceof GroupedQueryCache
? $this->cache->lookup($group, $key)
: $this->cache->get($key);
if ($cached !== null) {
return $cached;
}
Expand All @@ -341,7 +350,11 @@ final class DefaultQueryBus implements QueryBus
$result = $handler($query);

if ($key !== null) {
$this->cache->put($key, $result, $this->cacheTtl);
if ($this->cache instanceof GroupedQueryCache) {
$this->cache->store($group, $key, $result, $ttl);
} else {
$this->cache->put($key, $result, $ttl);
}
}
// …
$this->metrics->recordQuerySuccess($query, microtime(true) - $startedAt);
Expand All @@ -366,7 +379,7 @@ Todos los colaboradores excepto el propio `HandlerRegistry` son, por defecto, un
| Autorización | `AllowAllAuthorizer` — permite todo | `SecurityCommandAuthorizer`/`SecurityQueryAuthorizer` de `firefly/security` (Capítulo 10) |
| Correlación | Siempre activa — un id uuid4 por despacho, restaurado después | Nada que activar; se estampa en `x-correlation-id` cuando un evento de dominio llega al bus de eda (Capítulo 8) |
| Métricas | `NoOpCqrsMetrics` | `firefly/observability` (un capítulo posterior) |
| Caché de consultas | `NoOpQueryCache` — siempre falla | Un futuro adaptador de `firefly/cache` |
| Caché de consultas | `NoOpQueryCache` — siempre falla | `firefly.cqrs.query.cache.provider=cache` (`CacheQueryCache` sobre un almacén de caché de Laravel) para las consultas `Cacheable` y `#[CachedQuery]` |

Ninguno de los dos buses abre una transacción, y ninguno de los dos drena eventos de dominio. `#[Transactional]` sobre `OpenWalletHandler::handle()` de arriba hace ese trabajo enteramente por su cuenta, a través del proxy generado que el Capítulo 9 explica en detalle — el bus es completamente ajeno a él, y se comportaría idénticamente si `handle()` fuera un método plano, sin proxy, que llamara a `DB::beginTransaction()` a mano.

Expand Down
2 changes: 2 additions & 0 deletions book/src-es/13-cli-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ final class ManifestCacheWriter
$handlers['handlers'],
$handlers['destinations'],
$files[] = $dir.'/'.FireflyCachePaths::HANDLERS,
$handlers['cachedQueries'],
$handlers['invalidations'],
);

// eda
Expand Down
21 changes: 17 additions & 4 deletions book/src/07-cqrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,19 @@ final class DefaultQueryBus implements QueryBus
$this->validator->validate($query);
$this->authorizer->authorize($query);

$key = $query instanceof Cacheable ? $query->cacheKey() : null;
$policy = $this->policies?->policyFor($query::class);
$key = match (true) {
$query instanceof Cacheable => $query->cacheKey(),
$policy !== null => QueryCacheKey::of($query),
default => null,
};
$group = $policy !== null ? $policy->group : '';
$ttl = $policy->ttl ?? $this->cacheTtl;

if ($key !== null) {
$cached = $this->cache->get($key);
$cached = $this->cache instanceof GroupedQueryCache
? $this->cache->lookup($group, $key)
: $this->cache->get($key);
if ($cached !== null) {
return $cached;
}
Expand All @@ -341,7 +350,11 @@ final class DefaultQueryBus implements QueryBus
$result = $handler($query);

if ($key !== null) {
$this->cache->put($key, $result, $this->cacheTtl);
if ($this->cache instanceof GroupedQueryCache) {
$this->cache->store($group, $key, $result, $ttl);
} else {
$this->cache->put($key, $result, $ttl);
}
}
// …
$this->metrics->recordQuerySuccess($query, microtime(true) - $startedAt);
Expand All @@ -366,7 +379,7 @@ Every collaborator except `HandlerRegistry` itself defaults to a **no-op**, so t
| Authorization | `AllowAllAuthorizer` — allow everything | `firefly/security`'s `SecurityCommandAuthorizer`/`SecurityQueryAuthorizer` (Chapter 10) |
| Correlation | Always on — a uuid4 id per dispatch, restored after | Nothing to enable; stamped into `x-correlation-id` when a domain event reaches the eda bus (Chapter 8) |
| Metrics | `NoOpCqrsMetrics` | `firefly/observability` (a later chapter) |
| Query cache | `NoOpQueryCache` — always a miss | A future `firefly/cache` adapter |
| Query cache | `NoOpQueryCache` — always a miss | `firefly.cqrs.query.cache.provider=cache` (`CacheQueryCache` on a Laravel cache store) for `Cacheable` and `#[CachedQuery]` queries |

Neither bus opens a transaction, and neither bus drains domain events. `#[Transactional]` on `OpenWalletHandler::handle()` above is doing that work entirely on its own, through the generated proxy Chapter 9 explains in full — the bus is completely oblivious to it, and would behave identically if `handle()` were a plain, unproxied method that happened to call `DB::beginTransaction()` by hand.

Expand Down
2 changes: 2 additions & 0 deletions book/src/13-cli-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ final class ManifestCacheWriter
$handlers['handlers'],
$handlers['destinations'],
$files[] = $dir.'/'.FireflyCachePaths::HANDLERS,
$handlers['cachedQueries'],
$handlers['invalidations'],
);

// eda
Expand Down
16 changes: 13 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@
"pestphp/pest": "^4.5",
"pestphp/pest-plugin-browser": "^4.3",
"phpstan/phpstan": "^2.1",
"predis/predis": "^3.0",
"predis/predis": "^2.3 || ^3.0",
"symplify/monorepo-builder": "^11.2"
},
"autoload-dev": {
"psr-4": {
"Firefly\\Actuator\\Tests\\": "packages/actuator/tests/",
"Firefly\\Admin\\Tests\\": "packages/admin/tests/",
"Firefly\\AutoConfigure\\Tests\\": "packages/autoconfigure/tests/",
"Firefly\\Callbacks\\Tests\\": "packages/callbacks/tests/",
"Firefly\\Cli\\Tests\\": "packages/cli/tests/",
"Firefly\\Config\\Tests\\": "packages/config/tests/",
"Firefly\\Container\\Tests\\": "packages/container/tests/",
Expand Down Expand Up @@ -88,6 +89,7 @@
"Firefly\\Tests\\": "tests/",
"Firefly\\Validation\\Tests\\": "packages/validation/tests/",
"Firefly\\Web\\Tests\\": "packages/web/tests/",
"Firefly\\Webhooks\\Tests\\": "packages/webhooks/tests/",
"Lumen\\Tests\\": "samples/lumen/tests/",
"Firefly\\Kernel\\Tests\\": "packages/kernel/tests/",
"Lumen\\": "samples/lumen/src/"
Expand Down Expand Up @@ -127,6 +129,7 @@
"firefly/actuator": "self.version",
"firefly/admin": "self.version",
"firefly/autoconfigure": "self.version",
"firefly/callbacks": "self.version",
"firefly/cli": "self.version",
"firefly/config": "self.version",
"firefly/container": "self.version",
Expand Down Expand Up @@ -156,13 +159,15 @@
"firefly/security-oauth2-server": "self.version",
"firefly/testing": "self.version",
"firefly/validation": "self.version",
"firefly/web": "self.version"
"firefly/web": "self.version",
"firefly/webhooks": "self.version"
},
"autoload": {
"psr-4": {
"Firefly\\Actuator\\": "packages/actuator/src/",
"Firefly\\Admin\\": "packages/admin/src/",
"Firefly\\AutoConfigure\\": "packages/autoconfigure/src/",
"Firefly\\Callbacks\\": "packages/callbacks/src/",
"Firefly\\Cli\\": "packages/cli/src/",
"Firefly\\Config\\": "packages/config/src/",
"Firefly\\Container\\": "packages/container/src/",
Expand Down Expand Up @@ -191,7 +196,8 @@
"Firefly\\Security\\OAuth2\\Server\\": "packages/security-oauth2-server/src/",
"Firefly\\Testing\\": "packages/testing/src/",
"Firefly\\Validation\\": "packages/validation/src/",
"Firefly\\Web\\": "packages/web/src/"
"Firefly\\Web\\": "packages/web/src/",
"Firefly\\Webhooks\\": "packages/webhooks/src/"
},
"files": [
"packages/security-oauth2-client/src/Registration/casters.php",
Expand Down Expand Up @@ -275,6 +281,10 @@
"Firefly\\Security\\OAuth2\\Client\\SecurityOAuth2ClientWiringProvider",
"Firefly\\FeatureFlags\\FeatureFlagsServiceProvider",
"Firefly\\FeatureFlags\\FeatureFlagsWiringProvider",
"Firefly\\Webhooks\\WebhooksServiceProvider",
"Firefly\\Webhooks\\WebhooksWiringProvider",
"Firefly\\Callbacks\\CallbacksServiceProvider",
"Firefly\\Callbacks\\CallbacksWiringProvider",
"Firefly\\Orchestration\\OrchestrationServiceProvider",
"Firefly\\Orchestration\\OrchestrationWiringProvider",
"Firefly\\Cli\\Boot\\FireflyCacheServiceProvider"
Expand Down
Loading
Loading