Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Whois Laravel Package

io-developer/php-whois

PHP WHOIS client for looking up domain, IPv4/IPv6, and ASN registration data. It selects the right WHOIS servers, parses responses into clean objects, supports caching, rate limiting, retries, and PSR-18 HTTP clients, handles IDN and UTF-8, and works with no external dependencies.

Deep Wiki
Context7

Getting Started

Install and wire it once.

composer require io-developer/php-whois

The package is framework-agnostic — no service provider, facade, or config file ships with it. Treat that as your first task: give it a Laravel config entry and a singleton binding so the rest of the app never touches Factory::get() directly.

// config/whois.php
return [
    'connection' => [
        'type'    => env('WHOIS_CONNECTION', 'socket'), // 'socket' or 'curl'
        'timeout' => (int) env('WHOIS_TIMEOUT', 10),
    ],
    'cache' => [
        'dir' => env('WHOIS_CACHE_DIR', storage_path('framework/cache/whois')),
        'ttl' => (int) env('WHOIS_CACHE_TTL', 3600),
    ],
];
// app/Providers/AppServiceProvider.php
use Iodev\Whois\Config;
use Iodev\Whois\Factory;
use Iodev\Whois\Whois;

public function register(): void
{
    $this->app->singleton(Whois::class, function () {
        return Factory::get()->createWhois(
            Config::create(config('whois'))
        );
    });
}

Make sure storage_path('framework/cache/whois') exists and is writable — the package writes raw responses to that directory and silently loses the benefit if it can't. Adding storage/framework/cache/whois/.gitignore with * and !.gitignore keeps it clean.

First use case: domain availability at signup / domain search.

use Iodev\Whois\Whois;

public function check(string $domain, Whois $whois): string
{
    $info = $whois->lookupDomain(strtolower(trim($domain)));

    if (!$info->isValid()) {
        return 'unknown'; // no answer, unparsable, or TLD not supported
    }

    return $info->isNotRegistered() ? 'available' : 'taken';
}

The three things to look at first in the package: Whois (the two methods lookupDomain() and lookupAsn()), DomainInfo (parsed getters plus getRawText()), and Config (connection, cache, server overrides). Almost everything else is per-TLD regex plumbing you don't need to read until a parser misbehaves.

Implementation Patterns

Wrap the package behind your own interface. You will want to fake it in tests and swap it later (RDAP, registrar API), and DomainInfo is a package-specific type.

interface DomainWhois
{
    public function lookup(string $domain): ?DomainRecord;
}

final class PackageDomainWhois implements DomainWhois
{
    public function __construct(private Whois $whois) {}

    public function lookup(string $domain): ?DomainRecord
    {
        try {
            $info = $this->whois->lookupDomain($domain);
        } catch (\Iodev\Whois\Exceptions\ConnectionException $e) {
            report($e);
            return null;
        }

        if (!$info->isValid()) {
            return null;
        }

        return new DomainRecord(
            domain:     $info->domainName,
            registered: $info->isRegistered(),
            states:     $info->getStates(),
            owner:      $info->getOwner(),
            created:    $info->getCreationDate(),
            updated:    $info->getUpdateDate(),
            expires:    $info->getExpirationDate(),
            ns:         $info->getNameservers(),
            raw:        $info->getRawText(),
        );
    }
}

Persist both parsed and raw. Parsers change, TLDs change, and you'll be asked "what did the registry actually say?" six months later. Store the raw text in a longText column (or gzip it), and the parsed fields in typed columns so expiry queries are indexable.

Queue bulk lookups; never loop them in a request. Each lookup is a TCP connection to port 43. A search results page with 20 candidate domains is 20 network round-trips and 20 chances to hit a timeout.

final class LookupDomainWhois implements ShouldQueue
{
    use InteractsWithQueue, Queueable;

    public int $tries = 3;
    public array $backoff = [5, 30, 120];

    public function __construct(public string $domain) {}

    public function middleware(): array
    {
        return [new RateLimited('whois')];
    }

    public function handle(Whois $whois): void
    {
        $info = $whois->lookupDomain($this->domain);
        // ... persist
    }
}

Register the limiter per registry, not globally — the package resolves a different WHOIS server per TLD, so foo.com and foo.de don't share a throttle. Rate limit by the TLD's server host if you want to be precise; a conservative RateLimiter::for('whois', fn () => Limit::perMinute(20)) is a sane default for a single app.

Layer your own cache on top. The package cache is file-based and keyed by server+query, which is great for repeated raw lookups but isn't queryable. Use Cache::remember("whois:{$domain}", ...) with asymmetric TTLs: hours for registered domains, minutes for available ones (they sell fast), and never cache failures.

ASN lookups for enrichment, not for IP resolution.

$info = $whois->lookupAsn('AS15169');
$info->getRawText();

Use it when you already know the ASN — abuse triage, "which network is this customer on", building an internal registry of your own routes. It does not take an IP address; converting 203.0.113.5 to an ASN needs a separate mechanism (Team Cymru DNS, RDAP, or an IP-intel service).

Domain portfolio monitoring. If you track customer or company domains, schedule a nightly job that refreshes only domains expiring within 60 days using getExpirationDate(), and fire notifications when the date moves (renewals) or crosses a threshold.

Gotchas and Tips

Port 43 is often blocked. Many shared hosts, some Docker network configs, and a few cloud egress policies drop outbound 43. Symptom: ConnectionException for every TLD while dig works fine. Test early on your target infrastructure, keep the curl connection type as a fallback, and consider an HTTP-based RDAP path for production-critical flows.

isRegistered() === false is not the same as "available". Always gate on isValid() first. A failed lookup, an unsupported TLD, or a rate-limit page yields an invalid DomainInfo whose flags are meaningless. getStates() gives the nuance (free, registered, redemption/pending-delete variants) — check for the explicit free state rather than negating "registered" if you're making a purchase decision.

TLD coverage is community-maintained. Obscure ccTLDs and newer gTLDs may be missing from the bundled server list or have stale parsers. Handle ServerException/WhoisException and degrade to "unknown" in the UI rather than showing a wrong answer. If you need a specific TLD that's missing, that's your extension point: add/override the server entry in Config and point it at the registry host, or fork the parser.

Dates and owner fields are frequently null. GDPR redaction, privacy proxies, and varying registry formats mean getCreationDate() and getOwner() are guesses, not facts. Type-hint them as nullable, store null when absent, and never present getOwner() as legal ownership.

Normalize inputs before you spend a query. Lowercase, strip http(s)://, www., paths, and trailing dots. Convert IDNs with idn_to_ascii() — passing a Unicode domain to a registry can produce garbage or a spurious free result. Validate the hostname shape locally first.

Timeouts and retries belong in your layer, not the package. Set an explicit connection timeout in Config (the default is generous), wrap calls in try/catch for ConnectionException, and retry with backoff in jobs. Don't retry parse failures — those won't fix themselves.

Cache-directory quirks in local dev and tests. Multiple PHP processes writing the same directory is fine, but test suites that point at a shared cache will leak fixtures across runs. Use a per-test temp directory or disable the cache, and add storage/framework/cache/whois to your deployment's writable paths check.

Testing: fake the boundary, not the package. Bind a stub DomainWhois in the container and return DomainRecord objects. If you truly need to exercise DomainInfo parsing, construct one from a saved raw-text fixture rather than hitting the network — CI runners usually can't reach port 43 anyway.

Maintenance risk. Last release is 2023. Pin the version in composer.json, run your test suite on the PHP versions you actually deploy, and be prepared to fork if a parser for a TLD you depend on drifts. The code is compact and MIT-licensed, so a local fork is a realistic option — but budget for it rather than assuming upstream will patch a broken .co parser next week.

Octane / long-running workers. The Whois instance is effectively stateless, so the singleton is safe to reuse; just make sure your cache directory and config are resolved once at boot and not per-request.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules