Back to Course |
Design Patterns in Laravel 11

"Strict" Services: Interfaces, Injection, Strategy Pattern

After looking at Services, let's look at a Service example that gets us closer to the "classical" design patterns.

It's an example from our previous lesson. Remember TwitterService, auto-injected into the Controller?

app/Http/Controllers/PublishPostController.php:

use App\Models\Post;
use App\Services\TwitterService;
 
class PublishPostController
{
public function __invoke(Post $post, TwitterService $twitter)
{
// ... all other operations with `$post`
 
$tweetResponse = $twitter->tweet($tweetText);
 
if (! isset($tweetResponse['data']->id)) {
return;
}
 
$tweetUrl = "https://twitter.com/freekmurze/status/{$tweetResponse['data']->id}";
 
$post->onAfterTweet($tweetUrl);
 
$post->update(['tweet_sent' => true]);
}
}

TwitterService is a class built for interactions with Twitter via their external API. Look what may be inside that service:

app/Services/TwitterService.php:

namespace App\Services;
 
use Abraham\TwitterOAuth\TwitterOAuth;
 
class TwitterService
{
public function tweet(string $text): ?array
{
if (! app()->environment('production')) {
return null;
}
 
$twitter = new TwitterOAuth(
config('services.twitter.consumer_key'),
config('services.twitter.consumer_secret'),
config('services.twitter.access_token'),
config('services.twitter.access_token_secret')
);
 
return (array) $twitter->post('tweets', compact('text'));
}
}

In this case, it uses a third-party package for OAuth with Twitter. All good, right?

Now, two questions:

  1. What if that package gets abandoned?
  2. What if Twitter's official API changes again? (hi Elon)

Then, this Service class would probably need to find and use another package or use the Twitter API directly to post a tweet.

So then, you have a choice:

  • Change code inside the same TwitterService class
  • Or, create another new TwitterAPIService class, for example, for the new API, while the old one may still be working

To enable both, we need a mechanism to easily replace one Service class with another. This is where design patterns come in handy.


Introducing the Interface

First, we add the rules for what our Service should contain.

In this case, all we need is a tweet($text) method which may return array or NULL.

So, we create this:

app/Interfaces/TwitterServiceInterface.php:

namespace App\Interfaces;
 
interface TwitterServiceInterface {
 
public function tweet(string $text): ?array;
 
}

Notice: There is no php artisan make:interface command in Laravel. You need to create the PHP file manually in your IDE.

Then, the old Service should implement the new interface.

app/Services/TwitterService.php:

namespace App\Services;
 
use Abraham\TwitterOAuth\TwitterOAuth;
use App\Interfaces\TwitterServiceInterface;
 
class TwitterService implements TwitterServiceInterface
{
public function tweet(string $text): ?array
{
if (! app()->environment('production')) {
return null;
}
 
$twitter = new TwitterOAuth(
config('services.twitter.consumer_key'),
config('services.twitter.consumer_secret'),
config('services.twitter.access_token'),
config('services.twitter.access_token_secret')
);
 
return (array) $twitter->post('tweets', compact('text'));
}
}

Now, when we create our NEW class of another Service, we need to implement the same interface with the method tweet():

app/Services/TwitterAPIService.php:

namespace App\Services;
 
use App\Interfaces\TwitterServiceInterface;
 
class TwitterAPIService implements TwitterServiceInterface
{
public function tweet(string $text): ?array
{
// ... Some other implementation via Twitter API
}
}

Now, the final question: how do we enable/disable one or another Service from the Controller?

An easy answer would be to change two lines: type-hint in the method and use on top?

use App\Services\TwitterService;
use App\Services\TwitterAPIService;
 
class PublishPostController
{
public function __invoke(Post $post, TwitterService $twitter)
public function __invoke(Post $post, TwitterAPIService $twitter)
{

But this is not the "design pattern" strict way.

A few potential problems:

  1. What if there are some external conditions outside that Controller? For example, can one Service be enabled on the local server while another is on the production server?
  2. Also, what if we use that Service in multiple Controllers? To tweet about something other than Posts? Then, do we need to make the change in multiple files?

Controller should not know which Service to use. It just needs to see the interface, and then the configuration somewhere else decides which Service to enable.

So, we type-hint the interface instead.

use App\Services\TwitterService;
use App\Interfaces\TwitterServiceInterface;
 
class PublishPostController
{
public function __invoke(Post $post, TwitterService $twitter)
public function __invoke(Post $post, TwitterServiceInterface $twitter)
{

Now, how/where do we decide which Service to use?


Binding Interface with Classes: Strategy Pattern?

This is usually done in the AppServiceProvider of Laravel, which configures classes.

For example, if you want to use TwitterService on production but start testing the new TwitterAPIService locally without breaking anything, here's one option.

app/Providers/AppServiceProvider.php:

use App\Interfaces\TwitterServiceInterface;
use App\Services\TwitterService;
use App\Services\TwitterAPIService;
 
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
if (app()->isProduction()) {
$this->app->bind(TwitterServiceInterface::class, TwitterService::class);
} else {
$this->app->bind(TwitterServiceInterface::class, TwitterAPIService::class);
}
}

Guess what: we implemented a strategy design pattern here!

And no, we didn't use any "strategy" word here, but this is the thing about design patterns: you often use them without even realizing or specifically aiming to use them.


So yeah, if you have a Service for using a third-party API or package that is likely to change, it may be beneficial to create an interface for it.

Then, developers implementing the changes in the future would know the rules for that Service and be able to replace it with the future one.

In the next lesson, let's examine another familiar design pattern: Observer.