bumpcore/panorama

Query-backed Eloquent models for Laravel.

Maintainers

Package info

github.com/bumpcore/panorama

pkg:composer/bumpcore/panorama

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-06-01 13:48 UTC

This package is auto-updated.

Last update: 2026-06-01 13:52:25 UTC


README

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.

Table Of Contents

Version Table

Bumpcore Panorama Laravel PHP
0.x ^12.0 ^8.2
0.x ^13.0 ^8.3

Installation

Install the package with Composer:

composer require bumpcore/panorama

Quick Start

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

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().

Defining a Source

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.

Passing Source Params

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.

Changing the Source Alias

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.

Eloquent Compatibility

Panorama keeps the consumer-facing Eloquent builder intact. You can still use:

  • normal Eloquent builder methods;
  • local scopes;
  • relations and eager loading;
  • whereHas() against query-backed relation models;
  • custom Eloquent builders;
  • external query builder extensions.

The test suite includes compatibility coverage for:

  • staudenmeir/laravel-cte
  • tpetry/laravel-postgresql-enhanced
  • consumer-defined custom Eloquent builders

Exceptions

All package-specific failures extend:

Bumpcore\Panorama\Exceptions\PanoramaException

More specific exceptions are available:

  • InvalidQuerySourceException
  • MissingQuerySourceException

InvalidQuerySourceException 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().

Testing

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.

Contribution

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

Changelog

See CHANGELOG.md for version history.

Credits

License

The MIT License (MIT). Please see License File for more information.