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

Tld Extract Laravel Package

layershifter/tld-extract

Lightweight PHP library that extracts the top-level domain, domain name, and subdomain from URLs and email addresses using an accurate public suffix list. Simple static API returns structured results, making it easy to validate and parse domains in any Laravel or plain PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require layershifter/tld-extract

There is no service provider, no config file and no facade — it is a plain PHP library you instantiate yourself. That is the whole setup.

First use case: split a host into its parts

use LayerShifter\TLDExtract\Extract;

$extract = new Extract();

$result = $extract->parse('https://shop.example.co.uk/products/1?ref=newsletter');

$result->getSubdomain();         // 'shop'
$result->getHostname();          // 'example'
$result->getSuffix();            // 'co.uk'
$result->getFullHost();          // 'shop.example.co.uk'
$result->getRegistrableDomain(); // 'example.co.uk'
$result->isValid();              // true

Where to look first

  • src/Extract.php — the parser entry point (parse(), constructor arguments for custom suffix lists).
  • src/Result.php — the value object with the getters above.
  • data/ — the bundled Public Suffix List snapshot. Check its date; it is frozen at the time of the last release.
  • tests/ — the fastest source of truth for edge-case behaviour (IPs, IDNs, wildcards).

Do this first in a Laravel app — bind it once so you do not reload the suffix list on every parse:

// AppServiceProvider::register()
$this->app->singleton(\LayerShifter\TLDExtract\Extract::class, fn () => new \LayerShifter\TLDExtract\Extract());

Before you build anything on top of it, be aware the repo is archived and marked DEPRECATED (last release 2019-02-11). For a greenfield project, reach for jeremykendall/php-domain-parser or league/uri instead. Everything below applies when you are maintaining a codebase that already depends on it, or when you deliberately want a tiny zero-dependency PSL splitter and accept the frozen list.

Implementation Patterns

Validation rule for user-supplied URLs

The most common day-to-day use is rejecting junk before it hits the database:

use LayerShifter\TLDExtract\Extract;
use Illuminate\Contracts\Validation\Rule;

class PublicHostname implements Rule
{
    public function __construct(private Extract $extract) {}

    public function passes($attribute, $value): bool
    {
        $result = $this->extract->parse((string) $value);

        return $result->isValid() && $result->getSuffix() !== null;
    }

    public function message(): string
    {
        return 'The :attribute must be a public hostname.';
    }
}

Note the extra getSuffix() !== null check: isValid() is true for plenty of things you probably do not want stored as a "domain".

Registrable domain for cookie scoping and tenant detection

$host = $request->getHost();                       // 'acme.app.example.co.uk'
$domain = $extract->parse($host)->getRegistrableDomain(); // 'example.co.uk'

// share a session cookie across subdomains
config(['session.domain' => '.'.$domain]);

// resolve the tenant from the subdomain
$subdomain = $extract->parse($host)->getSubdomain(); // 'acme.app'
$tenant = explode('.', $subdomain)[0];

This is the pattern that pays for itself: never hand-roll substr()/explode('.') logic for cookie domains, co.uk will break it.

Normalise and canonicalise before persisting

$result = $extract->parse($input);

$record->host       = $result->getFullHost();          // lowercased, port/path stripped
$record->domain     = $result->getRegistrableDomain();
$record->tld        = $result->getSuffix();

Storing the three fields separately makes reporting ("group by registrable domain") trivial and avoids re-parsing in list views.

Reuse one instance in imports and jobs

Extract reads and indexes the suffix list at construction. In a queued job or an artisan import loop, construct once outside the loop — resolved from the container it is already a singleton, so inject it and you are done.

Memoise per request where it is hot

class DomainResolver
{
    private array $cache = [];

    public function __construct(private Extract $extract) {}

    public function registrable(string $host): ?string
    {
        return $this->cache[$host] ??= $this->extract->parse($host)->getRegistrableDomain();
    }
}

Useful when a Blade partial or an API resource serialises hundreds of rows that each carry a hostname.

Custom suffix list

The constructor accepts paths to your own suffix files, which is how you patch the frozen list without forking:

$extract = new Extract(
    base_path('resources/psl/public_suffix_list.dat'),
    base_path('resources/psl/registrable_domains.dat')
);

Use absolute paths. Pair this with a scheduled job that pulls the upstream PSL and rewrites the file, so a 2019 snapshot does not silently misclassify newer suffixes as part of the registrable domain.

Wrap it in your own value object

Given the package is archived, insulate your codebase:

final class Host
{
    // getters you actually use: subdomain(), registrableDomain(), suffix()
}

Then a future swap to jeremykendall/php-domain-parser is a single class rewrite instead of a grep across the app.

Gotchas and Tips

  • It is archived and deprecated. No security patches, no PSL updates, no PHP 8.x maintenance. Audit before adding it to anything new; on existing projects, plan an exit path.
  • The bundled PSL is from ~2019. Newer TLDs and, more painfully, newer multi-label suffixes (private-section entries, new ccTLD second levels) will be misparsed — foo.bar.newtld may come back with newtld as the hostname instead of the suffix. Test against the specific TLDs your app sees and override the data file if needed.
  • isValid() is not "is a public domain". It is closer to "the string parsed into something hostname-shaped". Always check getSuffix() too, and decide explicitly whether IPs, localhost, and single-label hosts are acceptable in your context.
  • Null-returning getters. Getters return null when the input cannot be parsed. Null-safe operators or explicit ?? fallbacks everywhere, or you will get null cast to '' in string columns.
  • Scheme, path, port, credentials are stripped for you — but a bare example.co.uk:8080 is not the same as a full URL, and inputs without a scheme can be ambiguous. Normalise inputs (trim, lowercase) before parsing rather than after.
  • IDN needs ext-intl. Punycode/Unicode conversion depends on it. Store the ASCII form as the canonical key and keep the Unicode form for display; comparing mixed forms in where() clauses will silently miss rows.
  • Trailing dots and case. Example.CO.UK. is an FQDN. The parser lowercases, but a trailing dot can leak into getFullHost() — rtrim it if you use the result as a lookup key.
  • Wildcard and exception rules (*.ck, !www.ck) are the classic place naive parsers fail and this one usually gets right — but only as of the snapshot date. Put these in your test fixtures.
  • Performance is construct-time, not parse-time. Building a new Extract per call in a loop is the one real performance trap. Register it as a singleton, inject it, never new it inside a method that runs per row.
  • No Laravel integration surface. No facade, no config, no events, no extension hooks beyond constructor arguments and subclassing Extract. If you need behaviour changes, wrap rather than subclass — subclassing ties you harder to an abandoned package.
  • Debugging misparses: dump getSuffix() first. If the suffix is wrong, the data file is wrong; if the suffix is right but the hostname is wrong, the input normalisation is wrong. Those are the only two failure modes in practice.
  • Test with assertions, not the network. Everything is deterministic against the bundled list — no HTTP calls, no cache directory, no flakiness. Write fixture-driven tests now, while the behaviour is pinned, so the eventual migration has a spec to satisfy.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console