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

Idna Convert Laravel Package

mso/idna-convert

A PHP library for converting internationalized domain names between Unicode and ASCII/Punycode. Supports IDNA 2003 and 2008, making it useful for handling domains in URLs, email addresses, and web applications. Pure PHP implementation with straightforward encode/decode APIs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the namespace first. The package was originally mso/idna-convert and is now maintained as algo26-matthias/idna-convert; the namespace changed with the fork. On v2.x you get Mso\IdnaConvert\IdnaConvert, on v3.x you get Algo26\IdnaConvert\IdnaConvert. Pin the major version in composer.json and check what actually landed in vendor/ before writing code:

composer require algo26-matthias/idna-convert:^3.0
ls vendor/algo26-matthias/idna-convert/src

First use case: turn a user-typed domain into a canonical ASCII host. This is the 90% case — a settings form where the user enters münchen.de, and you need xn--mnchen-3ya.de for DNS, Host headers, and storage.

use Algo26\IdnaConvert\IdnaConvert;

$idn = new IdnaConvert();

$ascii   = $idn->toAscii('münchen.de');      // xn--mnchen-3ya.de
$unicode = $idn->toUnicode('xn--mnchen-3ya.de'); // münchen.de

(The v2 Mso\... class exposes the same toAscii() / toUnicode() pair. Very old 1.x code used encode() / decode() — don't copy snippets from blog posts without checking.)

Where to look first:

  1. The README on the repo — it is short and shows the constructor options (idn_version, transitional handling).
  2. src/IdnaConvert.php — the two public methods are thin wrappers; the real work is in Transcode/Punycode classes.
  3. tests/ — this is the best source of truth for edge cases (emoji domains, ß, RTL scripts). Copy fixtures from there into your own test suite rather than inventing them.
  4. EncodingHelper — if you ever accept non-UTF-8 input (legacy CSV imports), this is the class that converts before transcoding.

Why this package instead of idn_to_ascii(): it is pure PHP with no ext-intl requirement. That matters on shared hosting, Alpine containers without intl, and CI images where intl is missing. If you do have intl, native idn_to_ascii($d, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) is faster and better maintained — consider using this package as a fallback rather than the primary path.

Implementation Patterns

Bind it once as a singleton

The converter is stateless and cheap to construct, but resolving it through the container keeps your call sites testable and lets you swap in a fake.

// app/Providers/AppServiceProvider.php
use Algo26\IdnaConvert\IdnaConvert;

public function register(): void
{
    $this->app->singleton(IdnaConvert::class, fn () => new IdnaConvert());
}

Then inject it: public function __construct(private IdnaConvert $idn) {}.

Wrap it in a small domain value object

Do not sprinkle toAscii() across controllers. Centralize the rules — lowercasing, trailing-dot stripping, error handling — in one place.

namespace App\Support;

use Algo26\IdnaConvert\IdnaConvert;
use InvalidArgumentException;

final class Domain
{
    public function __construct(
        public readonly string $ascii,
        public readonly string $unicode,
    ) {}

    public static function fromUserInput(string $input, IdnaConvert $idn): self
    {
        $input = rtrim(trim($input), '.');          // FQDN trailing dot
        $input = mb_strtolower($input, 'UTF-8');    // case-fold before converting

        try {
            $ascii = $idn->toAscii($input);
        } catch (\Throwable $e) {
            throw new InvalidArgumentException("Unconvertible domain: {$input}", 0, $e);
        }

        return new self($ascii, $idn->toUnicode($ascii));
    }
}

Store ascii as the canonical, unique-indexed column. Store unicode (or recompute on read) purely for display — users should see münchen.de in the UI, never xn--mnchen-3ya.de.

Custom validation rule

Laravel's url and email rules do not do IDN conversion. Write a rule that converts first, then validates the ASCII form with FILTER_VALIDATE_DOMAIN.

namespace App\Rules;

use Algo26\IdnaConvert\IdnaConvert;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class IdnDomain implements ValidationRule
{
    public function __construct(private IdnaConvert $idn) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || $value === '') {
            $fail('The :attribute must be a valid domain.');

            return;
        }

        try {
            $ascii = $this->idn->toAscii(mb_strtolower(rtrim($value, '.'), 'UTF-8'));
        } catch (\Throwable) {
            $fail('The :attribute contains characters that cannot form a domain.');

            return;
        }

        if (! filter_var($ascii, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
            $fail('The :attribute is not a valid domain name.');
        }
    }
}

Note the ordering: convert, then validate. Validating the Unicode string with a regex is the single most common bug in IDN handling.

prepareForValidation in form requests

Normalize on the way in so the rest of the request lifecycle never sees Unicode hosts.

protected function prepareForValidation(): void
{
    if ($this->filled('domain')) {
        $this->merge([
            'domain' => app(IdnaConvert::class)->toAscii(
                mb_strtolower(trim($this->input('domain')), 'UTF-8')
            ),
        ]);
    }
}

Email addresses: convert only the domain part

The local part of an email is not IDNA-encoded — it stays Unicode (SMTPUTF8) or must be rejected. Splitting on the last @ is required because quoted local parts can contain @.

$at = strrpos($email, '@');
$local  = substr($email, 0, $at);
$domain = substr($email, $at + 1);

$normalized = $local . '@' . $idn->toAscii($domain);

Then lowercase the domain only — lowercasing the local part is technically a protocol violation, though most providers do it anyway.

Host header / multi-tenant middleware

If you support per-tenant subdomains on an IDN apex, browsers send the punycode form in the Host header. Laravel's router matches against whatever string arrives, so you need either punycode route definitions or a normalizing middleware.

public function handle(Request $request, Closure $next)
{
    $host = $idn->toAscii(mb_strtolower($request->getHost(), 'UTF-8'));

    // Rewrite the host on the request so route()/url() generate punycode
    $request->headers->set('HOST', $host);
    $request->server->set('HTTP_HOST', $host);

    return $next($request);
}

Pair this with Route::domain('xn--mnchen-3ya.de') — route definitions must be the ASCII form.

Outbound URL generation

route() and url() emit APP_URL verbatim. If you let admins set APP_URL=https://münchen.de, generated links will contain Unicode and some HTTP clients (Guzzle, older curl builds, payment gateway callbacks) will break. Normalize APP_URL at boot or in a config cache step:

// config/app.php or a booted provider
'url' => (new IdnaConvert())->toAscii(rtrim(env('APP_URL'), '/')),

Caching

Conversion involves per-label punycode arithmetic. In a request that loops over thousands of rows (imports, sitemaps, feed generation) this adds up. Memoize by input string:

final class CachedIdna
{
    private array $memo = [];

    public function __construct(private IdnaConvert $idn) {}

    public function toAscii(string $d): string
    {
        return $this->memo[$d] ??= $this->idn->toAscii($d);
    }
}

Register this per-request (not as a singleton across Octane workers unless you add size bounds — an unbounded memo array is an Octane memory leak).

Gotchas and Tips

It converts, it does not validate. toAscii() will happily process not a domain!!! into something. Every call site needs a FILTER_VALIDATE_DOMAIN | FILTER_FLAG_HOSTNAME check afterward. This is the number-one source of bugs with every IDNA library.

Failure modes are version-dependent. Some versions return false, some throw InvalidArgumentException / IdnaException, some emit warnings. Do not assume — write a throwaway script against your installed version and test with '', 'xn--', a 300-character label, and "\xFF\xFE". Then wrap accordingly. Your wrapper should normalize all of this into one exception type.

LGPL-2.1 is a copyleft license. For a PHP library loaded at runtime this is normally fine for proprietary applications (analogous to dynamic linking), but many corporate OSS policies flag LGPL by default. Get it cleared before it enters a commercial codebase, or use ext-intl instead.

IDNA2003 vs IDNA2008 changes results. ß becomes ss under 2003 but is preserved as xn--zca under 2008. ς/σ and ZWJ-containing emoji domains behave differently too. The constructor accepts an idn_version option — pick one and record it, because switching later silently changes what your stored domains mean. IDNA2008 is the modern default; 2003 exists for legacy compatibility only.

Length limits are checked on the ASCII form, not the Unicode form. A label is max 63 bytes and the whole name max 253 characters after punycode expansion. A 40-character Unicode label can exceed 63 bytes in ASCII. Validate after conversion.

Strip the trailing dot and the port before converting. toAscii('example.com.') and toAscii('example.com:8080') do not do what you want. Use parse_url() for anything that might be a full URL, and convert only $parts['host'].

Case-fold with mb_strtolower($s, 'UTF-8'), never strtolower(). strtolower is byte-wise and will corrupt multi-byte characters into invalid UTF-8, which then fails conversion with a confusing error.

EncodingHelper exists for a reason. If you import domains from CSV/Excel dumps or legacy databases, the strings may be ISO-8859-1 or Windows-1252. Detect and convert to UTF-8 before handing anything to IdnaConvert. Garbage in produces silent punycode garbage out.

toUnicode() on an already-Unicode string is a no-op, not an error. That means round-tripping is safe but also that you cannot use toUnicode() as a "is this punycode?" detector. Check for the xn-- prefix per label instead.

Never trust the Host header for security decisions. Even after normalization, host headers are attacker-controlled. IDN conversion does nothing about that — keep using signed URLs / configured domains for anything authorization-related. Also watch for homograph attacks (аpple.com with a Cyrillic а) when displaying domains to users; show the punycode next to the Unicode form for anything security-sensitive (OAuth redirect URIs, email sender domains).

Queue workers and Octane. Don't hold converted state in properties on a singleton if you're running Octane; the converter itself is stateless so it is safe, but your own memoization wrappers need eviction or a flush() on request termination.

Testing. Use fixtures from the package's own test suite and add a few of your own: münchen.de, 例え.テスト, παράδειγμα.δοκιμή, faß.de, and an emoji domain. Assert both directions (toAscii then toUnicode returns the input, modulo case folding) — round-tripping catches most encoding mistakes.

Pin the version and watch the fork. With 0 dependents and a namespace that changed mid-life, the ecosystem is thin. Composer-audit reviews, check the CHANGELOG before bumping majors, and consider adding a single integration test that asserts the exact expected punycode for two or three fixtures — that test is your early warning if a version bump changes IDNA behavior.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin