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 →