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 Dna Laravel Package

domainreseller/php-dna

PHP client for the Domain Reseller API supporting both REST and SOAP. Automatically detects whether you're using Reseller ID/API Key or legacy username/password credentials. Manage domains, contacts, and orders from any PHP 7.4+ app. Installs via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

domainreseller/php-dna is not a Laravel package. It is a thin, framework-agnostic PHP client for the DomainNameApi reseller endpoint (the same vendor behind domainnameapi.com). There is no service provider, no facade, no config file, no published migrations — you get a stateful client class that takes credentials + server and returns associative arrays. That means your first hour is really about wrapping it, not installing it.

1. Install and verify the transport layer

composer require domainreseller/php-dna:^4.0
php -m | grep -Ei 'curl|json|intl|mbstring'

As of V4.0.0 the library is REST-only: the legacy SOAP transport has been retired end-to-end and ext-soap is no longer required. You still need ext-curl and ext-json (and ext-intl for IDN handling — see below). Slim php:8.x-fpm images ship neither curl nor intl by default.

If you are upgrading from a v3 install, this is a major version: pin ^4.0, run the sandbox smoke tests before deploying, and grep -rn 'setUsername\|setPassword' app/ config/ — those credential setters are gone in favour of the unified Reseller ID + API Key model.

2. Get Reseller ID + API Key before writing code. Panel → API Settings gives you both. The old username/password pair is no longer accepted: if your saved settings were already Reseller ID + API Key you need no reconfiguration, but if you were on legacy username/password you must generate an API Key before upgrading, or every call will fail auth. The OTE / test environment is still supported on the REST gateway — use it.

DOMAINRESELLER_URL=https://api.domainnameapi.com/
DOMAINRESELLER_RESELLER_ID=
DOMAINRESELLER_API_KEY=
DOMAINRESELLER_SANDBOX=true

3. Read src/DomainNameApi.php first, not the README. The README is a translated banner stack with sparse usage; the class itself is the only accurate reference for the method surface (getList, getDetails, check, create, renew, transfer, modify, delete, getTldList, getPrices, getContacts, saveContacts, getDnsRecords, addDnsRecord, updateDnsRecord, deleteDnsRecord, lock, unlock, getIsLocked, getPrivacy, setPrivacy, child name server methods). Method names, setter names and response keys have shifted again in this release — pin the version you develop against and re-check the method surface after the bump.

4. Wire a singleton and a config file. Config first: config/domainreseller.php with url, reseller_id, api_key, and markup rules. Then:

// app/Providers/DomainResellerServiceProvider.php
public function register(): void
{
    $this->mergeConfigFrom(config_path('domainreseller.php'), 'domainreseller');

    $this->app->singleton(DomainNameApi::class, fn () => tap(new DomainNameApi())
        ->setResellerId(config('domainreseller.reseller_id'))
        ->setApiKey(config('domainreseller.api_key'))
        ->setServer(config('domainreseller.url')));
}

(Verify the exact setter names in src/ — v4 replaced the old setUsername() / setPassword() pair.) Because the client stores credentials as instance state, a container singleton is fine for a single reseller account, but is a trap if you ever hold multiple credentials (see Gotchas).

5. First use case: a read-only availability check. Do not start with registration. A domain search box on a checkout page is one check() call, has no financial side effects, and forces you to solve the three things you actually care about early: normalization (IDN → punycode), error envelope handling, and TLD price display.

Route::get('/domains/search', function (Request $request) {
    $request->validate(['q' => ['required', 'string', 'max:255']]);

    $name = app(DomainLookup::class)->availability($request->q);

    return view('domains.search', compact('name'));
});

Implementation Patterns

Wrap it behind your own interface from day one

The most valuable thing you can do as a Laravel developer here is treat the library as a swappable transport. Give it a narrow port and never let its array shapes leak into controllers or Blade:

interface Registrar
{
    public function check(string $domain): DomainAvailability;
    public function register(RegisterRequest $r): RegistrationResult;
    public function renew(string $domain, int $years): OperationResult;
    public function setDns(string $domain, array $records): OperationResult;
}

The package has 19 stars, one maintainer (README credits @bakcay), and zero dependents. That is a real bus-factor — and V4.0.0 is a reminder that a single maintainer can drop an entire transport in a major bump. An interface plus an Http::fake()-friendly fake implementation costs you an hour now and is the difference between a weekend migration and a rewrite if the library goes stale.

Normalize every domain string in one place

final class DomainName
{
    public static function normalize(string $input): string
    {
        $input = Str::lower(trim($input));
        $input = rtrim($input, '.');

        // IDN support: check that php-intl is installed
        $ascii = idn_to_ascii($input, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);

        if ($ascii === false || ! preg_match('/^(?=.{1,253}$)([a-z0-9-]{1,63}\.)+[a-z]{2,}$/', $ascii)) {
            throw new InvalidDomainException($input);
        }

        return $ascii;
    }
}

Call this at the boundary and store both forms (tld, sld, unicode_name, ascii_name) in your domains table. Turkish/Arabic/Chinese IDN support is a selling point for this registrar — the README ships in TR/SA/CN/AZ/NL — so you will hit non-ASCII input in production.

Registration as a queued, stateful job

Registration is a multi-second API call with money attached. Never call it inline in a web request. (REST is leaner than the old SOAP path, but it is still remote I/O — do not be tempted to make it synchronous.)

final class RegisterDomainJob implements ShouldQueue
{
    public int $tries = 1;          // NOT 3 — see Gotchas
    public int $timeout = 120;

    public function handle(Registrar $registrar): void
    {
        $order = DomainOrder::whereKey($this->orderId)->firstOrFail();

        // Contact handles must exist server-side before create()
        $contacts = app(DomainContacts::class)->ensureHandles($order->customer);

        $order->update(['status' => 'submitting']);

        $result = $registrar->register(new RegisterRequest(
            domain: $order->domain->ascii_name,
            years: $order->years,
            contacts: $contacts,
            nameServers: $order->nameservers ?: null,
            privacy: $order->privacy_enabled,
            lock: true,
        ));

        $result->ok
            ? $order->update(['status' => 'active', 'registrar_ref' => $result->reference])
            : $order->update(['status' => 'failed', 'error_code' => $result->code, 'error' => $result->message]);
    }
}

Model the flow as an explicit state machine on your local domain_orders / domains table (pending → submitting → active | failed | awaiting_transfer). The API is asynchronous in spirit — DNS and status changes lag — so your UI should reflect your status, not re-derive it from every page load.

Pre-create contact handles

DomainNameApi registrations carry contact references, not inline contact objects. Build a DomainContacts service that takes your users/customers model, calls saveContacts once per distinct contact set, caches the returned handle id on your customer record, and reuses it. Re-saving contacts on every registration is a common and unnecessary round trip; but stale handles are just as bad, so invalidate the cached id whenever a customer edits their profile or changes country.

DNS: reconcile desired state, don't fire-and-forget deltas

The record API is per-record (addDnsRecord, updateDnsRecord, deleteDnsRecord) against a live zone. Rather than pushing individual edits from your UI and hoping they land, keep a local dns_records table as the source of truth and let a job reconcile:

public function reconcile(Domain $domain): void
{
    $remote = collect($this->registrar->dnsRecords($domain->ascii_name))
        ->keyBy(fn ($r) => "{$r['type']}|{$r['name']}|{$r['content']}");

    foreach ($domain->dnsRecords as $local) {
        $key = "{$local->type}|{$local->name}|{$local->content}";

        match (true) {
            ! $remote->has($key) && $local->wanted   => $this->registrar->addDns($domain, $local),
            $remote->has($key) && ! $local->wanted   => $this->registrar->deleteDns($domain, $remote[$key]['id']),
            default                                  => null,
        };
    }
}

This survives partial failures, is safely re-runnable, and gives you a diff to show in the UI ("3 changes pending"). It also means a failed API call is a retryable reconciliation, not a lost user action.

Scheduled sync command

There are no webhooks. Your expiries, statuses and transfer completions go stale silently unless you poll.

Schedule::command('domains:sync --limit=200')->everyThirtyMinutes()->withoutOverlapping()->onOneServer();
Schedule::command('domains:refresh-prices')->dailyAt('03:15');

domains:sync should iterate your active domains, call getDetails, update status/expiry/lock/privacy, and dispatch renewal reminders. Batch it and use ->withoutOverlapping() — a slow API plus a schedule:run every minute is how you accidentally DDoS your registrar.

Cache the TLD catalogue and price list aggressively

getTldList and getPrices change on the order of days, not seconds. Cache them in Redis for 12–24 hours, keyed by currency, and apply your markup in a DomainPricing service. Do not call getPrices on a search request — that turns a 200 ms page into a 2 s page and burns the reseller API for data that is effectively static.

Testing: fake the port, not the library

Because the library makes real HTTP calls from inside its own methods, Http::fake() will not intercept them (it doesn't route through the Laravel HTTP client — v4 uses its own cURL transport). Bind a FakeRegistrar in tests:

$this->app->instance(Registrar::class, new FakeRegistrar([
    'example.com' => ['available' => false, 'premium' => false],
]));

Keep a small set of recorded JSON responses from the OTE environment as fixtures for the adapter's own unit tests, so a bad mapping in DomainNameApiRegistrar fails fast without network access in CI. Re-record those fixtures on the v4 upgrade — the SOAP-era fixtures are dead weight now.

Per-tenant credentials

If you resell to sub-users, do not share the singleton. Build the client per operation from a ResellerAccount model (Reseller ID + API Key encrypted with Crypt::encryptString), and construct a fresh DomainNameApi instance each time — the setters mutate instance state, and a reused object will happily send the previous tenant's credentials. This matters more in v4, not less: an API Key is a bearer credential with full reseller scope.


Gotchas and Tips

  • V4 is a breaking major. SOAP is gone, setUsername/setPassword are gone, and the credential model is now Reseller ID + API Key. Pin ^4.0, re-read src/ for renamed methods, and do not assume v3 response envelopes are byte-identical — validate your adapter against the OTE environment before shipping.
  • Legacy username/password users must migrate, and the failure is silent-ish. The changelog says saved settings keep working when you are already on Reseller ID + API Key. If you had username/password stored, generate an API Key in Panel → API Settings first. Otherwise an upgrade turns every call into an auth error — and since the client returns arrays rather than throwing, that can surface as "no domains found" instead of "unauthorised".
  • You can drop ext-soap from your images and CI, but keep ext-curl and ext-intl. Removing SOAP is a genuine win for slim containers and kills a whole class of "SoapClient not installed" tickets. Add ext-intl to your composer require block so CI catches it, otherwise idn_to_ascii() doesn't exist and your normalization silently breaks.
  • No exceptions. Nearly every method returns an array with a result key (OK / ERROR) and an error block. If you forget to check it, a failed registration looks like a successful one to your code because you got an array back. Wrap every call in the adapter and convert to a result object or a typed exception — one place, no exceptions (pun intended) to the rule.
  • Non-idempotent calls + queue retries = double charges. create and renew are not safely retryable. Set $tries = 1, use failed() to mark the order for human review, and check the domain's real state via getDetails before any manual retry.
  • The client is stateful. Calling a credential setter on a shared singleton mutates it for the whole request lifecycle — and worse, for the whole worker if you cache the instance across jobs in a long-running queue worker. Reset or re-instantiate per operation. With API Keys this is a security issue, not just a correctness one.
  • No webhooks, no events. Everything is polling. Budget for sync frequency versus API goodwill; withoutOverlapping() and onOneServer() are not optional if you run more than one scheduler.
  • REST gateway means rate limiting is your problem. The single-transport gateway is faster, but it will still throttle you. Implement exponential backoff on 429/5xx in the adapter — not in the library, which has no retry policy — and cap concurrency on bulk syncs.
  • Telemetry is part of the product now. V4 explicitly streamlines "error reporting and performance tracing". Confirm with your security/compliance owner what is transmitted off-box (payloads? domain names? credentials? PII?), and redact accordingly. If the egress is unacceptable, disable it if a switch exists, or block it at the network layer.
  • Transfer prerequisites fail late. Transfers need the auth code, an unlocked domain, and often a confirmed admin contact. Validate all three on your side before calling transfer, otherwise you get a generic error after the customer has paid.
  • DNS TTL floors and propagation. Many TLDs reject TTLs below 300–600 s and some reject certain record types at the apex. Normalize/clamp TTLs on input, and tell users changes can take minutes to hours — never imply instant propagation.
  • Currency and rounding. Prices come back in the reseller's currency. Store amounts as integers in minor units, apply markup with bcmath or explicit rounding, and reconcile against the actual invoice — FX drift between your cached price and the API's charged price is a margin leak.
  • Redact on log. API Keys, auth codes and contact PII flow through this client. Log method, domain, and result code — never the full request payload at debug in production. Telescope's HTTP watcher won't see these calls (the library uses raw cURL), so add a thin logging decorator around your Registrar port.
  • Timeout alignment. A queue worker's retry_after must exceed your job timeout, which must exceed the library's own cURL timeout. A stall inside a shorter worker timeout gets your job killed mid-flight and re-delivered — the exact scenario that double-registers a domain.
  • Sandbox/OTE first, and check getBalance regularly. Prepaid reseller balances are the usual failure mode of a live launch: your integration is perfect, the account is empty, and every registration fails with an opaque error. Surface low balance as an alert in your admin panel.
  • Extension points are thin by design. There is no middleware, no client injection, no retry policy. Your leverage is entirely in the adapter: that is where you put retries (with idempotency keys for reads only), logging, metrics, timeouts, rate limiting, and the swap-to-a-fork escape hatch. Pin the version in composer.json to ^4.0, and if you need a fix, prefer a vcs fork over waiting on a single-maintainer release cycle — this release is the proof that major changes land with little warning.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package