Skip to content

Random Port Generator — PHP source

Generate one or many random TCP/UDP port numbers across registered, ephemeral, or the full range - optionally unique. Runs entirely in your browser with crypto-grade randomness.

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

<?php
/**
 * random-port-generator — random/dynamic port picker over well-known ranges.
 *
 * Language: PHP (8.1+, standard library only)
 * Source:   CosmoDev polyglot showcase port of the Random Port Generator tool,
 *           ported from src/lib/random-port.ts (the canonical TypeScript
 *           implementation) and kept in lock-step with cli/random-port-generator
 *           (the Go twin). Pure + deterministic via an injectable RNG; invalid
 *           custom ranges throw InvalidArgumentException (mirroring the TS throw).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Design goals:
 *   - Pure + deterministic; never throws on valid input.
 *   - Functionally equivalent to the TS reference and the Go twin: same named
 *     ranges, same validation, same Fisher–Yates partial-shuffle unique pick.
 *   - Self-contained: stdlib only (no Composer packages). The default RNG is
 *     `random_int` (a CSPRNG, matching the TS `crypto.getRandomValues` default).
 *
 * The injectable `$options['rng']` is the key that makes a *random* tool
 * deterministic and therefore testable: every showcase assertion in the
 * `realpath($argv[0]) === __FILE__` block uses a fixed closure and reproduces a
 * vector from src/lib/random-port.test.ts exactly.
 */

declare(strict_types=1);

/**
 * Range preset string constants. Mirrors the TS `PortRange` union and the Go
 * `Range` enum. The default is registered.
 */
const PORT_RANGE_ANY = 'any';
const PORT_RANGE_REGISTERED = 'registered';
const PORT_RANGE_EPHEMERAL = 'ephemeral';
const PORT_RANGE_CUSTOM = 'custom';

// Named ranges mirror RANGES in random-port.ts (and the Go bounds() switch).
const RANGES = [
    'any'        => [1, 65535],
    'registered' => [1024, 49151],
    'ephemeral'  => [49152, 65535],
];

/**
 * Default options for random_port() / random_ports(). Each key is optional;
 * defaults match the TS source. Note `rng` is intentionally absent so the
 * `?? 'default_rng'` fallback in the call sites kicks in.
 *
 * @return array{range: string, min: ?int, max: ?int, count: int, unique: bool}
 */
function port_default_options(): array
{
    return [
        'range'  => PORT_RANGE_REGISTERED,
        'min'    => null,
        'max'    => null,
        'count'  => 1,
        'unique' => false,
    ];
}

/**
 * Default RNG: a CSPRNG float in [0, 1) — mirrors the TS
 * `crypto.getRandomValues` default (read a 32-bit unsigned int, divide by 2**32).
 */
function default_rng(): float
{
    return random_int(0, 0xFFFFFFFF) / 4294967296.0;
}

/**
 * Resolve an options array to its `['lo' => int, 'hi' => int]` bounds. Mirrors
 * the TS `resolveRange`.
 *
 * @param array $options
 * @return array{lo: int, hi: int}
 */
function resolve_range(array $options): array
{
    $range = $options['range'] ?? PORT_RANGE_REGISTERED;
    if ($range === PORT_RANGE_CUSTOM) {
        return ['lo' => $options['min'] ?? 1, 'hi' => $options['max'] ?? 65535];
    }
    [$lo, $hi] = RANGES[$range];
    return ['lo' => $lo, 'hi' => $hi];
}

/**
 * Validate a resolved range. Mirrors the TS `assertRange` (the integer check is
 * `is_int`, since PHP types carry that directly).
 */
function assert_range($lo, $hi): void
{
    if (!is_int($lo) || !is_int($hi) || $lo < 0 || $lo > 65535 || $hi > 65535 || $lo > $hi) {
        throw new InvalidArgumentException("Invalid port range $lo-$hi");
    }
}

/**
 * Return a single random port within the resolved range. Mirrors the TS
 * `randomPort`.
 *
 * @param array $options
 */
function random_port(array $options = []): int
{
    $options = array_merge(port_default_options(), $options);
    $rng = $options['rng'] ?? 'default_rng';
    $b = resolve_range($options);
    assert_range($b['lo'], $b['hi']);
    return $b['lo'] + (int) floor($rng() * ($b['hi'] - $b['lo'] + 1));
}

/**
 * Return `count` ports. When `unique`, a Fisher–Yates partial shuffle over the
 * range yields distinct values (capped at range capacity). Mirrors the TS
 * `randomPorts`.
 *
 * @param array $options
 * @return list<int>
 */
function random_ports(array $options = []): array
{
    $options = array_merge(port_default_options(), $options);
    $count = max(1, $options['count']);
    $b = resolve_range($options);
    assert_range($b['lo'], $b['hi']);
    $lo = $b['lo'];
    $hi = $b['hi'];

    if (empty($options['unique'])) {
        // Mirrors TS: Array.from({ length: count }, () => randomPort(opts)).
        return array_map(fn () => random_port($options), array_fill(0, $count, null));
    }

    // Fisher–Yates partial shuffle over the range to pick $n unique ports.
    $capacity = $hi - $lo + 1;
    $n = min($count, $capacity);
    $rng = $options['rng'] ?? 'default_rng';
    $pool = range($lo, $hi);
    for ($i = 0; $i < $n; $i++) {
        $j = $i + (int) floor($rng() * ($capacity - $i));
        [$pool[$i], $pool[$j]] = [$pool[$j], $pool[$i]];
    }
    return array_slice($pool, 0, $n);
}

// ---------- showcase vectors (mirror src/lib/random-port.test.ts) ---------------
// Run directly: `php random-port-generator.php`.
if (isset($argv) && realpath($argv[0]) === __FILE__) {
    assert(random_port(['rng' => fn () => 0.0]) === 1024);
    assert(random_port(['range' => 'any', 'rng' => fn () => 0.0]) === 1);
    assert(random_port(['range' => 'ephemeral', 'rng' => fn () => 0.0]) === 49152);
    assert(resolve_range(['range' => 'registered']) === ['lo' => 1024, 'hi' => 49151]);
    assert(random_port(['range' => 'custom', 'min' => 8000, 'max' => 8000, 'rng' => fn () => 0.9]) === 8000);
    assert(resolve_range(['range' => 'custom']) === ['lo' => 1, 'hi' => 65535]);
    assert(random_ports(['count' => 5, 'rng' => fn () => 0.0]) === [1024, 1024, 1024, 1024, 1024]);
    $uniq = random_ports(['count' => 5, 'unique' => true, 'range' => 'custom', 'min' => 1, 'max' => 3, 'rng' => fn () => 0.5]);
    sort($uniq);
    assert($uniq === [1, 2, 3] && count($uniq) === 3);
    try {
        random_port(['range' => 'custom', 'min' => 100, 'max' => 50]);
        assert(false, 'expected InvalidArgumentException');
    } catch (InvalidArgumentException $e) {
        // expected
    }
    echo "ok\n";
}

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 →