Skip to content

URL Encode / Decode — PHP source

Percent-encode or decode URLs and query parameters. Choose component (encodeURIComponent) or full-URI (encodeURI) mode. 100% client-side.

This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
/**
 * URL encode / decode — component-level (encodeURIComponent) and full URI (encodeURI).
 *
 * Language: PHP
 * CosmoDev polyglot showcase port of the `url-encode` tool.
 * Ported from src/tools/UrlEncodeTool.tsx — display source, part of CosmoDev's
 * polyglot tool pages.
 *
 * Explicit percent-encoding matching JavaScript's encodeURIComponent/encodeURI.
 * PHP strings are byte arrays, so we walk bytes: each non-safe byte becomes
 * %XX (uppercase hex). Component scope leaves A-Za-z0-9-_.!~*'() unescaped;
 * full URI scope additionally leaves the reserved set ;,/?:@&=+$# unescaped.
 * Decode reverses this, returning null on malformed % sequences; full URI
 * leaves encoded reserved bytes intact. PHP's rawurlencode differs from
 * encodeURIComponent on !*'(), so this is hand-rolled for exact parity.
 */

/**
 * True when a byte is never escaped by encodeURIComponent.
 */
function url_encode_is_component_safe(int $c): bool
{
    if (($c >= 0x41 && $c <= 0x5A) || ($c >= 0x61 && $c <= 0x7A) || ($c >= 0x30 && $c <= 0x39)) {
        return true;
    }
    return strpos("-_.!~*'()", chr($c)) !== false;
}

function url_encode_is_uri_extra(int $c): bool
{
    return strpos(";,/?:@&=+$#", chr($c)) !== false;
}

/**
 * Percent-encode $input. When $fullUri is true it matches encodeURI;
 * otherwise it matches encodeURIComponent.
 */
function url_encode(string $input, bool $fullUri = false): string
{
    $out = '';
    $len = strlen($input);
    for ($i = 0; $i < $len; $i++) {
        $c = ord($input[$i]);
        if (url_encode_is_component_safe($c) || ($fullUri && url_encode_is_uri_extra($c))) {
            $out .= chr($c);
        } else {
            $out .= '%' . strtoupper(bin2hex(chr($c)));
        }
    }
    return $out;
}

/**
 * Read the two hex digits after '%' at index $i. Returns the byte value or null.
 */
function url_encode_unhex_pair(string $s, int $i): ?int
{
    if ($i + 2 >= strlen($s) || !ctype_xdigit($s[$i + 1]) || !ctype_xdigit($s[$i + 2])) {
        return null;
    }
    return (hexdec($s[$i + 1]) << 4) | hexdec($s[$i + 2]);
}

/**
 * Percent-decode $input. Returns null on malformed sequences.
 * When $fullUri is true it matches decodeURI (encoded reserved bytes preserved).
 */
function url_decode(string $input, bool $fullUri = false): ?string
{
    $reserved = ";/?:@&=+$#";
    $out = '';
    $len = strlen($input);
    $i = 0;
    while ($i < $len) {
        if ($input[$i] !== '%') {
            $out .= $input[$i];
            $i++;
            continue;
        }
        $b = url_encode_unhex_pair($input, $i);
        if ($b === null) {
            return null;
        }
        if ($b < 0x80) {
            if ($fullUri && strpos($reserved, chr($b)) !== false) {
                $out .= substr($input, $i, 3);
            } else {
                $out .= chr($b);
            }
            $i += 3;
            continue;
        }
        // Multi-byte UTF-8 lead byte.
        if (($b & 0xE0) === 0xC0) {
            $seqLen = 2;
        } elseif (($b & 0xF0) === 0xE0) {
            $seqLen = 3;
        } elseif (($b & 0xF8) === 0xF0) {
            $seqLen = 4;
        } else {
            return null;
        }
        $buf = chr($b);
        for ($n = 1; $n < $seqLen; $n++) {
            $pi = $i + 3 * $n;
            if ($pi >= $len || $input[$pi] !== '%') {
                return null;
            }
            $cb = url_encode_unhex_pair($input, $pi);
            if ($cb === null || ($cb & 0xC0) !== 0x80) {
                return null;
            }
            $buf .= chr($cb);
        }
        $out .= $buf;
        $i += 3 * $seqLen;
    }
    return $out;
}

// Demo
echo url_encode('hello world & café', false), "\n";
var_dump(url_decode('hello%20world%20%26%20caf%C3%A9', false));

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →