Skip to content

Subnet / CIDR Calculator — PHP source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

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

<?php
/*
 * IPv4 subnet / CIDR math — pure, deterministic, never throws.
 * Invalid input returns null (or [] for the splitter) so the UI can show a
 * graceful error.
 *
 * Language: PHP 8
 * CosmoDev polyglot showcase port of `subnet-cidr`.
 * Ported from src/lib/subnet-cidr.ts — functionally equivalent.
 *
 * Display source — part of CosmoDev's polyglot tool pages.
 */

declare(strict_types=1);

/*
 * IPv4 addresses are manipulated as unsigned 32-bit integers. PHP integers are
 * 64-bit on modern builds, so every bitwise result that must live in the IPv4
 * space is masked back into 32 bits with `& 0xFFFFFFFF` — the equivalent of
 * JavaScript's `>>> 0`. Without that mask, `~$mask` would be a 64-bit value
 * whose low half is the wildcard but whose high half is all set bits.
 */

/**
 * Convert a dotted-quad IPv4 string to an unsigned 32-bit integer.
 * Returns null if the string is malformed or any octet exceeds 255.
 */
function ip_to_int(string $ip): ?int
{
    $parts = explode('.', $ip);
    if (count($parts) !== 4) {
        return null;
    }
    $result = 0;
    foreach ($parts as $part) {
        // Only plain decimal digits — no whitespace, signs, or hex.
        if (!preg_match('/^\d+$/', $part)) {
            return null;
        }
        $octet = (int) $part;
        if ($octet > 255) {
            return null;
        }
        $result = $result * 256 + $octet;
    }
    return $result & 0xFFFFFFFF;
}

/**
 * Convert an unsigned 32-bit integer to a dotted-quad IPv4 string.
 */
function int_to_ip(int $n): string
{
    $v = $n & 0xFFFFFFFF;
    return sprintf('%d.%d.%d.%d', ($v >> 24) & 255, ($v >> 16) & 255, ($v >> 8) & 255, $v & 255);
}

/**
 * Parse "ip/prefix" into ['ip' => ..., 'prefix' => ...] with prefix in 0–32.
 * Returns null if invalid.
 */
function parse_cidr(string $cidr): ?array
{
    // strpos uses a strict === false check so a leading slash (position 0) is
    // handled correctly rather than mistaken for "not found".
    $slash = strpos($cidr, '/');
    if ($slash === false) {
        return null;
    }
    // Reject multiple slashes ("a.b.c.d/24/16").
    if (strpos($cidr, '/', $slash + 1) !== false) {
        return null;
    }
    $ipPart = substr($cidr, 0, $slash);
    $prefixPart = substr($cidr, $slash + 1);
    if (!preg_match('/^\d+$/', $prefixPart)) {
        return null;
    }
    $prefix = (int) $prefixPart;
    if ($prefix > 32) {
        return null;
    }
    $ip = ip_to_int($ipPart);
    if ($ip === null) {
        return null;
    }
    // Re-emit through int_to_ip so the address is in canonical form.
    return ['ip' => int_to_ip($ip), 'prefix' => $prefix];
}

/**
 * Return the 32-bit netmask for a prefix length (0–32).
 */
function mask_from_prefix(int $prefix): int
{
    // Guard the prefix === 0 case for parity with the TS source: a 64-bit shift
    // by 32 is well-defined in PHP but the explicit branch keeps the intent plain.
    if ($prefix === 0) {
        return 0;
    }
    return (0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF;
}

/**
 * Full details for a CIDR block. Returns null if the CIDR is invalid.
 *
 * @return array{
 *     network:string, broadcast:string, first_host:string, last_host:string,
 *     netmask:string, wildcard:string, host_count:int, prefix:int,
 *     ip_class:string, is_private:bool
 * }|null
 */
function subnet_details(string $cidr): ?array
{
    $parsed = parse_cidr($cidr);
    if ($parsed === null) {
        return null;
    }
    $prefix = $parsed['prefix'];

    $mask = mask_from_prefix($prefix);
    $wildcard = (~$mask) & 0xFFFFFFFF;
    $network = (ip_to_int($parsed['ip']) & $mask) & 0xFFFFFFFF;
    $broadcast = ($network | $wildcard) & 0xFFFFFFFF;

    // /31 (point-to-point) and /32 (single host) have no host/broadcast split.
    if ($prefix >= 31) {
        $firstHost = $network;
        $lastHost = $broadcast;
    } else {
        $firstHost = ($network + 1) & 0xFFFFFFFF;
        $lastHost = ($broadcast - 1) & 0xFFFFFFFF;
    }

    // Host count: 1 for /32, 2 for /31, otherwise 2^(32-prefix) − 2. PHP ints
    // are 64-bit so the /0 case (2^32 − 2 = 4294967294) fits comfortably.
    if ($prefix >= 32) {
        $hostCount = 1;
    } elseif ($prefix === 31) {
        $hostCount = 2;
    } else {
        $hostCount = (1 << (32 - $prefix)) - 2;
    }

    $firstOctet = ($network >> 24) & 255;
    if ($firstOctet < 128) {
        $ipClass = 'A';
    } elseif ($firstOctet < 192) {
        $ipClass = 'B';
    } elseif ($firstOctet < 224) {
        $ipClass = 'C';
    } else {
        $ipClass = '-'; // class D (multicast) / E (reserved)
    }

    $secondOctet = ($network >> 16) & 255;
    $isPrivate = $firstOctet === 10
        || ($firstOctet === 172 && $secondOctet >= 16 && $secondOctet <= 31)
        || ($firstOctet === 192 && $secondOctet === 168);

    return [
        'network' => int_to_ip($network),
        'broadcast' => int_to_ip($broadcast),
        'first_host' => int_to_ip($firstHost),
        'last_host' => int_to_ip($lastHost),
        'netmask' => int_to_ip($mask),
        'wildcard' => int_to_ip($wildcard),
        'host_count' => $hostCount,
        'prefix' => $prefix,
        'ip_class' => $ipClass,
        'is_private' => $isPrivate,
    ];
}

/**
 * Split a CIDR block into smaller subnets of $newPrefix.
 * Returns [] if the CIDR is invalid or newPrefix is outside (prefix, 32].
 * The base IP is normalized to the network address before splitting.
 *
 * @return list<array{network:string, prefix:int}>
 */
function split_subnet(string $cidr, int $newPrefix): array
{
    $parsed = parse_cidr($cidr);
    if ($parsed === null) {
        return [];
    }
    // $newPrefix is typed int, so it is always integral; only the range against
    // the parent prefix needs validating (mirrors the TS bounds check).
    if ($newPrefix < $parsed['prefix'] || $newPrefix > 32) {
        return [];
    }

    $mask = mask_from_prefix($parsed['prefix']);
    $network = (ip_to_int($parsed['ip']) & $mask) & 0xFFFFFFFF;
    $count = 1 << ($newPrefix - $parsed['prefix']); // 2^(newPrefix − prefix)
    $blockSize = 1 << (32 - $newPrefix);            // 2^(32 − newPrefix)

    $out = [];
    for ($i = 0; $i < $count; $i++) {
        // The final & 0xFFFFFFFF makes addition wrap like a uint32.
        $offset = ($i * $blockSize) & 0xFFFFFFFF;
        $out[] = [
            'network' => int_to_ip(($network + $offset) & 0xFFFFFFFF),
            'prefix' => $newPrefix,
        ];
    }
    return $out;
}

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 →