How to Build a Command Line Application (CLI) to View Your Twilio Account Usage

August 30, 2022
Written by
Reviewed by

How to Build a Command Line Application (CLI) to View Your Twilio Account Usage

Building APIs and web-based applications are the mainstays of what we as web developers do. However, command line applications are also an essential skill to master. They help us do what we do more effectively and easily, such as by allowing us to build small tools and utilities to help us with routine, repetitive tasks.

In this tutorial, I'm going to show you how to use Symfony's Console Component to build a command line application that can retrieve usage records from a Twilio account.

Prerequisites

To follow along with this tutorial, you will need the following:

Create the project directory structure

The first thing to do is to create the core project directory structure (to aid in project readability) and then change into the top-level directory. Do that by running the following commands.

mkdir twilio-symfony-command
cd twilio-symfony-command
mkdir -p src/Command

Add the required dependencies

The next thing to do is to add the required dependencies. Only three are required. These are:

To install them, run the command below in the project directory.

composer require \
    symfony/console \
    twilio/sdk \
    vlucas/phpdotenv

Retrieve your Twilio credentials

The next thing to do is to make your Twilio credentials (your Twilio Account SID and Auth Token) available to the application. First, create a new file named .env in the top-level directory of the project.

Then, in that file paste the code below, which adds environment variables and a placeholder for them.

TWILIO_ACCOUNT_SID=<TWILIO_ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<TWILIO_AUTH_TOKEN>

Next, from the Twilio Console's Dashboard, copy your Account SID and Auth Token and, in .env, replace the respective placeholder values with them (<TWILIO_ACCOUNT_SID> and <TWILIO_AUTH_TOKEN>).

Retrieve your Twilio auth token and account SID

Create a class to retrieve the usage records

Next, you need to create TwilioUsage, the first of three classes, by creating a new file named TwilioUsage.php in src. Then, in that file paste the code below.

<?php

declare(strict_types=1);

namespace PhoneNumberUsage;

use DateTime;
use Twilio\Rest\Api\V2010\Account\Usage\RecordInstance;
use Twilio\Rest\Client;

use function array_keys;
use function in_array;
use function strtolower;

class TwilioUsage
{
    public const MAX_RESULTS = 20;

    private Client $client;

    private array $allowedCategories = [
        'calls'          => 'calls',
        'pfax minutes'   => 'pfax-minutes',
        'pfax pages'     => 'pfax-pages',
        'phone numbers'  => 'phonenumbers',
        'pv'             => 'pv',
        'recordings'     => 'recordings',
        'sms'            => 'sms',
        'total price'    => 'totalprice',
        'transcriptions' => 'transcriptions',
    ];

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function getClient(): Client
    {
        return $this->client;
    }

    /**
     * @return RecordInstance[]
     */
    public function __invoke(
        int $maxResults = self::MAX_RESULTS,
        ?string $startDate = null,
        ?string $endDate = null,
        ?string $category = null
    ): array {
        $options = [];

         if ($startDate !== null) {
            $options['startDate'] = (new DateTime($startDate))->format('Y-m-d');
        }

        if ($endDate !== null) {
            $options['endDate'] = (new DateTime($endDate))->format('Y-m-d');
        }

        if ($category !== null 
            && in_array(strtolower($category), array_keys($this->allowedCategories))) 
        {
            $options['category'] = $this->allowedCategories[strtolower($category)];
        }

        return $this->client
            ->usage
            ->records
            ->read($options, $maxResults);
    }
}

The class starts by defining a constant and two properties. The constant holds the default number of records to retrieve. The first property is a Twilio Client, which provides access to all of Twilio's APIs. The second property maps the internal usage categories to more human-readable versions. While not strictly necessary, I try to avoid exposing too much internal structure of an API if I can.

The __invoke() magic method contains the class' core logic. It takes four parameters:

ParameterDescription
$maxResultsThe maximum number of results to return.
$startDateThis is the date that records must be created on or after.
$endDateThis is the date that records must be created on or before.
$categoryA usage category.

The start and end dates are converted to DateTime objects and, if the category provided is in the list of allowed categories, its internal mapping is retrieved.

$startDate, $endDate, and $category are then stored in an options array ($options) and passed, along with $maxResults, to $client's usage->records->read() method. This method reads all of the usage records from the user's account.

Create the custom Symfony command

Now, you need to create the custom Symfony command. In src/Command create a new file named TwilioUsageCommand.php. Then, in that file, paste the code below.


<?php

declare(strict_types=1);

namespace PhoneNumberUsage\Command;

use NumberFormatter;
use PhoneNumberUsage\TwilioUsage;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use function strtoupper;

class TwilioUsageCommand extends Command
{
    public const DEFAULT_RECORD_LIMIT = 20;

    /** @var string */
    protected static $defaultName = 'twilio:show-usage';

    /** @var string */
    protected static $defaultDescription = "Lists a Twilio account's usage details.";

    /** @phpcs:disable Generic.Files.LineLength */
    protected string $helpMessage = "The command lists a Twilio account's usage details. It supports the ability to list account usage for SMS, MMS, and voice calls within a given date range, and filter by usage categories (daily, monthly, today, yesterday, etc).";

    private TwilioUsage $twilioUsage;

    private NumberFormatter $formatter;

    public function __construct(TwilioUsage $twilioUsage)
    {
        parent::__construct();

        $this->twilioUsage = $twilioUsage;
        $this->formatter   = new NumberFormatter(
            'en_US',
            NumberFormatter::CURRENCY
        );
    }

    protected function configure(): void
    {
        $this->setHelp($this->helpMessage);

        $this->addOption(
            'limit-records',
            'l',
            InputOption::VALUE_OPTIONAL,
            'The record limit'
        );

        $this->addOption(
            'start-date',
            's',
            InputOption::VALUE_OPTIONAL,
            "The usage range's start date"
        );

        $this->addOption(
            'end-date',
            'e',
            InputOption::VALUE_OPTIONAL,
            "The usage range's end date"
        );

        $this->addOption(
            'category',
            'c',
            InputOption::VALUE_OPTIONAL,
            "The usage range's category"
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $outputStyle = new OutputFormatterStyle('#56be4e', null, ['bold']);
        $output->getFormatter()->setStyle('fire', $outputStyle);

        $startDate    = $input->getOption('start-date') ?: null;
        $endDate      = $input->getOption('end-date') ?: null;
        $category     = $input->getOption('category') ?: null;
        $limitRecords = (int) $input->getOption('limit-records')
            ?: self::DEFAULT_RECORD_LIMIT;
        
        $table = new Table($output);
        $table
            ->setHeaders(['Start Date', 'End Date', 'Category', 'Price', 'Currency'])
            ->setRows(
                    $this->getUsageData(
                    $limitRecords,
                    $startDate,
                    $endDate,
                    $category
                )
            );
        $table->render();

        return Command::SUCCESS;
    }

    public function getUsageData(
        int $limitRecords,
        ?string $startDate = null,
        ?string $endDate = null,
        ?string $category = null
    ): array {
            $records = $this->twilioUsage->__invoke(
            $limitRecords,
            $startDate,
            $endDate,
            $category
        );

        $rows = [];

        foreach ($records as $record) {
                $rows[] = [
                $record->startDate->format('d/m/Y H:i e'),
                $record->endDate->format('d/m/Y H:i e'),
                $record->category,
                $this->formatter->formatCurrency(
                    (float) $record->price,
                    strtoupper($record->priceUnit)
                ),
                strtoupper($record->priceUnit),
            ];
        }

        return $rows;
    }
}

The class starts off by defining the command's core settings, including the command's name ($defaultName), description ($defaultDescription), and a help message ($helpMessage). Then, it initialises a TwilioUsage and a NumberFormatter object in the class constructor.

You've already seen TwilioUsage. NumberFormatter, part of PHP's Internationalization extension, formats usage pricing information in a more human-readable way, which you'll see shortly.

The configure() method adds four options to the command, listed in the following table.

Option Name
LongShort
--limit-records-l
--start-date-s
--end-date-e
--category-c

Next, comes the execute() method. The method starts by retrieving the value for each of the four options or provides a default if the user hasn't provided them. Then, it writes a header to the terminal output so it's clear what the output is about.

The command could just print the usage information. But I believe that adding a preamble and conclusion/footer help make console commands more meaningful to the user.

After that, it retrieves the user's usage information with the getUsageData() method, covered next, and renders it in tabular form. I know some people aren't a fan of tables, regardless of where they're used. But I believe — when used appropriately — they make information easier to scan, allowing it to be used more efficiently.

Finally, Command::SUCCESS (0) is returned, showing that the command completed successfully.

The getUsageData() method invokes the TwilioUsage object to retrieve the user's usage information, and then formats some of the call information while storing it in an array. The intent is to filter out the usage properties that won't be displayed and make the currency information easier to read.

Create the command container

There's one final class to create, the one that allows the Symfony custom command to be used. In the project's top-level directory, create a new file named application.php. Then, in that file, paste the code below.

<?php

declare(strict_types=1);

require __DIR__.'/vendor/autoload.php';

use PhoneNumberUsage\Command\TwilioUsageCommand;
use PhoneNumberUsage\TwilioUsage;
use Symfony\Component\Console\Application;
use Twilio\Rest\Client;

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

$usage = new TwilioUsage(
    new Client(
        $_SERVER['TWILIO_ACCOUNT_SID'],
        $_SERVER['TWILIO_AUTH_TOKEN']
    )
);

$application = new Application();
$application->add(new TwilioUsageCommand($usage));
$application->run();

This class is a container for a collection of commands. In this case, only one command is defined. It connects the command with the user input and allows it to write output to the terminal.

There's not a lot going on, but it's still worth stepping through. It uses PHP Dotenv to put the environment variables defined in .env into PHP's $_ENV and $_SERVER superglobals.

Then, it initialises a new TwilioUsage object, an Application object, and adds an instance of the custom command, TwilioUsageCommand, to the application. After that, the Application object's run() method is called to run the application, so that the command can be called.

Add a PSR-4 Namespace

The last thing to do is to add a custom PSR-4 Autoloader so that the two classes that you have created can be autoloaded when required. To do that, update composer.json to match the configuration below.

hl_lines="7,8,9,10,11"
{
    "require": {
        "symfony/console": "^6.1",
        "twilio/sdk": "^6.41",
        "vlucas/phpdotenv": "^5.4"
    },
    "autoload": {
        "psr-4": {
            "PhoneNumberUsage\\": "src"
        }
    }
}

Finally, add update Composer's autoloader, by running the command below.

composer dump-autoload

Test the command

Now that the code's ready, it's time to test it. To do that, run the following command in the terminal:

php application.php twilio:show-usage

You should see output similar to the following.

Twilio Account Usage Statistics

+----------------------+----------------------+-------------------------------------------------------+-------+----------+
| Start Date           | End Date             | Category                                              | Price | Currency |
+----------------------+----------------------+-------------------------------------------------------+-------+----------+
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | wireless-usage                                        | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | pv-basic-rooms                                        | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | ip-messaging-data-storage                             | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | marketplace-bot-msg.ai-deliveryaware                  | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | wireless-usage-commands                               | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | conversations-participant-events                      | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | group-rooms-media-recorded                            | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | marketplace-algorithmia-named-entity-recognition      | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | wireless-usage-data-northamerica                      | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | marketplace-cadence-transcription                     | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | wireless-super-sim-smscommands-europe                 | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | conversations-endpoint-connectivity                   | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | experiment-india-sms                                  | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | voice-insights-sip-trunking-insights-on-demand-minute | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | calls-inbound                                         | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | sms-inbound-longcode                                  | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | wireless-super-sim-smscommands-africa                 | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | phonenumbers-tollfree                                 | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | autopilot-other                                       | $0.00 | USD      |
| 19/08/2021 00:00 UTC | 19/08/2021 00:00 UTC | sms-outbound-longcode                                 | $0.00 | USD      |
+----------------------+----------------------+-------------------------------------------------------+-------+----------+

Now, try out the options to see how the output changes. This time, use the --limit-records and --category options, as in the following example:

php application.php twilio:show-usage --limit-records 5 --category sms

This time, you should see fewer records, similar to the output below.

Twilio Account Usage Statistics

+----------------------+----------------------+----------+--------+----------+
| Start Date           | End Date             | Category | Price  | Currency |
+----------------------+----------------------+----------+--------+----------+
| 28/01/2021 00:00 UTC | 19/08/2022 00:00 UTC | sms      | $18.01 | USD      |
+----------------------+----------------------+----------+--------+----------+

Finally, experiment with the --start-date and --end-date options. You can use any date that PHP's DateTime::format() function supports. Here's an example:

php application.php twilio:show-usage --start-date "19.07.2022" --end-date "19.08.2022"

That's how to build a command line application to view your Twilio account usage

Symfony's Console Component makes it trivial to create console commands in PHP — so much so that it's the de facto package for creating console commands, used by Laravel, laminas-cli and so many other frameworks.

I hope that this tutorial has shown you two key things:

  • How quick they can be to create
  • How they help you interact with Twilio's APIs from the command line just as readily as you do within your web-based applications.

Check out the Usage Records documentation to see what else you can do with the API, and have fun playing with the code.

I can't wait to see what you build!

Matthew Setter is a PHP Editor in the Twilio Voices team and (naturally) a PHP developer. He’s also the author of Mezzio Essentials and Docker Essentials. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at msetter[at]twilio.com, on Twitter, and on GitHub.