/laravel-sms

Adds the ability to send text messages through various drivers.

Primary LanguagePHPMIT LicenseMIT

Laravel SMS

Latest Stable Version Laravel Version Build Status

Introduction

This package provides a clean, simple service that integrates with SMS drivers, allowing you to quickly get started sending mail through a local or cloud based service of your choice.

Several driver implementations have been offloaded into separate packages so that you only have to include the integration for the drivers you need. Only drivers that don't require third party packages are included by default.

Supported Drivers

This package was also built such that anyone can add or override existing drivers, so that you aren't limited by what is provided out of the box.

Configuration

The sms services may be configured via the sms configuration file. Each sms provider configured within this file may have its own options and even its own unique "transport", allowing your application to use different sms services to send certain sms messages. For example, your application might use Twilo to send transactional text messages while using Nexmo/Vonage to send bulk text messages.

Driver Prerequisites

The API based drivers such as Nexmo and Zenvia require the Guzzle HTTP library, which may be installed via the Composer package manager:

composer require guzzlehttp/guzzle

Additional drivers that require third party packages are not provided by default. You will need to install the driver specific package (which will include the third party dependencies for you). You can refer to the list of Supported Drivers to see which package you need to install.

Email Driver

To use the Email driver, first ensure that your mailer is set up correctly. You may use any mail driver integrated with Laravel.

Twilio Driver

To use the Twilio driver, first install the driver specific package:

composer require reedware/laravel-sms-twilio

Then set the default option in your config/sms.php configuration file to twilio. Next, verify that your twilio provider configuration file contains the following options:

'your-driver-name' => [
    'transport' => 'twilio',
    'account_sid' => 'your-twilio-account-sid',
    'auth_token' => 'your-twilio-auth-token'
],

If you are not using the "US" Twilio region, you may define your region id in the provider configuration:

'your-driver-name' => [
    'transport' => 'twilio',
    'account_sid' => 'your-twilio-account-sid',
    'auth_token' => 'your-twilio-auth-token',
    'region' => 'sg1' // singapore
],

Additionally, ssl host and peer verification is disabled by default. To enable this, you may include the verify flag in the provider configuration:

'your-driver-name' => [
    'transport' => 'twilio',
    'account_sid' => 'your-twilio-account-sid',
    'auth_token' => 'your-twilio-auth-token',
    'verify' => true
],

Additional instructions can be found in the Laravel SMS Twilio package documentation.

Generating Textables

Each type of text message sent by your application is represented as a "textable" class. These classes will be stored in the app/SMS directory by default. You can generate a new textable using the make:sms command:

php artisan make:sms OrderShipped

Writing Textables

All of a textable class' configuration is done in the build method. Within this method, you may call various methods such as from, to, and view to configure the message's presentation and delivery.

Configuring the Sender

Using the from Method

First, let's explore configuring the sender of the text message. Or, in other words, who the message is going to be "from". There are two ways to configure the sender. First, you may use the from method within your textable class' build method:

/**
 * Build the message.
 *
 * @return $this
 */
public function build()
{
    return $this->from('9995551234')
                ->view('sms.orders.shipped');
}

Using a Global from Address

However, if your application uses the same "from" number for all of its text messages, it can become cumbersome to call the from method in each textable class you generate. Instead, you may specify a global "from" number in your config/sms.php configuration file. This address will be used if no other "from" address is specified within the textable class:

'from' => '9995551234',

Alternatively, you may also define a "from" number for the sms provider specifically:

'your-driver-name' => [
    'transport' => 'email',
    'from' => '9995551234'
],

Configuring the View

Within a textable class' build method, you may use the view method to specify which template should be used when rendering the text message's contents:

/**
 * Build the message.
 *
 * @return $this
 */
public function build()
{
    return $this->view('sms.orders.shipped');
}

You may wish to create a resources/views/sms directory to house all of your sms templates; however, you are free to place them wherever you wish within your resources/views directory.

Plain Text Messages

If you would like to define your message as pre-rendered text, you may use the text method. Unlink the view method, the text method the already rendered contents the message.

/**
 * Build the message.
 *
 * @return $this
 */
public function build()
{
    return $this->text('Your order has been shipped!');
}

View Data

Via Public Properties

Typically, you will want to pass some data to your view that you can utilize when rendering the text message. There are two ways you may make data available to your view. First, any public property defined on your textable class will automatically be made available to the view. So, for example, you may pass data into your textable class' constructor and set that data to public properties defined on the class:

<?php

namespace App\SMS;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Reedware\LaravelSMS\Textable;

class OrderShipped extends Textable
{
    use Queueable, SerializesModels;

    /**
     * The order instance.
     *
     * @var \App\Models\Order
     */
    public $order;

    /**
     * Creates a new message instance.
     *
     * @param  \App\Models\Order  $order
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Builds the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('sms.orders.shipped');
    }
}

Once the data has been set to a public property, it will automatically be available in your view, so you may access it like you would access any other data in your Blade templates:

<div>
    Price: {{ $order->price }}
</div>

Note: A $message variable is always passed to sms views, so you should avoid passing a message variable in your view payload.

Via the with Method:

If you would like to customize the format of your text message's data before it is sent to the template, you may manually pass your data to the view via the with method. Typically, you will still pass data via the textable class' constructor; however, you should set this data to protected or private properties so the data is not automatically made available to the template. Then, when calling the with method, pass an array of data that you wish to make available to the template:

<?php

namespace App\SMS;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Reedware\LaravelSMS\Textable;

class OrderShipped extends Textable
{
    use Queueable, SerializesModels;

    /**
     * The order instance.
     *
     * @var \App\Models\Order
     */
    protected $order;

    /**
     * Creates a new message instance.
     *
     * @param  \App\Models\Order $order
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Builds the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('sms.orders.shipped')
                    ->with([
                        'orderName' => $this->order->name,
                        'orderPrice' => $this->order->price,
                    ]);
    }
}

Once the data has been passed to the with method, it will automatically be available in your view, so you may access it like you would access any other data in your Blade templates:

<div>
    Price: {{ $orderPrice }}
</div>

Sending Text Messages

To send a message, use the to method on the SMS facade. The to method accepts a phone number, a user instance, or a collection of users. If you pass an object or collection of objects, the sms provider will automatically use their number and carrier properties when setting the sms recipients, so make sure these attributes are available on your objects. Once you have specified your recipients, you may pass an instance of your textable class to the send method:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Order;
use App\SMS\OrderShipped;
use Illuminate\Http\Request;
use Reedware\LaravelSMS\SMS;

class OrderController extends Controller
{
    /**
     * Ships the given order.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  integer                   $orderId
     *
     * @return \Illuminate\Http\Response
     */
    public function ship(Request $request, $orderId)
    {
        $order = Order::findOrFail($orderId);

        // Ship order...

        SMS::to($request->user())->send(new OrderShipped($order));
    }
}

Looping Over Recipients

Occasionally, you may need to send a textable to a list of recipients by iterating over an array of recipients / phone numbers. Since the to method appends phone numbers to the textable's list of recipients, you should always re-create the textable instance for each recipient:

foreach (['9995551234', '9995556789'] as $recipient) {
    SMS::to($recipient)->send(new OrderShipped($order));
}

Sending Text Messages via a Specific Provider

By default, the sms provider configured as the default provider in your sms configuration file will be used. However, you may use the provider method to send a message using a specific provider configuration:

SMS::provider('twilio')
    ->to($request->user())
    ->send(new OrderShipped($order));

Closure Messages

If using textables isn't something you want to do, you can also send text messages by using a closure implementation. To send a closure-based message, use the send method on the SMS facade, and provide it three arguments. First, the name of a view that contains the text messages. Secondly, an array of data that you wish to pass to the view. Lastly, a Closure callback which receives a message instance, allowing you to customize the recipients, body, and other aspects of the sms message:

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\Order;
use Illuminate\Http\Request;
use Reedware\LaravelSMS\SMS;

class OrderController extends Controller
{
    /**
     * Ships the given order.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  integer                   $orderId
     *
     * @return \Illuminate\Http\Response
     */
    public function ship(Request $request, $orderId)
    {
        $order = Order::findOrFail($orderId);

        // Ship order...

        SMS::send('sms.orders.shipped', ['order' => $order], function($m) use ($request) {
            $m->to($request()->user->number);
        });
    }
}

If you want to send a plain message instead, you can instead use the raw method on the SMS facade:

SMS::raw('Your order has been shipped!', function($m) use ($request) {
    $m->to($request()->user->number);
});

Queueing Messages

Queueing a Text Message

Since sending text messages can drastically lengthen the response time of your application, many developers choose to queue text messages for background sending. This is made easy using Laravel's built-in queues. To queue an sms message, use the queue method on the SMS facade after specifying the message's recipients:

SMS::to($request->user())
    ->queue(new OrderShipped($order));

This method will automatically take care of pushing a job onto the queue so the message is sent in the background. You will need to configure your queues before using this feature.

Delayed Message Queueing

If you wish to delay the delivery of a queued sms message, you may use the later method. As its first argument, the later method accepts a DateTime instance indicating when the message should be sent:

$when = now()->addMinutes(10);

SMS::to($request->user())
    ->later($when, new OrderShipped($order));

Pushing to Specific Queues

Since all textable classes generated using the make:sms command make use of the Illuminate\Bus\Queueable trait, you may call the onQueue and onConnection methods on any textable class instance, allowing you to specify the connection and queue name for the message:

$message = (new OrderShipped($order))
    ->onConnection('sqs')
    ->onQueue('sms');

SMS::to($request->user())
    ->queue($message);

Queueing By Default

If you have textable classes that you want to always be queued, you may implement the ShouldQueue contract on the class. Now, even if you call the send method when texting, the textable will still be queued since it implements the contract:

use Illuminate\Contracts\Queue\ShouldQueue;

class OrderShipped extends Textable implements ShouldQueue
{
    //
}

Rendering Textables

Sometimes you may wish to capture the message content of a textable without sending it. To accomplish this, you may call the render method of the textable. This method will return the evaluated contents of the textable as a string:

$invoice = App\Models\Invoice::find(1);

return (new App\SMS\InvoicePaid($invoice))->render();

Previewing Textables in the Browser

When designing a textable's template, it is convenient to quickly preview the rendered textable in your browser like a typical Blade template. For this reason, you are allowed to return any textable directly from a route Closure or controller. When a textable is returned, it will be rendered and displayed in the browser, allowing you to quickly preview its design without needing to send it to an actual cellular device:

Route::get('textable', function () {
    $invoice = App\Models\Invoice::find(1);

    return new App\SMS\InvoicePaid($invoice);
});

Localizing Textables

You can also send textable in a locale other than the current language, and the locale will even be remembered if the text message is queued.

To accomplish this, the SMS facade offers a locale method to set the desired language. The application will change into this locale when the textable is being formatted and then revert back to the previous locale when formatting is complete:

SMS::to($request->user())->locale('es')->send(
    new OrderShipped($order)
);

User Preferred Locales

Sometimes, applications store each user's preferred locale. By implementing the HasLocalePreference contract on one or more of your models, you may instruct the application to use this stored locale when sending text messages:

use Illuminate\Contracts\Translation\HasLocalePreference;

class User extends Model implements HasLocalePreference
{
    /**
     * Returns the user's preferred locale.
     *
     * @return string
     */
    public function preferredLocale()
    {
        return $this->locale;
    }
}

Once you have implemented the interface, the preferred locale will automatically be used when sending textables and notifications to the model. Therefore, there is no need to call the locale method when using this interface:

SMS::to($request->user())->send(new OrderShipped($order));

SMS & Local Development

When developing an application that sends text messages, you probably don't want to actually send texts to live cellular devices. There are several ways to "disable" the actual sending of text messages during local development.

Log Driver

Instead of sending your text messages, the log sms driver will write all text messages to your log files for inspection.

Universal To

Another solution is to set a universal recipient of all text messages sent by the application. This way, all the text messages generated by your application will be sent to a specific phone number, instead of the phone number actually specified when sending the message. This can be done via the to option in your config/sms.php configuration file:

'to' => [
    'number' => '9995551234',
    'carrier' => 'Example'
],

Events

Two events are fired during the process of sending text messages. The MessageSending event is fired prior to a message being sent, while the MessageSent event is fired after a message has been sent. Additionally, the MessageFailed event is fired when transport of a text message was unsuccessful. Remember, these events are fired when the text message is being sent, not when it is queued.

You may register an event listener for this event in your EventServiceProvider:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Reedware\LaravelSMS\Events\MessageSending' => [
        'App\Listeners\LogSendingMessage',
    ],
    'Reedware\LaravelSMS\Events\MessageSent' => [
        'App\Listeners\LogSentMessage',
    ],
    'Reedware\LaravelSMS\Events\MessageFailed' => [
        'App\Listeners\LogFailedMessage',
    ]
];

Adding Custom Providers

You may define your own sms providers using the extend method on the SMS facade. You should place this call to extend within a service provider. For a basic example, we're going to use the AppServiceProvider that ships with Laravel applications, but ideally you would want a dedicated service provider. Here's the code that we can place in that provider:

<?php

namespace App\Providers;

use App\Services\SMS\CallfireTransport;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstraps the application services.
     *
     * @return void
     */
    public function boot()
    {
        SMS::extend('callfire', function($app, $name, array $config) {
            // Return an instance of Reedware\LaravelSMS\Contracts\Transport...
            
            return new CallfireTransport(new Client, $config['username'], $config['password']);
        });
    }
}

As you can see in the example above, the callback passed to the extend method should return an implementation of Reedware\LaravelSMS\Contracts\Transport. This interface contains the methods you will need to implement to define a custom sms provider. Once your custom sms provider has been defined, you may use this provider in the providers configuration of your sms.php configuration file:

'providers' => [
    'callfire' => [
        'transport' => 'callfire',
        'username' => 'my-callfire-username',
        'password' => 'my-callfire-password'
    ]
]

If you need working examples to get started, you can look at any of the reedware/laravel-sms-* packages, as these define custom sms providers.

Deferred Registration

When it comes to registering a custom sms provider, the simplified example above used the SMS facade. However, the SMS service is deferred, meaning it won't be initialized until it is invoked for the first time. By invoking it within a service provider, you are essentially forcibly booting the SMS service. The overhead runtime cost of this is low, since this package is fairly lightweight, but for much larger applications this may not be ideal. For smaller applications, this performance difference is likely negligble.

The more performance friendly version involves extending the SMS service after it has been booted, rather than booting the SMS service only to extend it, and possibly never using it for the current request. Here's a modified example similar to the one above, but using deferred registration instead:

<?php

namespace App\Providers;

use App\Services\SMS\CallfireTransport;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;
use Reedware\LaravelSMS\Events\ManagerBooted;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstraps the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app['events']->listen(ManagerBooted::class, function($event) {
            $event->manager->extend('callfire', function($app, $name, array $config) {
                return new CallfireTransport(new Client, $config['username'], $config['password']);
            });
        });
    }
}

This implementation listens to an event that is fired after the SMS service has been booted, which does not boot the service itself. Again, the only real overhead runtime cost saved here for requests that don't use the SMS service, and therefore shouldn't have to boot the SMS service or define your new sms provider. This cost is typically measured in milliseconds, but for large applications that use several third-party packages, any time saved is valuable.