th3mouk/materialized-view-bundle
Symfony bundle for defining and managing PostgreSQL materialized views with Doctrine. Map views as entities, create and refresh them via console commands, and query cached results for faster dashboards, reports, and analytics.
This package adds a Doctrine-migrations "lane" for deploying materialized views in Laravel. Day-to-day you interact with it through the matview:doctrine-lane artisan command rather than by calling Doctrine directly.
Minimal steps:
composer require the package alongside doctrine/migrations (the lane builds on Doctrine's DependencyFactory, so Doctrine must already be configured for your project).php artisan matview:doctrine-lane instead of (or ahead of) a bare doctrine:migrations:migrate for anything that owns materialized views.doctrine_migration_versions already exists, then again — the second run should be a fast no-op.First real use case: a multi-server deploy where refresh work must not run twice. The lane takes an advisory lock and runs the migration set once per cluster, which is exactly the case where hand-rolled artisan migrate calls in a release script start double-executing.
Deploy pipeline integration. Put the lane immediately after php artisan migrate --force and before traffic switch / cache warm. Because the lane serialises on an advisory lock, it is safe to invoke from every pod in a rolling deploy — the losers wait and then observe a current state.
Lock as the coordination primitive. Treat the lane as the only writer of matview-owned objects. Any second code path that refreshes the same views (a queued job, a cron entry, a db:seed) defeats the lock. Route all of them through the command, or schedule refreshes with Schedule::command('matview:doctrine-lane')->withoutOverlapping() and let the lane own the timing.
Use the same command in local and CI environments. The v1.3.1 bug was only visible when the metadata table existed but had drifted — a state you get by upgrading doctrine/migrations, restoring a dump, or copying a database between environments. Running the lane (not raw Doctrine) in CI against a scratch DB is what surfaces those states early.
Strategy selection per environment. Set the strategy explicitly in config per environment rather than relying on a default, and keep it identical across every replica of the same environment. Mixed strategies on the same table is the classic way to reintroduce concurrent-refresh errors that the lock was meant to prevent.
Ordering against Laravel migrations. Keep the lane downstream of Laravel's own schema migrations if any matview depends on tables those migrations create; the reverse ordering only works when the matview migrations are self-contained.
The v1.3.1 fix matters if you have ever seen the boot abort. Before this release, the lane called hasPendingMigrations() before anything called ensureInitialized(), so a doctrine_migration_versions table whose schema had drifted from what doctrine/migrations expects made matview:doctrine-lane abort with The metadata storage is not up to date… before a single migration ran — while a plain doctrine:migrations:migrate self-healed fine. The lane now initialises the metadata storage on the guard's DependencyFactory before the first status read, under the advisory lock, for every strategy. The call is idempotent and a no-op once current. Upgrade is a drop-in patch release with no config or API changes.
The self-heal only covers the lane's own path. Anything else that queries migration status at boot — a health check, a custom artisan bootstrapper, a dashboard that instantiates DependencyFactory itself — still hits the raw Doctrine behaviour. If you have such a check, either run it after the lane or call ensureInitialized() yourself.
Don't "fix" a drifted doctrine_migration_versions by hand. Dropping or truncating the table loses the applied-migration record and makes the lane try to re-run everything. Let the lane initialise it, or run one plain doctrine:migrations:migrate and re-run the lane.
Advisory locks have a wait budget. Long view refreshes plus a short lock timeout means later pods time out rather than queue. Size the timeout against your slowest refresh, and make sure the DB user has the privileges to create the metadata table on first run — the self-heal path needs DDL, not just DML.
Debugging a stall is usually lock contention, not the lane. Run with -v and check pg_locks / performance_schema.metadata_locks for a session holding the advisory lock from a previous deploy. An aborted pod with a dropped connection is the usual culprit.
Extension point. Because the lane works through a Doctrine DependencyFactory, the natural customisation seam is binding your own factory or configuration rather than wrapping the command in shell scripts. Keep that override identical across environments or you lose the consistency the lane provides.
How can I help you explore Laravel packages today?