Over the course of building a Laravel application, most developers utilize numerous composer packages to implement common functionality. It's important to think about future-proofing these 3rd-party dependencies or you may find yourself battling considerable technical debt in the future.
One way to do this is to apply the DRY principle to your dependencies. Keep them in one place, so that if you ever need to change, upgrade, or replace a 3rd-party package, you can do it with minimal headache. I'll explore a couple strategies for doing this in the rest of the article.
Note: This is by no means exclusively applicable to Laravel or even PHP, but I'll be focusing on Laravel for examples and specific scenarios for this article.
#1: Create an Interface and a Wrapper Implementation
I was once working on a codebase that used Guzzle in several places in the application. When Guzzle released a breaking upgrade, it was a lot of extra work hunting down all the places it was called to make the upgrade.
To prevent us from having to spend the same amount of time if the scenario came up again (it did, and always will), I first created a HttpClient
interface that described all the functionality we wanted to use from the new version of Guzzle. Then, I created a GuzzleWrapper
class that wraps Guzzle's Client class and adheres to the interface.
Once I added a binding in Laravel's IoC container, I replaced every place that Guzzle was called with the new Interface instead. This ensures that the next time I had to do an upgrade, I only had to do it in that one class.
One day, a new Http package might come out that performs better, or has better security, or shiny new features. All I would have to do is create a new implementation using that new package, swap out the binding, and it is good to go.
Example: Guzzle
Here is a real-world example of how you could create a wrapper for the popular Http package Guzzle. Since Guzzle uses PSR-7, we can use the ResponseInterface
included in the psr/http-message package. The wrapper implementation is incomplete, but demonstrates the concepts that can be used to create a wrapper for a 3rd-party package.
Note: Examples are written with features that are new in PHP 7.4 such as class property type declarations.
<?php
namespace Acme\Core\Http;
use Psr\Http\Message\ResponseInterface;
interface HttpClient
{
public function get(string $uri, array $options = []): ResponseInterface;
public function post(string $uri, array $options = []): ResponseInterface;
}
<?php
namespace Acme\Core\Http;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
class GuzzleWrapper implements HttpClient
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function get(string $uri, array $options = []): ResponseInterface
{
return $this->client->get($uri, $options);
}
public function post(string $uri, array $options = []): ResponseInterface
{
return $this->client->post($uri, $options);
}
}
You can add the interface and concrete implementation to a $bindings
property in one of your app's service providers.
<?php
namespace Acme\Core\Providers;
use Acme\Core\Http\GuzzleWrapper;
use Acme\Core\Http\HttpClient;
use Illuminate\Support\ServiceProvider;
class CoreServiceProvider extends ServiceProvider
{
public array $bindings = [
HttpClient::class => GuzzleWrapper::class
];
}
?>
Now you can resolve the class out of the service container anywhere you need it! Just make sure to inject the HttpClient
interface, and not the GuzzleWrapper
class directly.
<?php
namespace Acme\Feature;
use Acme\Core\Http\HttpClient;
class ApiIntegration
{
private HttpClient $httpClient;
public function __construct(HttpClient $httpClient)
{
$this->httpClient = $httpClient;
}
}
?>
You should avoice creating Leaky Abstractions to the best of your ability. In our example, we are returning a typehint of a PSR-7 compliant ResponseInterface
rather than the Guzzle Response implementation that actually gets in the hands of the caller. That way, if you change the wrapped class, as long as they are PSR-7 compliant, nothing will break. If it's not PSR-7 complaint, you can simply write a PSR-7 compliant adapter of whatever it does return.
If whatever class you are wrapping does not have a PSR standard, you can create your own wrapper class based on current behavior for its return or input classes to prevent your abstractions from leaking. The end-goal is to make sure whatever classes call your wrapper don't know the details of the 3rd-party implementation.
#2: Create a standalone wrapper class
A quicker and dirtier method would be to create a wrapper class without an interface. Almost identical to the first method, but you instead call your wrapper class as a dependency directly. This makes it harder to test, harder to change, and makes it impossible to run multiple implementations simultaneously, so I would recommend avoiding this method.
#3: Confine dependency to one class.
If your application only uses a package in one place, and will for the foreseeable future, you may not need to abstract it. The whole point is easing upgrades or changes, so if it truly will only ever live in one place like a service or job class then you gain little benefit from abstraction. Doing this can definitely be a slippery slope, as you may think it only needs to be in one place, to find that it was needed elsewhere, and you or somewhere else simply injected the 3rd-party class directly elsewhere. To be safe, I would avoid this method as well.
Considerations
Write Everything Twice
It's important to protected against premature optimizations. This article shares some good ideas regarding when exactly is a good time to abstract something and apply the DRY principle.
https://dev.to/wuz/stop-trying-to-be-so-dry-instead-write-everything-twice-wet-5g33
Avoid Laravel Facades
While Facades are technically okay since they resolve out of the IoC container, I still prefer to inject the dependencies into the constructor. It makes it a bit easier to tell exactly what dependencies a class has, and helps improve testability in certain scenarios.
Wrap Laravel Helper Methods
While I don't think it's beneficial to wrap all the Laravel dependencies you use unless you suspect you'll one day migrate out of Laravel, which in some cases might be something worth considering, I would consider wrapping Laravel's helper methods if you plan to use them. When the str*()
methods got deprecated in favor of Str::*()
, I had to spend an hour or two between all my codebases making sure that I swapped all of them out.
I should have created a wrapper class for them so that I can always rely on only changing them in one place should they ever change again.
Conclusion
Creating abstractions for 3rd-party dependencies is a bit of a time investment to get setup, but it provides a lot of benefits and saves time and headache in the future. As always, thanks for reading, and leave your comments and questions in the section below.
The post Abstracting Laravel Dependencies appeared first on Matt Does Code.
Top comments (0)