I want to resolve a class instance by a string which has dependency in its constructor from the container. I want to pass the constructor dependency in when I resolve the class.
A simple example I tested: Foo is the class I want to resolve and $id is a constructor argument:
class Foo
{
public function __construct(public $id)
{}
}
Binding in the service container:
$this->app->bind('foo', function($app, $id) {
return new Foo($id);
});
And resolving it from the container:
$foo = App::makeWith('foo', ['id' => 1]);
$foo is then resolved as
Foo
{
+id: ["id" => 1],
}
Which shows that the public property $id is set as an array.
However, what if I don't want to have $id as an array, but an int?
I'd have to do the binding as follows:
App::bind('foo', function($app, $arg) {
return new Foo($arg['id']);
});
This feels extremely hacky. Is this an abuse of the service container?
Some may argue that it's not a good idea to pass in a constructor argument when resolving from the service container, rather make it stateless or use a setter. But if many methods in Foo will be using, say $id, it is most convenient and best practice to pass $id, as a constructor right?
As the user mrhn already replied with the solution, I am going just to explain WHY you are getting an array instead of an int in your case.
First of all, ALWAYS type hint want you want:
class Foo
{
public function __construct(public int $id) {}
}
See that I have type hinted int for $id.
Secondly, you are getting an array because, you have a custom "instantiator" (a closure), your mistake is that, $id is not $id but $parameters.
See the source code, it is doing: $concrete($this, $this->getLastParameterOverride()); so, in your code (as below), you are passing the first parameter ($app) as $this and the second one ($id) as $this->getLastParameterOverride() and this last method will return the array, hence ['id' => 1] instead of 1.
$this->app->bind('foo', function($app, $id) {
return new Foo($id);
});
So, as the other used said, ALWAYS reference to concrete classes hence app(Foo::class, ['id' => 1]); will fix your problem (my way is less cumbersome but does exactly the same).
You are doing custom bindings and off course there you would have to parse the arguments yourself. Instead why not just let the straight out of the box binding handle it? e.g. delete your current binding and use the class bindings.
app()->makeWith(Foo::class, ['id' => 1]);
Nothing more is needed then the following, tested and ran on a Laravel 8 project.
Secondly the container design and parameters has always been flakey by design. Coming from Java / C# background in academia and moving to Laravel in my professional career. I was often facing the same problem as you here.
The solution in my opinion is to avoid this approach and often solve it with a fluent service design instead.
class Foo {
private $id;
public function setId($id) {
$this->id = $id;
}
}
Set the id with a setter instead, now you will have easily container instantiating, instead you have to do some working invoking the class.
resolve(Foo::class)->setId($model->id);
This can seem redundant, but i think this code looks way better than this. The approach also alleviate some of the problems you can run in to, if constructors does to much work as Laravel as a framework need to instantiate classes to do certain things (routes needs to instantiate controllers, comes to mind).
$foo = App::makeWith('foo', ['id' => 1]);