Domain Catalogue Hooks

1 views Markdown

The ten hooks running while a customer searches for a name and an operator builds the extension catalogue: availability, suggestions, categories, the price matrix.

Overview

Selling domains has two sides. On the customer's side there is the search: a name is typed, the provider is asked, a result and suggestions come back. On the operator's side there is the catalogue: which extensions sell, in which category, at what price.

Every hook here works by reference: you are handed an array, you change it, and your return is not read. The shape contract is tight, because the screen and the cart read the output directly.

Reference

Changing the availability answer

filterdomain.availability
AdminOrders::check_domain():336 a panel order

Runs after the provider's availability answer was worked out, before the response is built.

Parameters 4
$availableboolrefThe availability the provider reported. Setting it to false takes the name off sale.
$sldstringThe name itself, without the extension.
$tldstringThe extension.
$check_resultarrayThe provider's raw answer. Read the detail here to justify your decision.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.availability', 10,
    function (&$available, $sld, $tld, $check_result) {
        // Brand protection: nobody registers our own name.
        if (Acme::brandTerm($sld)) $available = false;
    });
filterdomain.client_search_results
ClientDomain::check():124 already sorted

Runs after the primary result and the suggestions were built, the cart state was applied and the suggestions were sorted.

Parameters 2
$responsearrayrefThe whole answer: status, type (register or transfer), currency, primary (the primary result or null), suggestions. Cart state arrives on each entry as in_cart.
$searchContextarrayThe query context: sld, tld, transfer, ucid (the display currency).
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.client_search_results', 10,
    function (&$response, $searchContext) {
        // Sorting already ran: put your suggestion FIRST, not last.
        $extra = Acme::suggest($searchContext['sld'] ?? '');
        if ($extra) array_unshift($response['suggestions'], $extra);
    });

Changing the featured extensions

filterdomain.spotlight_tlds
ClientDomain::spotlight_tlds():275 no dot, lower case

Runs after the list from the language file was lower-cased and any leading dot trimmed.

Parameters 1
$tldsarrayrefThe extension list: plain strings, no dot and lower case (["net","org","io"]). A dot or a capital makes the extension unfindable.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.spotlight_tlds', 10, function (&$tlds) {
    $tlds = ['com', 'net', 'co.uk'];         // no dot, lower case
});

Changing the extension categories

filterdomain.tld_categories
domain::build_categories():86 key and label

Runs after the category keys were matched with their translated labels.

Parameters 1
$outarrayrefThe ordered category list: every row carries key and label. The key has to match the one on the extension rows; an invented key returns no extensions.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.tld_categories', 10, function (&$out) {
    // The key HAS TO MATCH the category list on the extension rows.
    $out[] = ['key' => 'local', 'label' => 'Local'];
});

Changing the extension price table

filterdomain.tld_table
website/domain prices resolved

Runs after the extension rows were built and the prices resolved into the display currency.

Parameters 2
$outarrayrefThe extension rows. Each row: name (without a dot), categories (comma-separated keys), the register and renew prices.
$ucidintThe display currency id. The prices are in this currency; give any row you add in the same one.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.tld_table', 10, function (&$out, $ucid) {
    // Hide rows without a price so nobody clicks a priceless extension.
    $out = array_values(array_filter($out,
        fn ($r) => (float) ($r['register'] ?? 0) > 0));
});

Changing the renewal price

filterdomain.premium_renewal_price
Invoices::_renewal_pricing_domain() already converted

Runs after the extension's renewal price was found and the currency conversion and year multiplier applied.

Parameters 4
$renewal_amountfloatrefThe unit price in the target currency. The multiplier is applied already, and what you write here reaches the invoice.
$servicearrayThe domain service being renewed.
$tldarrayThe extension record: id, name, min_years.
$target_currencyintThe currency the amount is in.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.premium_renewal_price', 10,
    function (&$renewal_amount, $service, $tld, $target_currency) {
        // A premium name renews at the provider's price, not the catalogue's.
        $real = Acme::premiumRenewal($service['name'] ?? '', $target_currency);
        if ($real > 0) $renewal_amount = $real;
    });

Changing the price matrix before it is saved

filterdomain.pricing_save
AdminProducts type by year by currency

Runs after the costs pulled from the provider were turned into prices with the profit rate, before they are written.

Parameters 2
$pricingarrayrefThe price matrix: [type][year][currency]cost and promo. Types: register, renewal, transfer.
$contextarrayThe context: module, tld, cost_cid, profit_rate.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.pricing_save', 10, function (&$pricing, $context) {
    // Sell the first year at cost; leave renewals alone.
    foreach ($pricing['register'][1] ?? [] as $cid => $row)
        $pricing['register'][1][$cid]['promo'] = $row['cost'];
});

Changing a new extension record

filterdomain.tld_save_data
AdminProducts on create only

Runs before a new extension is written to the database.

Parameters 2
$insert_dataarrayrefThe record about to be written: name, module, status, rank, dns_manage, forwarding, whois_privacy, epp_code.
$extensionstringThe extension being added.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:domain.tld_save_data', 10, function (&$insert_data, $extension) {
    // A new extension arrives switched off, so nothing sells without a price.
    $insert_data['status'] = 0;
});

Stopping an extension delete

gatedomain.tld_delete
AdminProducts takes it off the catalogue

Runs before an extension is deleted from the catalogue.

Parameters 2
$extensionstringThe extension about to be deleted.
$tldarrayThe extension record.
Return 1
stringA non-empty string stops the delete; the text is thrown as the error.
Listener
Hook::add('gate:domain.tld_delete', 10, function ($extension, $tld) {
    // Deleting an extension with sold names leaves those records orphaned.
    if (Acme::soldCount((int) ($tld['id'] ?? 0)) > 0)
        return 'Names are sold on this extension; it cannot be deleted.';

    return null;
});

Following a new extension

actiondomain.tld_created
AdminProducts the id is filled in

Runs after the extension was created in the catalogue.

Parameters 3
$tld_idintThe id of the created record.
$extensionstringThe extension name.
$registrarstringThe provider module assigned. Without a module chosen, an empty key arrives.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.tld_created', 10,
    function ($tld_id, $extension, $registrar) {
        Ops::note('tld-added', $extension . ' -> ' . ($registrar ?: 'no module'));
    });

Taking over a single lookup

filterdomain.available
Registrar::check takes over by returning

Runs before the availability question is asked for one extension. Return a filled answer and the system asks neither WHOIS nor the registrar: you answered.

Parameters 1
$queryarrayWhat is being asked: sld, tld, module. An empty module puts you on the WHOIS path, a filled one on the registrar path. Both pass through here.
Return 1
array|falseA filled array takes over the lookup: status (available, unavailable or error), with optional message, premium, premium_price. An empty array, false or no return lets the system carry on. With several listeners answering, the last one holds.
Listener
Hook::add('filter:domain.available', 10, function ($query) {
    // Never ask the registrar about names on your block list.
    if (Acme::blocked($query['sld'] ?? ''))
        return ['status' => 'unavailable', 'message' => 'this name is not offered'];

    return false;   // let the system ask for the rest
});

Changing the collected result map

filterdomain.whois_result
Registrar::check passed by link

Runs once every extension has answered, right before the result goes back to the caller. You hold the whole map, not one extension.

Parameters 2
$resultarrayby linkDomain against result; each result carries sld, tld, status, and may carry message, premium, premium_price.
$contextarrayWhat was asked: sld and tlds.
Return 1
voidThe return is ignored; you write over the map. Overriding a premium price here is easier than catching each extension lookup.
Listener
Hook::add('filter:domain.whois_result', 10, function (&$result, $context) {
    foreach ($result as $domain => $row) {
        if (($row['premium'] ?? false) !== true) continue;

        // Add your own margin to the premium price.
        $result[$domain]['premium_price'] = round(((float) $row['premium_price']) * 1.15, 2);
    }
});

Masking the raw record

filterdomain.whois_record
ClientDomain::whois passed by link personal data

Runs before the raw text from the registrar is shown to a visitor. That text holds somebody else's name, email and phone, so this is where masking belongs.

Parameters 2
$rawstringby linkThe full record text. It arrives empty when no record could be fetched, which is also where you can feed text from a source of your own.
$whoisContextarrayWhich name was asked for: sld, tld, status.
Return 1
voidThe return is ignored; you write over the text.
Listener
Hook::add('filter:domain.whois_record', 10, function (&$raw, $whoisContext) {
    if ($raw === '') return;

    // Hide the email and phone lines.
    $raw = preg_replace('~^(.*(?:Email|Phone).*)$~mi', '[hidden]', $raw);
});

Following a document requirement change

actiondomain.doc_saved
AdminProductsDomain compliance record

Runs after the required document list of an extension changes. The change reaches backwards: orders already waiting on that extension may now need a different document.

Parameters 4
$tldstringThe extension, without a dot and in lower case.
$addedarrayNewly added document definitions.
$updatedarrayChanged definitions, id against data.
$removedarrayThe ids of definitions taken away.
Return 1
voidThe return is ignored. The record is already written.
Listener
Hook::add('action:domain.doc_saved', 10, function ($tld, $added, $updated, $removed) {
    // A new document means waiting orders on that extension deserve a second look.
    if ($added) Acme::reviewPendingOrders($tld);
});

Pitfalls

The search filter runs AFTER sorting

The suggestions are sorted by state before you see them. A suggestion appended to the list stays at the bottom, where most customers never look. To put yours forward, place it at the front.

The extension format is strict

The featured list and the table rows want the extension without a dot and in lower case. Writing .COM breaks the match: the extension is found in no catalogue, no price resolves, and the screen quietly shows nothing.

Price hooks come after conversion

Both the renewal price and the table rows hand you a converted amount. Write your own price in that same currency; dropping the provider's cost in as-is shows the customer a number from another currency.

A category key has to match the extension rows

Adding a key to the category list opens the button and nothing else. That key also has to appear in the category list on the extension rows, or a customer pressing it sees an empty list.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.