Monitoring Domain Expiration Dates using Laravel's Process Facade

Domain name renewal can be a pain in the backside. As developers, we all know the pang of shame that comes with a renewal charge for a hobby domain we've not gotten around to doing anything with yet. But worse than that is the domain that you are actually using, which expires due to a combination of missed renewal notifications and an expired credit card!

Ideally we want a reliable system which can alert us in plenty of time if we've an upcoming renewal. And, as luck would have it, Laravel makes it a breeze to put together such a system! It'll also give us a convenient excuse to take a closer look at Laravel's Process Facade.

Checking Expiry Dates

The whois tool is a command-line utility which gives registration information for a domain. When run for a specific domain, it will return information on the registration and expiry dates, who the registrar is, and contact information for the domain owner. Since GDPR, for privacy reasons this contact info is typically obfuscated by the registrar, but in our case we're only really concerned about the expiry date, which is still readily available!

~ whois conroyp.com
   Domain Name: CONROYP.COM
   ....
   Registrar WHOIS Server: whois.blacknight.com
   Registrar URL: http://www.blacknight.com
   Creation Date: 2009-04-27T20:01:43Z
   Registry Expiry Date: 2024-04-27T20:01:43Z
   ....

The Registry Expiry Date is the date we need to worry about. This is the point at which the current registration expires. Past this date, depending on the registrar there may be a grace period before it is fully suspended, but better safe than sorry, let's worry about this date.

We have a list of domains, and want to know the expiry details of each of them. We can get the info by calling whois on our command line, but how can we best run this command from inside a Laravel app? The tried and trusted way of calling external programs from PHP is running shell_exec() and trying to capture the output.

$output = shell_exec(`whois conroyp.com`);

One of the challenges of using shell_exec is that error handling can be tricky - if there's a command error, we're typically getting null back rather than anything more structured. Is there a nicer way of handling this?

Introducing Laravel's Process Facade

Laravel's Process Facade adds a nice wrapper around Symfony's Process Component. It gives us a clean and fluent API for managing external program calls from inside a Laravel app. It's a nice abstraction which not only makes our code cleaner but also significantly improves testability. It also provides us with powerful features for managing calls asynchronously, and is neatly integrated into the whole Laravel ecosystem.

$result = Process::run('ping -c www.google.com');

echo $result->output();
PING www.google.com (74.125.193.99): 56 data bytes
64 bytes from 74.125.193.99: icmp_seq=0 ttl=58 time=12.359 ms
64 bytes from 74.125.193.99: icmp_seq=1 ttl=58 time=8.110 ms
64 bytes from 74.125.193.99: icmp_seq=2 ttl=58 time=97.419 ms
64 bytes from 74.125.193.99: icmp_seq=3 ttl=58 time=11.136 ms
64 bytes from 74.125.193.99: icmp_seq=4 ttl=58 time=9.722 ms

--- www.google.com ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.110/27.749/97.419/34.864 ms

As well as fetching the output, there are a number of other nice helper functions to help us inspect the status of our command.

$result->command();    // The original command sent to the process
$result->successful();
$result->failed();
$result->exitCode();

Now that we have a way to call an external process, let's set up a command and start checking our domains!

Setting up the command

First we create a command, which will contain our renewal logic:

php artisan make:command CheckDomainExpiry

This will create a command CheckDomainExpiry.php inside Console/Commands. Open the file, and let's customise the signature used to trigger the command:

protected $signature = 'domain:check-expiry';

Inside the handle() function, load up our domains, and we're nearly ready to start checking:

public function handle()
{
    // Get an array of domains from DB, config, etc
    $domains = DomainService::fetchDomains();

    // Start checking
    foreach ($domains as $domain) {
        ...
    }
}

Using the Process Facade

Adding in the Process Facade, our code starts to take more shape

use App\Exceptions\DomainCheckFailedException;
use App\Notifications\DomainExpiryWarning;
use Illuminate\Support\Facades\Process;
...
public function handle()
{
    // Get an array of domains from DB, config, etc
    $domains = DomainService::fetchDomains();

    // Start checking
    foreach ($domains as $domain) {
        $process = Process::run(['whois', $domain]);
        if ($process->failed()) {
            throw new DomainCheckFailedException($domain);
        }

        if ($this->willExpireSoon($process->output())) {
            // Send custom notification to our admin list
            Notification::send(
                $this->getUsersToBeNotified(),
                new DomainExpiryWarning($domain)
            );
        }
    }
}

Now we are iterating over each domain, and calling whois. After the process runs, we can call ->failed() to check if there was a problem. If so, we can fire a custom exception for our app to handle.

With the Process facade, it's possible to pass either the full command to the run() function, or to pass the command and arguments as an array, whichever you prefer! Once we're happy that the command has run successfully, we'll check if the domain expires soon, and if so, send a simple custom notification using Laravel's in-built Notifications.

But how are we checking if the domain has expired?

private function willExpireSoon(string $commandOutput)
{
    if (!preg_match('/Registry Expiry Date: (.*)/', $commandOutput, $matches)) {
        throw new ExpiryNotFoundException();
    }

    // How many days notice do we want?
    $warningDays = 10;
    $expiryDate = new Carbon($matches[1]);
    return ($expiryDate->lt(Carbon::now()->addDays($warningDays)));
}

In this case, we're doing a simple regex on the command output. If the output doesn't contain the pattern we need, we'll fire a custom exception. If we find the output we expect, then we convert it to a Carbon object to help with date comparison. We check if the expiry date we've found is within the $warningDays window, and return true if so.

With the command all done, we can schedule it to run once a day, and we're done!

Schedule::command(CheckDomainExpiry::class)->daily();

Improving Performance with Process Pooling

When monitoring multiple domains, performance may become a concern. As each command will run synchronously, we may be waiting a long time for our full command to finish running. Laravel's Process Facade can utilize connection pooling to execute processes in parallel, significantly reducing the overall runtime. This approach is particularly beneficial when dealing with a large number of domains.

$pool = Process::pool(function (Pool $pool) use ($domains) {
    foreach ($domains as $domain) {
        $pool->as($domain)
            ->command(['whois', $domain]);
    }
});
$results = $pool->wait();
/**
 * $results = [
 *    'domain1.com' => <result>,
 *    'domain2.co.uk' => <result>,
 * ];
 */
foreach ($domains as $domain) {
    $result = $results[$domain];
    // Carry on checks as above...
    if ($process->failed()) {
        ...

Within the pool, calling ->as($domain) allows us to assign a name to the process, which will be used as the index in the array of results. Calling $pool->wait() means that we'll wait until all processes are completed. However, because we're running in parallel rather than sequentially, our max waiting time will be the time of the slowest process, rather than a sum of all processes.

N.B! if we do have a ridiculously-large number of domains to check, we'll be spawning a lot of system processes at the same time, which can cause problems with memory usage. If you've got a large number of jobs to run in parallel, it may be worth chunking them into groups, based on the available system resources.

Wrapping Up

Laravel's Process facade offers powerful, flexible tools for accessing system commands - in this case, allowing us to automate domain expiry monitoring. By transitioning from manual checks to an automated Laravel command, we significantly reduce the risk of human error and ensure timely alerts. This will allow us to have plenty of time to decide whether or not we want to renew that unused hobby domain for the fifth time - maybe this is the year we'll finally get around to launching that side project and making use of the domain!


CyberWiseCon 2025 Speaker

CyberWiseCon 2025

In May 2025, I'll be giving a talk at CyberWiseCon 2025 in Vilnius, Lithuania. From selling 10 Downing St, to moving the Eiffel Tower to Dublin, this talk covers real-world examples of unconventional ways to stop scrapers, phishers, and content thieves. You'll gain practical insights to protect assets, outsmart bad actors, and avoid the mistakes we made along the way!

Get your ticket now and I'll see you there!


Share This Article

Related Articles


Lazy loading background images to improve load time performance

Lazy loading of images helps to radically speed up initial page load. Rich site designs often call for background images, which can't be lazily loaded in the same way. How can we keep our designs, while optimising for a fast initial load?

Using Google Sheets as a RESTful JSON API

Save time by not building backends for simple CRUD apps. Use Google Sheets as both a free backend and JSON API endpoint!

Idempotency - what is it, and how can it help our Laravel APIs?

Idempotency is a critical concept to be aware of when building robust APIs, and is baked into the SDKs of companies like Stripe, Paypal, Shopify, and Amazon. But what exactly is idempotency? And how can we easily add support for it to our Laravel APIs?

Calculating rolling averages with Laravel Collections

Rolling averages are perfect for smoothing out time-series data, helping you to gain insight from noisy graphs and tables. This new package adds first-class support to Laravel Collections for rolling average calculation.

More