layershifter/tld-database
A PHP package providing an up-to-date database of top-level domains, with tools to parse domain names, extract and validate TLDs, and refresh the TLD list from IANA. Useful for domain validation, normalization, and routing logic.
Install (with eyes open):
composer require layershifter/tld-database
Composer will likely flag it as abandoned. That is accurate — the repo is archived, last tagged 2019-08-04, and the bundled Public Suffix List snapshot is frozen at roughly that date. Verify it installs cleanly on your PHP version (it predates PHP 8; expect deprecations on 8.1+).
There is no Laravel service provider. The package is a plain PHP library. You instantiate it yourself:
use LayerShifter\TLDDatabase\Store;
$store = new Store(); // loads the bundled data file
$store->isExists('com'); // TLD is in the list
$store->isValid('co.uk'); // suffix is a valid public suffix
$store->getRules('uk'); // ['uk', 'co.uk', 'me.uk', ...]
Where to look first:
src/Store.php — the entire public surface area (three or four methods).tests/ — confirms exact semantics of isValid vs isExists and whether wildcard/exception rules are included.First use case: a registration or "add your domain" form where you want to reject [email protected] or a tenant subdomain on a bogus suffix. Wrap it in a custom validation rule and you have the package earning its keep in ten minutes.
Bind it once as a singleton. The data file is parsed into an array on construction; you do not want that per request.
// AppServiceProvider::register()
$this->app->singleton(Store::class, fn () => new Store(
config('tld.path') // null = bundled file; override if you vendor a fresher PSL
));
Custom validation rule.
class ValidTld implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$normalized = Str::lower(rtrim(trim($value), '.'));
if (! app(Store::class)->isExists($normalized)) {
$fail("The {$attribute} has an unrecognised top-level domain.");
}
}
}
Use it in a FormRequest: 'domain' => ['required', 'string', new ValidTld]. Pair with dns only if you actually want a blocking DNS lookup — PSL membership says nothing about whether the domain resolves or is registered.
Registrable domain / tenant resolution. This is the highest-value day-to-day pattern for a Laravel app: deriving eTLD+1 so acme.co.uk and blog.acme.co.uk share a tenant, cookie scope, and cache namespace.
$suffixes = $store->getRules($lastLabel); // ['uk', 'co.uk', ...]
usort($suffixes, fn ($a, $b) => substr_count($b, '.') <=> substr_count($a, '.'));
$suffix = collect($suffixes)->first(fn ($s) => Str::endsWith($host, ".$s"));
$registrable = implode('.', array_slice(explode('.', $host), -1 - substr_count($suffix, '.')));
Push this into a DomainService class, not a middleware closure — see the migration note in Gotchas.
Normalize before every call. Strip scheme/path with parse_url, lowercase, trim the trailing dot, convert IDN with idn_to_ascii() (ext-intl) — the store will not do this for you.
Bulk work belongs in a command or job. Importing 50k customer domains? Resolve the store once, loop, and chunk. Never new Store() inside a loop.
Test with a fake. Because Store is a concrete class, bind your own DomainService interface in the container and swap it in tests. Otherwise your test suite inherits the 2019 PSL snapshot as an implicit fixture.
.dev, .app, .zip, and every TLD delegated since mid-2019 may be missing or wrong. Decide explicitly: accept staleness, regenerate the JSON from publicsuffix.org with the repo's generator, or replace the dependency.isValid ≠ domain is valid. It answers "is this a public suffix under PSL rules," not "does this domain exist." Do not conflate it with the dns rule or with egulias/email-validator (email:rfc already handles syntax; PSL adds the TLD check).*.ck and !www.ck. Check whether this package's isValid honors both before relying on it for cookie-domain scoping — a mis-scoped cookie domain is a security bug, not a UX bug.*.github.io, *.herokuapp.com). Whether those are included materially changes tenant resolution and cookie scoping for apps hosted on shared suffixes. Test your fixture explicitly.münchen.de will fail a naive lookup. idn_to_ascii() before the check, and store the punycode form as canonical.App\Support\Domain\DomainService with an interface today. When you migrate to jeremykendall/php-domain-parser (maintained, IDN-aware, gives you eTLD+1 directly), the swap is one binding instead of a grep across forty controllers.config/tld.php and point it at storage_path('app/tld.json') if you vendor your own snapshot. That makes refreshing data a deploy artifact rather than a dependency bump you cannot make.How can I help you explore Laravel packages today?