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

Punycode Laravel Package

rowbot/punycode

rowbot/punycode is a pure PHP implementation of the Punycode algorithm (RFC 3492) for encoding and decoding internationalized domain names. It offers a simple API with strict error handling, making it easy to convert Unicode labels to ASCII and back again.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package with Composer:

composer require rowbot/punycode

First, read the README and the public API in src/Punycode.php; then glance at the test suite for RFC 3492 vectors and exception behavior. The package is a low-level Punycode codec, not a full IDNA processor, so the main entry point is the codec’s encode() / decode() pair.

A good first use case is converting a single non-ASCII domain label to its ASCII A-label and back:

use Rowbot\Punycode\Punycode;

$codec = new Punycode();

$label = $codec->encode('mañana');   // "maana-pta"
$domain = 'xn--' . $label;           // "xn--maana-pta"

$unicode = $codec->decode('maana-pta'); // "mañana"

Keep in mind that Punycode operates on individual labels, not whole domains. The xn-- ACE prefix is normally added/removed by your IDNA layer, not by the raw Punycode algorithm.

Implementation Patterns

Wrap it in a small IDN service. Avoid scattering encode()/decode() calls throughout controllers and models. A dedicated service makes prefix handling, caching, validation, and swapping implementations much easier.

final class IdnService
{
    public function __construct(private Punycode $codec) {}

    public function labelToAscii(string $label): string
    {
        if ($label === '' || preg_match('/^[\x00-\x7F]+$/', $label)) {
            return strtolower($label);
        }

        return 'xn--' . strtolower($this->codec->encode($label));
    }

    public function labelToUnicode(string $label): string
    {
        if (!str_starts_with(strtolower($label), 'xn--')) {
            return $label;
        }

        return $this->codec->decode(substr($label, 4));
    }
}

Process domains label by label. Split on ., encode only labels containing non-ASCII characters, then rejoin. Do not pass münchen.de to encode() as one string.

public function domainToAscii(string $domain): string
{
    $trailingDot = str_ends_with($domain, '.');
    $labels = explode('.', rtrim($domain, '.'));

    foreach ($labels as &$label) {
        $label = $this->labelToAscii($label);
    }

    return implode('.', $labels) . ($trailingDot ? '.' : '');
}

Store a canonical ASCII form. In Laravel, keep the A-label form as the normalized value used for uniqueness, lookup, and outbound requests, and keep the Unicode form for display. For example, a domains table can have ascii_name indexed and unicode_name for UI.

$domain->ascii_name = $idn->domainToAscii($request->input('domain'));
$domain->unicode_name = $request->input('domain');

Normalize email domains. Split at the last @, apply domainToAscii() to the domain part, and leave the local part untouched. This is useful when validating or de-duplicating users with internationalized email domains.

[$local, $domain] = explode('@', $email, 2);
$normalized = $local . '@' . $idn->domainToAscii($domain);

Normalize URL hosts before validation or fetching. Use parse_url() to isolate the host, convert it with the IDN service, then rebuild carefully. Remember that parse_url() is not a full URL parser; for complex URLs, prefer a dedicated URL library.

Bind it as a singleton in Laravel. The codec is stateless and cheap to reuse.

$this->app->singleton(Punycode::class, fn () => new Punycode());

Add a validation rule. A custom rule that attempts conversion and catches codec exceptions gives much better errors than letting a PunycodeException bubble up as a 500.

try {
    $ascii = $idn->domainToAscii($value);
} catch (\Throwable $e) {
    $fail('The :attribute is not a valid internationalized domain.');
}

Cache repeated conversions. Domains are often processed repeatedly in imports, crawls, or list views. A small in-request array cache keyed by label is usually enough.

Gotchas and Tips

  • Punycode is not IDNA. It does not perform UTS #46 mapping, case folding, NFC normalization, bidi checks, or disallowed-character checks. If you need standards-compliant IDNA, use ext-intl (idn_to_ascii() / idn_to_utf8()) or a full IDNA package such as rowbot/idna. Use this package when you specifically want a small RFC 3492 codec or need to avoid ext-intl.
  • Do not encode whole domains. encode('münchen.de') is not the same as encoding münchen and leaving .de alone. Always split labels first.
  • Handle the xn-- prefix explicitly. Most Punycode implementations return the encoded label without the ACE prefix. Add it when producing DNS/ASCII labels and strip it before decoding.
  • Normalize before encoding. mañana and its canonically equivalent forms can produce different A-labels. Apply NFC at minimum, and use full IDNA mapping if you need full compatibility.
  • Lowercase consistently. DNS is case-insensitive. Normalize ASCII labels to lowercase before storing or comparing.
  • Catch decode failures. Invalid or malformed xn-- labels can throw. Treat decode errors as “not a valid IDN” rather than allowing an exception to crash the request.
  • Check label and domain length limits. Encoded labels should be at most 63 octets, and the full domain at most 253 octets in common DNS usage. Punycode encoding can expand input significantly.
  • Preserve trailing dots. example.com. is a valid absolute domain name; naive explode()/implode() handling can drop the trailing empty label.
  • Watch for Unicode dot variants. Characters such as U+3002 IDEOGRAPHIC FULL STOP and full-width dots are not the same as ASCII .. Full IDNA mapping handles these; raw Punycode does not.
  • Beware homograph attacks. Punycode makes Unicode domains representable, not safe. Displaying the Unicode form of an untrusted domain can enable visually deceptive lookalikes. Consider showing both forms or warning on mixed-script domains.
  • Use known vectors in tests. bücher → bcher-kva, mañana → maana-pta, 例え → r8jz45g, and round-trip assertions (decode(encode($label)) === $label) catch most integration mistakes.
  • Pin the version. This is a small, low-traffic package; locking a tested version avoids surprises from infrequent releases. Check composer.json for the required PHP version and extensions before adopting it in older Laravel projects.
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/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
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route