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:
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:
TwitterService
classTwitterAPIService
class, for example, for the new API, while the old one may still be workingTo enable both, we need a mechanism to easily replace one Service class with another. This is where design patterns come in handy.
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:
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?
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.