Introduction
You've probably heard of — or even use on a daily basis — libraries or applications in laravel that implement "dependency inversion", but have you ever taken the time to understand what it's actually for, or looked for the best way to apply it in your own project? in this article, we'll explore what dependency inversion is, why it matters, and the best ways to implement it in your laravel project.
Dependency Inversion
dependency inversion is a fundamental software design principle that aims to increase code flexibility and maintainability. in short, it suggests that high-level classes should not depend directly on low-level classes — instead, both should depend on abstractions.
but what does that actually mean in practice?
imagine the following scenario: we have an application where one of the core business rules is sending sms messages to multiple users in the system. this creates an external dependency for something that is actually an internal rule of the system. so how can we address this?
a practical and efficient solution would be to implement multiple sms delivery systems. that way, if one service goes down, another can be activated to send the message to the user.
Usage Example
now you might be thinking, "couldn’t this just be done with a simple if-else?" — and technically, yes, it could. but now think in terms of maintainability and scalability: what if we need to integrate 10 more sms providers? or what if we need to change the behavior of just one specific provider?
with that in mind, a much better approach would be to create a separate class for each sms delivery system. this way, when one fails, we can easily switch to another — and our code remains clean, modular, and easy to maintain.
namespace App\Clients\Sms;
use App\Models\User;
class TwilioClient
{
public function send(User $user): void
{
// send sms using twilio sdk
}
}
example of the twilio provider for sending messages
namespace App\Clients\Sms;
use App\Models\User;
class VonageClient
{
public function send(User $user): void
{
// send sms using vonage sdk
}
}
example of the vonage provider for sending messages
you can notice that they both share the same responsibility, even having the same function names — the only difference is the implementation of each provider. in this way, they can be summarized into a single thing, right?
if you thought that, congratulations — you’ve just grasped the concept of dependency inversion. this single “thing” that we use to unify our providers is an interface, which determines which provider should be used during the project’s execution.
in our case, we’ll use laravel as an example, but you can apply these concepts in any framework and any programming language.
namespace App\Clients\Sms\Contracts;
use App\Models\User;
interface SmsClientContract
{
public function send(User $user): void
}
this interface should be implemented by all sms sending providers — in our case, it should be implemented in the
Twilioclient
andVonageclient
classes.
now, we need to inform laravel that our interface refers to a specific provider. this way, whenever we need to use any of these providers, we just bind it to the interface through a service provider. then, every time this interface is referenced in the code, laravel will know to instantiate the specified class. example below:
namespace App\Providers;
use App\Clients\Sms\Contracts\SmsClientContract;
use Illuminate\Support\ServiceProvider;
use App\Clients\Sms\{
TwilioClient,
VonageClient
};
class SmsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SmsClientContract::class, TwilioClient::class);
}
}
now, you just need to call the reference (the interface) somewhere in your code, either by dependency injection or by creating an instance from the application. laravel will always return a new instance of the class you registered in the service provider’s register
method.
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use App\Clients\Sms\Contracts\SmsClientContract;
class UserController extends BaseController
{
public function __construct(
private SmsClientContract $smsClient
) { }
public function sendSms(int $userId)
{
$user = User::findOrFail($id);
$this->smsClient->send($user);
return response()->noContent();
}
}
in the example above, the controller retrieves the user and sends the sms through the twilio
provider, since that’s the one registered in the service provider. notice how this makes the code more modular? whenever twilio is down, you can simply push a hotfix by changing the registration in the service provider to point to the vonage client — restoring the system without needing a new implementation.
dependency inversion is so modular that, in this case, you could even implement a validation flow before registering twilio, checking if it’s working properly before binding it. plus, you can safely use the sms client anywhere in your code with its public methods through the interface.
Conclusion, is this really necessary?
throughout my years of experience in web development, i've noticed that, over time, people tend to aim for more modularized projects, since that makes maintenance much easier. however, it's important to highlight that not everything actually needs to be modularized.
for example, one of the most common cases in laravel applications is creating separate containers for raw sql query builders and another for eloquent. but why is that a problem? actually, it's not as bad as it seems — it just doesn't make much sense.
laravel is a php framework designed with large, well-structured projects in mind. the framework itself offers solid tools for database interaction, which are unlikely to cause issues as long as your project is running on a secure and up-to-date version.
that means creating different providers to handle something that the framework already guarantees may just end up being unnecessary extra work.
Top comments (0)