No matter where your data comes from — a user, database, an environment variable, or somewhere else entirely — it must be validated. If not, how can you be sure that it's valid?
This is such an important and accepted part of modern software development that all of the major PHP frameworks, such as Laravel, Symfony, and Mezzio provide a validation component. If you're not using a framework, Composer can quickly integrate a third-party library such as laminas-validator.
However, what if a validation library or a framework's validation component doesn't support a validator that matches your use case? In that case, it's time to write a custom validator.
In this tutorial, I'm going to show you how to write a custom phone number validator for laminas-validator using Twilio's Lookup API.
Tutorial Requirements
To follow this tutorial you need the following components:
- PHP 7.4.
- A free Twilio account. If you are new to Twilio click here to create a free account now and receive a $10 credit when you upgrade to a paid account.
- Composer installed globally.
Why write a custom phone number validator?
In my fictitious small business, The Little PHP Shop, customers can register for an account using an email address and phone number. laminas-validator already supports email address validation, but it doesn't support phone number validation. Given that, we need to write a custom validator.
We're not verifying if a phone number is in the possession of a given person, active, and so on. We're only validating if a given string forms a valid phone number.
Validating phone numbers isn't an easy task, partly because there are just so many ways to write valid ones. For example, in the list below you can see various combinations of valid German phone numbers.
- +49 123 4567 1234
- +49 123 45671234
- +49 (0)123 45671234
- +49 12345671234
- +4912345671234
- 004912345671234
- 0123 4567 1234
- 0123 45671234
- 012345671234
Bearing in mind that this is just one country, I hope you see that it could be quite complex to code up a solution to validate phone numbers from every country in the world covering all of their possible permutations.
So to avoid that path of rabid complexity, in this tutorial I'm going to show you how to use Twilio's Lookup API to implement phone number validation in a comparatively simple way.
Before I describe how if you're not familiar with the Lookup API, here's a quick overview:
The Lookup API provides a way to retrieve additional information about a phone number.
The API returns two things if a phone number is valid:
- An HTTP 200 status code, showing that the request succeeded.
- A JSON payload containing a range of details about the number. It includes the carrier, phone number type, and phone number in multiple formats, which you can see below.
{
"caller_name": null,
"carrier": {
"error_code": null,
"mobile_country_code": "310",
"mobile_network_code": "456",
"name": "verizon",
"type": "mobile"
},
"country_code": "US",
"national_format": "(510) 867-5310",
"phone_number": "+15108675310",
"add_ons": null,
"url": "https://lookups.twilio.com/v1/PhoneNumbers/+15108675310"
}
If the number isn't valid, then an HTTP 404 status code is returned, along with a JSON response body with applicable details as to why and what to do.
{
"code": 20404,
"message": "The requested resource /PhoneNumbers/+1 was not found",
"more_info": "https://www.twilio.com/docs/errors/20404",
"status": 404
}
To keep the validator somewhat simplistic, we're only going to validate phone numbers in two formats, national: "(510) 867-5310" and international: "+15108675310".
In short, how the validator will then work is like this:
- It will make a request to the Lookup API
- If the request returns phone number details, then we use that as confirmation that the phone number exists and is therefore valid.
- If it returns an error, then it's safe to say that the phone number does not exist, and is not valid.
To simplify interacting with the Lookup API, we're going to use Twilio's PHP helper library. Let's begin!
Setup the project directory
The first thing we need to do is to create the project directory structure and then change into the new directory. To do that, run the commands below. If you are using a Unix or macOS computer:
mkdir -p twilio-phone-number-validator/src/Twilio
cd twilio-phone-number-validator
If you're using Microsoft Windows, use the commands below instead.
mkdir twilio-phone-number-validator/src/Twilio
cd twilio-phone-number-validator
Write the code
Create a Composer configuration
Next, in the project's root directory, create a new file named composer.json, and paste the code below into it.
{
"require": {
"php": "^7.4 || ~8.0.0"
},
"autoload": {
"psr-4": {
"Settermjd\\Validator\\": "src/"
}
}
}
The configuration sets up a PSR-4 autoloaded namespace for our code Settermjd\Validator
which points to src, and the required versions of PHP.
Install the required dependencies
Next, we need to install the project's dependencies:
- Twilio's PHP Helper Library: You need this to interact with Twilio's Lookup API.
- PHP dotenv: This avoids storing your Twilio credentials in with the PHP code, which is one of the 12 principles of modern application design.
- laminas-validator: as we're building a custom Laminas validator, then we need to base classes so that we can build upon them.
Run the command below, in the root directory of the project, to install them. We could have added them to composer.json in the previous step, but using composer require will ensure that the latest versions are installed—no matter when you follow this tutorial.
composer require --sort-packages \
laminas/laminas-validator \
twilio/sdk \
vlucas/phpdotenv
Retrieve your Twilio credentials
Next, you need to retrieve your Twilio credentials so that the validator can make authenticated requests to Twilio's Lookup API. You will store these credentials in a new file named .env. Create the file in the root directory of the project, then paste the code below into it.
TWILIO_AUTH_SID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_AUTH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Next, from the Twilio Console's Dashboard, copy your "Account SID" and "Auth Token" and paste them in place of the respective placeholder values in .env.
Create the phone number validator
Next, create a new file named PhoneNumber.php in src/Twilio/ and copy the code from GitHub into it. Then, let's step through it.
<?php
declare(strict_types=1);
namespace Settermjd\Validator\Twilio;
use Laminas\Stdlib\ArrayUtils;
use Laminas\Validator\AbstractValidator;
use Traversable;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Client;
It starts by setting the namespace and importing the required classes.
class PhoneNumber extends AbstractValidator
{
public const PHONE_NUMBER_INTL = 'phone_number_intl';
public const PHONE_NUMBER_NTL = 'phone_number_ntl';
protected array $messageTemplates = [
self::PHONE_NUMBER_INTL => "'%value%' is not a valid phone number in E.164 format.",
self::PHONE_NUMBER_NTL => "'%value%' is not a valid nationally formatted phone number.",
];
After that, it defines a new class, PhoneNumber
which extends Laminas\Validator\AbstractValidator
. We're doing this as AbstractValidator
greatly simplifies implementing a custom laminas validator.
PhoneNumber
then defines two constants and an array named $messageTemplates
. The array defines message templates for when a phone number cannot be validated, letting us return a different message based on whether the phone number being validated is in national or international format.
private Client $client;
private string $countryCode = '';
public function __construct($options = null)
{
parent::__construct($options);
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}
if (array_key_exists('client', $options)) {
$this->client = $options['client'];
}
if (array_key_exists('countryCode', $options) && $options['countryCode'] !== '') {
$this->countryCode = $options['countryCode'];
}
}
It then defines two private class member variables:
$client
: This will store the TwilioClient
object which we'll use to make requests to Twilio's Lookup API.$countryCode
: This, optionally, stores the phone number's country code, which is required if the number is in national format.
After that, it defines the class' constructor. The method takes one parameter, an array named $options
, which contains one or more initialization options. At most, the constructor will make use of two: an initialized Client
object and a country code. These are used to initialize the applicable class member variables if present.
public function isValid($value)
{
if ($value === '') {
return false;
}
$this->setValue($value);
Next, it defines the isValid
method, required by AbstractValidator
, where we define the validation logic. The role of the method is to return true
if the value passed to it validates, and false
otherwise.
Here's how it will work. It takes one argument, $value
, which is a string containing the phone number to validate. If the phone number is empty, the code returns false
immediately. It does this as an empty phone number, whether national or international, would always return an HTTP 404. Given that, there's no point wasting the user's time by making a request to Twilio that is bound to fail.
try {
$fetchOptions = ["type" => ["carrier"]];
if ($this->countryCode !== '') {
$fetchOptions['countryCode'] = $this->countryCode;
}
If the phone number isn't empty, it initializes an array, $fetchOptions
, based on the number format being validated.
In the table below, you can see how it needs to be composed, based on the phone number's format.
International Number |
National Number |
[ "type" => ["carrier"], ] |
[ "type" => ["carrier"], "countryCode" => "US", ] |
I've deliberately skipped over many options that the API supports, as they weren't relevant in this simplistic example.
$this->client
->lookups
->v1
->phoneNumbers($value)
->fetch($fetchOptions);
return true;
} catch (TwilioException $e) {
if ($this->countryCode === '') {
$this->error(self::PHONE_NUMBER_INTL);
} else {
$this->error(self::PHONE_NUMBER_NTL);
}
return false;
}
}
}
Finally, a request is made to Twilio's Lookup API wrapped in a try/catch block. This is because if the request returns an HTTP 404, then a TwilioException
is thrown. If one is thrown, then we know that the number isn't valid. Here, we set the appropriate error message, based on whether the country code was set and return false
. However, if the request succeeds, true
is returned.
Before we test the code, there's one final thing I want to cover. As the request returns phone number information in the response, it might seem wasteful to throw that information away. However, using the Lookup API can be far less work than creating a regular expression capable of validating every possible phone number yourself. Plus, the Lookup API doesn't have a dedicated validation endpoint.
This is especially the case when you consider that requests for carrier and caller name information aren't free; Carrier information costs $0.005 per phone number looked up. Caller name information (currently available only in the US) costs $0.01 per phone number looked up.
Test the code
Now that the code's written, it's time to test that it works. Create a new file, named index.php in the root directory of the project. In it, paste the code below.
<?php
use Settermjd\Validator\Twilio\PhoneNumber;
use Twilio\Rest\Client;
require_once 'vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$sid = $_ENV['TWILIO_ACCOUNT_SID'];
$token = $_ENV['TWILIO_AUTH_TOKEN'];
$client = new Client($sid, $token);
$validator = new PhoneNumber([
'client' => $client,
]);
print $validator->isValid('xxxxxxxxxxxxxx')
? 'Is valid.'
: 'Is not valid';
Replace the phone number placeholder, 'xxxxxxxxxxxxxx', with a valid international phone number, such as "+14151231234". Then, in the terminal, run the command below to test that the validator works. If successful, you should see "Is valid." printed to the terminal.
php index.php
Now, let's test it using a valid national number. First, replace the phone number you used with a phone number in national format, such as "0417111111".
Secondly, add a second key/value pair to the array passed to PhoneNumber
's constructor, where the key's name is countryCode
and its value is the two-letter country code of the phone number you're validating, so that it resembles the example below.
$validator = new PhoneNumber([
'client' => $client,
'countryCode' => 'AU',
]);
With the changes made, run the command below to validate the number.
php index.php
If you've entered a valid combination of phone number and country code, then you should see "Is valid." printed to the terminal.
Finally, let's test the validator using an invalid phone number. Replace the phone number you used with "+1" and run the command above, again. This time, you should see "Is not valid." printed to the terminal.
You can remove the countryCode
element from the array passed to PhoneNumber
's constructor, or leave it if you prefer.
That's how to build a PHP phone number validator using Twilio
There's a lot more functionality that we could have tapped into. However, our custom validator does what we need. If the available laminas validators don't quite do what you need, why not have a go at writing a custom one yourself.
I'd love to see what you build!
Matthew Setter is a PHP Editor in the Twilio Voices team and a polyglot 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@twilio.com; he's also settermjd on Twitter and GitHub.

In the article, you will learn how to create a drag-and-drop file upload system in CakePHP using Dropzone.js, which leverages AJAX to upload files without requiring a page refresh.

This tutorial will teach you the essentials of implementing CRUD operations in CakePHP. It illustrates how users can create, read, update, and delete records, thus providing a guide to managing data in your application in CakePHP.

In this tutorial, you will learn how to export data from a MySQL database to a CSV file with the CakePHP framework.

In this tutorial, you will get an in-depth exploration of Docker — in the context of Laravel. Then, rather than relying on Laravel Sail's pre-configured environment, you will learn how to run Laravel inside a Docker and deploy it with Docker Compose.

x

In this tutorial, you will learn how you can use the Mercure protocol in your Symfony applications to broadcast updates to the frontend.