markrogoyski/ipv4-subnet-calculator
Install with Composer. There is no service provider, no config file, and nothing to publish — it is a plain, dependency-free library:
composer require markrogoyski/ipv4-subnet-calculator
The first thing to open is src/IPv4SubnetCalculator.php. It is a single class with a small, obvious surface area, and the README's method table is effectively the whole API. Skim tests/ once for the edge cases (/31, /32, 0.0.0.0) before you trust it in production.
The namespace and class share a name, which trips people up on the first use:
use IPv4SubnetCalculator\IPv4SubnetCalculator;
$subnet = new IPv4SubnetCalculator('10.20.30.41', 24);
$subnet->getIPAddress(); // 10.20.30.41
$subnet->getSubnetMask(); // 255.255.255.0
$subnet->getWildcardMask(); // 0.0.0.255
$subnet->getNetworkAddress(); // 10.20.30.0
$subnet->getBroadcastAddress(); // 10.20.30.255
$subnet->getFirstHost(); // 10.20.30.1
$subnet->getLastHost(); // 10.20.30.254
$subnet->getNumberIPAddresses(); // 256
$subnet->getNumberHostAddresses(); // 254
First real use case in a Laravel app: an admin screen or inventory importer where someone types 10.20.30.41/24 and you need to normalise it to a network address, broadcast address, and usable host range before persisting. That is a ten-line controller method with this package.
Wrap the constructor in one place. CIDR strings arrive as "10.0.0.0/24" but the constructor wants an IP plus a bit count. Parse once, in a small helper, so nobody re-implements the explode:
namespace App\Support;
use IPv4SubnetCalculator\IPv4SubnetCalculator;
final class Subnet
{
public static function fromCidr(string $cidr): IPv4SubnetCalculator
{
[$ip, $bits] = array_pad(explode('/', trim($cidr), 2), 2, '32');
return new IPv4SubnetCalculator($ip, (int) $bits);
}
}
Guard at the boundary, not everywhere. In a FormRequest or a custom validation rule, validate the shape yourself and let the library throw only on genuinely broken input:
public function passes($attribute, $value): bool
{
[$ip, $bits] = array_pad(explode('/', $value, 2), 2, null);
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false
&& $bits !== null
&& ctype_digit($bits)
&& (int) $bits <= 32;
}
Persist ranges as integers, not strings. String IPs cannot be range-queried efficiently. Store network_long and broadcast_long columns (unsignedInteger, populated with ip2long()) alongside the CIDR, then:
Device::whereBetween('ip_long', [
ip2long($subnet->getNetworkAddress()),
ip2long($subnet->getBroadcastAddress()),
])->get();
Use getAllSubnetDetails() for API responses. It returns the whole computed set as an array, which maps straight onto a JSON resource and saves you listing ten getters:
return response()->json($subnet->getAllSubnetDetails());
Enumerate child subnets in an Artisan command. This is where the package earns its keep day-to-day — planning VLANs, generating seed data, or sanity-checking an allocation:
$parent = new IPv4SubnetCalculator('10.0.0.0', 22);
$start = ip2long($parent->getNetworkAddress());
$step = 2 ** (32 - 24);
for ($offset = 0; $offset < $parent->getNumberIPAddresses(); $offset += $step) {
$child = new IPv4SubnetCalculator(long2ip($start + $offset), 24);
$this->line($child->getNetworkAddress() . '/24');
}
Pair it with IpUtils for containment checks. The package computes ranges but does not answer "is this IP inside that subnet". Laravel already ships Symfony's helper for that:
use Symfony\Component\HttpFoundation\IpUtils;
IpUtils::checkIp('10.20.30.77', '10.20.30.0/24'); // true
Compute the ranges with this package, test membership with IpUtils, and you avoid writing your own bitwise comparisons.
Cache derived values. Subnet maths is cheap but not free in a seeder loop over thousands of rows. Cache::remember("subnet:{$cidr}", now()->addDay(), fn () => $subnet->getAllSubnetDetails()) is usually enough.
getNumberIPAddresses() ≠ getNumberHostAddresses(). The first is 2^(32-n) including network and broadcast; the second subtracts two. Mixing them up is the classic off-by-two in IPAM code./31 and /32 behaviour explicitly. The classic host formula yields 0 or negative for point-to-point links and single addresses. Verify what getNumberHostAddresses() and getFirstHost() return for these before you use them to generate firewall rules or DHCP scopes.contains() / overlaps() / sort(). Comparing two ranges means ip2long() on the network and broadcast addresses yourself. Overlap is $aStart <= $bEnd && $bStart <= $aEnd.sort(['10.0.0.10', '10.0.0.9']) puts .10 first. Use ip2long(), sort, then long2ip().isValid(). Invalid IPs/masks surface as exceptions when set. Wrap the construction site in try/catch at the boundary (FormRequest, console command) instead of scattering isValid() guards through your domain code.getClass() / getClassDetails() are historical. Class A/B/C has no bearing on modern CIDR allocation — use them for display only, never for logic.0.0.0.255 is correct for ACLs and wrong for a netmask field. Pick the getter that matches the field you're filling.composer.json so a minor release doesn't quietly change your array keys from getAllSubnetDetails().use IPv4SubnetCalculator\IPv4SubnetCalculator as SubnetCalculator; keeps call sites legible, and it is trivially easy to unit test — pure input, pure output, no I/O, no container, no mocking.How can I help you explore Laravel packages today?