sairiz / filament-adaptive-query
Automatic runtime listing projection for Filament tables, including visible columns, relationships and simple callback dependencies.
Requires
- php: ^8.3
- filament/filament: ^5.11.1
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- nikic/php-parser: ^5.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 18:16:13 UTC
README
Automatically select the fields a Filament listing needs, including visible columns, relationship linking keys, native aggregates, and simple callback dependencies. The plugin follows applied column toggles and preserves Filament's filters, sorting, scopes, pagination, actions, and exports.
Requirements
- PHP 8.3 or later
- Laravel 13
- Filament 5.11.1 or later within 5.x
- An existing Filament panel
Install
For a local installation, place the package at packages/filament-adaptive-query inside your Laravel application. Add a path repository to the application's composer.json:
{
"repositories": [
{
"type": "path",
"url": "packages/filament-adaptive-query",
"options": { "symlink": true }
}
]
}
From the application directory:
composer require sairiz/filament-adaptive-query:dev-main
Laravel automatically discovers the service provider.
Register once per panel
Add the plugin to your existing panel provider:
use Filament\Panel; use Sairiz\FilamentAdaptiveQuery\AdaptiveQueryPlugin; public function panel(Panel $panel): Panel { return $panel // Your existing panel configuration. ->plugin(AdaptiveQueryPlugin::make()); }
Registration enables automatic selection. Pages remain ordinary Filament pages; no special base class, page trait, field registry, or join declaration is required.
To disable it for a panel:
->plugin(AdaptiveQueryPlugin::make()->enabled(false))
To configure the application default, optionally publish the configuration:
php artisan vendor:publish --tag=filament-adaptive-query-config
// config/filament-adaptive-query.php return ['enabled' => true, 'batch_loading' => false];
An explicit ->enabled(bool) overrides this default for that panel without modifying shared configuration. Panels without the plugin retain native behavior. AdaptiveQueryPlugin::get() retrieves the current panel's registered plugin.
Limit a trial to one table
Disable both features by default on the panel:
->plugin(AdaptiveQueryPlugin::make()->projection(false)->batchLoading(false))
Then opt the trial resource into batching in its existing table() method:
use Filament\Tables\Table; use Sairiz\FilamentAdaptiveQuery\AdaptiveQueryPlugin; public static function table(Table $table): Table { AdaptiveQueryPlugin::configureTable($table, batchLoading: true); return $table ->columns([ // Your existing columns. ]); }
Other tables retain native fetching. To opt an individual table out on a panel whose features are enabled:
AdaptiveQueryPlugin::configureTable($table, projection: false, batchLoading: false);
The settings belong to that table instance and override the panel's feature defaults. Omitted options leave existing table settings unchanged; otherwise they inherit the panel defaults. A panel's enabled(false) remains authoritative. Enabling projection still applies every dependency and safety check; it cannot force partial models through a policy or unresolved callback.
Changing a table option invalidates its native record cache. These options require the plugin's AdaptiveTable binding; an incompatible application-owned Table implementation raises a clear configuration error.
What is selected
For supported tables the plugin retains:
- The model's primary key and database route key.
- The configured record title attribute.
- Visible root database fields, including the root of JSON paths such as
metadata.reference. - Root linking fields needed by visible relationships and configured eager loads.
- Fields read by supported presentation callbacks.
Filament appends its native counts(), exists(), sum(), avg(), min(), and max() subqueries after the root selection. Native relationship queries retain their scopes.
Automatic inference currently supports native TextColumn components. A table containing an unsupported visible column keeps its original query.
Toggleable columns
Configure columns normally:
use Filament\Tables\Columns\TextColumn; TextColumn::make('customer.name')->toggleable(), TextColumn::make('items_count')->counts('items')->toggleable(), TextColumn::make('metadata.reference') ->toggleable(isToggledHiddenByDefault: true),
After column-manager changes are applied, the next listing fetch recomputes its requirements. Showing Reference selects metadata. Hiding Customer or Items removes their display eager load or aggregate when no other feature needs it. An active search or sort can still require a hidden field in SQL.
Callback dependencies
Literal root-field reads in simple callbacks are selected automatically:
TextColumn::make('name') ->description(fn ($record) => $record->internal_notes);
The plugin adds internal_notes to the original SELECT. It does not issue a separate query for each row or require a hidden-field declaration.
Supported expressions include direct and null-safe root-property reads, literal values, comparisons, concatenation, arithmetic, casts, JSON array access, and ternary/coalescing expressions. Inputs from both conditional branches are included. A normal closure with one return statement is supported too.
The plugin reads callback source with PHP-Parser without executing it for discovery. Function/method calls, dynamic property names, extra statements, unreadable source, and ambiguous same-line closures keep the original query. Reading a relationship inside an otherwise hidden callback also retains the original query; it does not silently introduce lazy loading.
Actions, lookups, and exports
Projection applies only while the rendered table fetches listing records. Native record lookup, selected-record queries, and exports retain complete models and their scope constraints. Execution-only action callbacks can therefore read fields omitted from the listing.
Simple action visibility/label callbacks participate in field inference. Unresolved presentation callbacks, model policies, and retrieved observers retain complete records. Native action and group visibility behavior remains in Filament.
Batched loading
Enable batched loading for a panel:
->plugin(AdaptiveQueryPlugin::make()->batchLoading())
Or set batch_loading to true in the published configuration. ->batchLoading(false) overrides that default for a panel.
For eligible listings, the plugin restricts enrichment to the page's IDs and loads complete models for the whole page, retaining native aggregates and relationship eager loading. PostgreSQL embeds ID selection in the enrichment query; SQLite fetches IDs separately and restores their order in memory. Count, ID selection and enrichment run in one consistent database snapshot; model observers only see complete models. There is no per-row missing-field query. Filament's record cache still handles toggles, filters, sorting and pagination.
This mode currently supports PostgreSQL and SQLite, numbered pagination with 1–100 rows per page, and sorting by real root-table columns. Custom SELECTs (including scope-added selects), opaque/raw predicates or filters on SELECT aliases, locking queries, joins, grouped queries, relationship tables, simple/cursor pagination, “all” rows, layouts, summarizers, reordering, and overridden record-fetch/pagination hooks use the existing listing path. Sorting by aggregate outputs or related fields also uses that path. Unsupported batching may still use automatic projection when eligible.
Batching retains full root attributes, including hidden wide fields. SQLite adds one ID query; PostgreSQL combines ID selection and enrichment in one statement, while retaining the snapshot transaction. It can avoid expensive page-wide aggregate work during offset selection, but is not necessarily faster on every table. It remains opt-in. PostgreSQL correctness is covered by real-database tests. With the fixture correctly indexed, published PostgreSQL 18 measurements do not establish a request-latency benefit; see measured results. Larger indexed fixtures do show benefits for some wide/deep listings; see the staged PostgreSQL stress benchmark. MySQL and other engines retain the existing listing path.
PostgreSQL
PostgreSQL 17 and 18 are covered by real-database integration tests and the CI matrix.
Use the same ->batchLoading() registration with your Laravel pgsql connection; no table-specific setup is required. The plugin opens a REPEATABLE READ transaction so count, IDs and enriched records share a snapshot even during concurrent writes.
Inside an existing transaction, batching runs only when that transaction already uses REPEATABLE READ or SERIALIZABLE. Otherwise the listing uses its ordinary query path. The plugin does not change the caller's isolation level or session defaults. Related models on another database connection are outside the root transaction's snapshot.
When the original query is retained
The plugin conservatively avoids projection for:
- Unresolved callbacks, selected accessors/custom cast classes, appended attributes, custom model attribute-reading behavior, retrieved observers, or registered model policies.
- Custom columns/views/content or resource authorization/title overrides.
- Custom SELECTs, existing joins, aliased/subquery roots, DISTINCT, unions, grouping/HAVING, and root aggregate queries.
- Relationship tables, cursor pagination, column layouts, grouping, summarizers, or reordering.
- Unavailable schema metadata or a dependency that cannot be matched to supported model/schema metadata.
It does not create raw joins, rewrite predicates, remove security scopes, change search semantics, switch pagination modes, or load missing fields on demand.
Automatic analysis is not complete tracing of arbitrary PHP. Review custom helpers, rendering hooks, and application integrations before broad rollout. Enable strict missing-attribute and lazy-loading checks in development/tests, while recognizing that they do not intercept every attribute-read path.
Query diagnostics
After the listing has rendered, inspect its actual path:
use Sairiz\FilamentAdaptiveQuery\AdaptiveTable; if ($table instanceof AdaptiveTable) { $diagnostics = $table->getQueryDiagnostics(); // strategy: batched, projected, native, cached, not_fetched, or failed // reason: explanation of the selected path // batching_reason: why batching ran, was disabled, was skipped, or failed }
For example, sorting on items_count reports a batching skip because it is not a root database column. The strategy can then be projected or native, depending on projection eligibility. An existing PostgreSQL read-committed transaction reports its isolation as the skip reason.
Diagnostics describe the most recent adapter fetch and do not issue queries. Repeated cached reads retain a known fetch's diagnostics. Records fetched outside the adapter are reported as cached rather than guessing their strategy. Read strategy programmatically; reason messages are human-readable explanations.
The demo displays these values. Its benchmark records the path and batching reason after each scenario request so fallback cannot be mistaken for a batched measurement.
Integration details
The plugin registers a container-resolved AdaptiveTable. It preserves an existing application-owned binding for Filament\Tables\Table; automatic selection is unavailable through that binding unless it integrates AdaptiveTable. Directly constructed tables and component fetches that bypass the rendered adapter can retain full records.
Schema metadata and parsed callback source are cached within the request scope. To clear schema metadata after a schema/search-path change inside a long-running job:
use Sairiz\FilamentAdaptiveQuery\AdaptiveTableQuery; app(AdaptiveTableQuery::class)->flushSchemaCache();
Keep worker/opcode caches consistent with deployed source. Callback analysis assumes the source files match the code executing in that worker.
Demo, tests, and performance
The Runtime Query Lab is a runnable SQLite app with authenticated tenants, real models, fake orders, and baseline, automatic-projection, and batched panels.
From the package root:
composer install
composer test
composer lint
composer analyse
Validation and measured results distinguish tested correctness from timing evidence. Architecture describes the runtime path. The benchmark compares all three strategies through real Filament rendering. The plugin reduces selected fields for eligible listings; it does not guarantee lower request latency for every table.