Does Filament change Laravel's default Route Model Binding?

My understanding of RMB from all the docs I've read is that the app() facade should be able to resolve the current Model, based on the URL parameters. This works within the scope of say, a Resource, but not within a custom class.
class TestSetup
{

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo());
$project = app(Project::class);
\Debugbar::alert($project);
// ---
}
}
class TestSetup
{

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo());
$project = app(Project::class);
\Debugbar::alert($project);
// ---
}
}
This logs the current route, with the correct ID in the correct path, e.g.
/company/2/projects/3
/company/2/projects/3
, however the $project value is a blank model - it is not resolving the ID in the path. Is this because the routes Filament gets us to create use {record} (e.g.
'view' => Pages\ViewProject::route('/{record}')
'view' => Pages\ViewProject::route('/{record}')
instead of {project}, thus rendering it unable to resolve? I've tried renaming the {record} to {project}, only to have it immediately throw a raft of errors. I'm really stuck with this one. By everything I've read, this stuff should just work, but it isn't. Is there some trick I'm missing? A trait my class needs to use? A setting on my Model? I don't know enough about how it works to know for sure, but it is beginning to feel like Filament is doing something tricky in the middle here with DI, and that trickiness is either undocumented / or I'm completely missing something obvious.
18 Replies
Dennis Koch
Dennis Koch8mo ago
I don't understand where you are doing your logging. Filament shouldn't mess with the binding, but as you pointed out, it is using $record as a placeholder.
Malcolm Turntbull
I'm calling this function something like this (massively cut down and simplified):
class Playbook
{
public static function make($class = null): Playbook
{
$playbook = app($class ?? static::class);
$playbook->configureDto();
return $playbook;
}
}
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;


protected static function repositorySection(): ?Section
{
return Section::make('repository-setup')
->heading("Setup Actions")
->schema(
function (Project $project) {
$playbook = Playbook::make($project->template);
return $playbook->getPlays()->map(function (Play $play) {
//---
});
}
);
}
}
class Playbook
{
public static function make($class = null): Playbook
{
$playbook = app($class ?? static::class);
$playbook->configureDto();
return $playbook;
}
}
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;


protected static function repositorySection(): ?Section
{
return Section::make('repository-setup')
->heading("Setup Actions")
->schema(
function (Project $project) {
$playbook = Playbook::make($project->template);
return $playbook->getPlays()->map(function (Play $play) {
//---
});
}
);
}
}
The horrible irony is that in the context of that repositorySection() static method, the closure with Project $project? It resolves perfectly fine just there. Yes, I could pass it in as a parameter, but I'm trying to rely on DI as much as possible, and I can't for the life of me understand why RMB would work perfectly well in this context here, but a couple of method calls away, it doesn't. Maybe there is magic in the Resource that allows this to work, and it's not using RMB at all? OK, I just tried this:
function (Project $project) {
\Debugbar::alert($project);
\Debugbar::alert(app(Project::class));
$playbook = Playbook::make($project->template);
return $playbook->getPlays()->map(function (Play $play) {
//---
});
}
function (Project $project) {
\Debugbar::alert($project);
\Debugbar::alert(app(Project::class));
$playbook = Playbook::make($project->template);
return $playbook->getPlays()->map(function (Play $play) {
//---
});
}
The first Debug shows the correct Model instance, but the second one shows a blank instance. Am I going crazy, or am I completely misunderstanding how all this is meant to work? I've tried cleaning up the code to make it more obvious as to what goes where, and I've also commented all the various attempts that do and do not work:
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static function repositorySection(): ?Section
{
return Section::make('repository-setup')
->heading("Setup Actions")
->schema(
function (Project $project) {
\Debugbar::alert($project); // WORKS
\Debugbar::alert(app(Project::class)); // DOES NOT WORK
$playbook = app(Playbook::class)::make();
\Debugbar::alert($playbook->project); // CONSTRUCTOR INJECTION, DOES NOT WORK AS EITHER $project OR $record
//---
}
);
}
}

class Playbook
{
public function __construct(
public Project $project,

) {}
public static function make($class = null): Playbook
{
$playbook = app($class ?? static::class);
\Debugbar::alert(app(Project::class)); // DOES NOT WORK
$playbook->configureDto();
return $playbook;
}

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo()); //SHOWS PATH WITH /projects/ EVEN THIS DEEP
$project = app(Project::class);
\Debugbar::alert($project); //DOES NOT WORK
\Debugbar::alert($this->project); //TESTED ALREADY, BUT DOES NOT WORK
}
}
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
protected static function repositorySection(): ?Section
{
return Section::make('repository-setup')
->heading("Setup Actions")
->schema(
function (Project $project) {
\Debugbar::alert($project); // WORKS
\Debugbar::alert(app(Project::class)); // DOES NOT WORK
$playbook = app(Playbook::class)::make();
\Debugbar::alert($playbook->project); // CONSTRUCTOR INJECTION, DOES NOT WORK AS EITHER $project OR $record
//---
}
);
}
}

class Playbook
{
public function __construct(
public Project $project,

) {}
public static function make($class = null): Playbook
{
$playbook = app($class ?? static::class);
\Debugbar::alert(app(Project::class)); // DOES NOT WORK
$playbook->configureDto();
return $playbook;
}

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo()); //SHOWS PATH WITH /projects/ EVEN THIS DEEP
$project = app(Project::class);
\Debugbar::alert($project); //DOES NOT WORK
\Debugbar::alert($this->project); //TESTED ALREADY, BUT DOES NOT WORK
}
}
Is there a limit on the number of times a record can be injected? Or do nested injections not work? I'm losing my mind here, what am I doing wrong?
Dennis Koch
Dennis Koch8mo ago
Makes perfect sense. The first one is injected by Filament. The second one tries to resolve a Project object from the container without any information. It doesn't know which one you want so it just returns a new pone. You are confusing Filaments injection with Laravel's container dependency injection
Malcolm Turntbull
OK, so I think I may have stumbled onto an underlying truth that I didn't really understand: Route Bindings and Container Bindings are two completely separate things.
Malcolm Turntbull
Stack Overflow
Laravel: Using mock instance in Customised Route::model binding
Route model binding in Laravel, helps bind an instance to the service container, to be used in a controller action. And the beautiful thing is that when I want to write tests, I can bind a mock ins...
Malcolm Turntbull
So when Filament is injecting the correct model in that instance, it's doing so via its own methodology, and has nothing to do with the container, OR route model bindings (which I don't think actually work at all in Filament, because we're not using Controllers?) I have also clearly misunderstood the Laravel documentation - when it says "The Laravel service container is a powerful tool for managing class dependencies and performing dependency injection", it does not mean that it is THE tool for managing dependency injection.
Malcolm Turntbull
Laravel - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.
Malcolm Turntbull
I have foolishly believed that if there is DI happening, that it must be the SC doing it. And that if the SC was able to resolve a specific instance of a Model, then it was Route Model Binding doing it. This whole thing has been such a handbrake on my project, I'm writing some documentation on how it all actually works - only baby steps so far, as I clearly don't actually understand wtf I am doing, but if I am running into these sorts of misunderstandings, then it's guaranteed other people are as well, and I've spent... WEEKS looking at documentation on this.
Malcolm Turntbull
https://docs.google.com/document/d/19YhjppE5qSw4v1xqaDQFiuHmEjiPCpJyfFOHHY21YTE/edit?usp=sharing this is the start of my documentation, I'll write it in here for now, and eventually publish on Medium.
Google Docs
Things I wish someone had explained to me: Dependency Injection in ...
As someone who is new to both Laravel and Filament, the one area I have struggled with the most is Dependency Injection. When it works, it is seemingly magical, but when it doesn’t work as you are expecting, it is incredibly frustrating trying to determine why. This is not helped when documentat...
Malcolm Turntbull
There are countless thousands of examples of how DI works, exactly like the base Laravel docs show you. There's almost nothing on why and when it DOESN'T work.
Dennis Koch
Dennis Koch8mo ago
Route binding works because it uses dependency injection and the provided id of the route. This way it can resolve the correct model. That doesn’t mean that it will always resolve this model afterwards since it won’t be registered in the service container. Filament takes the model from the route and injects it into all the closures where it’s needed via its own dependency injection mechanism. So it’s just passing on the already resolved model.
Malcolm Turntbull
There has to be some sort of context around which Filament's DI does and does not work - for an example, there's this:
class EnvironmentService
{
use Filament\Support\Concerns\EvaluatesClosures;

public function getEnvironment()
{
return $this->evaluate(fn (Environment $environment) => $environment);
}
}

Action::make("action")
->action(function (Environment $environment) {
\Debugbar::alert($environment); //This works, shows the current Environment
$service = app(EnvironmentService::class);
\Debugbar::alert($service->getEnvironment()); //Blank Environment
});
class EnvironmentService
{
use Filament\Support\Concerns\EvaluatesClosures;

public function getEnvironment()
{
return $this->evaluate(fn (Environment $environment) => $environment);
}
}

Action::make("action")
->action(function (Environment $environment) {
\Debugbar::alert($environment); //This works, shows the current Environment
$service = app(EnvironmentService::class);
\Debugbar::alert($service->getEnvironment()); //Blank Environment
});
Even if I call Filament's internal evaluate() method, it doesn't just resolve a model from wherever you like
Dennis Koch
Dennis Koch8mo ago
There is no magic happening. Filament passes the object around that was resolved in the beginning. The evaluate method always required that object to inject it again. This is done via defaultEvaluationParameters() method in all Filament components. If you omit the record, it resolved a new one from the container as Laravel does. Tl;DR: Filament's DI is pretty much the same as Laravels (other thtat it can resolve based on type, too). And it needs context to inject the right stuff otherwise it get's resolved from the containers.
Malcolm Turntbull
Is there any documentation on that "context"? I would love to be able to add it to my article, and maybe help the next idiot like me who doesn't know when to quit 😛
Dennis Koch
Dennis Koch8mo ago
That’s the defaultEvaluationMethods I mentioned before.
Malcolm Turntbull
I've done some digging, and so far I've worked out that essentially, the "context" is that you're evaluating the closure on an object that has $record set. so in my instance, the first one worked because it was on an $infoList, which has its record already defined now to try and find where the infoList gets that record in the first place... although for the sake of explaining "where" you can use this sort of injection, I think I probably know enough to explain it
Dennis Koch
Dennis Koch8mo ago
It's injected into the Livewire component by route model binding Then passed to the infolist. Then injected to all the methods that request it
Malcolm Turntbull
ok, that actually makes a lot of sense!