Laravel: Dependency Injection on methods and closures
Hey, you can type-hint your dependencies on everything!
If you have been using a PHP framework with a Service Container for a while now, you may have noted that you can use Dependency Injection on almost anywhere, easing most of the hard-wiring in a constant manner. In Laravel, you note this when instantiating classes like Controllers, Middlewares, Jobs, Listeners, and even Notifications.
Controllers methods are also called using type-hinting, which means you can put the class you need in the method and the Service Container will resolve it automatically. Pretty much like you do when you receive a Request and you want to validate some input:
public function post(Request $request)
{
$request->validate([
// ...
]); // .. do something}
Okay. Wouldn’t be nice if you could do the same with your code?
Well, the Service Container allows this out of the box.
Call this method with magic
The Service Container can be called using the app()
method, since this is basically a helper that returns the Application instance, which extends the original Container class. Most of the time you’re dealing with this instance when asking something to the application, like the environment its on.
Going back to the magic, the call()
method is what we’re looking for, which in return calls the BoundMethod
class — a bunch of static helpers to check what and how to call something. The documentation in the source code says you can push a Closure or a class name with the method to call and it will automatically execute it while injecting its dependencies.
I’m gonna put this in sections, so you don’t get lost looking for a particular way of using this method.
Using a simple string
So, for example, we will call a class method using the class name and method like this:
$result = app()->call('App\MyClass@myMethod');
Wait a moment… Is this like defining a Route? Well, yeah, the Router uses the same syntaxis, but weirdly it doesn’t use the call method directly but uses its own way to do it. Anyway, that’s for another article.
The call()
method will automatically resolve the class, inject dependencies if it asking for them, and do the same to the method itself. No need to instance the class manually:
class MyClass
{
protected $foo; public function __construct(Foo $foo)
{
$this->foo = $foo;
} public function myMethod(Bar $bar)
{
return $foo->doSomethingWith($bar);
}
}
Why would you do this? Well, this can become very convenient to not clog your class full of dependencies in the __construct()
, specially when you need only some some in particular places, like when it happens when using Controllers methods. Also, a method may need only one thing, while in the construct it will be always instantiated no matter what method is hit later.
The “problem” with this technique is that is just for one-time only uses. In other words, once the container class fires the method for you, no class instance will be saved inside the Service Container, but instead, you will get the method result. The only exception for not-saving-the class is when you registered the class as “shared” (singleton) beforehand, like it happens inside Service Providers:
public function register()
{
// When someone calls for this class, anywhere, ensure
// the instance is saved inside the container so we
// can use it in other parts of the application.
$this->app->singleton(\App\MyClass::class);
}
When you need the class instance to do something more, you’re better of instantiating the class separately and then calling the method manually, or asking the Service Container to do it for you.
Using an instance
Luckily, if you already have your class instanced, then you can just tell the container to use the instance and call the method by putting them both inside an array.
$instance = new MyClass($foo);$result = app()->call([$instance, 'myMethod']);$instance->doSomethingMore();
Okay, that’s nice, the container will inject whatever the method needs automatically.
Again, instantiating the class is up to you. Personally, I would avoid calling the Service Container unless strictly necessary.
Adding parameters
What about parameters? What if you need to put something that the Service Container wouldn’t guess. Easy, we can just issue them as an array:
$result = app()->call('App\MyClass@myMethod', [
'red',
'cool',
]);
Hey, but what if we need to add a class that uses Dependency Injection? No problem, you don’t need to instance the class manually or call the Service Container to resolve it beforehand. Just add it and the call()
method will automatically call the container to resolve the dependencies of that class if needed.
$result = app()->call('App\MyClass@myMethod', [
'status' => 'good',
'cool' => true,
'service' => SuperService::class,
]);
Some parameter shenanigans: Always try to use
$key => $value
for setting parameters. The framework may not guess properly what goes where if you have optional parameters.
Obviously, it will respect the default variable value only if it can’t be instanced. For example, if the a method expects an instance of something, but it’s null
, and you don’t pass neither an instance or the class name, the Service Container will try to resolve it. If you really want to pass null, just pass null.
app()->call('App\MyClass@myMethod', [
'service' => null,
]);
Default method
The call method accepts a third parameter which is a default method to call. In other words, if you dare to only call a class without pointing the method name, a default will be used.
$result = app()->call('App\MyClass', [
// ... may be some arguments
], 'defaultMethod');
This default method is good when you don’t have control on what the developer will put there, like just the class name, or you may expect an Invokable class (more on that later). In the latter case, the third method it’s a way to cover your ass if you’re making a package.
Calling closure, callbacks and others callables
Wait, what? You can call Closures? You can inject dependencies on a Closure? WHAT SORCERY IS THIS? Okay, calm down, you can and its awesome.
When the method receives callable, like a Closure or an invokable class, it will use the service container to resolve the arguments of it. Let’s start with the first.
Methods and Closures
Callables are very useful when you need to inject executable logic for later. The Service Container can resolve it like it was nothing.
$closure = function (Foo $foo) {
return $foo->doSomething();
};$result = app()->call($closure);
If the closure needs parameters, you can use them with no problem:
$closure = function (Foo $foo, string $bar) {
return $foo->doSomethingWith($bar);
};$result = app()->call($closure, [
'bar' => 'rofl',
]);
Obviously, when using a Closure or any other callable, you won’t be able to use a default method, but in that case you may want to replace it for your own default callable if none is received before the actual call.
Invokables
Invokables Classes, those that implement an __invoke()
method, are sort of left in limbo. If you issue an invokable class name, you will receive an exception:
ReflectionException : Function MyInvokableClass() does not exist
This is because the Class name is mistaken by a function name, so when its called, PHP will try to get the function name which doesn’t exists. Don’t get me wrong, but if you plan to call an invokable class, it’s recommended to put your dependencies in the method signature,
<?phpnamespace App;use \Illuminate\Contracts\Config\Repository as Config;class MyInvokableClass
{
public function __invoke(Config $config)
{
return $config->get('app.env');
}
}
and then call it by just instancing it:
app()->call(new MyInvokableClass);
And I hope you use this power with responsibility.