Search by

samsmithcodes / laravel-cloudflare-doh-json

samsmithcodes

A Laravel package to query DNS records from Cloudflare's DOH JSON endpoint.

Package info

gitlab.samsmith.codes/samsmithcodes/laravel-cloudflare-doh-json

Homepage

pkg:composer/samsmithcodes/laravel-cloudflare-doh-json

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

1.0.0 2026-10-02 15:47 UTC

This package is auto-updated.

Last update: 2026-10-02 21:21:22 UTC


README

Laravel Cloudflare DOH JSON

Packagist PHP from Packagist Laravel versions Total Downloads

Look up DNS records from Laravel using Cloudflare's DNS over HTTPS JSON API.

The package sends the lookup, gets Cloudflare's JSON, and turns each record into a class for its type, such as MxRecord or TxtRecord, with its data split into properties.

use SamSmithCodes\CloudflareDohJson\Facades\CloudflareDohJson;

foreach (CloudflareDohJson::mx('gmail.com') as $record) {
    echo "{$record->priority} {$record->exchange}"; // 5 gmail-smtp-in.l.google.com
}

Installation

You can install the package via Composer:

composer require samsmithcodes/laravel-cloudflare-doh-json

Requires PHP 8.3+ and Laravel 13. The service provider and the CloudflareDohJson facade are registered automatically.

You may publish the configuration file:

php artisan vendor:publish --tag="cloudflare-doh-json"

Usage

There's a method for each record type, which returns a collection of that type's record class.

use SamSmithCodes\CloudflareDohJson\Facades\CloudflareDohJson;

$records = CloudflareDohJson::a('example.com');

$records->first()->address;     // "93.184.216.34"
$records->pluck('address');     // ["93.184.216.34", ...]
$records->first()->ttl;         // 300

CloudflareDohJson::txt('_dmarc.google.com')->first()->value;  // "v=DMARC1; p=reject; ..."
CloudflareDohJson::mx('gmail.com')->sortBy('priority');       // lowest priority first

To choose the type at runtime, pass a RecordType to records():

use SamSmithCodes\CloudflareDohJson\Enums\RecordType;

CloudflareDohJson::records('example.com', RecordType::MX);

reverse() finds the hostname of an IPv4 or IPv6 address:

CloudflareDohJson::reverse('1.1.1.1')->first()->hostname; // "one.one.one.one"

If you'd rather not use the facade, inject SamSmithCodes\CloudflareDohJson\CloudflareDohJson instead. It has the same methods.

A few things to know:

  • No records returns an empty collection, while a domain that doesn't exist throws a DomainNotFoundException.
  • Aliases (CNAMEs) are followed by Cloudflare, and only the records of the type you asked for are returned. For example, www.github.com is an alias for github.com, so CloudflareDohJson::a('www.github.com') returns github.com's A records.
  • Domain names have their trailing dot removed, so you get mail.example.com rather than mail.example.com..

Record types

Every record class has these properties:

PropertyDescription
nameThe domain name the record belongs to
ttlHow many seconds the record may be cached for
dataThe record's data exactly as Cloudflare returned it, e.g. "10 mail.example.com."

Each class also has properties for its type, which are all read only. Records can be converted with toArray() or json_encode().

MethodClassProperties
a()ARecordaddress (IPv4)
aaaa()AaaaRecordaddress (IPv6)
caa()CaaRecordflags, tag (e.g. issue), value (e.g. letsencrypt.org)
cname()CnameRecordtarget
dnskey()DnskeyRecordflags, protocol, algorithm, publicKey (base64)
ds()DsRecordkeyTag, algorithm, digestType, digest (lowercase hex)
https()HttpsRecordpriority, target, params (e.g. ['alpn' => 'h3,h2'])
mx()MxRecordpriority, exchange
naptr()NaptrRecordorder, preference, flags, services, regexp, replacement
ns()NsRecordnameserver
ptr()PtrRecordhostname
soa()SoaRecordprimaryNameserver, hostmaster, serial, refresh, retry, expire, minimumTtl
srv()SrvRecordpriority, weight, port, target
sshfp()SshfpRecordalgorithm, fingerprintType, fingerprint (lowercase hex)
svcb()SvcbRecordpriority, target, params
tlsa()TlsaRecordusage, selector, matchingType, certificateData (lowercase hex)
txt()TxtRecordvalue (the full text), segments (the strings of up to 255 characters the text is stored as)

Each property has a description in its class under src/Records.

Errors

Every failed lookup throws a LookupException, with a message explaining why, e.g. Cloudflare couldn't be reached or rejected the domain name. When the domain doesn't exist, it's the more specific DomainNotFoundException, which extends LookupException.

use SamSmithCodes\CloudflareDohJson\Exceptions\DomainNotFoundException;
use SamSmithCodes\CloudflareDohJson\Exceptions\LookupException;

try {
    $records = CloudflareDohJson::mx($domain);
} catch (DomainNotFoundException $e) {
    return back()->withErrors(['domain' => "{$domain} doesn't exist."]);
} catch (LookupException $e) {
    report($e);

    return back()->withErrors(['domain' => 'The DNS lookup failed, please try again.']);
}

Configuration

The defaults work without any configuration. To change them, set these in your .env file, or publish the config file.

KeyEnvDefaultDescription
endpointCLOUDFLARE_DOH_ENDPOINThttps://cloudflare-dns.com/dns-queryAny endpoint that uses Cloudflare's JSON format
timeoutCLOUDFLARE_DOH_TIMEOUT5Seconds to wait for a response
attemptsCLOUDFLARE_DOH_ATTEMPTS2How many times a query is tried before throwing an exception

Testing your application

Queries are sent with Laravel's Http facade, so you can fake them in your tests with Http::fake(). Fake responses use Cloudflare's JSON format, where type is the record type's number, e.g. RecordType::MX->value.

use Illuminate\Support\Facades\Http;

Http::fake([
    'cloudflare-dns.com/*' => Http::response([
        'Status' => 0,
        'Answer' => [
            ['name' => 'example.com', 'type' => 15, 'TTL' => 300, 'data' => '10 mail.example.com.'],
        ],
    ]),
]);

Testing

Run the full test suite with Docker, so you don't need PHP installed locally:

docker run --rm -it \
  -v ${PWD}:/app \
  registry.samsmith.codes/samsmithcodes/laravel-starter-tools:latest composer test

To run something else, swap composer test at the end of the command for:

  • One of the steps below, e.g. composer lint to fix the code style.
  • A single test file, e.g. vendor/bin/pest tests/Feature/CloudflareDohJsonTest.php.
  • Only the tests with a name matching a filter, e.g. vendor/bin/pest --filter="attempts".

What composer test runs

The steps are Composer scripts, defined under scripts in composer.json. They run in this order, and stop at the first failure:

StepToolWhat it checksConfig
composer analysePHPStan with LarastanThe code is type safe and has no bugs it can findphpstan.neon.dist
composer lint:checkLaravel PintThe code style is consistentpint.json
composer test:typesPest type coverageEvery parameter, property and return in src has a typecomposer.json
composer test:unitPest with Orchestra TestbenchThe tests in tests pass, run in parallelphpunit.xml.dist, tests/Pest.php, tests/TestCase.php

The tools

Pest runs the tests. It's built on PHPUnit, so it reads PHPUnit's config:

  • phpunit.xml.dist lists the test suites (Arch, Feature and Unit) and the src directory being tested. It runs the tests in a random order, so they can't rely on each other, and fails on warnings or unexpected output. To add a test, create a file ending in Test.php in tests/Feature or tests/Unit and it's picked up automatically.
  • tests/Pest.php runs before the tests, and makes every test use TestCase. Add any helpers shared between test files here. It also gives each parallel worker its own service provider manifest in the temp directory, as workers rewriting a shared one at the same time can fail on a Docker volume.
  • tests/ArchTest.php uses Pest's architecture testing to enforce rules across the code, e.g. strict types, every record class extending Record, and no dd() or insecure functions like md5().

Orchestra Testbench boots a minimal Laravel app for the tests, so the package is tested just as an app would use it:

  • tests/TestCase.php loads the package's service provider in getPackageProviders(), and calls Http::preventStrayRequests() before every test, so a test that forgets to fake a response fails rather than querying Cloudflare. A test can change config for itself with config([...]).
  • testbench.yaml and the workbench directory configure the app used by composer serve, for trying the package in a browser. The tests don't use them.

Pest type coverage checks everything in src has a type declared. It's configured by the options on the test:types script in composer.json: --min=100 is the percentage required, and --memory-limit raises PHP's memory limit, which the Docker image's default is too low for.

PHPStan, with the Larastan extension so it understands Laravel's facades and helpers, reads the code without running it to find bugs, e.g. passing the wrong type or calling a method that doesn't exist. phpstan.neon.dist sets:

  • level, how strict it is, from 0 to 10, currently 7.
  • paths, the directories it checks.
  • ignoreErrors, for false positives, each with a comment saying why. Only add one when the error is wrong, not to hide a real problem.

Laravel Pint checks the code style. pint.json uses Laravel's preset, with a few extra rules, e.g. a blank line before every return. composer lint:check only reports problems, while composer lint fixes them.

The tests

  • tests/ArchTest.php: architecture rules, e.g. every file declares strict types, and every record class extends Record.
  • tests/Feature/CloudflareDohJsonTest.php: lookups against a fake Cloudflare built with Http::fake(). It covers every record type, CNAMEs being left out, reverse lookups, the errors, and the configured number of attempts. No real requests are made.
  • tests/Feature/ServiceProviderTest.php: the singleton, config, publishing and Facade are registered.
  • tests/Unit/Records/RecordTest.php: every record class parses real Cloudflare data into its properties, including quoted text, escapes and brackets.

Adding a record type

  1. Add a case to RecordType in src/Enums/RecordType.php, with the type's number from the IANA list, and create its class in toRecord().
  2. Create the record class in src/Records, copying a similar one, e.g. MxRecord. Split the data with $this->fields() and set each property.
  3. Add a method named after the type to src/CloudflareDohJson.php, and a matching @method line to the facade.
  4. Add the type to the dataset in tests/Feature/CloudflareDohJsonTest.php, and some real data from Cloudflare to tests/Unit/Records/RecordTest.php.

License

Laravel Cloudflare DOH JSON is open-sourced software licensed under the MIT license.