Version 3.11.0
for patchlevel/event-sourcing is here. This release comes with some
excellent improvements, including enhancements to certain console commands, query bus integration via attributes, and a
gap detection mechanism for subscriptions. These are some really interesting updates so we shouldn't waste any time and
go through the changes immediately!
Console Command Improvements
The first improvement is the addition of the ConsoleLogger
for our WatchCommand
. This enables you to see the logs
from the worker in the terminal if desired, significantly enhancing the developer experience.
The next enhancement concerns our DebugCommand
. Previously, we only displayed information about the configured
aggregates and events. Now, we also display the configured subscribers, such as the id
, group
, run_mode
, and the
subscribed events
.
These two improvements were contributed by the community. If you’re interested in contributing to
patchlevel/event-sourcing, please feel free to open a PR or an issue if
you’d like to discuss your idea first — we’re happy to help!
Query Bus
After adding a command bus in 3.8.0
, we have now also
introduced a query bus. Unlike the command bus, the query bus
is not intended to perform actions on the system, instead, it retrieves information. It allows you to fully utilize the
read/write split and use small, independent, tailored projections.
To enable this feature as a standalone solution, we introduced a simple built-in query bus. For advanced use cases, we
still recommend external options like symfony/messenger. If you are
using our event sourcing symfony bundle,
the handlers are automatically registered as query handlers for Symfony Messenger when using the #[Answer]
attribute.
Below is an example of how a projection can be implemented as a query handler:
use Patchlevel\EventSourcing\Attribute\Projector;
#[Projector('profiles')]
final class ProfileProjector
{
#[Answer]
public function answerQueryProfile(QueryProfile $query): Profile
{
$builder = $this->projectionConnection->createQueryBuilder();
$builder->select('*')
->from($this->tableName)
->where('id = :id')
->setParameter('id', $query->profileId->toString());
return Profile::fromArray($builder->executeQuery()->fetchAssociative());
}
}
For more details, check out our documentation, where you will
find an in-depth explanation of the query bus and how to configure it.
Gap Detection for Subscriptions
When using an RDBMS for the event store instead of a designated database for event sourcing, it may happen that
subscriptions skip some events. Why does this occur? This behavior is due to the internals of these systems and how they
handle auto increments. When saving takes too long and a parallel save operation is started and finished earlier, the
later reserved auto-incremented ID will be saved before the lower one. This issue happens more frequently when using
transactions, especially when they remain open for an extended period. To mitigate this, we already have a locking
mechanism in place that only allows one write at a time. But what if this happens regardless, or if you want to improve
write performance by deactivating the locking?
Now, the subscription engine has the capability
to check for these gaps
by using an additional MessageLoader
. TheGapResolverStoreMessageLoader
can detect gaps and, if necessary, wait a
defined amount of time before re-fetching the messages. You can freely configure how often the retry should occur and
how long the system should wait between retries. By default, the configuration will retry 4 times with different
waiting periods in between. The first retry occursimmediately, the second after 5ms, the third after 50ms,
and the final retry after 500ms.
You can also configure the timeframe during which this gap detection is considered. For example, if you want to
re-create a projection and encountered a gap months ago, you might not want to retry and wait for several seconds if the
gap closes, since you already know it will never happen.
$messageLoader = new GapResolverStoreMessageLoader(
$store,
$clock,
[0, 5, 50, 500],
new \DateInterval('PT5M'),
);
When using our symfony bundle, you can
enable it like this:
patchlevel_event_sourcing:
subscription:
gap_detection: ~
You can also configure it with custom settings:
patchlevel_event_sourcing:
subscription:
gap_detection:
retries_in_ms: [0, 500, 1000]
detection_window: 'PT5M'
Conclusion
Version 3.11.0 of patchlevel/event-sourcing
introduces several valuable enhancements aimed at improving the overall developer experience and system reliability.
From refined console commands and the addition of a flexible, attribute-driven query bus, to a new gap detection
mechanism for subscriptions—this release addresses practical needs and enables more robust event-driven architectures.
Some of these improvements were inspired by community feedback, and contributions continue to be welcome. Whether you're
exploring the project or have ideas to share, feel free to open an issue or pull request. As always, be sure to check
out the documentation for further details and guidance.