Creating a new resource reusing an existing model
I'm trying to set up an "all users" resource that uses the same 'users' DB table as my "users" resource. However, in order for Filament to recognise it as a different resource (so that I can customise the fields and get a new link in the sidebar etc.) I have had to create a new resource and a new model (allUsers).
Or am I going about this all wrong? What is the most sensible way of essentially replicating a resource (with a different name and URL) and then re-customising it with different options, whilst sharing the same database table?
53 Replies
Hi @benshawuk, what's the issue when having 2 resources with the same model? I haven't had a need for it yet but just tried it quickly and it seemed to work... I had to set the
$slug
explicitely in the second resource :
@pboivin Thanks! That looks like it might work.
Only reason I got side tracked from pursuing this approach earlier is that the resource is called the same thing in the nav sidebar.. (and resource title at the top of the page). Do you know if it's possible to rename this anywhere?
Ah got it! It's the $label string
Amazing, thank you so much! 🙂
Just a quick note:
I had to rename the action labels also:
protected function getActions(): array
{
return [
Actions\CreateAction::make()
->label('New user')
->modalHeading('Create user')
];
}
etc.
@pboivin this also doesn't seem to work with Filament Shield, as granting access to a model gives access to both resources 😦Yikes, did not think of that!
What if you create a new model, using the same table as the other one?
protected $table = 'original_model_table';
Not sure that would work - I think it will be looking at the wrong model.
I've asked over on the shield channel as well - not sure how active it is.
It's a shame you can't just block/restrict a resource by hard coding something. I tried registering a policy but that doesn't seem to like the conflict either, as it's also on a model level.
You could certainly rig up some hard coded middleware, but it’s not the safest route. Polices work at the model level for a reason. Better question to ask is why do 2 resources use the same model. That seems off to me.
I think I've resolved this by hard coding:
protected static function shouldRegisterNavigation(): bool
{
// return if user is super admin if (auth()->id() === 1) { return true; } else { return false; }. // example logic } I'll explain the reasoning behind the use case: I need a users table (for site admins), and then an "all users table" for super admins. The all users table has less restrictions (and different queries), and shows additional fields etc.
// return if user is super admin if (auth()->id() === 1) { return true; } else { return false; }. // example logic } I'll explain the reasoning behind the use case: I need a users table (for site admins), and then an "all users table" for super admins. The all users table has less restrictions (and different queries), and shows additional fields etc.
But they can still go to the url even if it’s hidden in the navigation.
That would still be one resource. You can check the user in ListUsers class to determine the queries, etc based on the user.
Then if you need different abilities you can still check the user in the policy methods to make sure they can’t do bad things.
But I need separate "pages" - and separate nav links.
I don't want it to share the same page for organisational reasons.
I suppose I could restrict the second page to only show the same fields and queries as the standard one, should they browse to it by URL. It feels a bit hacky though.
Ok but that’s really an anti pattern. Not even a Filament thing.
Yea. If you have to have two separate resources, then it’s going to get pretty hacky.
Maybe, but at the end of the day, I'm writing an application and I want it to behave in a certain way regardless of the technologies used.
Fair enough, but like I said it’s going to be hacky even with just standard laravel. Or any authorization/ framework.
I come from a vanilla PHP background and am new to Laravel as well as Filament. Something like this wouldn't be too difficult. But I'll take Filament any day for the time it saves with the boilerplate of getting everything working quickly and easily.
I guess you could potentially hijack the routes in your web.php file to apply authorization there. But that still won’t protect your database or models.
I mean, it's not that weird a requirement!
Even with vanilla php it’s not good to have separate authorization at the model level unless they are explicitly only modifying fields that each other don’t have access to.
I can think of other use cases.
It’s not a weird requirement, it’s just not the safest way to handle it.
Well, I'm open to suggestions! 🙂
I think restricting the additional fields within the table columns and the queries would be safe? That's what I'd be doing if it was all on the same page/resource anyway.
I could bang a redirect/forbidden on the web.php route for good measure.
What I said earlier would be my suggestion.
"You can check the user in ListUsers class to determine the queries, etc based on the user."
Thanks! I'll investigate this further.
Yea. Check out ->when() on eloquent builder.
And you could return a completely different set of columns to the table too.
And the form.
It’s all standard php OOP
I use it to disable editing super admins unless the logged in user is a super too. Thant way user with access to edit users can’t just delete modify supers too.
Since you want this resource only for super admins, I'd just override the "can" method on the resource and do the check for super admin there, thus avoiding the use of policies generated by shield
Yep. Perfect use case for a gate. But still no need for multiple resources.
I also have this same approach with my Customer model.. Mian customer resource for listing and view only, another (new nav) for linking each other customer (parent->child) so I search a solution long way.. I am also using Shield, I thought everything is working by overriding
url
but it's not. How I overcome is create new Model and use protected $table
and in new resource use my new Model, and shield also recognize and everything is working perfectly in my use-case..Yes, this method is working almost perfectly for me. Except that I cannot view the correct user roles in a relationship column. (I have a thread going on over on the Shield channel).
@benshawuk I forgot the context here but I'm curious, is the second resource read-only?
No. Everything works but I cannot see the "roles" for the correct model in my second users resource:
Tables\Columns\BadgeColumn::make('roles.name')->searchable(),
It's looking at the roles for the wrong model.
Ok I see! I thought of a workaround but it would only apply to a read-only resource.
I'm happy for the roles to be read-only - I just need to view the correct ones from the user model!
I'm thinking I can amend
Tables\Columns\BadgeColumn::make('roles.name')
to somehow return the correct onesSo Landlord is your "virtual" model (for lack of a better term...)? You have Users and then Landlords is kind of a subset of users?
and:
Forms\Components\Select::make('roles')
->required()
->multiple()
->relationship('roles', 'name')
->preload(),
Yes, it's just a test name - it's going to become "All Users"
I just used a different name to avoid confusion whilst I'm testing this method out.
So it will be User model, then AllUser model.
AllUser extends User
It actually works perfectly apart from this roles thing - I think it just needs to know to look up the correct User model roles instead of the extended model's roles. I just can't figure out how!
Relationship of some sort, or a function to process it?It could be that I really don't understand the domain/problem, but I'm starting to think like Awcodes... AllUser vs. User seems like a data structure issue at this point. (No offense, just trying to think with you!)
Non taken, and I appreciate all the help here!
You are essentially trying to provide 2 different "views" on the Users table in your app, right?
Correct
Both views are in distinct parts of the app and possibly require different roles/permissions, etc.
Yes, it's basically just an extra column (company) and a couple of differences in the builder query.
Could I create a custom page, and return a different table/form view using the same model maybe?
Yeah exactly what I was trying to get at 🙂
Does this new page require more permissions that the UserResource?
Maybe you're right and I'm overcomplicating it. It's just that I have a "super admin" section where I want all of those type of functions to be. With a shared model, enabling access to the model makes both resources visible.
No, as I'm using builder to filter out the unwanted data on the standard users table. I can also block fields by permissions that the users can't see their "company".
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
])
// Don't show the current user in the list of users
->where('id', '!=', auth()->id())
// Only show users from current user's company
->where('company_id', '=', auth()->user()->company_id)
;
}
Whereas the super admin one doesn't have/need those builder restrictions.
The entire app is being built depending on users only being able to access data from their company.
The super admin resources need to see data from all companies.Alright, I think I'm starting to have a full picture. Do you think you can find a way to handle all the query/permission stuff within the UserResource? If so, I'm thinking at this point that the issue becomes simply buiding the right Nav, right?
Yes, that works!
Only, when the super admin accesses the standard user page, they need to only see the users from their company, like a normal user would. So they get the same experience as the user.
Maybe if there was a way of checking based on the route or page name for this?
Yeah totally, I think you could make 2 variations on the EditUser page for that
I'm using simple resources btw! ;). But yes. It could work!
If could check the route/page in the getEloquentQuery(): Builder function (as well as the permissions) and do it with a custom page, that could work?
Can custom pages use normal tables/forms like a resource?
I think so... except you may run into a issue if you rely on the route in the context of a Livewire action. (The route is not your page anymore, it's an internal Livewire thing)
Yes
Hmm.. I need to do some more messing around! I see where you and awcodes are coming from though - it feels a bit weird to be creating an extra model for no good reason, just for view purposes.
Yeah... it just feels like a "data scoping" issue more than anything. But the virtual resource/model thing is a clever idea 🤓
I'll report back when I have it working! 🙂
public static function getEloquentQuery(): Builder
{
if (!auth()->user()->hasRole('super_admin')) { abort(403); } Hacky, but works. I'm sure you'll shoot that down!
if (!auth()->user()->hasRole('super_admin')) { abort(403); } Hacky, but works. I'm sure you'll shoot that down!
If it works for you that’s great. And my intention is never to shoot anyone down, just offer things I’ve learned along the way.
Thanks! I meant shoot down the methodology, as it seems inherently wrong! 🙂
I doubt the query builder function was intended for routing hacks! 😉
If it smells bad, then there’s a good chance it is bad. But my methodology has always been to make it work then make it better.
If anyone still cares after reading all this, my final solution (unless anything better comes along) is this:
- Share the user model with both resources
- On the "all users" page: block navigation item based on shield "all users" resource permissions:
protected static function shouldRegisterNavigation(): bool {
return auth()->user()->hasPermissionTo('view_any_all::user'); }
- Deny access to the page if the user doesn't have the correct permissions (and manually enters the URL):
public static function getEloquentQuery(): Builder
{
if (!auth()->user()->hasPermissionTo('view_any_all::user')) {
abort(403);
}
This is the best of all worlds. I keep to one model, Filament Guard permissions work properly, and unauthorised users (without the correct guard permission) cannot access the page or the unfiltered builder query results.thanks for sharing your solution for others!