Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Read Models Bundle Laravel Package

somnambulist/read-models-bundle

A Laravel package for building CQRS-style read models and projections, offering base classes and Eloquent-backed query models to create denormalized, read-optimized views from domain events and write-side data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register

composer require somnambulist/read-models-bundle

Register the bundle the normal Symfony way (config/bundles.php on 5.4+, or AppKernel/bundles.php on older versions). The bundle name will resolve to Somnambulist\Bundles\ReadModelsBundle\ReadModelsBundle — confirm the namespace by looking at the *Bundle.php file in vendor/somnambulist/read-models-bundle/src/.

Where to look first

Before writing any code, open these three files in the vendor directory:

  1. src/DependencyInjection/Configuration.php — this is the authoritative list of config keys, defaults, and whether sections are required. It is more reliable than any README.
  2. src/DependencyInjection/*Extension.php — shows what services get registered (DBAL connections, the read-model registry, tagged services, compiler passes).
  3. composer.json — confirms which somnambulist/read-models major version you are pinned to, since the underlying API changes between majors.

Because this package has ~1 star and no dependents, assume documentation is thin and treat the source as the spec. Don't trust blog posts.

Minimal config

The bundle exists to wire one or more Doctrine DBAL connections into the read-model layer. A minimal config/packages/read_models.yaml will look roughly like:

read_models:
    connections:
        default:
            # dbal connection service id or connection params
            connection: doctrine.dbal.default_connection

The exact key names come from Configuration.php; copy the tree from there. If there is a dump command, bin/console config:dump-reference <alias> gives you the full tree instantly.

First use case

The fastest end-to-end win: build a denormalized "read" table (e.g. order_summary) with raw SQL/DBAL migrations, then expose it through a read model:

use Somnambulist\Components\ReadModels\ReadModel;

final class OrderSummary extends ReadModel
{
    protected ?string $table = 'order_summary';
    protected ?string $alias = 'os';
    protected array $casts = [
        'id'          => 'uuid',
        'total_cents' => 'integer',
        'created_at'  => 'datetime',
    ];
}

Then inject it (or the registry) into a controller and call OrderSummary::query()->...->fetch(). If autowiring picks up the read-model services, this works with zero extra wiring; if not, tag or explicitly define the service.

Implementation Patterns

Read models as plain services

Treat each read model as a small, immutable query object. Keep them free of business logic — they should only express how to fetch a shape of data. Put projections/updaters elsewhere. This keeps them trivially testable against a real (or in-memory SQLite) DBAL connection.

CQRS flow that fits this bundle

The natural integration is: write model (Doctrine ORM or event sourcing) → domain events → projector/listener → INSERT/UPDATE into the read table → read model queries it directly. The bundle does not provide the projector; you write a Symfony event subscriber or Messenger handler that does the writes, and the read model only ever does SELECT. That separation is the whole point — reads never touch the write-side ORM.

#[AsMessageHandler]
final class ProjectOrderPlaced
{
    public function __construct(private Connection $dbal) {}

    public function __invoke(OrderPlaced $event): void
    {
        $this->dbal->insert('order_summary', [
            'id'          => $event->orderId->toString(),
            'total_cents' => $event->total->getMinorAmount(),
            'created_at'  => $event->occurredAt->format('Y-m-d H:i:s'),
        ]);
    }
}

Multiple connections / read replicas

Use the bundle config to declare a second (read-only replica) connection and point heavy read models at it. The underlying read-models library resolves connections by name, so a read model can declare which connection it uses. This is one of the main reasons to use the bundle rather than bootstrapping read-models by hand — connection registration is centralized.

Constructor injection vs static queries

Prefer injecting the model or the registry over calling static ReadModel::query() in deep code. Static calls hide the DB dependency and make tests need global state. In controllers, however, the static style is idiomatic for read-models and acceptable; just don't spread it through your domain layer.

Eager loading and shape control

Use the library's relationship/eager-load methods (e.g. ->include(...), ->with(...) — check the version's API) rather than lazy-loading per row. Read models should be tuned for the exact endpoint that consumes them. If a table serves two very different screens, write two read models against the same table with different select/projection settings — that is cheaper than conditional query building.

Testing

Point a read model at an SQLite DBAL connection in a unit test, run your migrations, insert fixtures, and assert on the returned objects. Because read models are read-only, most tests are arrange → insert → query → assert, with no transaction/identity-map surprises.

Console/rebuild tooling

If the bundle registers build/rebuild or projection commands (check src/Console), wire them into your deploy pipeline: bin/console <read-model-build> after migrations, before traffic. If it does not, write a small Symfony command that truncates and re-projects from the event store — you will want it.

Gotchas and Tips

  • API drift is real. The underlying somnambulist/read-models has changed its static configuration between majors (connection, alias, table, casting). Pin the version in composer.json and read the changelog of the library, not just the bundle, when upgrading.
  • Config alias mismatch. The bundle's root config alias (e.g. read_models vs somnambulist_read_models) is defined in the Extension. If Symfony says "unrecognized option," you guessed wrong — run config:dump-reference or read the Extension's getAlias().
  • Compiled container caching hides config edits. After changing read_models.yaml, bin/console cache:clear in dev before concluding your key is wrong. It is almost never the bundle's fault.
  • The bundle registers connections, not tables. Migrations and schema for read tables are your responsibility. Keep them in the normal Doctrine migrations pipeline (or a dedicated read-schema migrations path) so deploys are deterministic.
  • Eventual consistency is a product decision. Projectors via Messenger are async by default if you queue them. If a screen reads immediately after a write and looks stale, that is the queue, not the bundle. Either dispatch synchronously for that flow or show pending state.
  • Don't let read models import write-side entities. It's tempting to reuse a Doctrine entity's constants/enums. It couples your read layer to the write schema and defeats the point. Copy primitives or share a standalone enum class.
  • Casting quirks. Casts are applied post-fetch; a datetime cast returns the library's datetime object, not \DateTimeImmutable. If you serialize to JSON, verify the shape — you may need an explicit transformer.
  • Alias/table collisions. If you set a short SQL alias on a read model, ensure every join uses the same alias; the library interpolates it into generated SQL. A mismatch produces "unknown column" errors that are annoying to trace.
  • Low adoption = low bus factor. There is essentially no community to search for errors (1 star, 0 dependents). Budget time to read vendor/somnambulist/read-models/src. If that's unacceptable for your team, skip the bundle and use plain DBAL — for a single app, the bundle mostly saves you ~50 lines of connection wiring.
  • Extension points to know. Tagged services and compiler passes are the clean way to auto-register read models if autowiring doesn't pick them up. Look for addCompilerPass in the bundle's *Bundle.php, and use #[AutoconfigureTag]/service tags on your own models if a registry needs to discover them (e.g. for a rebuild command).
  • Logging. Point a Monolog channel at your projector handlers, not the read models. Read models are stateless; failed writes to the read side are what you need visibility into.

Bottom line for assessment: this is a thin, small-bundle integration layer over a capable read-model library. It is worth using only if you are already committing to somnambulist/read-models; otherwise, the day-to-day cost of sparse docs and no community outweighs the saved wiring. If you do adopt it, read Configuration.php first, pin versions, and own your projectors and migrations explicitly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect