How to View Your Twilio Account Usage Using Laravel

March 02, 2023
Written by
Reviewed by
Ben Link
Twilion

How to View Your Twilio Account Usage Using Laravel

Twilio provides an API to retrieve actions made by your Twilio account and to filter them by both time and category. Using that API, in this tutorial, you’re going to learn how to build a small analytics dashboard for your Laravel application.

Let's begin!

Prerequisites

To get the most out of this tutorial, you're going to need the following things:

In addition, it'd be ideal if you have prior experience working with Laravel, Vue.js, and the command line.

Application overview

So how will the application work? There’s not much to it, actually. It will have one route that will follow the pattern below:

 '/twilio/usage/{recordLimit}/{timePeriod?}',

It will start off with /twilio/usage/, so that it's clear what information the route provides. It will also use two route parameters to help filter the information returned:

  • recordLimit: This limits the total number of usage records to be retrieved
  • timePeriod: This determines the time range to filter by

When requested, the route's handler will make a request for your Twilio usage records, filter them based on the value of the two route parameters, and render them in the dashboard.

The end result is a response that should look similar to the screenshot below.

The user records dashboard displaying records for the entire account history in Firefox

Scaffold a new Laravel project

The first thing that you need to do is to scaffold a new Laravel application. Do that and change into the generated application directory by running the following commands.

composer create-project laravel/laravel twilio-account-usage
cd twilio-account-usage

Check that the application works

Now, start the application by running the following command.

php artisan serve

Then, open it in your browser of choice where it should look similar to the screenshot below.

The default Laravel route after scaffolding a Laravel application being displayed in Firefox

As the application’s working, press Ctrl + C to stop it.

Add the required environment variables

The next thing to do is to set two environment variables: Twilio Account SID and Auth Token. These are required for the application can make authenticated requests to Twilio’s Usage Record API.

To do that, at the bottom of .env, in the application’s top-level directory, paste the following configuration.

TWILIO_ACCOUNT_SID="xxx"
TWILIO_AUTH_TOKEN="xxx"

The Account Info section of the Twilio Console

Then, from the Account Info section of Twilio Console's Dashboard, copy your Account SID and Auth Token. Paste the values into .env, in place of the placeholders for TWILIO_ACCOUNT_SID, and TWILIO_AUTH_TOKEN respectively.

Install the required dependencies

There are only two required PHP dependencies:

  • Laravel-money: This package is an abstraction on MoneyPHP which simplifies formatting values in localised currency formats, e.g., R$10,00 and €10,00;
  • Twilio's PHP Helper Library: This package simplifies interacting with Twilio's APIs in PHP

Everything else which the application needs comes standard in Laravel.

To install them, run the following command.

composer require \
    cknow/laravel-money \
    twilio/sdk

There are a few frontend dependencies, though. The key ones are Tailwind CSS, Vue.js, and Vite's Vue plugin. These will simplify the development of the application's frontend.

To install them, run the following commands.

npm install -D tailwindcss postcss autoprefixer vue-loader@next
npm install --save vue@next @vitejs/plugin-vue

Update Vite's configuration

The next thing to do is to update Vite's configuration to use the Vue.js plugin to build the Vue component into the application's app.js file.

To do that, update vite.config.js, located in the top-level directory of the project, to match the code below.

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        vue(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});

Create Tailwind's configuration file

Then, create a Tailwind CSS configuration file, by running the following command.

npx tailwindcss init -p

The file will be located in the root directory of the project and named tailwind.config.css. Update it to match the following configuration.

hl_lines="4,5,6"
/** @type {import('tailwindcss').Config} */
module.exports = {
    content: [
        "./resources/**/*.blade.php",
        "./resources/**/*.js",
        "./resources/**/*.vue",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
}

Update Laravel’s Service Container

The next thing to do is to register a Twilio Client object with Laravel’s Service Container; one initialised with your Twilio Account SID and Auth Token. The simplest way to do this is to create a custom ServiceProvider class, by running the following command.

php artisan make:provider TwilioServiceProvider

A new file, named TwilioServiceProvider.php, is now available in app/Providers/. Update the body of the file's register() method and add the required use statements, to match the code below.

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Twilio\Rest\Client;

class TwilioServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(Client::class, function (Application $app) {
            return new Client(
                config('twilio.account_sid'),
                config('twilio.auth_token')
            );
        });
    }

    public function boot(): void
    {
        //
    }
}

The register() method initialises a new Twilio Client object, using the values of twilio.auth_token and twilio.account_sid from Laravel’s global configuration and registers the Client object as a new service in Laravel’s Service Container.

To add twilio.auth_token and twilio.account_sid to Laravel’s global configuration, create a new file in the config directory named twilio.php. Then, paste the code below into the file.

<?php

return [
    'auth_token' => env('TWILIO_AUTH_TOKEN', null),
    'account_sid' => env('TWILIO_ACCOUNT_SID', null),
];

The two variables will contain the values of the TWILIO_AUTH_TOKEN and TWILIO_ACCOUNT_SID environment variables, respectively.

And finally, you need to enable the new TwilioServiceProvider class. To do that, in config/app.php, add the following code to the end of the "Application Service Providers…" section of the providers element, which the file returns.

\App\Providers\TwilioServiceProvider::class,

Create a custom controller

The next thing to do is to create a single action controller to handle requests to render the Twilio usage dashboard. Create the controller by running the following Artisan command.

php artisan make:controller TwilioUsageController --invokable

The new controller will be created in app/Http/Controllers and named TwilioUsageController.php. Update it to match the code below.

<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Iterators\Filters\UsageWithCountFilter;
use ArrayIterator;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Twilio\Rest\Api\V2010\Account\Usage\Record\LastMonthInstance;
use Twilio\Rest\Client;

class TwilioUsageController extends Controller
{
    public const DEFAULT_RECORD_LIMIT = 20;
    public const DEFAULT_USAGE_TYPE = 'last_month';

    public function __construct(private readonly Client $twilio) {}

    public function __invoke(
        Request $request,
        int $recordLimit = self::DEFAULT_RECORD_LIMIT,
        string $timePeriod = self::DEFAULT_USAGE_TYPE
    ): View
    {
        $usageRecords = match ($timePeriod) {
            'all_time' => $this->getAllUsageRecords($recordLimit),
            'today' => $this->getTodaysUsageRecords($recordLimit),
            default => $this->getLastMonthUsageRecords($recordLimit),
        };

        $recordsIterator = new ArrayIterator($usageRecords);
        $recordsIterator->uasort(function ($recordOne, $recordTwo) {
            return ($recordOne->category >= $recordTwo->category);
        });
        $filteredRecords = new UsageWithCountFilter($recordsIterator);

        return view(
            'twilio.account.usage',
            [
                'title' => 'Twilio Account Usage',
                'usageRecords' => $filteredRecords,
                'recordCount' => iterator_count($filteredRecords)
            ]
        );
    }

    /**
     * @param int $recordLimit
     * @return LastMonthInstance[]
     */
    public function getLastMonthUsageRecords(int $recordLimit): array
    {
        return $this->twilio
            ->usage
            ->records
            ->lastMonth
            ->read([], $recordLimit);
    }

    public function getTodaysUsageRecords(int $recordLimit): array
    {
        return $this->twilio
            ->usage
            ->records
            ->today
            ->read([], $recordLimit);
    }

    public function getAllUsageRecords(int $recordLimit): array
    {
        return $this->twilio
           ->usage
            ->records
            ->read([], $recordLimit);
    }
}

The controller is initialised with a Twilio Client object. It uses that object in the __invoke() method to:

  1. Retrieve usage records for a given time period;
  2. Sort the retrieved records in ascending order of category;
  3. Filter out inactive categories using a custom FilterIterator named UsageWithCountFilter;
  4. Render a table with the filtered and sorted records

Twilio supports filtering records in several ways. For the purposes of a concise example, however, the code will only filter in three ways:

  • last month: returns usage records for the prior month. This is implemented in getLastMonthUsageRecords();
  • all time: returns all usage records. This is implemented in getAllUsageRecords();
  • today: returns usage records for the current day. This is implemented in getTodaysUsageRecords()

Create the usage record filter

The next thing to do is to create a class which extends FilterIterator. It's part of the SPL (Standard PHP Library) and filters out unwanted values from a traversable object, such as an array.

To build it, first, in the app directory, create a new directory named Iterators. Then, inside the Iterators directory, create a directory named Filters. Inside the Filters directory, create a file named UsageWithCountFilter.php. Finally, paste the code below into the new PHP file.

<?php

declare(strict_types=1);

namespace App\Iterators\Filters;

use Iterator;

class UsageWithCountFilter extends \FilterIterator
{
    public function accept(): bool
    {
        $usageRecord = $this->getInnerIterator()->current();
        return (! empty($usageRecord->count));
    }
}

The accept() method returns true if the current record’s count property is not empty.

Create the controller's template file

The next thing to do is to create the controller’s Blade template. To do that, in the resources/views directory, create the following directory structure: twilio/account. Then, in views/twilio/account create a new file named usage.blade.php, and add the following code to the file.

@php use Cknow\Money\Money;@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title }}</title>
    @vite('resources/css/app.css')
</head>
<body>
<main>
    <div class="content-wrapper">
        <header>
            <div class="flex-auto">
                <h1 class="text-xl font-bold">{{ $title }}</h1>
                <p class="text-base mt-2">A list of your current account usage.</p>
            </div>
            <div class="flex-none mr-4 mt-3">
                <img src="/images/twilio-logo.svg"
                    alt="Twilio's logo"
                    class="w-12">
            </div>
        </header>

        <div id="timePeriodForm"></div>

        <div class="usage-records-wrapper">
            <table class="w-full" id="usageRecords">
                <thead>
                <tr>
                    <th>Category</th>
                    <th>Qty</th>
                    <th>Amount</th>
                </tr>
                </thead>
                @foreach($usageRecords as $record)
                    @php $currency = \strtoupper($record->priceUnit); @endphp
                    <tbody>
                    <tr>
                        <td class="font-bold">{{ $record->category }}</td>
                        <td>{{ $record->count }}</td>
                        <td>{{ Money::$currency($record->price) }}</td>
                    </tr>
                    </tbody>
                @endforeach
                <tfoot>
                <tr>
                    <td colspan="3">
                        @if($recordCount)
                            {{ $recordCount }} record{{ ($recordCount > 1) ? 's' : '' }} available.
                        @else
                            No records available
                        @endif
                    </td>
                </tr>
                </tfoot>
            </table>
        </div>
    </div>    
</main>
<footer>
    <section>
        &copy; Twilio {{ date('Y') }}.
        Designed and developed by 
        <a href="https://matthewsetter.com"
             target="_blank"
             class="underline underline-offset-2 decoration-2 decoration-indigo-700">Matthew Setter</a>.
    </section>
</footer>
@vite('resources/js/app.js')
</body>
</html>

The template has four sections:

  • A header which provides the dashboard overview;
  • A form (timePeriodForm), rendered by a Vue.js Component, where the user can set the date range;
  • A table where the usage records are rendered, if available.
    Usage records have a number of properties, but the table renders just three; these are:
    • category: the usage category
    • count: the number of usage events, such as the number of calls
    • price: the total price of the usage in the currency specified in the price_unit property
  • A small footer

Update the application's stylesheet

The next thing to do is to update the application's stylesheet to include most of the dashboard's required styles, by updating resources/css/app.css to match the following code.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
    body {
        @apply antialiased bg-gray-100 p-3;
    }

    th {
        @apply text-left;
    }

    th, td {
        @apply py-1 px-2 md:py-3 lg:py-5 lg:px-3;
    }

    thead {
        @apply border-b-2 border-slate-300 font-bold;
    }

    tbody tr {
        @apply border-b-2 border-slate-200;
    }

    tfoot tr td {
        @apply py-2 lg:pb-1 text-sm text-slate-600 font-semibold;
    }

    main {
        @apply relative lg:flex lg:flex-col lg:items-center bg-center selection:bg-red-500 selection:text-white lg:mt-8 mb-3 lg:mb-6;
    }

    header {
        @apply w-full flex flex-row px-3 md:px-4 lg:px-0;
    }

    footer {
        @apply lg:flex lg:flex-col lg:items-center mb-4 lg:mb-8;
    }

    footer section {
        @apply w-full lg:w-3/4 lg:max-w-6xl ml-1 text-slate-700 text-sm text-center md:text-left;
    }
}

@layer components {
    .content-wrapper {
        @apply w-full lg:w-3/4 lg:max-w-6xl bg-white py-2 md:py-3 lg:py-4 lg:px-5 border-2 border-slate-300 rounded-md drop-shadow-md;
    }

    .usage-records-wrapper {
        @apply flex justify-center mt-2 sm:items-center sm:justify-between px-3 md:px-4 lg:px-0 w-full;
    }

    #timePeriodForm {
        @apply mt-4 sm:justify-between px-4 md:px-6 lg:px-0 w-full;
    }
}

While it's not initially recommended to do this, I prefer to reuse styles in this way, rather than replicating them throughout one or more templates.

Lastly, create a new directory in public, named images. Then, download the Twilio logo to public/images and name it twilio-logo.svg.

Create a custom Vue Component

In the resources directory, create a new directory named components. Then, in that new directory create a new file named TimePeriodForm.vue; create a Vue Component stub, if your editor or IDE supports it.

Then, update the file to match the following code.

<template>
    <form method="get"
        name="timePeriodForm"
        id="timePeriodForm">
        <label>
            <span class="font-bold">Time Period:</span>
            <select v-model="timePeriod"
                    @change="changeTimePeriod"
                    class="ml-2 py-2 px-3 rounded-md border-2 border-indigo-900 bg-indigo-700 text-white">
                    <option v-for="timePeriod in timePeriods"
                        v-bind:key="timePeriod.value"
                        v-bind:value="timePeriod.value"
                    >{{ timePeriod.label }}</option>
            </select>
        </label>
    </form>
</template>

<script>
    export default {
        name: "TimePeriodForm",
        data() {
            return {
                urlPathBase: '/twilio/usage/',
                recordLimit: 20,
                type: 'last_month',
                timePeriod: '',
                timePeriods: [
                    {
                        'label': 'Last Month',
                        'value': 'last_month'
                    },
                    {
                        'label': 'Today',
                        'value': 'today'
                    },
                    {
                        'label': 'All Time',
                        'value': 'all_time'
                    },
                ]
            }
        },
        methods: {
            initTimePeriod() {
                let url = new URL(window.location.href);
                let subPath = url.pathname.replace(this.urlPathBase, '').split('/');
                this.recordLimit = subPath[0];
                this.timePeriod = subPath[1];
            },
            changeTimePeriod(event) {
                this.timePeriod = event.target.value;
                window.location.href = `${this.urlPathBase}${this.recordLimit}/${this.timePeriod}`
            }
        },
        mounted() {
            this.initTimePeriod()
        }
}
</script>

<style scoped></style>

In the template section, you can see that it renders a form with a select box. That select box:

  • Contains an option element for each element of the timePeriods property;
  • Is bound to the timePeriod property

When the component is initialised the initTimePeriod() function is called. This method retrieves the current URL and parses it to retrieve the record limit and usage type parameters. With these values, it sets the recordLimit and timePeriod properties respectively.

When the value of the select box changes the changeTimePeriod() function is called. This function redirects the user to the current URL but updates it for the specified usage type.

Enable the Vue Component

With the component created, you now need to enable it. To do that, update resources/js/app.js to match the following code.

hl_lines="3,4,6"
import './bootstrap';

import { createApp } from 'vue';
import TimePeriodForm from '../components/TimePeriodForm.vue';

createApp(TimePeriodForm).mount("#timePeriodForm")

Update the routing table

Now, there’s one final thing to do, which is adding a route to the application’s routing table for the dashboard endpoint. To do that, add the following to the end of routes/web.php.

Route::get(
    '/twilio/usage/{recordLimit}/{timePeriod?}',
    \App\Http\Controllers\TwilioUsageController::class
)
    ->where('recordLimit', '[0-9]+')
    ->whereIn('timePeriod', ['last_month', 'today', 'all_time'])
    ->name('usage');

It adds a GET-accessible route named usage to the routing table, which is handled by TwilioUsageController’s __invoke() method. The route’s path starts with /twilio/usage/ and has two route parameters:

  • recordLimit: This value limits the total number of records to be retrieved. Thanks to the regular expression ('[0-9]+') in the call to where() it can only accept a numerical value
  • timePeriod: This determines the time range to filter by. Thanks to the second parameter to the whereIn function, it only accepts three values: last_month, today, and all_time.

Test the application

With all of the code written, it’s time to test that it works. To do that, start it with the following command.

php artisan serve

Then, in a separate terminal window or tab, run the following command.

npm run dev

Then, open https://localhost:8000/twilio/usage/20/last_month in your browser of choice; change the port if Laravel didn’t bind to port 8000. There, you should see that it renders similarly to the example below.

The user records dashboard displaying records for the entire account history in Firefox

Now, click any of the options in the “Usage Type” select dropdown and see how the usage records change. Depending on your account usage, you may have a number of records appear for each usage type or very few to none.

That's how to view your Twilio account usage in Laravel apps

While there were a number of parts that had to be brought together for this application to work, Laravel and Twilio’s PHP Helper Library, more or less, make it trivial to retrieve your Twilio usage information and render it professionally, as this app does.

How would you improve the app? Share your thoughts with me on Twitter, or create an issue in the GitHub repository.

Matthew Setter is a PHP Editor in the Twilio Voices team. He’s also the author of Mezzio Essentials and Docker Essentials. You can find him at msetter@twilio.com, Twitter, and GitHub.