Introduction
The concept of interfaces is a popular topic when we're talking about the design patterns. If you are an experienced developer, you should already know about design patterns in object-oriented programming. In case you don't know, that's fine because it all comes down to what kind of work you did, and what kind of work you are planning to do.
When I was building WordPress plugins and themes in the past, I didn't even think about interfaces because I've never had a use case for that. But when I've started getting into Model–view–controller (MVC) pattern, I've started learning design patterns because I had a need for that.
I'm not going to touch the design patterns in this article because my main focus will be on interfaces. I'll give you a clear example of how to abstract SMS service from the business logic and apply an interface for that.
The main goal
If you've started reading this article, I assume you already know what an interface is and how to define an interface in PHP. This article doesn't provide you with any information about the basics of PHP and Laravel framework.
But instead, you'll get a clear explanation of what an interface is and when to use it. The main goal is to explain to you the concept of the interfaces and convince you to start abstracting logic that might change in the future.
I will be using PHP version 8.0.20, but the version doesn't really matter at this point because we are looking at use cases for interfaces in Laravel and not the latest PHP features.
Understanding the concept
Before we delve into talking about Laravel, let's understand the concept of the interface and what problems it solves for us. In Object-Oriented Programming, an interface describes the actions of the class. Every class that implements an interface must have the exact methods described in the interface.
The important thing here is that the interface only describes the actions. They do not have any logic, but only method names and types.
A great example is USB-C that I have to charge my smartphone. My USB-C charger doesn't care what to charge as long as you plug a USB-C cable into it. I can charge a JBL speaker, the watches, the phone, the e-book reader and more. As long as those devices support USB-C interface, I'll be able to charge them.
Let's write my example in PHP code just to have a visual to better understand it.
<?php
interface UsbC {
/**
* Connect USB to a device or charger
*
* @return bool Successful connect or not
*/
public function connect(): bool;
/**
* Disconnect USB from the device or charger
*
* @return bool Successful disconnect or not
*/
public function disconnect(): bool;
}
Tip. Tip. It's recommended to always add descriptive comments for each method declaration inside interfaces for a better understanding and clarity.
Interface in action
Say we have a Pigeon class and an Eagle class. Each of these classes should have a method fly
because that's what all the birds do. To ensure that we have a method fly on both classes, we create an interface Bird, and make Pigeon and Eagle implement it.
<?php
interface Bird {
public function fly(): string;
}
class Pigeon implements Bird {
public function fly(): string {
return 'Pigeon is flying';
}
}
class Eagle implements Bird {
public function fly(): string {
return 'Eagle is flying';
}
}
function forceBirdToFly(Bird $bird): string {
return $bird->fly();
}
echo forceBirdToFly(new Pigeon());
The output of this code is Pigeon is flying
because we've passed the Pigeon class to a forceBirdToFly
function. We can pass any bird that we want as long as our bird class will be implementing the Bird interface.
Tip. Tip. In the Laravel community, interfaces are often called contracts. Contracts and interfaces are actually not the same thing. You can read about that in Laravel Contracts documentation on Laravel website.
In forceBirdToFly
function we accept not only one specific class but any class as long as it is implementing the Bird interface. Do you see how simple and useful the interfaces really are?
Installing the latest Laravel
We'll see how we can integrate our knowledge about interfaces inside the Laravel application.
I will install the latest Laravel version with Laravel installer by running a laravel new using-interfaces
command in the terminal. All the code from the article will be on GitHub in the repository called using-interfaces.
You should be able to just run php artisan serve
in your project directory and see the default Laravel welcome page on http://localhost:8000
.
And also, we'll install Twilio SDK via composer by running composer require twilio/sdk
command.
Twilio registration
Another great example is when we use an SMS-service like Twillio, which lets us send SMS messages to any phone number. It's incredibly useful with authorization via code confirmation, where a user fills-in the phone number and gets a code confirmation on the phone.
Twilio is a paid service, fortunately for us, they have a trial where you can use their service for free for certain amount of SMS messages. And I'm not sponsored by them, I just like this service because it's easy to work with and implement it in PHP apps.
I'm quickly going to register a Twilio account just for this tutorial because I want to show you an example of a working app and not just an abstract example that I haven't tested myself.
After I've filled the form on the register page, I've met with this page.
Click the button “Get a trial phone number” to have some number. In Account Info section we have Account SID and Auth Token which we will copy to our configuration files along with My Twilio phone number field that will appear after getting a trial phone number.
SMS service example
I'll go and copy the Account info to variables in my .env
file.
TWILIO_ACCOUNT_SID=AC45e3882289faa800153fa083e8e06c91
TWILIO_AUTH_TOKEN=XXX
TWILIO_NUMBER=+12055962XXX
I'll also update config/services.php
by adding Twilio configurations:
'twilio' => [
'sid' => env('TWILIO_ACCOUNT_SID'),
'token' => env('TWILIO_AUTH_TOKEN'),
'number' => env('TWILIO_NUMBER'),
],
To make sure, Twilio's configurations are properly set up, let's create a controller SmsController and the route /send-sms
in the Laravel app.
php artisan make:controller Api/SmsController
Define a route in routes/api.php
:
<?php
use App\Http\Controllers\Api\SmsController;
use Illuminate\Support\Facades\Route;
Route::get('send-sms', [SmsController::class, 'sendSms']);
I'll navigate to the SmsController and quickly create a sendSms
action, just to test the route in a browser.
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
class SmsController extends Controller
{
public function sendSms(): string
{
return 'works';
}
}
Great, when I test the route in a browser by going to http://localhost:8000/api/send-sms
, I get “works”. It's time to create a TwilioSmsService class that will be responsible for sending SMS messages. I'll create a app/SmsServices/TwilioSmsService.php
file with logic that allows sending SMS messages.
<?php
declare(strict_types=1);
namespace App\SmsServices;
use App\Exceptions\SmsServiceException;
use Throwable;
use Twilio\Rest\Client;
class TwilioSmsService
{
public function sendMessage(string $phone, string $message): void
{
$sid = config('services.twilio.sid');
$token = config('services.twilio.token');
if (!$sid || !$token) {
throw new SmsServiceException('Check if you have correct configurations for twilio service');
}
try {
$twilio = new Client($sid, $token);
$twilio->messages->create($phone, [
'from' => config('services.twilio.number'),
'body' => $message,
]);
} catch (Throwable $e) {
throw new SmsServiceException($e->getMessage());
}
}
}
This code is self-explanatory and doesn't need any explanation. We can call this class in our newly created controller.
public function sendSms(TwilioSmsService $service): void
{
$service->sendMessage('<your-phone>', 'Test message');
}
Before we see that it works, let's not forget to create a custom exception.
php artisan make:exception SmsServiceException
To make sure that it works, you can put your phone number instead of <your-phone>
to see if you get an SMS on your phone. I'll put mine, and visit http://localhost:8000/api/send-sms
. Here is what I got:
It works nicely and with small amount of code. In my case, I'm just calling sendMessage
method in our controller action without any logic around it. Of course, I can change the content of the sendSms
action so that it looks more like a real-world one, but I will leave it like this to simplify this article.
Tip. Tip. In Laravel world, an action is usually a method that takes care of a specific route.
In one of my projects, I have an authentication by a code confirmation. The user types in his phone number, receives a randomly generated code, types in the code into a dialog window and enters the personal cabinet.
It's a pretty common confirmation system.
As we saw, it's relatively easy to add something like Twilio to your project, but we forgot one rule. When you are building an app, the last thing you want is to make it depend on external service. The only thing the application should know about the SMS service, is that it can send messages.
Why is that? One of the reasons is that you might switch from Twilio to Vonage for example. And what's going to happen is that you will go back to a TwilioSmsService class, rename it to VonageSmsService and change the code in it to match the Vonage API.
By doing that, you'll break the Open-closed principle, which states:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Bertrand Meyer
I won't go into details about why should you care about all of these principles in object-oriented programming, but I'll tell you this, people who came up with SOLID principles have tons of experience in software development and architecture.
It's probably a good idea to follow advices of such experience software developers if we want to grow individually and as the whole industry.
Knowledge has to be improved, challenged, and increased constantly, or it vanishes.
Peter Drucker
I love this quote from Peter Drucker, maybe it's a little random, but it certainly underlines the reason behind learning all of those lessons from dinosaurs of our industry about SOLID, DRY, Clean Code, Design Patterns, Data structures, etc.
Let's continue with our SmsController and replace the dependency from TwilioSmsService to something general, something that is not so specific. Let's call the dependency as SmsService.
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\SmsServices\SmsService;
class SmsController extends Controller
{
public function sendSms(SmsService $service): void
{
$service->sendMessage('<your-phone>', 'Test message');
}
}
We obviously don't have the SmsService yet, let's create an app/SmsServices/SmsService.php
interface with only one method declaration.
<?php
declare(strict_types=1);
namespace App\SmsServices;
use App\Exceptions\SmsServiceException;
interface SmsService
{
/**
* Send SMS message with given message content to a given phone number
*
* @param string $phone
* @param string $message
*
* @throws SmsServiceException
*/
public function sendMessage(string $phone, string $message): void;
}
Notice that we say that the sendMessage
method also throws an SmsServiceException. That's also important to generalize the exception into our custom one because different services throw different exceptions.
That's why we wrapped our Twilio logic that send message into a try-catch block previously.
try {
$twilio = new Client($sid, $token);
$twilio->messages->create($phone, [
'from' => config('services.twilio.number'),
'body' => $message,
]);
} catch (Throwable $e) {
throw new SmsServiceException($e->getMessage());
}
In this case, everything that is thrown from Twilio SDK will be an SmsServiceException.
Let's not forget and add implementing SmsService interface to a TwilioSmsService.
class TwilioSmsService implements SmsService
{
public function sendMessage(string $phone, string $message): void
{
// ....
We are doing it the same way as we did with Pigeon and Eagle classes, they both implement a Bird interface, but here we implement SmsService.
Now is the critical part, how do we tell the Laravel to use TwilioSmsService each time when it sees the SmsService type in our controller action parameter?
Luckily enough, it's pretty easy in Laravel. We need to go to an AppServiceProvider
and register a binding.
<?php
declare(strict_types=1);
namespace App\Providers;
use App\SmsServices\SmsService;
use App\SmsServices\TwilioSmsService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SmsService::class, TwilioSmsService::class);
}
}
This line does exactly what it says, it binds our SmsService to the TwilioSmsService. That's why so many developers love Laravel, it's comprehensible and easy to understand.
So, let's make our old request to a http://localhost:8000/api/send-sms
and see if it works.
Great, I'm getting the second message on the phone. Our logic hasn't changed, it still works the same way as before, except that, now we can switch Twilio service to something else not touching the existing code.
Our classes are open for extension, but closed for modification. To prove it, I will switch from Twilio to file logging. You often do that for paid services, instead of wasting money on SMS messages you could temporarily replace the logic of sending request from Twilio to a file logger.
I'll show you what I mean. I've created an app/SmsServices/LoggerSmsService.php
file with this content:
<?php
declare(strict_types=1);
namespace App\SmsServices;
class LoggerSmsService implements SmsService
{
public function sendMessage(string $phone, string $message): void
{
logger()->debug($message, ['from' => config('services.twilio.number')]);
}
}
It's another SmsService that implements our interface, it means that it must have a sendMessage
method with the exact parameters and return type.
At this point, we have two SMS services and one interface in our app/SmsServices
directory.
Our app is currently using TwilioSmsService because we bound it in AppServiceProvider. Let's change the binding to LoggerSmsService instead.
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SmsService::class, LoggerSmsService::class);
}
}
That's how it would look. Sure enough, I get this message in storage/logs/laravel.log
instead of SMS message when I test the route:
[2022-07-22 11:55:40] local.DEBUG: Test message {"from":"+12055962982"}
Do you see how easy it is to add a new SMS service in the future? Let's say we want to switch from file logging to a database logging.
- We create a file
app/SmsServices/DatabaseSmsService.php
; - Implement the SmsService interface;
- Create a method
sendMessage
where we write all the logic for saving a message; - In the AppServiceProvider we change the binding to the DatabaseSmsService;
It might seem as if it's a lot of steps, but at least you follow good practices and don't break any SOLID principles.
Tip. If you are eager to learn more about Laravel and PHP, I highly recommend using Laracasts. I've been watching Laracasts for a while, and it's been growing very rapidly thanks to a remarkable team of content creators there.
Conclusion
As we saw in this article, interfaces is a compelling tool which cannot be replaced by anything else. Laravel is an object-oriented PHP framework that uses interfaces everywhere in its core.
Knowing how and when to use them is significant for everyone who's working with object-oriented programming.
❤️ Thanks to Mohammad Rahmani for the beautiful photo for this post that I've modified a little just to match the theme of the post. Thank you, Mohammad.
🧑💻 Link to the code on GitHub: github.com/SerhiiChoBlog/using-interfaces
Keywords: twilio, sms, backend, drucker, meyer