rowbot/idna
rowbot/idna provides IDNA 2008 and UTS #46 processing for PHP: convert internationalized domain names to ASCII (Punycode) and back, with validation and normalization for safe, spec-compliant handling of Unicode domains.
Architecture fit
rowbot/idna is a framework-agnostic, pure-PHP implementation of UTS#46 (Unicode IDNA Compatibility Processing) — the algorithm that converts Unicode domain names into their ASCII A-label (punycode) form and back into human-readable U-labels. It has no Laravel service provider, no facade, no config surface, and no global state; it is a low-level primitive that fits into the application as a wrapped service, a validation rule, and a value object rather than as a "package integration."
For a Laravel codebase the realistic consumers are:
münchen.example instead of xn--mnchen-3ya.example in UI, emails, and PDFs while keeping the ASCII form as the canonical stored value.rowbot/url depends on this package, so choosing it now consolidates a future dependency rather than adding one.The API surface is small: ASCII↔Unicode conversion with a UTS#46 options object controlling strictness, hyphen/bidi/joiner checks, STD3 ASCII rules, transitional vs. non-transitional processing, and DNS length verification. That smallness is a feature — it is trivially wrappable and testable — and a limitation, in that it does not provide confusable/homograph detection (UTS#39), public-suffix awareness, or any policy layer. Those remain ours to build.
Integration feasibility
Straightforward mechanically: composer require rowbot/idna, wrap in one class, done. The friction is not code, it is overlap. PHP's ext-intl already exposes idn_to_ascii($d, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $info), which is also UTS#46 — backed by ICU. So the honest feasibility question is not "can we integrate this?" but "why this instead of the function we already have?" The defensible answers are narrow but real:
libicu build, which differs between a developer's Mac, the CI image, and the production base image. That produces environment-dependent validation — a domain that passes in staging and fails in prod. A package that ships its own generated UTS#46 data tables eliminates that class of bug. This is the single strongest argument.ext-intl dependency for the conversion path (it still requires mbstring, and I would confirm whether Normalizer is used internally before assuming intl is optional — verify against composer.json rather than the README).rowbot/url if WHATWG parsing is on the roadmap.Technical risk
^ ranges in composer.json.ext-intl and this package perform normalization in different code paths (e.g., Laravel's email rule, filter_var, and our wrapper), we get two Unicode versions disagreeing. Mitigation: a single IdnNormalizer boundary and a lint rule against calling idn_to_ascii directly.faß.example is fass.example under transitional and xn--fa-hia.example under non-transitional. Flipping the flag post-launch silently orphans stored identifiers. This is a configuration decision that must be made once, documented, and covered by tests — not left to a default.аpple.com with Cyrillic а) pass validation cleanly. If the driver is anti-spoofing for user-facing domains, this package is necessary but insufficient and the assessment must say so up front.Key questions
ext-intl, and does it require mbstring? What is the PHP floor, and does that floor survive our next Laravel major upgrade?memory_limit FPM pool? Measure with opcache.preload, not by reading the source.Stack fit
Laravel 10/11+, PHP 8.1+. No provider required. Recommended shape:
App\Support\Idn\IdnNormalizer — the single application boundary. Methods toAscii(string): AsciiDomain, toUnicode(string): string, validate(string): ValidationResult. Inject the package's converter; never let the raw library escape this class.App\Casts\AsciiDomainCast — Eloquent cast that normalizes on set and guarantees the ASCII form is what hits the column.App\Rules\IdnDomain / IdnEmail — custom validation rules. Note that Laravel's email rule and filter_var(..., FILTER_VALIDATE_EMAIL) both reject U-labels outright, so IDN email support requires a rule that splits at the last @, normalizes the domain, and validates the local part separately.App\Http\Middleware\NormalizeIdnInput — optional, applied to registration/tenant-creation routes only. Avoid global input rewriting; it invites subtle bugs in request logging and signed-URL verification.Data modeling
Store both forms, with the A-label canonical:
domain_ascii — varchar(253), ascii_bin collation, UNIQUE. Uniqueness belongs on the ASCII form, otherwise two accounts can claim visually identical names via different Unicode encodings.domain_unicode — varchar(255), display only, never used in WHERE.Enforce the 63-octet-per-label and 253-octet-total limits in application code via the library's DNS-length verification, and mirror them as column lengths so the database is a backstop rather than a surprise.
Migration path
ß, Greek final sigma, ZWJ emoji sequences, mixed-script, over-length labels, and known-invalid inputs. This corpus is the asset — it survives package swaps and Unicode table bumps and is the artifact that makes a future fork or replacement decision cheap.idn_to_ascii or a regex is currently in use, run the new normalizer in parallel on real inputs for a sprint and log divergences. Divergences are the whole point; do not skip to cutover.UNIQUE, and remove the shadow logging.Compatibility
Watch three seams. Laravel's url/email rules and Request::validate assume ASCII hostnames — custom rules are mandatory, not optional. Guzzle and Laravel's HTTP client pass hostnames through as bytes and will happily attempt DNS on a U-label and fail; normalize before any outbound call, and pair with an SSRF guard since normalization can change the resolved host. Database collation is the third: utf8mb4_unicode_ci will case-fold and accent-fold the ASCII column in ways that break the uniqueness guarantee, so the binary collation is a requirement, not a preference.
Sequencing
Corpus and wrapper with unit tests (2–3 days) → validation rule and cast behind a feature flag (2 days) → shadow comparison on production traffic (1 sprint, near-zero effort) → backfill and collision review (scoped after shadow data) → enable writes → optionally surface Unicode on display. Total engineering effort to a safe production state is roughly one to two weeks; the flag and shadow phases are what keep it safe and are the phases most likely to be cut under schedule pressure.
Maintenance
Pin to an exact version. Add the repository to the dependency-watch list with an explicit owner. Budget roughly 1–2 days per quarter for: reviewing upstream releases, re-running the golden corpus, and verifying the Unicode table version. On Unicode majors, treat the upgrade as a behavioral change requiring a regression pass, not a patch bump. If upstream stalls for more than 18 months, fork to an internal namespace — the license is MIT, the code is self-contained, and the fork cost is low precisely because the corpus already exists.
Support
No commercial support, no SLA, no security response process. Escalation path is: read the source (small), then fork. Do not route production incidents to a GitHub issue queue with two stars. Internal documentation should state plainly that this is a forkable primitive, and one engineer should be designated as the owner with enough context to patch it.
Scaling
Favorable. Pure CPU, no I/O, no network, no external service, no cache to warm. Costs are per-request table loading and per-call conversion — microseconds to low milliseconds for typical inputs, negligible against any database round-trip. Two things to actually measure rather than assume: memory footprint of the Unicode tables under constrained FPM pools (consider opcache.preload and a container singleton), and throughput on bulk paths — a million-row backfill should be chunked and timed, and if it exceeds the batch window, a memoization cache keyed on sha1($input) in Redis collapses the cost, since domain inputs are highly repetitive.
Failure modes
ext-intl or mbstring absent in a rebuilt container → hard failure at first use. Add an extension assertion to the health check and the Dockerfile, not to runtime code.аpple.com with a Cyrillic а as perfectly well-formed. This is not a bug and will not be fixed upstream. If user-facing domain registration is in scope, a UTS#39 confusables check and a mixed-script policy are separate, additional work — scope them or explicitly accept the risk in writing.Ramp-up
Low, and pleasantly so. The API is two methods and an options object; a mid-level engineer can integrate, test, and ship in a day or two. The real cost is conceptual, not mechanical: IDNA2008 vs. UTS#46, transitional processing, bidi and joiner rules, and the A-label/U-label distinction are genuinely unfamiliar territory for most PHP developers. Budget half a day of reading plus a walkthrough of the golden corpus, and write the transitional-mode decision and the "UTS#46 is not an anti-spoofing control" caveat into the code comments where the next engineer will actually find them. The overall proposition is a small, focused, low-risk dependency whose main risks — bus factor and scope misreading — are both cheap to mitigate and expensive to ignore.
How can I help you explore Laravel packages today?