F
Filament3w ago
borg

Prevented dependency injection of Models (non existent?)

What was the reason for this change: https://github.com/filamentphp/filament/blame/4.x/packages/support/src/Concerns/EvaluatesClosures.php#L85 @ralphjsmit One can restore this by binding all the models, but this goes against Laravel DI since Models can be injected without issues. A clear commit agains this. I'd like to know the reasons behind this. Thank you.
GitHub
Blaming filament/packages/support/src/Concerns/EvaluatesClosures.ph...
A powerful open source UI framework for Laravel • Build and ship admin panels & apps fast with Livewire - Blaming filament/packages/support/src/Concerns/EvaluatesClosures.php at 4.x · filam...
Solution:
In Filament V3 it introduced that you can now alias the injected record to another parameter name instead of $record. For example, you could just do Post $post instead of Post $record. Whilst this was convenient, there are quite often situations where you think there should be a record, but there actually is not (e.g. in complex forms with relationships etc). In that case, returning Post $record would fail on that the argument definition does not allow null, prompting a change to ?Post $record. However, without the above change you linked, the Post $post would still return a Post model, but then an empty one that does not exist at all created by Laravel dependency injection. This causes then weird bugs in your code later one if you expect $post to be the current $record and it turns out to be just an empty unsaved model. Therefore, if the requested parameter is an Eloquent parameter that is not of the current record/model, this line has been added to prevent such unwanted empty model resolutions. See also here: https://github.com/filamentphp/filament/pull/15160...
GitHub
Prevent creating non-existent models from container by ralphjsmit ...
Currently, the closure evaluation system works great by resolving any dependencies not provided by name or type from the container. This is very useful. However, there is one situation where it isn...
Jump to solution
19 Replies
Solution
ralphjsmit
ralphjsmit3w ago
In Filament V3 it introduced that you can now alias the injected record to another parameter name instead of $record. For example, you could just do Post $post instead of Post $record. Whilst this was convenient, there are quite often situations where you think there should be a record, but there actually is not (e.g. in complex forms with relationships etc). In that case, returning Post $record would fail on that the argument definition does not allow null, prompting a change to ?Post $record. However, without the above change you linked, the Post $post would still return a Post model, but then an empty one that does not exist at all created by Laravel dependency injection. This causes then weird bugs in your code later one if you expect $post to be the current $record and it turns out to be just an empty unsaved model. Therefore, if the requested parameter is an Eloquent parameter that is not of the current record/model, this line has been added to prevent such unwanted empty model resolutions. See also here: https://github.com/filamentphp/filament/pull/15160
GitHub
Prevent creating non-existent models from container by ralphjsmit ...
Currently, the closure evaluation system works great by resolving any dependencies not provided by name or type from the container. This is very useful. However, there is one situation where it isn...
borg
borgOP3w ago
@ralphjsmit yeah, thanks! I've stumbled across this in my development in V3, but I've just implemented a proper check. So this is just a "convenience" update, because at the end of the day it's the programmers responsibility to catch "nullable"models. However, since I've already covered such cases, would you agree that binding my models in the register method to get passed the condition to the "old injection" would just revert the situation. Looking at the code though, you've deliberately added the check to see if it's bound in the container. Yeah, I see Dan pointing out the same reason.
ralphjsmit
ralphjsmit3w ago
So in V3 you were injecting empty models on purpose via the DI?
borg
borgOP3w ago
Yeah, I find DI useful and necessary, instead of using Facades all over the code. So i did not mind having an empty Model, because I $model->find() searched for the record I wanted.
ralphjsmit
ralphjsmit3w ago
Okay, yeah can be if you like that. For models I don't think it is really a facade but it is is a little magic indeed. I really couldn't imagine a situation where you would want to do that compared to just Model::create() or whatever tbh, and personally the injection of empty has caused me more headaches at least 5-10 times.
borg
borgOP3w ago
Of course I know where the model gets resolved and where not, so I dont have issues with "nullable models".
ralphjsmit
ralphjsmit3w ago
Yes and no, on surface that looks so but as soon you start doing deep nested relationships for example it already gets way more tricky
borg
borgOP3w ago
I'm not creating the models to be fair. I'm using the dependency to search records.
ralphjsmit
ralphjsmit3w ago
Can you show an example? So like $model->searchForSomething() or so?
borg
borgOP3w ago
Let's say I'm rendering a Select component on the "Post" model. The Post model gets injected without isses inside the ->options(callable), but the options i'm trying to render are (for example) living in a "Tags" model. So I inject the "Tags $model" (which is null in this case), but then i go return $model->all()->pluck('id', 'name'); Meaning, I don't expect the model record to be there, because the component is renderd in the "Post" resource, right? I just want the model to be available for me. Same thing happens if you have a service for retrieving all the posts.. you create a Service, inside the constructor you inject the "Post $model" model.. and of course it is null, but you know that and then just get all the posts by calling $model->all(). How would you otherwise solve this..?
ralphjsmit
ralphjsmit3w ago
Okay, yeah I understand why you're doing it but then I think in the Laravel community you're a bit an outlier that uses $model->all() instead of just Tags::all() Could be situations like this (in real life prob more complex but this is a bit what I remember):
// Post form
Group::make([
TextInput::make('name')
->required(),
Select::make('something')
// Now you writing this and forgot to handle the case where there's no author, and then $author ends up being empty
->options(fn (Author $author) => ...)
])
->relationship('author')
// Post form
Group::make([
TextInput::make('name')
->required(),
Select::make('something')
// Now you writing this and forgot to handle the case where there's no author, and then $author ends up being empty
->options(fn (Author $author) => ...)
])
->relationship('author')
Why not just do Select::make('tags')->relationship('tags', 'name') here btw if you are already on a PostResource?
borg
borgOP3w ago
Don't get hung up the business logic here, it was just an example. While your suggestion about the relationship makes sense (and would be the right way in this case), there are other closures where I'd like to inject he model and I don't mind it to be null ( or rather, does not contain a resolved record). I'm using Facades only when I'm really forced too (in static context), while I try to keep injecting things. It's the proper OOP way and while I do understand the convenience from Facades, the codes gets pretty dirty really quick if you get too comfortable with them. Would you mind explaining how would you tackle a CustomService that has a method which gets all the Posts, for example. And the CustomService implements a interface of "PostRepository" which has a contract to implement: public function all(): Collection; This is request is purely out of curiosity and somewhat outside if Filament, but this way you'll better understand my logic. I'll provide my implementation as well.
ralphjsmit
ralphjsmit3w ago
Yeah makes sense, it's probably a different developer mindset. Like, I think everyone sees that Laravel provides for everything one way to do it that can classify as the recommended framework way. Not saying other things are bad, but it's pretty much a fact that for database retrieval Laravel recommends Eloquent and not the repository pattern. My experiences over the years has learned me that generally it is actually long-term the best to stick to all Laravel defaults/recommended way of doing things as much as possible. So that's why I would not probably not have a CustomService or PostRepository at all, but just have 2 Eloquent models Post and Tag and just access them using Post::all() , Tag::pluck(), etc. I think for a developer it's just so much more convenient to have one Tag model of 10 lines and can do Tag::pluck() immediately compared to having to create a TagRepository interface, the repository and the implementation for each method, as you basically are repeating all the pluck(), all(), cursor(), etc methods from Laravel into your repository classes. So that's a bit where my philosophy comes from. And I recognize that for some developers it feels like too much magic and not as the theoretically correct way of doing things. But in the end it is us developers maintaining an app and if that goes very elegant and convenient with Eloquent then why would I make the experience worse for myself, the code more complex and less elegant to read just to prevent using magic things. In the end I don't develop with the goal of not using magics as I don't buy anything for that, but I want a clean, elegant and simple codebase that is fun and easy to work in. One aspect that I am with you on is that magics are not good auto-completion, so you will probably have better and stricter typing than me in some cases. Most issues can be solved with some of the Facade docblocks that Laravel provides and with Laravel Idea, so I don't experience as a very big blocker.
borg
borgOP3w ago
Yeah, I understand. I'm not forcing this upon you, so I hope the message is not coming too off putting. I just wanted to know the real reason behind it so I can implement a solution for this since it broke the my application (and have a little discussion on alternatives). While I know that Filament does not directly use Laravel DI (uses EvaluatesClosures on the frontline), It is necessary for me to understand the change which in turn will make my patch functional. I see Dan foresaw this scenario which I've hit and I'm happy that you added the option to allow bound models to still be resolved. Thanks for putting up with me and taking your time to reply, I really appreciate the effort @ralphjsmit . And I'm glad you defend your position, after all this is open source and we are happy to have people working on projects so others can use it. Cheers 🍻
ralphjsmit
ralphjsmit3w ago
Thank you too for your openness, definitely not forcing anything upon anyone! I definitely understand your viewpoint as well, though I didn't really expect it when I made the original PR. In your case binding would be great, and you could still use the app()/resolve() methods manually (though that will probably feel less like a DI scenario for you).
borg
borgOP3w ago
If i just add $this->app->bind(Post::class,Post::class) it works again 😉
ralphjsmit
ralphjsmit3w ago
Awesome, yes definitely :) Does $this->app->bind(Post::class) also work (just curious)?
borg
borgOP3w ago
It does indeed Just tested it.
ralphjsmit
ralphjsmit3w ago
Nice Okay interesting to know

Did you find this page helpful?