hiblaphp / dns
Async DNS resolver for HiblaPHP
Requires
- php: >=8.3
- hiblaphp/cache: dev-main
- hiblaphp/event-loop: dev-main
- hiblaphp/promise: dev-main
- hiblaphp/stream: dev-main
- laravel/pint: ^1.25
Requires (Dev)
- mockery/mockery: ^1.0
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
This package is auto-updated.
Last update: 2026-03-21 07:56:45 UTC
README
Async, non-blocking DNS resolution for PHP with a full executor pipeline.
hiblaphp/dns provides promise-based DNS resolution built on top of the Hibla
event loop. Queries never block the thread — UDP and TCP transports are driven
entirely by non-blocking I/O watchers. A composable executor pipeline handles
caching, deduplication, retries, timeouts, transport fallback, and hosts file
resolution out of the box.
Contents
Getting started
The API
- Choosing the Right Entry Point
- Quick Resolve
- Reusable Resolver
- Building a Resolver
- Resolving Records
- Raw Executor Usage
- Cancellation
Configuration
The Executor Pipeline
Reference
Meta
Installation
composer require hiblaphp/dns
Requirements:
- PHP 8.3+
hiblaphp/event-loophiblaphp/promisehiblaphp/stream
Introduction
PHP's built-in gethostbyname() and dns_get_record() are synchronous — they
block the entire thread while waiting for the DNS server to respond. In an async
application running on the Hibla event loop, a blocking DNS lookup stalls all
timers, fibers, and I/O for the duration of the call.
hiblaphp/dns replaces these with non-blocking alternatives. Queries are sent
over UDP or TCP sockets managed by the event loop, and the result is delivered
as a promise that resolves when the DNS server responds — without blocking
anything else.
The library is built around a composable executor pipeline. Each query passes
through a stack of decorators in order: hosts file check, cache lookup, query
deduplication, transport selection, timeout enforcement, and retry logic. The
full pipeline is assembled automatically when you call Dns::resolve(),
Dns::create(), or Dns::builder()->build().
->wait() vs await()
The examples in this README use ->wait() to drive the event loop until the
promise settles. This is the correct approach when using hiblaphp/dns on its
own — ->wait() only requires hiblaphp/event-loop and hiblaphp/promise,
which this package already depends on.
If you are using this library inside a larger Hibla async application where a
fiber is already running — for example inside an async() block — you can use
await() from hiblaphp/async instead. Both resolve the same promise. The
difference is that ->wait() blocks the calling code until the loop settles,
while await() suspends only the current fiber and lets the event loop continue
running other work in the meantime.
Quick Start
require __DIR__ . '/vendor/autoload.php'; use Hibla\Dns\Dns; // Resolve a domain to a single IPv4 address using system DNS configuration $ip = Dns::resolve('example.com')->wait(); echo $ip; // 93.184.216.34
Choosing the Right Entry Point
There are three ways to use this library, each suited to a different situation:
Dns::resolve() / Dns::resolveAll() ← one-off lookups with system defaults
Dns::create() ← multiple lookups with system defaults
Dns::builder()->build() ← any lookup needing custom configuration
Dns::resolve() and Dns::resolveAll() are one-line static shortcuts.
Each call reads the system DNS configuration from disk (/etc/resolv.conf on
Unix, wmic on Windows) and constructs a fresh resolver. This is fine for a
single lookup but wasteful for multiple lookups — each call pays the system
configuration cost again.
Dns::create() reads the system DNS configuration once and returns a
reusable resolver. Use this when making multiple lookups with default settings —
you pay the configuration cost once and reuse the resolver for every query:
use Hibla\Dns\Dns; // System config is read once here $resolver = Dns::create(); // All three queries reuse the same resolver — no repeated disk reads $ip1 = $resolver->resolve('example.com')->wait(); $ip2 = $resolver->resolve('google.com')->wait(); $ip3 = $resolver->resolve('github.com')->wait();
Dns::builder()->build() gives you full control over nameservers, timeouts,
retries, and caching. Use this whenever you need anything beyond the defaults:
use Hibla\Dns\Dns; $resolver = Dns::builder() ->withNameservers(['1.1.1.1', '8.8.8.8']) ->withTimeout(3.0) ->withRetries(2) ->withCache() ->build();
Quick Resolve
The static Dns::resolve() and Dns::resolveAll() methods are the fastest way
to perform a one-off lookup without any setup:
use Hibla\Dns\Dns; use Hibla\Dns\Enums\RecordType; // Resolve to a single IPv4 address $ip = Dns::resolve('example.com')->wait(); // Resolve all A records $ips = Dns::resolveAll('example.com', RecordType::A)->wait(); // Resolve MX records $mx = Dns::resolveAll('example.com', RecordType::MX)->wait();
For multiple lookups, use Dns::create() instead to avoid re-reading system
configuration on every call.
Reusable Resolver
Dns::create() builds a resolver with system defaults and returns it for reuse.
The system DNS configuration is read once at construction time:
use Hibla\Dns\Dns; use Hibla\Dns\Enums\RecordType; $resolver = Dns::create(); $ip = $resolver->resolve('example.com')->wait(); $mx = $resolver->resolveAll('example.com', RecordType::MX)->wait(); $txt = $resolver->resolveAll('example.com', RecordType::TXT)->wait();
Building a Resolver
Dns::builder() returns a fluent builder. Every configuration method returns a
new instance — the builder is immutable and safe to reuse across multiple
build() calls.
use Hibla\Dns\Dns; $resolver = Dns::builder() ->withNameservers(['1.1.1.1', '8.8.8.8']) ->withTimeout(3.0) ->withRetries(2) ->withCache() ->build();
Nameservers
Pass a single IP or an array. When multiple nameservers are provided, the first is treated as primary and the rest as fallbacks — if the primary fails, the next is tried automatically.
// Single nameserver $resolver = Dns::builder() ->withNameservers('8.8.8.8') ->build(); // Multiple nameservers — first is primary, rest are fallbacks $resolver = Dns::builder() ->withNameservers(['1.1.1.1', '8.8.8.8', '9.9.9.9']) ->build();
If withNameservers() is not called, the builder loads nameservers from the
system configuration automatically. If system configuration cannot be loaded,
Cloudflare (1.1.1.1) and Google (8.8.8.8) are used as defaults.
When three or more nameservers are configured, the fallback chain is nested.
For three nameservers the structure is effectively
FallbackExecutor(FallbackExecutor(NS1, NS2), NS3). If all servers fail, the
error message will contain each server's error concatenated in the chain — this
is expected and useful for diagnosing which servers were tried.
Timeout
Sets the maximum time in seconds to wait for a response from a nameserver before
the query is rejected with a TimeoutException. Applies per transport attempt —
a query that retries will apply the timeout to each individual attempt.
$resolver = Dns::builder() ->withTimeout(3.0) // 3 seconds per attempt ->build();
The default timeout is 5.0 seconds.
Retries
Sets the number of times a failed query is retried before the promise is
rejected. A value of 0 disables retries entirely.
$resolver = Dns::builder() ->withRetries(3) // try up to 4 times total (1 initial + 3 retries) ->build();
The default is 2 retries.
Retries apply to network-level failures such as timeouts and connection errors.
DNS protocol-level errors returned by the server — SERVFAIL, REFUSED,
FORMAT_ERROR — arrive as valid responses and are not retried. If a nameserver
returns SERVFAIL transiently, configuring multiple nameservers with
withNameservers() is a more effective mitigation than increasing the retry
count.
Note that in the default pipeline, RetryExecutor sits inside CoopExecutor.
When multiple concurrent callers request the same domain simultaneously, they
share a single deduplicated network request and therefore share the same retry
budget. If that single request exhausts its retries, all concurrent callers
receive the failure at the same time.
Caching
Enables response caching. Successful responses are stored with their DNS TTL as the cache expiry. Subsequent identical queries are served from cache until the TTL expires, skipping the network entirely.
// Default cache — in-memory ArrayCache with a 256 entry limit $resolver = Dns::builder() ->withCache() ->build(); // Custom cache implementation $resolver = Dns::builder() ->withCache(new MyRedisCache()) ->build();
The custom cache must implement Hibla\Cache\Interfaces\CacheInterface. Caching
is disabled by default.
Reusing Resolvers
Each call to Dns::builder()->build() constructs a new executor pipeline,
including a TcpTransportExecutor whose open sockets are only cleaned up when
the executor is garbage-collected. In long-running applications, creating a
resolver per request can leave TCP sockets open longer than expected if
references are held anywhere in scope.
Create the resolver once and reuse it for the lifetime of your application or service:
// Good — one resolver, many queries $resolver = Dns::builder() ->withNameservers(['1.1.1.1', '8.8.8.8']) ->withCache() ->build(); foreach ($domains as $domain) { $resolver->resolve($domain)->then(...); } // Avoid — a new TCP executor (and potentially open socket) per iteration foreach ($domains as $domain) { Dns::builder()->build()->resolve($domain)->then(...); }
The same applies to Dns::create() — call it once and store the result rather
than calling it inside a loop or request handler.
Cache Key Format
When using a custom CacheInterface implementation, cache keys follow this
format:
{name}:{type_value}:{class_value}
For example, an A record query for example.com produces the key:
example.com:1:1
Where 1 is the integer value of RecordType::A and 1 is the integer value
of RecordClass::IN. You can find all type integer values in the
Record Type Reference table.
This is relevant if your cache implementation needs prefix-based invalidation, key inspection, or manual eviction. For example, to invalidate all cached records for a domain in Redis:
// Invalidate all record types for example.com $types = [1, 2, 5, 6, 12, 15, 16, 28, 33, 35, 44, 255, 257]; // RecordType values foreach ($types as $type) { $redis->del("example.com:{$type}:1"); }
The class component is almost always 1 (IN) for standard DNS queries.
Resolving Records
resolve() — single IP address
Performs an A record lookup and returns one IPv4 address as a string. If the
response contains multiple A records, one is chosen at random. Rejects with
RecordNotFoundException if no A records are found.
$ip = $resolver->resolve('example.com')->wait(); echo $ip; // "93.184.216.34"
resolveAll() — all records of a type
Returns all records of the specified type as an array. The array contents depend on the record type — see the table below.
use Hibla\Dns\Enums\RecordType; // All A records $ips = $resolver->resolveAll('example.com', RecordType::A)->wait(); // ["93.184.216.34"] // All MX records $mx = $resolver->resolveAll('example.com', RecordType::MX)->wait(); // [['priority' => 10, 'target' => 'mail.example.com']] // All TXT records $txt = $resolver->resolveAll('example.com', RecordType::TXT)->wait(); // [['v=spf1 include:_spf.example.com ~all']]
Record types and return formats
The shape of each element in the resolveAll() result depends on the record
type:
| Record type | Element type | Fields |
|---|---|---|
A |
string |
IPv4 address (e.g. "93.184.216.34") |
AAAA |
string |
IPv6 address (e.g. "2606:2800:220:1:248:1893:25c8:1946") |
NS |
string |
Nameserver hostname |
CNAME |
string |
Canonical name |
PTR |
string |
Pointer hostname |
MX |
array |
['priority' => int, 'target' => string] |
TXT |
array |
Array of strings per record (e.g. ['v=spf1 ...']) |
SRV |
array |
['priority' => int, 'weight' => int, 'port' => int, 'target' => string] |
SOA |
array |
['mname' => string, 'rname' => string, 'serial' => int, 'refresh' => int, 'retry' => int, 'expire' => int, 'minimum' => int] |
CAA |
array |
['flags' => int, 'tag' => string, 'value' => string] |
SSHFP |
array |
['algorithm' => int, 'fptype' => int, 'fingerprint' => string] |
NAPTR |
array |
['order' => int, 'preference' => int, 'flags' => string, 'service' => string, 'regexp' => string, 'replacement' => string] |
CNAME chaining is handled automatically for A and AAAA lookups. If the response contains a CNAME record pointing to another name, the resolver follows the chain up to 10 levels deep before returning the final address records.
Raw Executor Usage
resolve() and resolveAll() cover most application needs, but you can query
the executor pipeline directly when you need the full DNS Message — for
example to inspect TTLs, authority records, additional records, or to build
DNS tooling.
To access query() you must construct an executor directly. Dns::builder()->build()
returns a ResolverInterface, not an ExecutorInterface, and does not expose
query():
use Hibla\Dns\Models\Query; use Hibla\Dns\Models\Message; use Hibla\Dns\Enums\RecordType; use Hibla\Dns\Enums\RecordClass; use Hibla\Dns\Queries\UdpTransportExecutor; use Hibla\Dns\Queries\TcpTransportExecutor; use Hibla\Dns\Queries\SelectiveTransportExecutor; use Hibla\Dns\Queries\TimeoutExecutor; use Hibla\Dns\Queries\RetryExecutor; use Hibla\Dns\Queries\CoopExecutor; $executor = new CoopExecutor( new RetryExecutor( new SelectiveTransportExecutor( new TimeoutExecutor(new UdpTransportExecutor('1.1.1.1'), 5.0), new TimeoutExecutor(new TcpTransportExecutor('1.1.1.1'), 5.0), ), retries: 2 ) ); $query = new Query('example.com', RecordType::A, RecordClass::IN); $executor->query($query)->then(function (Message $message) { // Answer records foreach ($message->answers as $record) { echo $record->name; // owner name echo $record->type->name; // 'A', 'MX', etc. echo $record->ttl; // TTL in seconds var_dump($record->data); // string or array depending on type } // Authority records (NS, SOA) — useful for negative caching and delegation foreach ($message->authority as $record) { ... } // Additional records — often glue A/AAAA records for nameservers foreach ($message->additional as $record) { ... } // Message metadata var_dump($message->isAuthoritative); var_dump($message->recursionAvailable); var_dump($message->responseCode); // ResponseCode enum })->wait();
The data field on each Record follows the same type rules as resolveAll()
— see Record types and return formats.
See the Executor Reference for all available executors and guidance on assembling a custom pipeline.
Cancellation
Every promise returned by resolve() and resolveAll() supports cancellation.
Calling cancel() on the promise immediately aborts the in-flight query, cleans
up the underlying socket or watcher, and cancels any pending retries or fallback
attempts in the pipeline. No further callbacks fire after cancellation.
use Hibla\Dns\Dns; use Hibla\EventLoop\Loop; $promise = Dns::resolve('example.com'); // Cancel the query after 1 second if it has not resolved $timerId = Loop::addTimer(1.0, function () use ($promise) { $promise->cancel(); }); $promise ->then(function (string $ip) use ($timerId) { Loop::cancelTimer($timerId); echo "Resolved: $ip\n"; }) ->catch(function (\Throwable $e) { echo "Failed or cancelled: " . $e->getMessage() . "\n"; }); Loop::run();
Cancellation propagates through the entire executor pipeline. Cancelling a
promise that is waiting on a retry will cancel the current attempt and prevent
any further retries from starting. Cancelling a promise on the CoopExecutor
decrements its reference count — the underlying network query is only cancelled
when every caller that shares it has cancelled their own promise.
If you are running inside a fiber with hiblaphp/async, you can use
CancellationTokenSource for structured cancellation that applies a timeout
across the entire lookup without managing timers manually:
use Hibla\Dns\Dns; use Hibla\Cancellation\CancellationTokenSource; use function Hibla\await; $cts = new CancellationTokenSource(2.0); // 2 second hard limit try { $ip = await(Dns::resolve('example.com'), $cts->token); echo "Resolved: $ip\n"; } catch (\Hibla\Promise\Exceptions\CancelledException $e) { echo "DNS lookup timed out\n"; }
System Configuration Detection
When no nameservers are explicitly provided, the builder calls
Config::loadSystemConfigBlocking() at build time to read the system DNS
configuration. This is a blocking operation — it reads from the filesystem or
executes a shell command — but it runs only once at resolver construction and the
result is used for the lifetime of the resolver.
On Unix and macOS, the method parses /etc/resolv.conf for nameserver
entries. IPv6 zone identifiers are stripped and all addresses are normalized
before use.
On Windows, the method executes wmic NICCONFIG get DNSServerSearchOrder
and parses the CSV output. Duplicate addresses across multiple adapters are
deduplicated automatically.
If system configuration cannot be read — missing file, permissions error, or
shell_exec disabled — an empty configuration is returned and the builder falls
back to Cloudflare (1.1.1.1) and Google (8.8.8.8) as defaults.
To load the system configuration manually or supply a custom resolv.conf path:
use Hibla\Dns\Configs\Config; use Hibla\Dns\Dns; // Load from system default $config = Config::loadSystemConfigBlocking(); // Load from a specific resolv.conf file $config = Config::loadResolvConfBlocking('/etc/resolv.conf'); // Use it in the builder $resolver = Dns::builder() ->withConfig($config) ->build();
Hosts File
The resolver checks the system hosts file before making any network query. On
Unix and macOS this is /etc/hosts. On Windows it is
%SystemRoot%\system32\drivers\etc\hosts. If the file cannot be read, the
resolver continues with network-based lookups only — no error is thrown.
Hosts file lookups support:
- Forward lookups (A and AAAA) — hostname to IP
- Reverse lookups (PTR) — IP to hostname
- Multiple aliases per line
- Both IPv4 and IPv6 entries
- IPv6 zone identifiers (stripped automatically)
- Case-insensitive hostname matching
Hosts file entries bypass caching, retries, and transport entirely — they resolve
synchronously before the network pipeline is involved. In the default pipeline,
HostsFileExecutor is the outermost layer, meaning a hosts file hit
short-circuits before the query ever reaches CachingExecutor. If you assemble
a custom pipeline, place HostsFileExecutor outside CachingExecutor to
preserve this behaviour.
You can also use the HostsFile class directly for hostname or IP lookups
against an arbitrary hosts file:
use Hibla\Dns\Configs\HostsFile; $hosts = HostsFile::loadFromPathBlocking('/etc/hosts'); // Forward lookup — get all IPs for a hostname $ips = $hosts->getIpsForHost('localhost'); // ["127.0.0.1", "::1"] // Reverse lookup — get all hostnames for an IP $names = $hosts->getHostsForIp('127.0.0.1'); // ["localhost", "myapp.local"]
Specifying Transport
By default, each nameserver uses UDP first and automatically retries with TCP if
the response is truncated. You can force a specific transport by prefixing the
nameserver address with udp:// or tcp://:
// Force UDP only $resolver = Dns::builder() ->withNameservers('udp://8.8.8.8') ->build(); // Force TCP only $resolver = Dns::builder() ->withNameservers('tcp://8.8.8.8') ->build(); // Default — UDP with automatic TCP fallback on truncation $resolver = Dns::builder() ->withNameservers('8.8.8.8') ->build();
TCP is also used automatically for any response whose size exceeds UDP's 512-byte limit — DNSSEC responses, domains with many MX or NS records, and large TXT records commonly trigger this fallback.
Both IPv4 and IPv6 nameserver addresses are supported. IPv6 addresses are wrapped in brackets automatically if not already formatted that way.
TCP connection reuse and idle period
When using TCP transport, the first query establishes a connection to the nameserver. Subsequent queries sent while that connection is still open are multiplexed over the same socket, avoiding the TCP handshake overhead for each query. Once all pending queries have settled and no new queries arrive within 50ms, the connection is closed automatically.
This idle period means TCP is most efficient for bursts of concurrent queries — multiple queries issued in rapid succession share one connection. Queries sent with longer gaps between them will each establish a fresh connection, which incurs the TCP handshake latency of approximately 10–50ms per query depending on network conditions. If this matters for your use case, batch queries together or use UDP transport, which has no connection overhead.
How the Pipeline Works
When you call Dns::builder()->build(), the builder assembles an executor
pipeline from the outside in. Each executor wraps the next and either handles
the query itself or delegates to the inner executor. A query entering the top of
the pipeline passes through each layer in order:
resolveAll('example.com', A)
│
▼
┌───────────────────┐
│ HostsFileExecutor│ ← checks /etc/hosts first; hit resolves instantly,
└────────┬──────────┘ bypassing all layers below including the cache
│ no match — pass through
▼
┌───────────────────┐
│ CachingExecutor │ ← only present when .withCache() is called
└────────┬──────────┘ checks cache; stores response on miss
│ cache miss — pass through
▼
┌───────────────────┐
│ CoopExecutor │ ← deduplicates concurrent identical queries;
└────────┬──────────┘ all concurrent callers share one network request
│ and therefore share its retry budget
▼
┌───────────────────┐
│ RetryExecutor │ ← retries on network-level failure only (default: 2 retries)
└────────┬──────────┘ DNS protocol errors (SERVFAIL, REFUSED) are not retried
│
▼
┌───────────────────┐
│ FallbackExecutor │ ← tries primary nameserver, then secondary
└────────┬──────────┘ chains additional executors for 3+ nameservers
│
▼
┌────────────────────────┐
│ SelectiveTransport │ ← UDP first, TCP on truncation
│ TimeoutExecutor (UDP) │ ← enforces timeout per attempt
│ TimeoutExecutor (TCP) │
└────────────────────────┘
The Resolver class sits above this stack, translating the raw Message
responses from the executor into the typed return values that resolve() and
resolveAll() expose.
Executor Reference
Each executor in the pipeline is available individually for custom pipelines. All
implement ExecutorInterface and accept a Query, returning
PromiseInterface<Message>. All returned promises support cancellation —
cancelling propagates down through the wrapped executor chain automatically.
| Executor | Purpose |
|---|---|
UdpTransportExecutor |
Sends queries over UDP. Fast and connectionless. Does not handle retries or timeouts |
TcpTransportExecutor |
Sends queries over TCP. Maintains a persistent connection per nameserver and multiplexes queries over it. Connection closes automatically after 50ms of idle time |
SelectiveTransportExecutor |
Tries UDP first, falls back to TCP automatically if the response is truncated |
TimeoutExecutor |
Wraps any executor and rejects with TimeoutException if the query exceeds the configured duration |
RetryExecutor |
Retries a failed query up to N times before rejecting. Only retries on network-level failures — DNS protocol errors are not retried |
FallbackExecutor |
Tries a primary executor, falls back to a secondary on failure |
CoopExecutor |
Deduplicates concurrent identical queries — only one network request is made regardless of how many callers ask for the same name simultaneously. Cancellation is reference-counted: the network query is only cancelled when every caller has cancelled. All concurrent callers share the same retry budget |
CachingExecutor |
Caches successful responses using the minimum TTL from the answer records |
HostsFileExecutor |
Checks a HostsFile instance before delegating to another executor. Supports A, AAAA, and PTR lookups |
To assemble a custom pipeline, construct the executors from the inside out and wrap each one:
use Hibla\Dns\Queries\UdpTransportExecutor; use Hibla\Dns\Queries\TcpTransportExecutor; use Hibla\Dns\Queries\SelectiveTransportExecutor; use Hibla\Dns\Queries\TimeoutExecutor; use Hibla\Dns\Queries\RetryExecutor; use Hibla\Dns\Queries\CoopExecutor; use Hibla\Dns\Queries\CachingExecutor; use Hibla\Dns\Queries\HostsFileExecutor; use Hibla\Dns\Configs\HostsFile; use Hibla\Cache\ArrayCache; use Hibla\Dns\Resolvers\Resolver; $transport = new SelectiveTransportExecutor( new TimeoutExecutor(new UdpTransportExecutor('8.8.8.8'), 5.0), new TimeoutExecutor(new TcpTransportExecutor('8.8.8.8'), 5.0), ); $executor = new CoopExecutor( new RetryExecutor($transport, retries: 2) ); $executor = new CachingExecutor(new ArrayCache(256), $executor); // HostsFileExecutor must wrap CachingExecutor, not the other way around, // to ensure hosts file entries are never cached $executor = new HostsFileExecutor( HostsFile::loadFromPathBlocking(), $executor ); $resolver = new Resolver($executor);
Exception Reference
All exceptions extend Hibla\Dns\Exceptions\DnsException, which extends
\RuntimeException. Catch the base class to handle any DNS error generically,
or catch specific types for granular handling.
| Exception | When it is thrown |
|---|---|
RecordNotFoundException |
Base class for all "no record found" conditions. Catch this if you do not need to distinguish between NXDOMAIN and NODATA |
NxDomainException |
The domain does not exist at all (NXDOMAIN / response code 3). Extends RecordNotFoundException. Retrying with a different record type will not help — the domain itself is absent |
NoDataException |
The domain exists but has no records of the requested type (NOERROR / NODATA). Extends RecordNotFoundException. The domain is alive — it simply has no records of the type you queried |
QueryFailedException |
A network or protocol error prevented the query from completing |
TimeoutException |
The query exceeded the configured timeout. Extends QueryFailedException |
ResponseTruncatedException |
The UDP response was truncated. Handled internally by SelectiveTransportExecutor — you will not normally see this unless using UdpTransportExecutor directly. Extends QueryFailedException |
The exception hierarchy:
DnsException
├── RecordNotFoundException
│ ├── NxDomainException ← domain does not exist (NXDOMAIN)
│ └── NoDataException ← domain exists, no records of this type (NODATA)
└── QueryFailedException
├── TimeoutException
└── ResponseTruncatedException
use Hibla\Dns\Dns; use Hibla\Dns\Exceptions\NxDomainException; use Hibla\Dns\Exceptions\NoDataException; use Hibla\Dns\Exceptions\RecordNotFoundException; use Hibla\Dns\Exceptions\TimeoutException; use Hibla\Dns\Exceptions\QueryFailedException; use Hibla\Dns\Exceptions\DnsException; // Granular handling — distinguish NXDOMAIN from NODATA try { $ip = Dns::resolve('example.com')->wait(); } catch (NxDomainException $e) { // Domain does not exist at all — retrying with a different record type // will not help echo "Domain does not exist: " . $e->getMessage() . "\n"; } catch (NoDataException $e) { // Domain exists but has no A records — may have AAAA or other types echo "No A records for this domain: " . $e->getMessage() . "\n"; } catch (TimeoutException $e) { echo "DNS server did not respond in time\n"; } catch (QueryFailedException $e) { echo "Network or protocol error: " . $e->getMessage() . "\n"; } catch (DnsException $e) { echo "DNS error: " . $e->getMessage() . "\n"; } // Generic handling — treat all "no record" conditions the same way try { $ip = Dns::resolve('example.com')->wait(); } catch (RecordNotFoundException $e) { // Catches both NxDomainException and NoDataException echo "Could not resolve: " . $e->getMessage() . "\n"; }
Record Type Reference
The RecordType enum lists all supported DNS record types. Pass any of these to
resolveAll() as the second argument.
| Value | Name | Description | RFC |
|---|---|---|---|
1 |
A |
IPv4 address | RFC 1035 |
2 |
NS |
Authoritative nameserver | RFC 1035 |
5 |
CNAME |
Canonical name / alias | RFC 1035 |
6 |
SOA |
Start of authority | RFC 1035 |
12 |
PTR |
Pointer for reverse DNS | RFC 1035 |
15 |
MX |
Mail exchange server | RFC 1035 |
16 |
TXT |
Text records | RFC 1035 |
28 |
AAAA |
IPv6 address | RFC 3596 |
33 |
SRV |
Service locator | RFC 2782 |
35 |
NAPTR |
Naming authority pointer | RFC 2915 |
44 |
SSHFP |
SSH public key fingerprint | RFC 4255 |
255 |
ANY |
Request for all records | RFC 1035 |
257 |
CAA |
Certification Authority Authorization | RFC 6844 |
Development
git clone https://github.com/hiblaphp/dns.git
cd dns
composer install
./vendor/bin/pest
./vendor/bin/phpstan analyse
Credits
- API Design: Inspired by ReactPHP DNS.
If you are familiar with ReactPHP's DNS resolver, the executor pipeline concept
and the
resolve()/resolveAll()interface will feel immediately familiar — with the addition of native promise cancellation and Fiber-aware async throughout. - Event Loop Integration: Powered by hiblaphp/event-loop.
- Promise Integration: Built on hiblaphp/promise.
- Stream Integration: Built on hiblaphp/stream.
License
MIT License. See LICENSE for more information.