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.
Architecture fit. mso/idna-convert (published as the algo26-matthias fork) is a pure-PHP implementation of IDNA2008 / UTS #46 and legacy IDNA2003, converting Unicode domain labels to ASCII/Punycode and back. It is a single-purpose, stateless library — no service provider, no config, no Laravel coupling — so it slots in as a low-level utility wrapped by our own service class. The realistic fit is narrow and well-defined: canonicalizing internationalized hostnames at ingress (registration, tenant subdomains, allow-lists), before egress (generated URLs, mail headers, sitemap links), and anywhere we must control which IDNA profile is applied rather than accepting the platform default.
Integration feasibility. Trivial mechanically: composer require, one wrapper class, one container binding, a validation rule and a URL normalizer on top. No extension dependency, which is its main differentiator versus ext-intl. The real feasibility question is not effort but necessity.
Technical risk.
symfony/polyfill-intl-idn transitively (email validation, Mime). That polyfill is MIT-licensed, far more widely deployed, and covers idn_to_ascii/idn_to_utf8 with UTS #46 variants. Adding this package is only justified if we need behavior the polyfill does not expose — precise IDNA2008 vs. 2003 selection, transitional processing of ß, explicit contextJ/contextO handling, or full control over error behavior.Mso\IdnaConvert.Key questions. Which concrete requirement does ext-intl / the existing polyfill fail? Do we need IDNA2008-specific behavior, or is UTS #46 non-transitional sufficient? Will Legal accept LGPL-2.1? Who owns the wrapper when the maintainer stops releasing? What is the measured performance delta on our hottest normalization path?
Stack fit. Laravel 10/11/12, PHP 8.1+. No framework hooks, so it composes cleanly and is framework-version agnostic — a genuine plus for long upgrade cycles.
Migration path. Do not call the library directly from controllers, rules, or models. Introduce an internal contract:
App\Support\Idna\IdnaConverter interface with toAscii(string $domain, IdnaProfile $profile): string and toUnicode(string $domain): string.IntlIdnaConverter (default, ext-intl) and PackageIdnaConverter (this package) implementations.DomainName value object that validates length (63 octets per label, 253/255 total), rejects empty labels, enforces trailing-dot policy, and lowercases deterministically.This buys optionality: the package becomes a swappable fallback rather than a load-bearing dependency, and we can A/B the two implementations against a corpus of real hostnames to decide whether the extra dependency earns its place. If any legacy code targets the upstream namespace, migrate it behind the same contract rather than in place.
Compatibility. Verify the fork's actual tagged releases and semantic versioning before pinning — the release metadata warrants direct inspection. Confirm UTF-8 input assumption, exception types, and whether invalid input throws or returns a sentinel. Test against ext-intl output for a fixed corpus: münchen.de, faß.de (transitional vs. non-transitional), Arabic and CJK labels, emoji TLDs, mixed-script inputs, over-length labels, and already-Punycode input (idempotency).
Sequencing. (1) Legal license review. (2) Benchmark against ext-intl; if the delta is immaterial, stop and use ext-intl. (3) If justified, build the contract + value object with the intl implementation only, and ship. (4) Add the package as a fallback, behind a feature flag, with a golden-file test suite. (5) Switch the flag only where ext-intl genuinely cannot meet the requirement. (6) Wire into inbound domain validation and outbound URL generation last, once canonicalization behavior is stable.
Maintenance. Minimal code surface — one dependency, one thin wrapper. The ongoing cost is dependency hygiene: pin exact versions, commit composer.lock, subscribe to releases and advisories, and keep a documented fallback to ext-intl in case the fork goes dormant. Budget a review each PHP minor release.
Support. No commercial backing and no vendor SLA. Internal ownership must be explicit — assign the wrapper and its test corpus to a named team. Because the library is small and pure PHP, forking and self-maintaining is a credible contingency, which materially lowers the abandonment risk. LGPL obligations (license notice, source availability for the library, no obfuscation) must be recorded in the SBOM and honored in distributed artifacts.
Scaling. Stateless and CPU-bound: cheap per call, but measurably slower than the C implementation in ext-intl. Normalization runs once per inbound request at most; memoize per-request and cache domain→ASCII mappings in Redis for high-volume, low-cardinality sets (tenant hosts, allow-lists). Memory footprint is negligible; horizontal scaling is unaffected. Avoid it in tight loops — batch and cache instead.
Failure modes. Wrong-profile selection producing labels that do not resolve; silent pass-through of invalid input if exceptions are swallowed; double encoding of already-Punycode input; encoding mismatches between UTF-8 and legacy charsets in imported data; inconsistent normalization between the registration path and the resolution path; homograph/phishing exposure that normalization alone does not mitigate. Mitigate with strict throw-on-invalid semantics, idempotency tests, a shared DomainName value object used everywhere, and a denylist/mixed-script policy layered above the converter. Watch for Unicode-version drift: newly assigned code points may be rejected until the library updates.
Ramp-up. Under a day for a competent PHP engineer — the API is a handful of methods and the domain concepts (Punycode, UTS #46, transitional processing) are well documented. The real onboarding cost is the policy knowledge: which profile we standardize on and why, which the wrapper's naming and tests should make self-evident.
How can I help you explore Laravel packages today?