Fix: servers per-page setting resets after session expiry - #2502
Conversation
Filament only caches the table's records-per-page choice in the PHP session, so it silently reverts to the hardcoded default once the session expires. Store the selection in the user's customization column, mirroring the existing dashboard_layout preference, so it survives session expiry.
|
All contributors have signed the CLA ✍️ ✅ |
📝 WalkthroughWalkthroughChangesServer pagination preferences
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
app/Filament/App/Resources/Servers/Pages/ListServers.php (1)
122-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd automated coverage for the pagination contract.
Add Livewire tests for fresh grid/list defaults, stored values after session expiry, valid session precedence, invalid values for each layout, and persistence without overwriting existing customization keys.
Also applies to: 158-176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 122 - 128, Add Livewire coverage around the ListServers pagination configuration, covering fresh grid/list defaults, stored values after session expiry, valid session precedence, invalid stored values for both layouts, and persistence that preserves unrelated customization keys. Anchor the tests to the pagination behavior configured by the ListServers table and verify the expected page option and customization outcomes for each scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php`:
- Around line 169-173: Update the customization payload in the per-page
persistence flow around the user update to merge with the raw stored
customization data rather than user()->getCustomization(), which includes enum
defaults. Preserve existing stored values and update only
CustomizationKey::ServersPerPage, using an existing raw-data accessor or a
dedicated method if available.
- Around line 122-128: Update the pagination configuration in the table-building
method so Filament receives a layout-compatible session per-page value before
applying defaultPaginationPageOption(). Validate or normalize the stored value
against the current $pageOptions, ensuring incompatible grid values fall back to
the configured list default while preserving valid values for each layout.
- Around line 158-176: Update updatedTableRecordsPerPage() to validate the
selected value against the current pageOptions, accepting only an exact allowed
page-size value rather than any numeric input. Persist only
CustomizationKey::ServersPerPage, and reset the invalid selection/state so
subsequent loads use a valid default.
---
Nitpick comments:
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php`:
- Around line 122-128: Add Livewire coverage around the ListServers pagination
configuration, covering fresh grid/list defaults, stored values after session
expiry, valid session precedence, invalid stored values for both layouts, and
persistence that preserves unrelated customization keys. Anchor the tests to the
pagination behavior configured by the ListServers table and verify the expected
page option and customization outcomes for each scenario.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 35bc52d7-f736-440f-a6f1-a50ff9b646ff
📒 Files selected for processing (3)
app/Enums/CustomizationKey.phpapp/Filament/App/Resources/Servers/Pages/ListServers.phpapp/Models/User.php
| $pageOptions = $usingGrid ? [10, 20, 30, 40] : [10, 20, 50, 100]; | ||
| $storedPerPage = (int) user()?->getCustomization(CustomizationKey::ServersPerPage); | ||
| $defaultPerPage = in_array($storedPerPage, $pageOptions, true) ? $storedPerPage : ($usingGrid ? 10 : 20); | ||
|
|
||
| return $table | ||
| ->paginated($usingGrid ? [10, 20, 30, 40] : [10, 20, 50, 100]) | ||
| ->defaultPaginationPageOption($usingGrid ? 10 : 20) | ||
| ->paginated($pageOptions) | ||
| ->defaultPaginationPageOption($defaultPerPage) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate target file:"
fd -a 'ListServers.php' . || true
target="$(fd 'ListServers.php' . | head -n 1 || true)"
if [ -n "${target:-}" ]; then
echo "Target: $target"
wc -l "$target"
sed -n '90,150p' "$target"
fi
echo
echo "Search pagination defaults/session hooks:"
rg -n "defaultPaginationPageOption|updatedTableRecordsPerPage|RecordsPerPage|paginationPageOption|perPage" -S app/Filament || true
echo
echo "Check Filament package version hints:"
for f in composer.lock composer.json; do
[ -f "$f" ] && { echo "--- $f"; sed -n '/"filamentphp\/filament"\|version"/,/},/p' "$f" | head -80; }
doneRepository: pelican-dev/panel
Length of output: 5858
🏁 Script executed:
#!/bin/bash
set -euo pipefail
target="app/Filament/App/Resources/Servers/Pages/ListServers.php"
echo "--- $target outline"
ast-grep outline "$target" || true
echo
echo "--- $target relevant lines"
cat -n "$target" | sed -n '1,220p'
echo
echo "--- search for custom page option/session key in project"
rg -n "CustomizationKey::ServersPerPage|TablesRecordsPerPage|pageOptions|usingGrid|storageSession.*page|page_option|RecordsPerPage|paginationPageOption" -S app .github config || trueRepository: pelican-dev/panel
Length of output: 13605
🌐 Web query:
Filament v5.6 CanPaginateRecords resolve existing session value before defaultPaginationPageOption
💡 Result:
In Filament, the pagination records per page (the number of items shown) is resolved by the getDefaultTableRecordsPerPageSelectOption method within the CanPaginateRecords trait [1][2]. This logic prioritizes the value stored in the user's session before falling back to the configured default [2]. Specifically, the resolution order follows this hierarchy: 1. Session Value: The system first attempts to retrieve the stored per-page option from the user's session using a table-specific session key (e.g., tables.{table_class_name}_per_page) [1][2]. 2. Configured Default: If no session value exists, it falls back to the value defined by defaultPaginationPageOption() (if set) or the application-level default configuration [2]. The implementation typically looks like this [2]: public function getDefaultTableRecordsPerPageSelectOption: int | string { $option = session->get( $this->getTablePerPageSessionKey, $this->defaultTableRecordsPerPageSelectOption?? $this->getTable->getDefaultPaginationPageOption,); $pageOptions = $this->getTable->getPaginationPageOptions; // Verify the retrieved option is valid; if not, remove the invalid session entry if (in_array($option, $pageOptions)) { return $option; } session->remove($this->getTablePerPageSessionKey); return $pageOptions; } This ensures that once a user selects a preferred pagination size, it persists across requests via the session [2]. If the retrieved session value is no longer valid (e.g., the developer updated the available options), Filament will automatically remove the invalid session key and revert to the first available option [1][2]. Note that this mechanism governs the number of records displayed, which is distinct from the current page index (e.g., page 3 of 10), which is managed by Livewire via the URL query string [3].
Citations:
- 1: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 3: Pagination page not persisted when navigating back filamentphp/filament#9773
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- vendor filament CanPaginateRecords if present:"
if [ -d vendor ]; then
fd -a 'CanPaginateRecords.php' vendor | while read -r f; do
echo "--- $f"
sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/}/p' "$f"
done
else
echo "vendor directory not present"
fi
echo
echo "--- composer filament dependency lines:"
sed -n '/filament/',/}/p' composer.json composer.lock 2>/dev/null | rg -n "filamentphp/filament|version|packages/tables/src/Concerns/CanPaginateRecords.php" -C 2 || trueRepository: pelican-dev/panel
Length of output: 337
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- composer relevant dependency lines:"
python3 - <<'PY'
import json, pathlib
p=json.loads(pathlib.Path('composer.lock').read_text())
for pkg in p.get('packages', [])+p.get('packages-dev', []):
if pkg.get('name') == 'filamentphp/filament':
print(json.dumps({k: pkg.get(k) for k in ('name','version','source','require')}, indent=2, sort_keys=True))
PY
echo
echo "--- fetch Filament tables trait with GitHub raw (version-independent source inspection):"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/}/p'Repository: pelican-dev/panel
Length of output: 733
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "--- fetch rest of invalid session handling:"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getDefaultTableRecordsPerPageSelectOption/,/^ }/p'
echo
echo "--- fetch default session key implementation:"
curl -fsSL 'https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php' | sed -n '/function getTablePerPageSessionKey/,/^ }/p'Repository: pelican-dev/panel
Length of output: 945
Use the layout-specific default when the session value is incompatible.
Filament resolves the session-per-page value before defaultPaginationPageOption() and reuses the same session key for both table layouts. If a grid user selects 30 or 40, then switches to list view, the session value is outside [10, 20, 50, 100], so Filament removes it and falls back to 10 instead of the configured list default. Validate the session value against $pageOptions before letting Filament apply it, or use a layout-specific session key.
🧰 Tools
🪛 ast-grep (0.45.0)
[error] 125-128: Prevent SQL queries built from unsanitized input
Context: $table
->paginated($pageOptions)
->defaultPaginationPageOption($defaultPerPage)
->query(fn () => $baseQuery)
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').
(sql-injection-php)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 122 -
128, Update the pagination configuration in the table-building method so
Filament receives a layout-compatible session per-page value before applying
defaultPaginationPageOption(). Validate or normalize the stored value against
the current $pageOptions, ensuring incompatible grid values fall back to the
configured list default while preserving valid values for each layout.
| public function updatedTableRecordsPerPage(): void | ||
| { | ||
| parent::updatedTableRecordsPerPage(); | ||
|
|
||
| $perPage = $this->getTableRecordsPerPage(); | ||
|
|
||
| // Livewire delivers the <select> value as a numeric string, not an int. | ||
| if (!is_numeric($perPage)) { | ||
| return; | ||
| } | ||
|
|
||
| user()?->update([ | ||
| 'customization' => array_merge( | ||
| user()->getCustomization(), | ||
| [CustomizationKey::ServersPerPage->value => (int) $perPage], | ||
| ), | ||
| ]); | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repository files around target:"
git ls-files | rg '(^|/)ListServers\.php$|composer\.json$|vendor/filamentphp/tables/src/Concerns/CanPaginateRecords\.php|vendor/filamentphp/forms/src/Components/Select\.php' || true
target=$(git ls-files | rg '(^|/)app/Filament/App/Resources/Servers/Pages/ListServers\.php$' | head -n1 || true)
if [ -n "$target" ]; then
echo "Target: $target"
wc -l "$target"
sed -n '1,240p' "$target" | cat -n
fi
echo
echo "Composer Filament deps:"
if [ -f composer.json ]; then
jq '.require // {} | with_entries(select(.key|test("filament"))) ' composer.json || true
fiRepository: pelican-dev/panel
Length of output: 12214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
target=$(git ls-files | grep '(^|/)app/Filament/App/Resources/Servers/Pages/ListServers.php$' | head -n1 || true)
if [ -n "$target" ]; then
echo "Target exists: $target"
ast-grep outline "$target" --view compact 2>/dev/null || true
sed -n '130,190p' "$target" | cat -n
fi
echo
echo "Search for updatedTableRecordsPerPage usages/overloads:"
rg -n "updatedTableRecordsPerPage|getTableRecordsPerPage|pageOptions|paginate\\(" app composer.json -S || trueRepository: pelican-dev/panel
Length of output: 2693
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Behavioral probe for PHP numeric/string validation semantics referenced by the concern.
tmpdir=$(mktemp -d)
cd "$tmpdir"
php -r '
$vals = ["1000000","10.5","1e6",null,"abc"," 10 "];
foreach ($vals as $v) {
$result = is_numeric($v);
echo var_export($v,true) . " => is_numeric=" . var_export($result,true) . "\n";
}
' || trueRepository: pelican-dev/panel
Length of output: 312
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "composer.lock Filament version:"
python3 - <<'PY'
import json
from pathlib import Path
p=Path("composer.lock")
if p.exists():
data=json.loads(p.read_text())
for pkg in data.get("packages",[]):
if pkg.get("name") in {"filament/filament","filament/tables","filament/widgets"}:
print(pkg.get("name"), pkg.get("version"), pkg.get("source",{}).get("url"))
PY
echo
echo "Fetch Filament 5.6 pagination hook source from packagist CDN:"
url=$(python3 - <<'PY'
import json, urllib.request
from pathlib import Path
data=json.loads(urllib.request.urlopen("https://repo.packagist.org/p2/filament/tables.json", timeout=20).read())
links=[]
for n,v in data.get("packages",{}).get("filament/tables",{}).items():
if n.startswith("v5."):
links.append((n, v.get("version") or ""))
links=sorted(links)[-2:]
for n,v in links:
ver=v or n
print(n, ver)
PY
)
# fallback to v5.6 explicitly for source extraction
raw_url="https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/CanPaginateRecords.php"
echo "$raw_url"
curl -s "$raw_url" | sed -n '1,240p' | cat -n
echo
echo "Fetch table resource hook if present:"
raw_url2="https://raw.githubusercontent.com/filamentphp/filament/v5.6.0/packages/tables/src/Concerns/HasRecordsPerPageOption.php"
curl -s "$raw_url2" | sed -n '1,220p' | cat -nRepository: pelican-dev/panel
Length of output: 2735
🌐 Web query:
GitHub filamentphp/filament v5.7.5 packages tables Concerns CanPaginateRecords updatedTableRecordsPerPage
💡 Result:
In Filament v5.7.5, the CanPaginateRecords trait is a core component within the filament/tables package used to manage table pagination logic [1]. Regarding your query about updatedTableRecordsPerPage: 1. Role of updatedTableRecordsPerPage: This method is a Livewire lifecycle hook triggered when a user changes the number of records per page in a table's pagination select input [1][2]. It is responsible for updating the pagination state, persisting this choice in the user's session, and resetting the current page to ensure the table display remains consistent [1][3]. 2. Integration with CanPaginateRecords: The CanPaginateRecords trait provides the necessary methods to handle the logic when updatedTableRecordsPerPage is called, including [1]: - getTableRecordsPerPage(): Retrieves the current records-per-page setting [1]. - getTablePerPageSessionKey(): Generates a unique session key for the table to persist the user's pagination preference [1]. - paginateTableQuery(): Uses the per-page setting (which may be an integer or the string 'all') to perform the actual query pagination [1][3]. 3. Usage: Developers typically do not need to interact with updatedTableRecordsPerPage directly unless they are overriding core pagination behavior. To customize pagination options, such as the available per-page values or the default value, you should use the paginated() and defaultPaginationPageOption() methods on the table configuration object within your table() method [4]. For example, to configure pagination options: php public function table(Table $table): Table { return $table ->paginated([10, 25, 50, 100, 'all']) ->defaultPaginationPageOption(25); } Filament automatically handles session persistence for the pagination state using the identifier returned by getTablePerPageSessionKey(), ensuring that the user's preference is remembered [1][3].
Citations:
- 1: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: pararang/filament@c142a2f
- 3: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 4: https://filamentphp.com/docs/5.x/tables/overview
🌐 Web query:
github raw filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php updatedTableRecordsPerPage
💡 Result:
In Filament v5.7.5, the updatedTableRecordsPerPage method (or similar logic handling table records per page updates) within the CanPaginateRecords concern underwent security improvements regarding the validation of the per-page value [1]. Specifically, a commit titled "security: Validate live table records per page value" (e5d689b) modified packages/tables/src/Concerns/CanPaginateRecords.php on July 11, 2026, to ensure that the value provided for table records per page is properly validated [1]. This change prevents potential issues with insecure input when users interact with the pagination per-page select component. In the implementation of CanPaginateRecords, the method responsible for handling the update typically involves: 1. Validating the requested per-page value against the table's allowed pagination options [2][3]. 2. Updating the user's session with the new value using getTablePerPageSessionKey() [2][4][3]. 3. Resetting the table to the first page using resetPage() [4][3]. While the specific method name in your query might vary depending on the exact version or Livewire component trait usage, the core functionality for updating pagination records resides in this concern, ensuring that session storage and Livewire state remain synchronized and secure [2][1]. Filament v5.7.5 was released on July 31, 2026 [5][6].
Citations:
- 1: filamentphp/filament@e5d689b
- 2: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 3: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 4: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 5: https://newreleases.io/project/github/filamentphp/filament/release/v5.7.5
- 6: https://packagist.org/packages/filament/filament
🌐 Web query:
raw.githubusercontent.com filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php
💡 Result:
The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament Tables package [1][2]. It provides the essential logic for managing record pagination, including configuration for pagination limits, page navigation, and query execution [1][2]. Key responsibilities and features of this trait include: - Pagination Execution: The paginateTableQuery() method handles the application of pagination to the table's database query [1][2]. It supports pagination modes such as LengthAware and is responsible for setting query limits based on the current per-page configuration [3][1]. - Per-Page Management: It manages how many records are displayed per page via methods like getTableRecordsPerPage() and getDefaultTableRecordsPerPageSelectOption(), which integrate with user sessions and configuration files to persist preferences [3][1]. - Integration with Livewire: The trait includes methods like resetPage() and getTablePaginationPageName() to ensure the table's pagination state is correctly synced with the underlying Livewire component [3][2]. - Customization: It provides hooks to customize pagination options, such as the available records-per-page select options (e.g., [5, 10, 25, 50, -1], where -1 often represents "all" records) [3]. As of August 9, 2026, the latest version of the Filament ecosystem is v5.7.5, released on July 31, 2026 [4][5]. While specific implementation details can evolve between major versions (from v2.x to v5.x), the core purpose of CanPaginateRecords remains constant within the Filament Tables architecture [3][1][2].
Citations:
- 1: https://deepwiki.com/filamentphp/filament/3.1-table-core-architecture-and-rendering
- 2: https://deepwiki.com/filamentphp/filament/3-tables-system
- 3: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 4: https://newreleases.io/project/github/filamentphp/filament/release/v5.7.5
- 5: https://packagist.org/packages/filament/filament
🌐 Web query:
github raw ead6642f filamentphp filament v5.7.5 packages tablesConcerns CanPaginateRecords.php ? ref e7ea91f8a86929779bb7c5f4d66f32f39b581a
💡 Result:
The requested file CanPaginateRecords.php is located in the filamentphp/filament repository at packages/tables/src/Concerns/CanPaginateRecords.php [1]. The specific version of this file associated with the commit hash ead6642f (as referenced in your query) can be accessed directly on GitHub at the following URL: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php [1]. This trait is part of the Filament Tables package, which handles pagination logic for Livewire-based tables, including support for cursor pagination, configurable records per page, and pagination state management [1]. While Filament v5.7.5 is the current version of the package [2], code structures for core concerns like CanPaginateRecords are often maintained across specific development branches in the repository [1][3][4].
Citations:
- 1: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: https://root.packagist.org/packages/filament/tables
- 3: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 4: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
🌐 Web query:
raw.githubusercontent.com filamentphp filament e7ea91f8a86929779bb7c5f4d66f32f39b581a packages tables src Concerns CanPaginateRecords.php
💡 Result:
The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament PHP library's Table package [1][2]. Its primary purpose is to provide pagination functionality for table components, enabling them to transition from a Laravel Eloquent Builder query to a paginated result set [3][4]. Key responsibilities and features of this trait include: 1. Paginated Query Execution: It provides the paginateTableQuery() method, which handles the application of pagination limits (e.g., paginate, simplePaginate, or cursorPaginate) based on the table's configured pagination mode [3][4]. 2. Records Per Page Management: It manages the records-per-page logic, including support for an "all" option (which retrieves all records by executing a count query) and storing the user's preference in a session key specific to the table instance [1][5][4]. 3. UI Helper Methods: It provides various utility methods used by Filament's Table components for rendering and interaction, such as: - getTablePage(): Retrieves the current page number [4]. - getTablePaginationPageName(): Determines the query string parameter name for pagination (defaulting to "page") [1][4]. - getDefaultTableRecordsPerPageSelectOption(): Logic to determine the default pagination option based on session data or configuration [1][5][4]. Throughout the development of Filament (from 2.x to 3.x), this trait has been updated to support different pagination modes (Simple, Cursor, and LengthAware) and to resolve issues related to query counting, especially when complex queries involve GROUP BY or JOIN statements [6][4]. It is frequently involved in internal framework operations rather than being intended for direct modification by end-users [1][3].
Citations:
- 1: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: https://deepwiki.com/filamentphp/filament/3-tables-system
- 3: https://deepwiki.com/filamentphp/filament/3.1-table-core-architecture-and-rendering
- 4: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 5: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 6: Pagination query count bug filamentphp/filament#5196
🌐 Web query:
GitHub filamentphp filament CanPaginateRecords updatedTableRecordsPerPage parent resetTable session GetTableRecordsPerPage SelectOptions
💡 Result:
In Filament PHP, table pagination and record counts are managed primarily through the CanPaginateRecords concern [1][2] and the Table configuration builder [3]. Key Mechanisms: 1. getTableRecordsPerPage: This method returns the currently selected number of records per page, typically retrieved from a Livewire property or session [1][2]. 2. Persistence: Filament persists the selected pagination choice in the user's session to ensure consistency across page reloads [1][2]. The session key is unique to the table class to prevent collisions, generated via getTablePerPageSessionKey [1][2]. 3. Session Logic: If a user selects a value that is no longer in the allowed options (e.g., after a developer changes the table configuration), getDefaultTableRecordsPerPageSelectOption validates the session value against current options [1][2]. If invalid, it clears the session and reverts to the first available option [1][2]. 4. Configuration: Developers typically customize these settings in the table method of their component rather than overriding internal methods directly [4][5][3]. Common Customizations: - Pagination Options: Use ->paginated([10, 25, 50, 'all']) in your table method to define available choices [3]. - Default Page Size: Use ->defaultPaginationPageOption(25) to set the initial number of records displayed [3]. - Resetting Pagination: While internal methods like resetPage exist for managing Livewire pagination states [1][4][2], these are generally invoked automatically by Filament when table filters or search states change [6]. If you need to perform advanced modifications (like custom pagination logic), you can override paginateTableQuery within your component to use simple or cursor pagination, as documented in Filament's advanced tables guide [7][3]. Sources: [1] https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php [4] https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/InteractsWithTable.php [2] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php [5] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/InteractsWithTable.php [7] https://filamentphp.com/docs/3.x/tables/advanced [6] https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php [8] https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Contracts/HasTable.php [3] https://mintlify.wiki/filamentphp/filament/tables/overview
Citations:
- 1: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 3: https://mintlify.wiki/filamentphp/filament/tables/overview
- 4: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/InteractsWithTable.php
- 5: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/InteractsWithTable.php
- 6: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 7: https://filamentphp.com/docs/3.x/tables/advanced
- 8: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Contracts/HasTable.php
🌐 Web query:
raw.githubusercontent.com filamentphp filament e7ea91f8a86929779bb7c5f4d66f32f39b581a packages tables src Concerns CanPaginateRecords.php | sed -n '1,240p'
💡 Result:
The file packages/tables/src/Concerns/CanPaginateRecords.php is a core trait within the Filament PHP package, specifically used within the tables system to manage pagination logic [1][2][3]. Purpose and Functionality: This trait is responsible for configuring and handling record pagination for tables [2][3]. It manages how the table transitions from a full query builder result to a paginated set, supporting different pagination modes such as LengthAware, Simple, and Cursor pagination [1][3]. Key Responsibilities: - Pagination Management: It provides the paginateTableQuery() method, which applies the necessary LIMIT and OFFSET (or cursor logic) to the query [2][3]. - Page Size Configuration: It tracks how many records to show per page using the $tableRecordsPerPage property and manages user-selected pagination options [1][4][3]. - Session Persistence: It handles storing and retrieving the user's preferred "records per page" setting in the session, allowing it to persist across navigation [1][4]. - URL Query String: It manages the pagination page number in the URL via getTablePaginationPageName(), which ensures that pagination state is reflected in the browser's address bar [1][4]. Evolution: The trait has undergone significant updates as Filament has evolved from version 2.x to 3.x [1][4]. Changes include improved type-hinting, support for different pagination modes (like Cursor), and internal refactoring to ensure better compatibility with Livewire and Filament's own widget system [1][5][6]. If you are inspecting this file, it is primarily used to control how data sets are broken into pages, how the "records per page" selector behaves, and how the underlying database queries are modified to fetch only the required page of data [1][7][3].
Citations:
- 1: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: https://deepwiki.com/filamentphp/filament/3-tables-system
- 3: https://deepwiki.com/filamentphp/filament/3.1-table-core-architecture-and-rendering
- 4: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 5: TypeError: getTablePage() expects int, string returned with PaginationMode::Cursor filamentphp/filament#17569
- 6: Broken paginator ? filamentphp/filament#9957
- 7: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/HasRecords.php
🌐 Web query:
Github raw filamentphp filament v5.7.5 packages tables src Concerns CanPaginateRecords.php updatedTableRecordsPerPage getTableSelectOptionsForRecordsPerPage
💡 Result:
In Filament, the updatedTableRecordsPerPage method is a Livewire lifecycle hook used to handle changes when a user selects a different "records per page" option in a table [1][2]. When this method is triggered, it typically performs the following actions: 1. It updates the user's session to persist the chosen "records per page" preference [1][3]. 2. It resets the table pagination to the first page (via resetPage()) to ensure the user is not left on a page number that may no longer exist with the new record count [1]. Regarding your query about getTableSelectOptionsForRecordsPerPage and CanPaginateRecords.php: * The trait CanPaginateRecords manages pagination state and logic within Filament tables [4][1][5]. * While getTableRecordsPerPageSelectOptions (or similar variants) is commonly used to define the available options in the dropdown, the specific method name getTableSelectOptionsForRecordsPerPage does not appear as a standard public API in the modern Filament 3.x codebase [4][5]. Configuration for these options is typically handled via the table() method and the paginationPageOptions() configuration method on the Table object [3]. * In older versions (Filament v2.x), these options were often retrieved via the getTableRecordsPerPageSelectOptions method, which defaulted to reading from the configuration file [1]. If you are looking for how records are fetched for pagination, the paginateTableQuery method is responsible for applying the limit based on the selected per-page value (often handling the special '-1' value to represent "All" records by performing a count query) [4][5][6][2].
Citations:
- 1: https://github.com/filamentphp/filament/blob/2.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 2: pararang/filament@c142a2f
- 3: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/InteractsWithTable.php
- 4: https://github.com/filamentphp/filament/blob/3.x/packages/tables/src/Concerns/CanPaginateRecords.php
- 5: https://github.com/filamentphp/filament/blob/ead6642f/packages/tables/src/Concerns/CanPaginateRecords.php
- 6: pararang/filament@8ea34fa
Validate the page-size value before persisting it.
is_numeric() accepts client-mutable inputs like 1000000, 10.5, and 1e6. The table only allows [10, 20, 30, 40] for grid layouts and [10, 20, 50, 100] otherwise. Parse and validate the selected value against the current pageOptions, then persist only CustomizationKey::ServersPerPage; reset invalid state if needed so future loads use a valid default.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 158 -
176, Update updatedTableRecordsPerPage() to validate the selected value against
the current pageOptions, accepting only an exact allowed page-size value rather
than any numeric input. Persist only CustomizationKey::ServersPerPage, and reset
the invalid selection/state so subsequent loads use a valid default.
| user()?->update([ | ||
| 'customization' => array_merge( | ||
| user()->getCustomization(), | ||
| [CustomizationKey::ServersPerPage->value => (int) $perPage], | ||
| ), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Merge into the raw customization data.
user()->getCustomization() returns enum defaults merged with stored values. For a fresh user, this includes top_navigation => 'sidebar', while User::$validationRules requires customization.top_navigation to be boolean. The model validation observer validates during saving, so this update can fail before servers_per_page is persisted. It also stores unrelated defaults as explicit user values. (raw.githubusercontent.com)
Merge into the raw stored customization array, or add a dedicated method that updates only CustomizationKey::ServersPerPage.
Suggested persistence shape
+ $rawCustomization = user()->getRawOriginal('customization');
+ $rawCustomization = is_string($rawCustomization)
+ ? json_decode($rawCustomization, true)
+ : $rawCustomization;
+
user()?->update([
'customization' => array_merge(
- user()->getCustomization(),
+ is_array($rawCustomization) ? $rawCustomization : [],
[CustomizationKey::ServersPerPage->value => (int) $perPage],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| user()?->update([ | |
| 'customization' => array_merge( | |
| user()->getCustomization(), | |
| [CustomizationKey::ServersPerPage->value => (int) $perPage], | |
| ), | |
| $rawCustomization = user()->getRawOriginal('customization'); | |
| $rawCustomization = is_string($rawCustomization) | |
| ? json_decode($rawCustomization, true) | |
| : $rawCustomization; | |
| user()?->update([ | |
| 'customization' => array_merge( | |
| is_array($rawCustomization) ? $rawCustomization : [], | |
| [CustomizationKey::ServersPerPage->value => (int) $perPage], | |
| ), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Filament/App/Resources/Servers/Pages/ListServers.php` around lines 169 -
173, Update the customization payload in the per-page persistence flow around
the user update to merge with the raw stored customization data rather than
user()->getCustomization(), which includes enum defaults. Preserve existing
stored values and update only CustomizationKey::ServersPerPage, using an
existing raw-data accessor or a dedicated method if available.
|
I don't think we need an extra customization for that. The panel also stores other values in the session and that's fine. Besides that, this issue is pretty niche because the default lifetime is 2 hours. |
|
Fair point on consistency, but I'd frame this less as a timing edge case and more as a basic UX expectation: once someone deliberately changes this setting, they expect it to stick, that's the whole point of changing it. As a concrete example, I have around 20 servers myself, so I bumped the per-page from 10 to 20 on purpose, to see them all on one page every time I load the dashboard. I don't want to have to re-select that every time my session happens to renew, I want it to just stay set, permanently, like any other preference. It's less about how rare a 2-hour session expiry is and more about the setting not actually behaving like a saved preference at all right now. |
|
I personally think this idea looks good to me, plus it can always be cached. I agree with the expectation that once a user sets this value, it should remain the same. FYI, I have NOT run this branch, NOR have I tested it yet though. |
Summary
Filament only caches the servers table's "records per page" selection in the PHP session. Once the session expires (Laravel's default
SESSION_LIFETIMEis 120 minutes, and Pelican doesn't override it), the setting silently reverts to the hardcoded default (10 in grid view / 20 in list view) instead of staying at whatever the user picked.This PR persists the selection in the user's
customizationcolumn (the same mechanism already used fordashboard_layout,top_navigation, etc.), so it survives session expiry instead of only lasting a couple of hours.customization.servers_per_pageinstead of the hardcoded default.ListServers) only, per the contributing guide's "one targeted change per PR" — the same session-only limitation exists on a few other tables (egg list, activity log, file manager) but I left those out of this PR.Test plan
Tested manually end-to-end in a local Docker dev build (
Dockerfile.dev), driving a real browser against it:customization.servers_per_page = 20in the database