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 Laravel Package

somnambulist/read-models

Read models for Laravel: lightweight, query-optimized projections for CQRS applications. Build dedicated read-side models from domain events, separate reads from writes, and keep query performance predictable without loading full aggregates.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer:

composer require somnambulist/read-models

The package is standalone — it depends only on illuminate/database and does not require the Laravel framework, so it works in Laravel, Lumen, Symfony, or bare PHP apps. Before you can query anything you must register a connection resolver on the base model. In a Laravel app, do this once in a service provider's boot() (or a dedicated ReadModelsServiceProvider):

use Somnambulist\Components\ReadModels\Model;

public function boot(): void
{
    Model::setConnectionResolver(app('db'));
}

For a standalone script, wire up an Illuminate Capsule:

$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection([...]);      // same config array as Laravel
$capsule->setAsGlobal();

Model::setConnectionResolver($capsule->getDatabaseManager());

Then define a read model. It's a thin class describing the table and how to cast columns:

namespace App\ReadModels;

use Somnambulist\Components\ReadModels\Model;

class ActiveUser extends Model
{
    protected $table  = 'users';
    protected $casts  = [
        'id'         => 'integer',
        'email'      => 'string',
        'created_at' => 'datetime',
        'is_admin'   => 'boolean',
    ];
}

First use case: fetch a list for a report or API endpoint:

$users = ActiveUser::query()
    ->select(['id', 'email', 'created_at'])
    ->where('active', true)
    ->orderBy('created_at', 'desc')
    ->fetch();                // Collection of hydrated models

$user = ActiveUser::find(42); // single model or null

Look first at the package's base Model class and its casts/custom-caster support — that's the main surface area you'll touch day to day.

Implementation Patterns

Read models as dedicated projections. Give each screen or API response its own read model pointed at the same table but with a different select(). A DashboardUserSummary and an ExportableUser both extend Model, both target users, but expose different column sets. This keeps queries colocated with the shape they return and prevents accidental "just one more column" growth on shared models.

Use a separate read connection. Because the resolver is global but the connection is per-class, you can point read models at a replica:

class ActiveUser extends Model
{
    protected $connection = 'read_replica';
    // ...
}

In Laravel, register a read_replica connection in config/database.php — no Eloquent setup required.

Compose with Laravel's container. In controllers or jobs, inject read models via method injection or resolve them, and treat fetch() collections as plain iterables. Casters (integer, boolean, datetime, string) mean you don't need to re-cast attributes in your response layer.

Custom casters for domain values. Register a caster once for value objects your app already uses:

Model::registerCaster('money', \App\Casts\MoneyCaster::class);

class Invoice extends Model
{
    protected $casts = ['total' => 'money'];
}

(Verify the exact registration API against the source — the package ships a Casters namespace you should inspect first.)

Pagination and streaming. Use ->limit()/->offset() for simple pagination and the underlying query builder for chunked iteration when exporting large sets. Because the package wraps illuminate/database, you get the familiar when(), whereIn(), whereNull(), orderBy() methods for free.

Coexistence with Eloquent. Don't mix a read model and an Eloquent model for the same table in the same code path. Use read models strictly for the read side (query endpoints, dashboards, list views) and leave writes to Eloquent / your command handlers. This keeps responsibilities clear and sidesteps the "why isn't save() working?" confusion.

Eager loading where supported. The package provides basic relationship support (belongsTo, hasOne, hasMany) and with() for eager loading, but this is intentionally minimal — verify each relation method against the source before relying on it, and prefer explicit joins or a single well-shaped query over deep relation graphs.

Gotchas and Tips

It is strictly read-only. There is no save(), update(), delete(), no model events, no observers, no soft deletes, and no global scopes. If you need any of those, you're using the wrong tool — keep that table's Eloquent model around for writes.

The connection resolver is a global prerequisite. Model::setConnectionResolver() must run before the first query, or you'll get a ConnectionResolverInterface / null connection error that looks like a bootstrapping problem rather than a config one. Put the call in a provider, not in a model, so tests and console commands get it too.

No migrations, no schema coupling. The package does not know your schema. Renaming a column silently breaks a read model; there's no cast-time validation. Add integration tests that actually execute the query against a real (SQLite in-memory is fine) schema.

Casting is your type layer. Attributes come back as strings from the database; the casts array is how you get booleans and datetimes. A missing cast means '1' instead of true and a raw string instead of a Carbon instance. Set casts for every non-string column you expose.

Method name differences from Eloquent. Terminal methods are fetch() (collection), first(), find(), all() — not the full Eloquent query-builder API. ->get() on a read-model builder is not the same thing and will either fail or behave unexpectedly; check the Builder class for the supported terminal set before assuming Eloquent parity.

Debugging queries. Use the underlying builder: ->toSql() and ->getBindings() on the read-model builder give you the SQL to paste into a client. If a query returns nothing when you expect rows, check the connection name first, then the select() column list — a missing column in select() is easy to overlook and won't throw.

Casts are not relations. A cast must map to a scalar or a registered caster class; if you point casts at a relation or Eloquent accessor syntax, you'll get null or a fatal error. Keep casts primitive and let relations handle related data.

Extension points are limited but clean. You can subclass the base Model, subclass the Builder, register casters, and override $connection/$table/$primaryKey. There are no lifecycle hooks, so if you need derived attributes, compute them in a DTO layer after fetch(), or override newFromBuilder() on your model.

Small project, thin ecosystem. This is a 12-star niche package with a single maintainer and a 2024-03 release. That's fine for its narrow purpose, but pin the version in composer.json, read the source of the Model/Builder/Casters classes you depend on, and don't rely on community answers. The tests and README in the repo are your best documentation.

Testing. Point Model::setConnectionResolver() at a test database manager in setUp() and tear it down after. Because read models are just query objects, you can assert on the returned models' casted attributes directly without a database reset between unrelated tests.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin