When working with Filament, your table may need to load data from a 3rd party API system. Filament does not support this out of the box, so you have to use a 3rd party package called Sushi. And even then, to be honest, the functionality is pretty limited. But let's see how to do it for simple cases.
Imagine you need to load this table data from the external API that returns the list of subscriptions:
You would think you can use the ->viewData()
method of Filament Tables with JSON data?
public function table(Table $table): Table{ $apiCall = Http::asJson() ->acceptJson() ->get(config('app.url') . '/api/subscription'); return $table // You might think this works, but it does not. // It's not meant for this use-case ->viewData($apiCall->json('data')) ->columns([ TextColumn::make('user_name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]);}
But you will encounter an immediate issue if you try to use a table, as it requires the query()
function to be called:
Now let's see how to make it work, with Sushi package.
What is Sushi and How Does It Work?
"Sushi" is a package that can create a fake model with its own SQLite database from any array data source. It's used like this:
app/Models/SubscriptionApiWrapper.php
use Http;use Illuminate\Database\Eloquent\Model;use Sushi\Sushi; class SubscriptionApiWrapper extends Model{ use Sushi; public function getRows(): array { $apiCall = Http::asJson() ->acceptJson() // This uses a local API endpoint for demonstration purposes. // You can use any API endpoint you want ->get(config('app.url') . '/api/subscription'); return $apiCall->json('data'); }}
Usage of this Model is the same as any other Eloquent Model:
app/Filament/Pages/SubscriptionsList.php
use App\Models\SubscriptionApiWrapper;use Filament\Pages\Page;use Filament\Tables\Columns\TextColumn;use Filament\Tables\Concerns\InteractsWithTable;use Filament\Tables\Contracts\HasTable;use Filament\Tables\Table; class SubscriptionsList extends Page implements HasTable{ use InteractsWithTable; protected static ?string $navigationIcon = 'heroicon-o-document-text'; protected static string $view = 'filament.pages.subscriptions-list'; public function table(Table $table): Table { return $table // Here we are taking the Sushi model and passing it as our query ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user_id'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]); }}
The display of the table is handled as usual:
resources/views/filament/pages/subscriptions-list.blade.php
<x-filament-panels::page> {{ $this->table }}</x-filament-panels::page>
Finally, loading the page will give us a table with the data from the API:
This works perfectly fine to display a simple table, but when you need to add some modifications to the table, you might run into issues. So let's see a few common ones to look out for.
Issue 1 - No Relationships
As you might have noticed, we use the user_id
instead of the user name. But if we look at our API response - we can see that there is a user
object that contains the user name:
Let's try telling Filament that this is a relationship:
app/Filament/Pages/SubscriptionsList.php
// ...public function table(Table $table): Table{ return $table ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user_id'), TextColumn::make('user.name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]);}
Now, if we attempt to load our table - we'll have no data:
This happens because Filament expects a user
relationship to be present on the SubscriptionApiWrapper
model. But since we are using a fake model - we don't have any relationships. Of course, we can solve this by doing something nasty:
app/Filament/Pages/SubscriptionsList.php
// ...public function table(Table $table): Table{ return $table ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user.name'), TextColumn::make('user_name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]);}
And then in our Wrapper:
app/Models/SubscriptionApiWrapper.php
// ...public function getRows(): array{ $apiCall = Http::asJson() ->acceptJson() ->get(config('app.url') . '/api/subscription'); return collect($apiCall->json('data')) ->map(function ($item) { // Sushi does not support Array fields, so you have to move them to new fields $item['user_name'] = $item['user']['name']; unset($item['user']); return $item; }) ->toArray();}
And now, we have our user name in the table:
But this seems like much work for something quite simple. And now imagine if you have multiple relationships available like this - how much work would you have to do to make this work? Not a great solution.
Note: Sushi has relationship support if used in the context of internal application. In this case, it fails to provide relationships in the Laravel way (via the belongsTo
method) as it uses a separate local sqlite
database to store all the records in our Model. Casting
Issue 2 - No Filters on Table
Another issue that we might run into is the need for filters. Let's say we want to filter our subscriptions by the user name. We can add a filter to our table:
app/Filament/Pages/SubscriptionsList.php
public function table(Table $table): Table{ return $table ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user_name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]) ->filters([ SelectFilter::make('user_id') ->relationship('user', 'name') ]);}
Now try to run this - you'll get an error:
That's because we are trying to load a relationship that does not exist on our fake model. We can try to add it:
app/Models/SubscriptionApiWrapper.php
use Illuminate\Database\Eloquent\Relations\BelongsTo; // ... public function user(): BelongsTo{ return $this->belongsTo(User::class);}
And now, we will get a filter on our table:
But if we attempt to filter by it - we'll get an error:
This happens because Sushi, by default, creates a single file database that's running on an SQLite
engine and does not have the users
table. We can, of course, apply a little hack to make this work:
app/Filament/Pages/SubscriptionsList.php
public function table(Table $table): Table{ return $table ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user_name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]) ->filters([ SelectFilter::make('user_id') ->relationship('user', 'name') ->options(User::pluck('name', 'id')) ]);}
Which will filter our table without any calls to the database:
But that might be better if you have a few records. And again, this is a lot of work for something that should be simple.
Issue 3 - Pagination Limitations
Another issue that we might run into is the need for pagination. The API returns you ten subscriptions at a time. How do you go to a second page? Well, you can't. At least not by using this code:
app/Filament/Pages/SubscriptionsList.php
// ...public function table(Table $table): Table{ return $table ->query(SubscriptionApiWrapper::query()) ->columns([ TextColumn::make('user_name'), TextColumn::make('name'), TextColumn::make('price'), TextColumn::make('start_date'), TextColumn::make('end_date'), ]) ->filters([ SelectFilter::make('user_id') ->options(User::pluck('name', 'id')) ]) ->paginated();}// ...
This does show us the page selection at the bottom:
And we can go to the next page:
But that's where it ends. We cannot load more than one page of data from the API itself, so we are limited to only what we receive from the API. For example, if the API returned 20 records, we can paginate in those 20, but only some of the ones the API might have. That's a significant limitation.
Conclusion - You Might Want To Rethink The Choice
We can make Filament work with a 3rd party API, but it could work better.
You will have to do a lot of work to make it fully compatible with Filament, and even then, you will run into some limitations.
A better option might be skipping Filament usage with the 3rd party API and building everything custom or even using a sync way (to sync API resources to local models) for better results.
Notice: this article was written in Autumn 2023, so things might have changed if you landed here months later. Let us know in the comments if that's the case, and we will update the article!
If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.
Great read! I was considering doing the exact same thing and now I know I should spend more time inspecting the case. Thank you!
This is really useful. I spend my life retrieving data from remote APIs. It's good to see how it can be done with something like Sushi. It's a challenging aspect of working with multiple datasets because you can spend your life building synchronization scripts, and when they break it's just a mission.
While sushi is a great way to solve that - it gives a different set of challenges for you!
There are a lot of cases where the synchronization scripts are better than this approach, but in some small use-cases this is good enough
I'm curious whether Sushi is the best and the only way to create tables in Filament from collections or arrays. After conducting some research, I have found that the table query in Filament does not support collections. Let's assume that I want to create a table from two or more concatenated collections of my models, not only from the data received from an API. In this case, should I use Sushi, or are there any other possibilities available for achieving this?
You are correct, the table builder does not accept an array. It will always require a
query()
to be present, which means that you must have a model available.So it does not matter if it is an API or just a collection put into a table - you need a package like Sushi to help you there. I have not found any other way to get the data into a table
Hi povilas, iam using filament 3. I have followed your tutorial, but always get an error
cURL error 28: Operation timed out after 30001 milliseconds with 0 bytes received (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://localhost:8000/api/attendance
Hi, this can be due to various reasons. One of them might be too much data being loaded and it times out.
Try to call that endpoint manually via postman or something similar and see what is happening. Check the logs, load less data and see if that changes anything :)
Hi, Modestas. Thank you for the reply.
So, i have AttendanceResource and the table is filtered by month and year. How to make API can display data according to the month and year selected by the user?
maybe the timeout that occurred before was due to the large of data I was displaying from the API...
Here is my screenshot https://prnt.sc/Fqt748qw7iPG
You can get the filters on a table by injecting parameters into a closure (for the query/data retrieval). I did that with
$livewire
injection myself to change the query.But the problem is - Filament is not designed to work with this. So you will encounter issues no matter what.
Generally - Filament tables are designed to directly work with database and nothing else. While this package helps - it doesn't really play nicely :(
Too bad i am having a problem using summaries
Yes, sadly summaries won't work with sushi as it requires database table :)
There are A LOT of limitations with this approach, but it's probably the easiest one we have