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
Patrick Boivin
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 :
class TireResource extends Resource
{
protected static ?string $model = Tire::class;

// ...
class TireResource extends Resource
{
protected static ?string $model = Tire::class;

// ...
class OtherResource extends Resource
{
protected static ?string $model = Tire::class;

protected static ?string $slug = 'other';

// ...
class OtherResource extends Resource
{
protected static ?string $model = Tire::class;

protected static ?string $slug = 'other';

// ...
benshawuk
benshawukOP2y ago
@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 😦
Patrick Boivin
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';
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
Fair enough, but like I said it’s going to be hacky even with just standard laravel. Or any authorization/ framework.
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
I mean, it's not that weird a requirement!
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
I can think of other use cases.
awcodes
awcodes2y ago
It’s not a weird requirement, it’s just not the safest way to handle it.
benshawuk
benshawukOP2y ago
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.
awcodes
awcodes2y ago
What I said earlier would be my suggestion.
benshawuk
benshawukOP2y ago
"You can check the user in ListUsers class to determine the queries, etc based on the user." Thanks! I'll investigate this further.
awcodes
awcodes2y ago
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.
ZedoX
ZedoX2y ago
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
awcodes
awcodes2y ago
Yep. Perfect use case for a gate. But still no need for multiple resources.
Vp
Vp2y ago
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..
benshawuk
benshawukOP2y ago
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).
Patrick Boivin
@benshawuk I forgot the context here but I'm curious, is the second resource read-only?
benshawuk
benshawukOP2y ago
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(),
benshawuk
benshawukOP2y ago
It's looking at the roles for the wrong model.
Patrick Boivin
Ok I see! I thought of a workaround but it would only apply to a read-only resource.
benshawuk
benshawukOP2y ago
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 ones
Patrick Boivin
So Landlord is your "virtual" model (for lack of a better term...)? You have Users and then Landlords is kind of a subset of users?
benshawuk
benshawukOP2y ago
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?
Patrick Boivin
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!)
benshawuk
benshawukOP2y ago
Non taken, and I appreciate all the help here!
Patrick Boivin
You are essentially trying to provide 2 different "views" on the Users table in your app, right?
benshawuk
benshawukOP2y ago
Correct
Patrick Boivin
Both views are in distinct parts of the app and possibly require different roles/permissions, etc.
benshawuk
benshawukOP2y ago
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?
Patrick Boivin
Yeah exactly what I was trying to get at 🙂 Does this new page require more permissions that the UserResource?
benshawuk
benshawukOP2y ago
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.
Patrick Boivin
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?
benshawuk
benshawukOP2y ago
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?
Patrick Boivin
Yeah totally, I think you could make 2 variations on the EditUser page for that
benshawuk
benshawukOP2y ago
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?
Patrick Boivin
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
benshawuk
benshawukOP2y ago
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.
Patrick Boivin
Yeah... it just feels like a "data scoping" issue more than anything. But the virtual resource/model thing is a clever idea 🤓
benshawuk
benshawukOP2y ago
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!
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
Thanks! I meant shoot down the methodology, as it seems inherently wrong! 🙂 I doubt the query builder function was intended for routing hacks! 😉
awcodes
awcodes2y ago
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.
benshawuk
benshawukOP2y ago
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.
Dan Harrin
Dan Harrin2y ago
thanks for sharing your solution for others!
Want results from more Discord servers?
Add your server