Beartropy Tables

Base Query
Override the base Eloquent query per table to scope data by tenant, permissions, or any custom condition — without global scopes.

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(): array
15 {
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

If you don't override 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 BeartropyTable
4{
5 public $model = User::class;
6 
7 // No query() override — all records returned
8}

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\Builder
11{
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()

You don't need to call ->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(): array
15 {
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\Builder
11 {
12 return User::query()->where('is_active', true);
13 }
14 
15 public function columns(): array
16 {
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(): array
25 {
26 return [
27 FilterString::make('Name', 'name'),
28 // Only shows departments from active users
29 FilterSelectMagic::make('Department'),
30 ];
31 }
32 
33 public function settings(): void
34 {
35 $this->setPerPageDefault(25);
36 }
37}

What gets scoped

The 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.
Beartropy Logo

© 2026 Beartropy. All rights reserved.

Provided as-is, without warranty. Use at your own risk.