FilamentLaravel

      Filament PHP: Creating a Searchable & Paginated Table from a Static Array

      In many web applications, we encounter scenarios where we need to display a list of resources that aren’t necessarily stored in a database. Perhaps it’s a directory of links, a list of static categories, or a “Map of the Application” for administrators.

      While Filament PHP is primarily designed to work with Eloquent models, it is flexible enough to handle static data. In this post, I’ll show you how to build a Read-Only Static Resource page using a standard PHP array, featuring Global Search and Manual Pagination.

      The Challenge

      Filament’s Table builder expects a database query by default. When you provide a static array (Collection), the built-in pagination and search mechanisms don’t automatically know how to “slice” your data. We need to manually bridge the gap using Laravel’s LengthAwarePaginator.

      Step 1: Generate the Filament Page

      First, create a new custom page in your Filament directory:

      Bash

      php artisan make:filament-page Data
      

      Select no when asked if you want to create it inside a resource.

      Step 2: Implementation (The Logic)

      Open your newly created file at app/Filament/Pages/Data.php. We will use the InteractsWithTable and HasTable traits.

      The Key Concepts:

      1. Static Data Source: We define our data in a simple getStaticData() method.
      2. Global Search: We filter the entire collection before it reaches the paginator.
      3. Manual Pagination: Since we aren’t using a database query, we use LengthAwarePaginator to tell Filament which “chunk” of the array to display on the current page.

      Step 3: The Full Code

      Here is the complete implementation. Note the use of /app/ as the URL prefix and the pagination set to 25 records per page.

      PHP

      <?php
      
      namespace App\Filament\Pages;
      
      use Filament\Actions\Action;
      use Filament\Pages\Page;
      use Filament\Tables\Table;
      use Filament\Tables\Columns\TextColumn;
      use Filament\Tables\Contracts\HasTable;
      use Filament\Tables\Concerns\InteractsWithTable;
      use Illuminate\Support\Collection;
      use Illuminate\Pagination\LengthAwarePaginator;
      
      class Data extends Page implements HasTable
      {
          use InteractsWithTable;
      
          protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-circle-stack';
          protected static ?string $navigationLabel = 'Resources';
          protected static ?string $title = 'Resource List';
      
          /**
           * Define your static data here. 
           * This structure can easily scale to 1000+ records.
           */
          protected function getStaticData(): array
          {
              return [
                  ['id' => 1, 'name' => 'Continents', 'url' => 'continents'],
                  ['id' => 2, 'name' => 'Countries', 'url' => 'countries'],
                  ['id' => 3, 'name' => 'Sides', 'url' => 'sides'],
                  ['id' => 4, 'name' => 'Regions', 'url' => 'regions'],
                  ['id' => 5, 'name' => 'Municipalities', 'url' => 'municipalities'],
                  ['id' => 6, 'name' => 'Cities', 'url' => 'cities'],
                  ['id' => 7, 'name' => 'Villages', 'url' => 'villages'],
                  ['id' => 8, 'name' => 'Districts', 'url' => 'districts'],
                  ['id' => 9, 'name' => 'Neighborhoods', 'url' => 'neighborhoods'],
                  ['id' => 10, 'name' => 'Streets', 'url' => 'streets'],
                  ['id' => 11, 'name' => 'Avenues', 'url' => 'avenues'],
                  ['id' => 12, 'name' => 'Squares', 'url' => 'squares'],
                  ['id' => 13, 'name' => 'Parks', 'url' => 'parks'],
                  ['id' => 14, 'name' => 'Gardens', 'url' => 'gardens'],
                  ['id' => 15, 'name' => 'Buildings', 'url' => 'buildings'],
                  // Add up to 1000+ records as needed...
              ];
          }
      
          public function table(Table $table): Table
          {
              return $table
                  ->query(fn () => null) // No database query required
                  ->records(function () {
                      $allRecords = $this->getFilteredRecords();
                      
                      // Fetching the current state from the table
                      $page = $this->getTablePage() ?: 1;
                      $perPage = (int) $this->getTableRecordsPerPage() ?: 25;
      
                      /**
                       * We manually create the paginator to allow Filament 
                       * to render the page links (1, 2, 3...).
                       */
                      return new LengthAwarePaginator(
                          $allRecords->forPage($page, $perPage),
                          $allRecords->count(),
                          $perPage,
                          $page,
                          ['path' => request()->url(), 'query' => request()->query()]
                      );
                  })
                  ->paginated([25, 50, 100])
                  ->defaultPaginationPageOption(25)
                  ->recordUrl(fn ($record) => "/app/{$record['url']}")
                  ->columns([
                      TextColumn::make('name')
                          ->label('Resource Name')
                          ->searchable()
                          ->weight('bold')
                          ->grow(),
                  ])
                  ->actions([
                      // Full path used to avoid conflict with Page Action
                      Action::make('view')
                          ->label('View')
                          ->icon('heroicon-m-eye')
                          ->color('gray')
                          ->url(fn ($record) => "/app/{$record['url']}"),
                  ])
                  ->bulkActions([]);
          }
      
          /**
           * Handles the search logic across the entire static array.
           */
          protected function getFilteredRecords(): Collection
          {
              $data = collect($this->getStaticData());
              $search = $this->tableSearch;
      
              if (blank($search)) {
                  return $data;
              }
      
              return $data->filter(function ($item) use ($search) {
                  return str_contains(
                      mb_strtolower($item['name']),
                      mb_strtolower($search)
                  );
              });
          }
      
          protected string $view = 'filament.pages.data';
      }
      

      Step 4: The Blade View

      Ensure your view file at resources/views/filament/pages/data.blade.php is set up to render the table:

      HTML

      <x-filament-panels::page>
          <div>
              {{ $this->table }}
          </div>
      </x-filament-panels::page>
      

      Why this approach is effective:

      1. Performance: Since there are no database hits, the page loads instantly, even with hundreds of rows.
      2. User Experience: By using recordUrl, the entire row is clickable. Combined with a View action, it provides a very intuitive navigation experience.
      3. Scalability: Using LengthAwarePaginator ensures that even if your list grows to 1000 items, the UI remains clean and fast.
      4. Global Search: Because we filter the collection before paginating, the search bar checks the entire dataset, not just the currently visible 25 rows.

      Conclusion

      This pattern is incredibly useful for creating administrative dashboards or custom navigation hubs within Filament. It keeps your code clean and your database light.

      Happy coding!

      Hi, I’m elliotkartvel