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:
- Composer installed globally.
- PHP, 7.4 or higher, with the Internationalization extension.
- A Twilio account (free or paid). If you are new to Twilio click here to create a free account.
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:
- The Symfony Console Component: This provides all of the core classes for creating a custom Symfony command.
- The Twilio PHP Helper Library: This simplifies interacting with Twilio's APIs.
- PHP Dotenv: This simplifies storing and retrieving environment variables.
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>
).
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:
Parameter | Description |
---|---|
$maxResults | The maximum number of results to return. |
$startDate | This is the date that records must be created on or after. |
$endDate | This is the date that records must be created on or before. |
$category | A 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 | |
---|---|
Long | Short |
--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.
{
"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.