Creating a PHP Nexmo API Client using Guzzle Web Service Client – Part 2

This is Part 2 in a series, you can read Part 1 here.

In Part 1 of this series we laid a foundation for consuming the Nexmo SMS API and covered a few ways to interact with it. In this part we’ll create the actual Guzzle Web Service Client to interact with it to demonstrate how simple it can be.

The first thing we’ll do is get our project space ready by creating a folder (these steps assume you’re working on a Mac or Linux based system):

$ mkdir nexmo
$ cd nexmo/

Next thing we need to do is make sure we have Composer for installing Guzzle dependencies and make it globally available on the command line:

nexmo $ curl -sS https://getcomposer.org/installer | php
nexmo $ mv composer.phar /usr/local/bin/composer

Now let’s create a very simple composer.json file that will get the Guzzle libraries we need:

nexmo $ vi composer.json

Insert these contents:

{
  "require": {
    "guzzlehttp/guzzle": "~5.0",
    "guzzlehttp/guzzle-services": "*",
    "guzzlehttp/retry-subscriber": "*",
    "guzzlehttp/log-subscriber": "*"
  }
}

Great, now we’ve told Composer that we need, so let’s install them:

nexmo $ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing psr/log (1.0.0)
    Loading from cache

  - Installing react/promise (v2.2.0)
    Loading from cache

  - Installing guzzlehttp/streams (3.0.0)
    Loading from cache

  - Installing guzzlehttp/ringphp (1.0.7)
    Loading from cache

  - Installing guzzlehttp/guzzle (5.2.0)
    Loading from cache

  - Installing guzzlehttp/log-subscriber (1.0.1)
    Loading from cache

  - Installing guzzlehttp/command (0.7.1)
    Loading from cache

  - Installing guzzlehttp/guzzle-services (0.5.0)
    Loading from cache

  - Installing guzzlehttp/retry-subscriber (2.0.2)
    Loading from cache

Writing lock file
Generating autoload files
nexmo $

So composer got the packages we required as well as any packages they required and put them into the vendor/ folder:

nexmo $ ls -al vendor/
total 8
drwxr-xr-x  7 phillip  staff  238 Apr  8 19:54 .
drwxr-xr-x  6 phillip  staff  204 Apr  8 19:54 ..
-rw-r--r--  1 phillip  staff  183 Apr  8 19:54 autoload.php
drwxr-xr-x  9 phillip  staff  306 Apr  8 19:54 composer
drwxr-xr-x  9 phillip  staff  306 Apr  8 19:54 guzzlehttp
drwxr-xr-x  3 phillip  staff  102 Apr  8 19:54 psr
drwxr-xr-x  3 phillip  staff  102 Apr  8 19:54 react

Ok, at this point we have all the dependencies we need, so we’re ready to do our part in writing the description of the API. Because we’ll want to share this library with others let’s make sure the source is structured well and update our composer.json to be ready to share:

nexmo $ mkdir src
nexmo $ mkdir src/descriptions
nexmo $ vi composer.json

Update composer.json to look like (update to use your own name and such):

{
    "name": "fillup/nexmo",
    "description": "Nexmo API client built with Guzzle Web Service descriptions",
    "require": {
        "guzzlehttp/guzzle": "~5.0",
        "guzzlehttp/guzzle-services": "*",
        "guzzlehttp/retry-subscriber": "*",
        "guzzlehttp/log-subscriber": "*"
    },
    "license": "MIT",
    "authors": [
        {
            "name": "Your Name",
            "email": "Your Email"
        }
    ],
    "autoload": {
        "psr-4": {
            "Nexmo\\": "src/"
        }
    }
}

Now let’s describe the Nexmo SMS API based on the documentation:

nexmo $ vi src/descriptions/Sms.php

As you can see, the description is pretty simple, we just need to enter each API (in this case just Send), all the parameters, whether or not they are required, their data type, and where in the request to put them. In this case they all went into a json body.

<?php return [
    'baseUrl' => 'https://rest.nexmo.com',
    'operations' => [
        'Send' => [
            'httpMethod' => 'POST',
            'uri' => '/sms/json',
            'responseModel' => 'SendResult',
            'parameters' => [
                'api_key' => [
                    'required' => true,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'api_secret' => [
                    'required' => true,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'from' => [
                    'required' => true,
                    'type'     => 'string',
                    'location' => 'json',
                ],
                'to' => [
                    'required' => true,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'type' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'text' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'status-report-req' => [
                    'required' => false,
                    'type' => 'int',
                    'location' => 'json',
                ],
                'client-ref' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'network-code' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'vcard' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'vcal' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'ttl' => [
                    'required' => false,
                    'type' => 'int',
                    'location' => 'json',
                ],
                'message-class' => [
                    'required' => false,
                    'type' => 'int',
                    'location' => 'json',
                ],
                'udh' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
                'body' => [
                    'required' => false,
                    'type' => 'string',
                    'location' => 'json',
                ],
            ]
        ],
    ],
    'models' => [
        'SendResult' => [
            'type' => 'object',
            'properties' => [
                'statusCode' => ['location' => 'statusCode']
            ],
            'additionalProperties' => [
                'location' => 'json'
            ]
        ]
    ]
];

I’m still learning about the models definition, but in this example the response will just be an associative array matching the API response plus the addition of [‘statusCode’] which will have the HTTP Status Code that was returned (hopefully 200).

The src/descriptions/Sms.php file just returns an array that describes the API. Now we need to write a basic class that can instantiate the Guzzle Web Service Client with this description to enable the interface we want. To keep the code organized and interfaces clean we’ll create a BaseClient that takes care of common tasks and extend it for each API we want to implement a client for:

nexmo $ vi src/BaseClient.php

Contents:

<?php
namespace Nexmo;

use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Command\Guzzle\GuzzleClient;
use GuzzleHttp\Command\Guzzle\Description;
use GuzzleHttp\Subscriber\Retry\RetrySubscriber;

/**
 * Nexmo SMS API Client implemented with Guzzle Web Service
 *
 * @method array send(array $config = [])
 */
class BaseClient extends GuzzleClient
{
    /**
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        // Apply some defaults.
        $config += [
            'max_retries'      => 3,
        ];

        // Create the Smartsheet client.
        parent::__construct(
            $this->getHttpClientFromConfig($config),
            $this->getDescriptionFromConfig($config),
            $config
        );

        // Ensure that the credentials are set.
        $this->applyCredentials($config);

        // Ensure that ApiVersion is set.
        $this->setConfig(
            'defaults/ApiVersion',
            $this->getDescription()->getApiVersion()
        );
    }

    private function getHttpClientFromConfig(array $config)
    {
        // If a client was provided, return it.
        if (isset($config['http_client'])) {
            return $config['http_client'];
        }

        // Create a Guzzle HttpClient.
        $clientOptions = isset($config['http_client_options'])
            ? $config['http_client_options']
            : [];
        $client = new HttpClient($clientOptions);

        // Attach request retry logic.
        $client->getEmitter()->attach(new RetrySubscriber([
            'max' => $config['max_retries'],
            'filter' => RetrySubscriber::createChainFilter([
                RetrySubscriber::createStatusFilter(),
                RetrySubscriber::createCurlFilter(),
            ]),
        ]));

        return $client;
    }

    private function getDescriptionFromConfig(array $config)
    {
        // If a description was provided, return it.
        if (isset($config['description'])) {
            return $config['description'];
        }

        // Load service description data.
        $data = is_readable($config['description_path'])
            ? include $config['description_path']
            : null;

        // Override description from local config if set
        if(isset($config['description_override'])){
            $data = array_merge($data, $config['description_override']);
        }

        return new Description($data);
    }

    private function applyCredentials(array $config)
    {
        // Ensure that the credentials have been provided.
        if (!isset($config['api_key'])) {
            throw new \InvalidArgumentException(
                'You must provide an Api Key.'
            );
        }
        if (!isset($config['api_secret'])) {
            throw new \InvalidArgumentException(
                'You must provide an Api Secret.'
            );
        }

        // Set credentials in default variables so that we don't
        // have to pass them to every method individually
        $this->setConfig(
            'defaults/api_key',
            $config['api_key']
        );
        $this->setConfig(
            'defaults/api_secret',
            $config['api_secret']
        );
    }
}

And now let’s extend it for an Sms client:

nexmo $ vi src/Sms.php

Contents:

<?php
namespace Nexmo;

use Nexmo\BaseClient;

/**
 * Nexmo SMS API Client implemented with Guzzle Web Service
 *
 * @method array send(array $config = [])
 */
class Sms extends BaseClient
{
    /**
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        // Set description_path.
        $config += [
            'description_path' => __DIR__ . '/descriptions/Sms.php',
        ];

        // Create the Smartsheet client.
        parent::__construct(
            $config
        );
    }

}

“Wait a minute, I thought you said this was the easy route!” Well, as you can see there is actually a decent amount going on in that class, but it really is quite simple. We have a constructor that accepts a configuration array that must contain at least api_key and api_secret, but in an example by Jeremy Lindblom I learned how to make it a bit more robust and support dependency injection of an alternate service description and/or HttpClient, so the methods getHttpClientFromconfig and getDescriptionFromConfig could be removed and a more basic version of their logic put into the constructor, but basically I just copy/paste these few methods into each client I need to write to keep it simple. An importent method in this client is the applyCredentials method. It checks the config for api_key and api_secret and if present it sets them as defaults in the clients requests so they are available when we make individual API calls.

“Again, I thought you said this was an easier way to implement an API client.” Relax dude, we’ll get to how this method of client development makes life easier a bit later. But just a hint: we did a decent amount of ground work in that client, and for this particular API with a single method of Send it seems like overkill, but most of that work is a one time thing, for each method we want to add we just have to describe it.

Now we have an SMS Client that we can use to make calls to the Nexmo API. To test it out we need to setup a config file to store things like key/secret and other variables for examples and create an example script to actually send a message.

Notice The following examples will require that you’ve registered for a Nexmo Developer account and have an api_key, api_secret, and a phone number you can send messages from.

Create config file:

nexmo $ vi config-local.php

Contents:

<?php return [
    'api_key' => '',
    'api_secret' => '',
    'from' => '',
    'to' => '',
    'text' => '',
];

And now create the example file:

nexmo $ vi examples/sms.php

Contents:

<?php
/**
 * Include Composer autoloader
 */
require_once __DIR__.'/../../vendor/autoload.php';

/**
 * Import Sms client
 */
use Nexmo\Sms;

/**
 * Load config, expecting an array with:
 * api_key, api_secret, to, from, text
 */
$config = include __DIR__.'/../../config-local.php';

/**
 * Get an SMS client object
 */
$sms = new Sms($config);

/**
 * Now let's send a message
 */
$results = $sms->send([
    'from' => $config['from'],
    'to' => $config['to'],
    'text' => $config['text'],
]);

/**
 * Dump out results
 */
print_r($results);

Now run it!

nexmo $ php src/examples/sms.php
Array
(
    [statusCode] => 200
    [message-count] => 1
    [messages] => Array
        (
            [0] => Array
                (
                    [to] => 14085559876
                    [message-id] => 0300000071BCAA3C
                    [status] => 0
                    [remaining-balance] => 15.23280000
                    [message-price] => 0.00480000
                    [network] => US-VOIP
                )

        )

)

There you have it, a working Nexmo SMS client! It doesn’t do a whole lot at this point since it only covers one API, but in Part 3 I’ll fill it out a bit more to show how easy it is now to add support for additional Nexmo APIs to this library. If you want to grab the source for project it is on github at https://github.com/fillup/nexmo

Links in this post:

Share
  • Pingback: Creating a PHP Nexmo API Client using Guzzle Web Service Client – Part 3 | The Boring Blog of Phillip Shipley()

  • PM

    First of all Thank you very much for sharing.

    I am getting this error when I run the example
    Fatal error: Class ‘NexmoSms’ not found in /data/home/username/proj_folder/client/src/examples/Sms.php on line 25

    Call Stack:
    0.0003 221496 1. {main}() /data/home/username/proj_folder/client/src/examples/Sms.php:0

    I did exactly same as you, but got this error. Please help me to solve this.
    Thanks again.

    • pshipley

      Hi PM, thank you for taking the time to check this library out and give it a try. After you clone the repo from github, be sure to run “composer update” to install the dependent libraries like Guzzle as well as create the autoloader that will know how to find the NexmoSms class.

      I just re-cloned the repo, ran composer update, set my api key/secret in the config-local.php file and ran the sms example code and it worked as expected. Let me know if you’ve already done that and still experience problems.

      Thanks,
      Phillip

      • PM

        Hello Philip,

        Thanks you very much for your response, Actually I got it fixed, I am creating a Client Library for my Company internal project. I didn’t clone it, I am building my own CL based on your files. It’s my mistake, I didn’t put “/” after src in composer.json.

        But I have few doubts,

        We are building RESTful web service for Authentication of Application and User. For this web service Client Library I am follwoing your approach for CL. In description folder I created Login & Logout methods as “Send” in your description. My 1 Question/Doubt is Do we need to have a method in a Class that extends BaseClient Login & Logout, so that in Client Application can say

        $Obj = new ClassName();

        $Result = $Obj->Login($username, $Password);

        2 Question: I started working on it, I got it working, and suddenly I am getting below error:
        Fatal error: Uncaught exception ‘GuzzleHttpExceptionServerException’ with message ‘Server error response [url]

        Please suggest me.

        Thank you very much for your time and help.

        • pshipley

          Hi PM,

          Glad to hear to got that issue resolved.

          For your first question, I like to use a BaseClient class so that I can put common features there, like creating the HTTP client, attaching authentication credentials, etc. Then extend the BaseClient with each individual service that has unique features, like the Sms class used for sending SMS messages.

          On your second question, when the Guzzle client calls an API, if the response is not successful (e. status 200, 201, 204, etc) but is an error, like (400, 403, 404, 500, etc.) then it throws an exception. Without the rest of the exception you received I’m not sure what problem you faced, but if you try to make calls manually to the url using a REST client like PostMan or Advanced Rest Client you can debug further to see if the problem is with your client and the API definition or if the problem is on the API side.

          Hope that helps,
          Phillip

          • PM

            Hello Philip,

            Thanks for your reply. I followed BaseClient and individual classes you suggested. I still have one more question regarding Exception Handling in your approach.

            If any parameter is missing or parameter value is wrong, I am getting Fatal Error, and I want to do some another operation after each operation in the service description. How would I do that? Can you please guide me?

            Thanks again for your help.
            -PM