splitstack / laravel-rome
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-drift: ^3.0
This package is auto-updated.
Last update: 2026-06-03 03:58:59 UTC
README
Make Database Views first-class citizens in your Laravel app.
A Laravel package for managing database views: scaffolding, regeneration, and materialized view refresh. Supports PostgreSQL and MySQL, with optional multi-tenant iteration.
Requirements
- PHP 8.2+
- Laravel 11+
- Database: PostgreSQL 9.3+ or MySQL 5.7+
Installation
composer require splitstack/laravel-rome
Publish the config:
php artisan vendor:publish --tag=rome-config
Configuration
// config/rome.php return [ // Path where your .sql view files live 'db_views_path' => database_path('views'), // Connections used for view operations. Must be configured. // Views are run against each connection in order. 'db_connections' => ['pgsql'], // e.g. ['pgsql'] or ['analytics', 'reporting'] // --- Multi-tenancy (optional) --- 'tenant_model' => null, // e.g. App\Models\Tenant::class 'tenant_status_column' => 'status', 'tenant_active_status' => 'active', ];
Scaffolding a view
php artisan make:dbview order_summary
This creates three files:
| File | Purpose |
|---|---|
database/views/order_summary.sql |
SQL definition — edit this |
database/migrations/{timestamp}_create_order_summary_view.php |
Runs the SQL on migrate |
app/Models/Views/OrderSummaryView.php |
Eloquent model backed by the view |
Regenerating views
Re-runs all .sql files in db_views_path against each configured connection, handling drop-and-recreate and view dependencies.
If some views depend on others existing first, declare them in priority_views in the config — they are created in the listed order before all remaining views (which are sorted alphabetically):
'priority_views' => ['base_metrics', 'aggregated_totals'],
# all views, all configured connections php artisan dbview:regen # single view php artisan dbview:regen order_summary # skip materialized views php artisan dbview:regen --no-materialized # preview which views would run without executing any SQL php artisan dbview:regen --dry-run
Multi-tenant mode
When tenant_model is configured, --multi-tenant iterates over all active tenants using eachCurrent (compatible with spatie/laravel-multitenancy):
# all active tenants php artisan dbview:regen --multi-tenant # specific tenants php artisan dbview:regen --tenants=abc123,def456
ReadOnlyModel
A base Eloquent model for views. Reads behave exactly like any Eloquent model. Direct writes (save, delete) always throw. Write operations that need to mutate data are proxied through a separate writable model that maps to the underlying table.
Setup
use Splitstack\Rome\Models\ReadOnlyModel; class OrderSummaryView extends ReadOnlyModel { protected $table = 'order_summary_view'; // Declare the writable model that owns the underlying data protected static $proxiedModelClass = Order::class; }
Primary key: ReadOnlyModel declares a non-incrementing primary key named id but makes no assumption about key type. Set $keyType, $incrementing, and any $casts on your model to match your actual key type. The proxied model must use the same primary key name and type, since all proxy lookups use $this->getKey() to locate the record in the proxied table.
Proxied writes
update(array $attributes)
Finds the matching record in the proxied model by primary key, calls update() on it, then re-fetches and returns the view record so computed columns reflect the change.
$summary = OrderSummaryView::find($id); $summary->update(['status' => 'shipped']); // returns OrderSummaryView
Throws if no matching record exists in the proxied table.
Accessing the underlying model
underlying(bool $forceFetch = false)
Returns a proxied model instance populated with the current record's data.
Without forceFetch (default): hydrates the instance in-memory from the view's attributes, intersected with the proxied model's $fillable. No database query is made, but attributes not listed in $fillable will be absent.
$order = $summary->underlying(); // no query; only $fillable attributes populated $order->status; // available if 'status' is in Order::$fillable
With forceFetch: true: queries the proxied model's table directly. All attributes are present, at the cost of an extra query.
$order = $summary->underlying(forceFetch: true); // hits the database
Use the default when you only need fillable attributes (e.g. to re-dispatch events or call methods). Use forceFetch when you need the complete record or attributes that aren't in $fillable.
Refreshing materialized views (PostgreSQL only)
Via the job
use Splitstack\Rome\Jobs\RefreshMaterializedView; // Basic dispatch RefreshMaterializedView::dispatch(viewName: 'order_summary_view'); // Concurrent refresh (requires a unique index on the view) RefreshMaterializedView::dispatch( viewName: 'order_summary_view', concurrent: true, ); // Explicit connection and tenant context RefreshMaterializedView::dispatch( viewName: 'order_summary_view', concurrent: true, tenantId: $tenant->id, // scopes the dedup lock; does not perform tenant switching connection: 'analytics', // overrides rome.db_connections ); // Custom failure callbacks (closures are serialized automatically) RefreshMaterializedView::dispatch( viewName: 'order_summary_view', onFailure: [ fn (\Throwable $e, $job) => \Sentry\captureException($e), fn (\Throwable $e, $job) => Notification::send($admin, new ViewRefreshFailed($job->viewName, $e)), ], );
The job includes a distributed lock so concurrent dispatches for the same view/tenant are deduplicated rather than stacked.
Job defaults: 3 tries, 5-minute timeout, 60-second backoff.
Directly
use Splitstack\Rome\Database\MaterializedViewRefresher; (new MaterializedViewRefresher('analytics'))->refresh('order_summary_view', concurrent: true);
RefreshableMaterializedView trait
Add to any model backed by a materialized view for convenience dispatch methods:
use Splitstack\Rome\Concerns\RefreshableMaterializedView; class OrderSummaryView extends ReadOnlyModel { use RefreshableMaterializedView; } // Queue a refresh OrderSummaryView::queueRefresh(concurrent: true, queue: 'low'); // Queue a refresh with tenant context (tenant switching is the caller's responsibility) OrderSummaryView::queueRefresh(tenantId: $tenant->id, connection: 'analytics'); // Queue a delayed refresh OrderSummaryView::queueRefreshIn(seconds: 30, concurrent: true, queue: 'low'); // Dispatch synchronously (blocks until complete, goes through the job's lock + logging) OrderSummaryView::refreshNow(concurrent: true);
ViewDialect
Driver-aware SQL builder. Used internally but available if you need to generate view DDL yourself:
use Splitstack\Rome\Database\ViewDialect; $dialect = ViewDialect::fromConnection('analytics'); $dialect->driver(); // 'pgsql' | 'mysql' $dialect->supportsMaterializedViews(); // true on pgsql, false on mysql $dialect->dropView('order_summary_view'); // driver-appropriate DROP VIEW $dialect->dropMaterializedView('...'); // pgsql only, throws on mysql $dialect->refreshMaterializedView('...', true); // REFRESH ... CONCURRENTLY $dialect->uniqueIndexSql(); // pg_indexes / information_schema query
Database support
| Feature | PostgreSQL | MySQL |
|---|---|---|
| Regular views | ✓ | ✓ |
| Materialized views | ✓ | — (skipped with warning) |
DROP VIEW … CASCADE |
✓ | ✓ (omitted) |
| Unique index check | ✓ | ✓ |
License
MIT