How to Send an SMS with Mezzio PHP Framework

September 28, 2020
Written by
Reviewed by

How to Send an SMS with Mezzio PHP Framework

Whether we’re voting for our favorite candidate on shows such as X Factor, or receiving two-factor authentication codes to log in to services like MailChimp and GitLab, SMS are virtually ubiquitous in modern life. Not only do they make communication in life and work much easier, it also doesn’t take a lot of code to send them either.

In this tutorial, you’ll learn how to create a simplistic API that can send an SMS using PHP’s Mezzio framework and Twilio’s PHP SDK.

Once completed, you will be able to send a POST request to the API’s default endpoint, supplying the phone number to send the message to, and the message to send. If the message was successfully sent, then a JSON response will be returned showing a number of details about the sent SMS. If the SMS was not able to be sent, then an appropriate JSON response will be returned, which will show what went wrong.

Overall, this tutorial will provide you with a basic understanding of creating endpoints in Mezzio and sending SMS with Twilio.

Prerequisites

In order to complete this tutorial, you will need the following:

  • PHP 7.4
  • Composer globally installed
  • A Twilio account and phone number
  • twilio-cli
  • curl
  • jq
  • libsecret
  • Node.js 10.12.0 or higher
            

NOTE: Twilio’s PHP SMS Quickstart shows how to install libsecret, Node.js, and twilio-cli.

Create a Mezzio Application

If you’re not familiar with the name, Mezzio is the latest iteration of Zend Framework and Zend Expressive. It was renamed earlier in the year so that it could be rehomed at the Linux Foundation. Mezzio is suited to create applications of any size. It makes growing from a small proof-of-concept to a large, enterprise-grade application possible — without requiring large architectural changes.

The first thing that you’ll need to do is to create a new Mezzio application. To save some time, use Composer’s create-project command, as in the example below, to bootstrap a basic application.

composer create-project mezzio/mezzio-skeleton sms-sender-api

This command will launch an installation wizard to help you create the right type of application, which asks five questions:

  1. What type of installation would you like? For this question, select 3, which is Mezzio’s recommended choice
            
  2. Which container do you want to use for dependency injection? For this question, accept the default option, which is laminas-servicemanager
            
  3. Which router do you want to use? For this question, accept the default option, which is FastRoute
            
  4. Which template engine do you want to use? For this question, choose n. We don’t need to use a template engine, as the application will, at most, return JSON responses.
            
  5. Which error handler do you want to use during development? For this question, accept the default option, which is Whoops. This package provides excellent support when attempting to track down application errors.

After you answer the fifth question, the wizard will bootstrap the application. Installation should be finished in under 60 seconds, depending on the speed of your network connection.

Add the Additional Composer Dependencies

Next, you need to install a few additional dependencies. These dependencies are required later in this tutorial to validate the phone number which the SMS will be sent to as well as the SMS body. While not strictly necessary, it’s responsible for a developer to write code that is properly validated.

To install them, change into the project directory with, cd sms-sender-api, and run the following command.

composer require twilio/sdk \
    laminas/laminas-form \
    laminas/laminas-validator \
    doctrine/common

NOTE: When prompted to inject Laminas\Validator\ConfigProvider into config/config.php, accept the default option (1) and accept the default option (Y) to remember this option for other packages of the same type.

Add your Twilio credentials

With the application bootstrapped and the additional dependencies installed, you need to supply the application with Twilio credentials available. Navigate to the Twilio Console and locate the ACCOUNT SID and AUTH TOKEN inside of the Project Info dashboard as seen below:

Twilio account console

Figure 1. Retrieve the account SID and authentication token from your Twilio account

To retrieve the phone number, run twilio phone-numbers:list in your command line and choose the applicable number from the list, copying the value from the Phone Number column. If you haven't done so already you can search and buy a Twilio Phone Number from the console.

Next, create a new file named twilio.local.php inside of the config/autoload folder. Copy the following PHP snippet into the file and update it with your Twilio credentials and phone number.

<?php

return [
    'twilio' => [
        'account_sid' => '<TWILIO_ACCOUNT_SID>',
        'auth_token' => '<TWILIO_AUTH_TOKEN>',
        'phone_number' => '<TWILIO_PHONE_NUMBER>',
    ],
];

NOTE: There are many ways to store secure credentials in Mezzio. The reason why I’m suggesting this approach is that by default, files ending in *.local.php are explicitly ignored by git, so they cannot be stored under version control — unless they are explicitly added using either the -f or --force options. We could store the credentials in a .env file, but to retrieve them would require additional third-party libraries, such as dotenv. What’s more, this approach is more succinct.

Create and Register a Twilio Service

Now that the credentials and phone number are ready to use, write the code that will use those credentials to send an SMS. Create a new file called TwilioService.php inside of the src/App/src/Service/ folder. Add the following code inside of that file:

<?php

declare(strict_types=1);

namespace App\Service;

use Twilio\Rest\Api\V2010\Account\MessageInstance;
use Twilio\Rest\Client;

class TwilioService
{
    private array $configuration;
    private Client $client;

    public function __construct(array $configuration)
    {
        $this->configuration = $configuration;
        $this->client = new Client(
            $this->configuration['twilio']['account_sid'],
            $this->configuration['twilio']['auth_token']
        );
    }

    public function sendSMS(string $sendTo, string $body): MessageInstance
    {
        return $this->client->messages->create(
            $sendTo,
            [
                'from' => $this->configuration['twilio']['phone_number'],
                'body' => $body
            ]
        );
    }
}

The TwilioService constructor receives an array called $configuration. This array will store the Twilio configuration which was created earlier. The array initializes a new Twilio\Rest\Client object which will send the SMS.

Next comes the sendSMS function. This function takes two arguments:

  1. $sendTo, which is the phone number to send the SMS to
  2. $body, which is the SMS message body

This function calls the create method, passing in the phone number to send the message to, and the message body details.

NOTE: The method returns a MessageInterface object. This is because the application will return some information about the SMS if it was successfully sent, which will be retrieved from this object.

With the TwilioService object created, you need to create a class to instantiate it. In src/App/src/Service/, create a new file named TwilioServiceFactory.php and add the following code:

<?php

declare(strict_types=1);

namespace App\Service;

use Interop\Container\ContainerInterface;
use Mezzio\Exception\InvalidArgumentException;

class TwilioServiceFactory
{
    public function __invoke(ContainerInterface $container): TwilioService
    {
        $config = $container->has('config') ? $container->get('config') : [];
        if (is_null($config) || !array_key_exists('twilio', $config)) {
            throw new InvalidArgumentException('Twilio configuration not able to be retrieved.') ;
        }
        return new TwilioService($config);
    }
}

The TwilioServiceFactory class effectively implements the factory pattern. This is a common paradigm in Mezzio for instantiating classes which have constructor dependencies. That’s why the __invoke magic method receives a ContainerInterface object so that it can retrieve the Twilio configuration details from the application’s dependency injection (DI) container.

The function does a bit of sanity checking to see if the DI container has a service called config, which contains the application’s global configuration. If it does, it is used to initialize a new variable called $config. However, if the Twilio configuration isn’t available within the global configuration, an InvalidArgumentException will be thrown. Assuming that config was available, it is then used to instantiate a TwilioService object, which is then returned.

With the TwilioService and accompanying TwilioServiceFactory classes defined, you then need to register the service in the DI container. To do that, update the getDependencies method in src/App/src/ConfigProvider.php to match the code below:

public function getDependencies(): array
{
    return [
        'invokables' => [
            Handler\PingHandler::class => Handler\PingHandler::class,
        ],

        'factories' => [
            Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class,
            \App\Service\TwilioService::class => \App\Service\TwilioServiceFactory::class
        ],
    ];
}

The getDependencies() function registers a service called TwilioService in the DI container. When that service is retrieved from the container, the result of calling TwilioServiceFactory’s __invoke magic method is returned.

NOTE: In Mezzio, it’s almost trivial to register services with the DI container and retrieve those services later. Doing so discourages direct dependency instantiation within using classes, making applications more maintainable.

Create an Object to Store the SMS Data

Create a new file under src/App/src/ValueObjects called SMSData.php, and add the code in the example below to it. This class will perform two roles in the application.

Firstly, it will provide us with an object-oriented way of managing the phone number to send the SMS to, along with the message to send. Secondly, it provides a compact way of instantiating a form that we can use to validate the POST data used to make a request to send an SMS.

<?php

declare(strict_types=1);

namespace App\ValueObjects;

use Laminas\Form\Annotation;

class SMSData
{
    /**
     * @Annotation\Attributes({"type":"text"})
     * @Annotation\Filter({"name":"StringTrim"})
     * @Annotation\Validator({
     * "name":"Regex",
     * "options": {
     *    "pattern": "^((\+1|001)?[ \-]?)(([\d]{3}[ \-]?)?|(\([\d]{3}\)[ \-]?))([\d]{3}[ \-]?[\d]{4}){1}$"
     * }
     * })
     * @Annotation\ErrorMessage("That is not a valid US phone number.")
     */
    public string $sendTo;

    /**
     * @Annotation\Attributes({"type":"textarea"})
     * @Annotation\Filter({"name":"StringTrim"})
     * @Annotation\Validator({"name":"StringLength", "options":{"max":500}})
     * @Annotation\ErrorMessage("The message body can be no longer than 500 characters.")
     */
    public string $body;
}

This class contains two public member variables; sendTo which will store the phone number to send the SMS to, and body, which will store the SMS message. For the sake of simplicity, this example uses Doctrine annotations, which \Laminas\Form\Annotation\AnnotationBuilder will later use to instantiate a form.

For sendTo, a text input field will be generated named sendTo. Input retrieved from it will be trimmed before it is returned. It will be also validated using the following regular expression: ^((\+1|001)?[ \-]?)(([\d]{3}[ \-]?)?|(\([\d]{3}\)[ \-]?))([\d]{3}[ \-]?[\d]{4}){1}$.

This regex only allows valid US phone numbers, such as the following:

  1. 754-3010
  2. 754 3010
  3. 7543010
  4. 541-754-3010
  5. 5417543010
  6. (541) 754-3010
  7. (541)-754-3010
  8. +1-541-754-3010
  9. +1 541 754 3010
  10. +15417543010
  11. +1-541-754-3010
  12. +1-541-754-3010
  13. 001 541 754 3010
  14. 0015417543010
  15. 001-541-754-3010

body will be rendered as a textarea field named body. As with sendTo, input retrieved from it will be trimmed before it is returned. Its content will be validated to ensure that it is no more than 500 characters long. Both of the fields have custom error messages set, as the default ones are not as human-readable as preferred for real-world application.

If you’d like to experiment with the regular expression, here’s a link to it on Regex101.com.

Refactor the Route’s Handler Class

You’ve retrieved and stored your Twilio details in the application’s global configuration. You’ve created all of the supporting classes and registered them where necessary with the DI container. Now it’s time to refactor the default route’s handler to use that configuration and functionality to send an SMS.

Refactor the class constructor

The first thing you need to do is to refactor src/App/src/Handler/HomePageHandler.php’s constructor to:

  1. Replace the first argument with a new \App\Service\TwilioService object called $twilioService, to initialize a new class variable also called $twilioService
  2. Instantiate a second class variable called $form which will be a \Laminas\Form\FormInterface object. To do this, use AnnotationBuilder’s createForm method, passing to it \App\ValueObjects\SMSData::class as the name of the class to use

When finished, the constructor should look like the example below:

use App\Service\TwilioService;

public function __construct(
    TwilioService $twilioService,
    Router\RouterInterface $router,
    ?TemplateRendererInterface $template = null
) {
    $this->router        = $router;
    $this->template      = $template;
    $this->twilioService = $twilioService;
    $this->form = (new \Laminas\Form\Annotation\AnnotationBuilder())
           ->createForm(\App\ValueObjects\SMSData::class);
}

Refactor the handle method

Remove all of the code from the body of the handle method. It’s all boilerplate code that you don’t need.

Then, add an if condition to check if the result of calling the $request object’s getMethod function matches POST. If it doesn’t, then return a new \Laminas\Diactoros\Response\EmptyResponse object, passing the integer 405 to its constructor.

Doing this will return an empty response body along with an HTTP status code of 405 (Method Not Allowed) if a request is made using an HTTP method other than POST.

if (!$request->getMethod() === 'POST') {
    return new \Laminas\Diactoros\Response\EmptyResponse(405);
}

Pass the result of calling $request’s getParsedBody method to $form’s setData method. This will attempt to initialize $form with any POST variables retrieved from the request.

$this->form->setData($request->getParsedBody());

Once that’s done, check if the supplied form data is valid by calling $form’s isValid method. This will run the validators that you set in SMSData’s Annotation docblocks.

If the form is valid, then it’s finally time to send the SMS. To do that, call the sendSMS method from twilioService by passing it the value of $form’s sendTo and body properties. Use the method’s response to initialize a new variable, called $response.

NOTE: We’re not going to handle any TwilioException’s which might be thrown when calling sendSMS.

Return a new \Laminas\Diactoros\Response\JsonResponse object, passing it an associative array containing the following properties from $response (from, to, body, and status). You don’t need to specify the second parameter, which is the response code, as it will return an HTTP 200 OK status code by default.

When finished, the if block should look like the following code:

if ($this->form->isValid()) {
    $response = $this
        ->twilioService
        ->sendSMS(
            $this->form->get('sendTo')->getValue(),
            $this->form->get('body')->getValue()
        );

    return new JsonResponse([
        'from' => $response->from,
        'to' => $response->to,
        'body' => $response->body,
        'status' => $response->status
    ]);
}

If isValid returns false, then you should be professional (and polite) and let your user know what went wrong. This doesn’t need to be exhaustive.

In an else block, return a new \Laminas\Diactoros\Response\JsonResponse object.

This time, however, pass it an array with just two keys: status, which is set to unsuccessful, and reason which is set to the result of calling $form’s getMessages method.

This method returns an associative array containing a key for each form element that failed validation, where the values for those keys is an array, containing all the error messages showing where they failed to validate. In addition to the array, pass it a second parameter set to 400 indicating that it was an HTTP Bad Request.

Here’s what the code should look like.

else {
    return new JsonResponse(
        [
            'status' => 'unsuccessful',
            'reason' => $this->form->getMessages()
        ],
        400
    );
}

Refactor how the route handler class is instantiated

As we’ve changed src/App/src/Handler/HomePageHandler.php’s constructor, replacing the first argument with a TwilioService object, we now need to refactor how the class is instantiated in src/App/src/Handler/HomePageHandlerFactory.php.

It’s a standard convention in Mezzio to use factory classes to instantiate classes that have constructor dependencies. So in the class’ __invoke magic method, after the initialization of $template, write an if condition using the $container’s has method. This will check if a TwilioService is available. If it’s not available, throw a new \\Laminas\ServiceManager\Exception\ServiceNotFoundException, with the message “Twilio service not found.”.

If the service is available, use $container’s get method to retrieve it, initializing a new variable, called $twilioService. Then, replace the original first argument to HomePageHandler’s constructor with $twilioService.

When finished, the __invoke magic method definition should look like the following example:

use App\Service\TwilioService;

public function __invoke(ContainerInterface $container): RequestHandlerInterface
{
    $router   = $container->get(RouterInterface::class);
    $template = $container->has(TemplateRendererInterface::class)
        ? $container->get(TemplateRendererInterface::class)
        : null;

    if (!$container->has(TwilioService::class)) {
        throw new \Laminas\ServiceManager\Exception\ServiceNotFoundException(
            'Twilio service not found.'
        );
    }
    $twilioService = $container->get(TwilioService::class);

    return new HomePageHandler($twilioService, $router, $template);
}

Refactor the Route Configuration

Now that the handle method has been refactored, you need to make one final change, which is to allow the route to receive POST requests. To do that, change the first method in config/routes.php from get to post, as in the example below.

$app->post('/', App\Handler\HomePageHandler::class, 'home');

Testing

The application is now finished. At this point, you can compare your code against the code I’ve based this article on which is available on GitLab.

Now it’s time to run the application so that we can test it. To do that, use the built-in Composer script serve that comes with Mezzio, as in the example below.

composer serve &

This will launch PHP’s internal web server, setting it to listen on localhost, on port 8080, and use the public directory as the project’s directory root. The ampersand & will put the command into the background so that you can run a curl request in the same terminal session.

You should see output similar to the following in your terminal:

Tue Sep 15 09:16:45 2020] PHP 7.4.3 Development Server (http://0.0.0.0:8080) started

Now that the application’s running let’s use it to send an SMS. To do that, send a POST request in Curl, as in the example below, changing the sendTo and body values as you prefer. I’ve used curl’s --silent switch to suppress progress meters and error messages, as we’re only interested in the JSON response.

curl --silent --data "sendTo=+17144001234&body="Let's send an SMS with Mezzio and Twilio" http://localhost:8080 | jq

NOTE: To make the JSON response easier to read, the JSON response is piped to jq which by default, neatly formats it and adds coloration.

Once the message is sent successfully, you should see JSON output similar to the example below in your terminal.

{
    "from": "<YOUR_TWILIO_NUMBER>",
    "to": "+17144001234",
    "body": "Sent from your Twilio trial account - Let's send an SMS with Mezzio and Twilio",
    "status": "queued"
}

In addition, you should receive an SMS on your phone, with the message that you sent, which you can see an example of below.

Sample SMS

Figure 2. View the SMS that you sent from the application

Now, let’s do a small test that invalid phone numbers cannot be used to send an SMS.

This time, change the number to an invalid phone number and then send the same message as before, as seen in the Curl example below:

curl --silent --data "sendTo=1234&body=Let's send an SMS with Mezzio and Twilio" http://localhost:8080 | jq

You should see an error response similar to the example below. You can see that the status is now marked unsuccessful, along with an error message telling you that the phone number was invalid, followed by the acceptable phone number formats.

{
    "status": "unsuccessful",
    "reason": {
        "sendTo": [
            "The receiving phone number has to be in one of the following forms: 041012345678, +141012345678, +6141012345678, 006141012345678, 00141012345678."
        ]
    }
}

Conclusion

Well, congratulations! You have just created a Mezzio application that can send an SMS using Twilio. If you’ve enjoyed the process, then I strongly recommend extending the application so that more details are displayed in the JSON response, as well as refactoring the code to respond to SMS.

Matthew Setter is a consulting PHP developer and technical writer. He can be reached via: