Beartropy Tables
The Problem
Why you need a customizable base query.
Global scopes are too broad
In model-based tables, the base query is always $this->model::query(). If you need to restrict data — for example, showing only users in the current organization — the only workaround is adding a global scope to the model itself. But global scopes affect the model everywhere in your application, not just in the table.
The query() method lets you scope the base query per table, without touching the model.
Basic Usage
Override the query() method to define custom base constraints.
Scoping by Organization
Override query() and return an Illuminate\Database\Eloquent\Builder instance with your constraints. All table operations — pagination, search, filters, sorting, bulk selection, data export — will respect this scope automatically.
1use App\Models\User; 2use Beartropy\Tables\BeartropyTable; 3 4class OrganizationUsersTable extends BeartropyTable 5{ 6 public $model = User::class; 7 8 public function query(): \Illuminate\Database\Eloquent\Builder 9 {10 return User::query()11 ->where('organization_id', auth()->user()->organization_id);12 }13 14 public function columns(): array15 {16 return [17 Column::make('Name', 'name')->searchable(),18 Column::make('Email', 'email')->searchable(),19 ];20 }21 22 public function settings(): void {}23}
Backward compatible
query(), it defaults to $this->model::query() — exactly the same behavior as before. Existing tables require no changes.
Default Behavior
Without overriding query(), everything works as before.
No Override Needed
When no query() method is defined, the table uses $this->model::query() as the base. This is identical to the previous behavior — no migration or changes required for existing tables.
1// Default behavior (no override needed)2// Equivalent to: User::query()3class UsersTable extends BeartropyTable4{5 public $model = User::class;6 7 // No query() override — all records returned8}
How It Works
Two methods: query() for you, newQuery() for the table internals.
query() vs newQuery()
query() — The method you override. Define your base constraints here (tenant filters, permission scopes, etc.). Do not add ->with() here — that's handled automatically.
newQuery() — Internal method. Wraps query() and applies $this->with eager loading. All internal data-fetching code calls newQuery(), so you never need to worry about re-applying eager loading in your override.
1// You override this — define your base constraints 2public function query(): \Illuminate\Database\Eloquent\Builder 3{ 4 return User::query() 5 ->where('organization_id', $this->organizationId); 6} 7 8// The table calls this internally — applies $with automatically 9// You never call or override newQuery()10protected function newQuery(): \Illuminate\Database\Eloquent\Builder11{12 $query = $this->query();13 14 if (!empty($this->with)) {15 $query->with($this->with);16 }17 18 return $query;19}
Don't add eager loading in query()
->with([...]) inside query(). Define public array $with = ['relation'] as usual and the table applies it automatically via newQuery().
With Eager Loading
$with works seamlessly alongside custom query().
Scoped + Eager Loaded
Define $with as you normally would. The table's internal newQuery() method applies ->with($this->with) on top of whatever query() returns. Relationships, dot-notation columns, and N+1 prevention all work the same.
1class TenantUsersTable extends BeartropyTable 2{ 3 public $model = User::class; 4 5 // $with is applied automatically on top of query() 6 public array $with = ['profile', 'role']; 7 8 public function query(): \Illuminate\Database\Eloquent\Builder 9 {10 return User::query()11 ->where('tenant_id', auth()->user()->tenant_id);12 }13 14 public function columns(): array15 {16 return [17 Column::make('Name', 'name')->searchable(),18 Column::make('Role', 'role.name'),19 Column::make('Bio', 'profile.bio'),20 ];21 }22 23 public function settings(): void {}24}
With Filters
Filters and FilterSelectMagic respect the scoped query.
Scoped + Filters
All filter types work within the scoped query. FilterSelectMagic is particularly useful here — it auto-generates dropdown options from the scoped dataset, not the full table. In this example, only departments belonging to active users appear in the dropdown.
1use Beartropy\Tables\BeartropyTable; 2use Beartropy\Tables\Classes\Columns\Column; 3use Beartropy\Tables\Classes\Filters\FilterString; 4use Beartropy\Tables\Classes\Filters\FilterSelectMagic; 5 6class ActiveUsersTable extends BeartropyTable 7{ 8 public $model = User::class; 9 10 public function query(): \Illuminate\Database\Eloquent\Builder11 {12 return User::query()->where('is_active', true);13 }14 15 public function columns(): array16 {17 return [18 Column::make('Name', 'name')->searchable()->sortable(),19 Column::make('Email', 'email')->searchable()->sortable(),20 Column::make('Department', 'department'),21 ];22 }23 24 public function filters(): array25 {26 return [27 FilterString::make('Name', 'name'),28 // Only shows departments from active users29 FilterSelectMagic::make('Department'),30 ];31 }32 33 public function settings(): void34 {35 $this->setPerPageDefault(25);36 }37}
What gets scoped
query() scope affects: pagination, global search, filter results, FilterSelectMagic dropdown options, getAllData(), getSelectedData(), getRowByID(), and data export. The only exception is Editable::find() which intentionally bypasses the scope for write operations.