Bumpcore Panorama is a Laravel package for building Eloquent models from query sources. It lets a model behave like a normal read model while its rows come from a subquery, aggregate, CTE, or custom query builder source.
Use it when you need query-backed models for reports, projections, aggregate tables, read-only relation targets, or custom database features without giving up Eloquent relations and builder chaining.
| Bumpcore Panorama | Laravel | PHP |
|---|---|---|
| 0.x | ^12.0 | ^8.2 |
| 0.x | ^13.0 | ^8.3 |
Install the package with Composer:
composer require bumpcore/panorama
Add the HasSource trait to an Eloquent model and define newSource().
use Bumpcore\Panorama\HasSource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
class CustomerBalance extends Model
{
use HasSource;
protected $primaryKey = 'customer_id';
public function newSource(int $customer_id): Builder
{
return DB::table('invoices')
->selectRaw('customer_id, sum(amount) as balance')
->where('customer_id', $customer_id)
->groupBy('customer_id');
}
}
$balances = CustomerBalance::query()
->where('balance', '>', 0)
->withParams(customer_id: $customer->getKey())
->get();
The model is still queried through Eloquent. Panorama only replaces the model’s
from clause with the source query when the builder is executed.
HasSource registers a global scope and a local withParams() scope on the
model. The global scope wraps the model query in the source returned by
newSource().
newSource() may return an Eloquent builder or a base query builder.
use Bumpcore\Panorama\HasSource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class OpenInvoiceTotal extends Model
{
use HasSource;
public function newSource(): Builder
{
return Invoice::query()
->selectRaw('customer_id, sum(amount) as open_total')
->where('status', 'open')
->groupBy('customer_id');
}
}
The source is not expected to hit a real table with the model name. It becomes the derived table Panorama queries from.
Use withParams() to pass named arguments to newSource().
$balance = CustomerBalance::query()
->withParams(customer_id: 1, status: 'open')
->first();
Array params are also supported:
$balance = CustomerBalance::query()
->withParams([
'customer_id' => 1,
'status' => 'open',
])
->first();
Params must use string keys because they are passed as named arguments. Later calls replace earlier values, just like normal builder state.
By default, Panorama uses the model table name as the SQL alias for the wrapped
source. Override querySourceAlias() when the source should use a different
alias.
class CustomerBalance extends Model
{
use HasSource;
public function querySourceAlias(): string
{
return 'customer_balances';
}
}
The alias must not be an empty string.
Panorama keeps the consumer-facing Eloquent builder intact. You can still use:
whereHas() against query-backed relation models;The test suite includes compatibility coverage for:
staudenmeir/laravel-ctetpetry/laravel-postgresql-enhancedAll package-specific failures extend:
Bumpcore\Panorama\Exceptions\PanoramaException
More specific exceptions are available:
InvalidQuerySourceExceptionMissingQuerySourceExceptionInvalidQuerySourceException is thrown when a model does not use the required
trait, when newSource() returns an unsupported value, or when the source alias
is empty.
MissingQuerySourceException is thrown when a model uses HasSource without
overriding newSource().
Install development dependencies:
composer install
Run the test suite:
composer test
Run static analysis:
composer analyse
Check code style:
composer cs:check
Run the 100% coverage gate:
composer test:coverage
Coverage requires PCOV, Xdebug, or phpdbg.
Contributions are welcome. If you find a bug or have a suggestion for improvement, please open an issue or create a pull request.
Please include tests for behavioral changes and run the quality checks before submitting a pull request:
composer cs:check
composer analyse
composer test
See CHANGELOG.md for version history.
The MIT License (MIT). Please see License File for more information.