longman/ip-tools
Longman IP Tools is a PHP/Laravel package for IPv4 and IPv6 address handling. It validates IPs, parses CIDR blocks, checks ranges and subnet membership, and helps build reliable IP-based validation and filtering logic.
Install via Composer — the package is longman/ip-tools and exposes the IpTools\ namespace:
composer require longman/ip-tools
There are only two classes you need to learn on day one; everything else is a detail:
IpTools\IP (with IPv4/IPv6 subclasses) — single address parsing, version detection, numeric conversion, and classification (private/public/reserved).IpTools\Range — CIDR, dash-range, and single-IP containment checks plus iteration.A good first read is src/IP.php, src/Range.php, then src/IPv4.php / src/IPv6.php to see how big-integer math is handled. Start with the use case that motivates most installs: deciding whether an incoming request IP is inside a known network.
use IpTools\IP;
use IpTools\Range;
$ip = new IP('192.168.1.25');
$ip->getVersion(); // 4
$ip->getLong(); // numeric form, handy for storage/sorting
$ip->isPrivate(); // true
$range = Range::parse('192.168.1.0/24');
$range->contains($ip); // true
$range->getFirstIP(); // IP object for 192.168.1.0
$range->getLastIP(); // IP object for 192.168.1.255
Range::parse() accepts a CIDR (10.0.0.0/8), a dash range (10.0.0.1-10.0.0.50), or a bare address (10.0.0.1), which makes it a single choke point for any "is this IP in this network" check.
Trusted-proxy / allow-list middleware. The most common day-to-day pattern is a small middleware that rejects or tags requests from IPs outside configured networks:
use IpTools\Range;
public function handle($request, Closure $next)
{
$trusted = config('services.trusted_ranges', []); // ['10.0.0.0/8', '192.168.0.0/16']
$client = $request->ip();
foreach ($trusted as $cidr) {
if (Range::parse($cidr)->contains($client)) {
return $next($request);
}
}
abort(403);
}
Because contains() accepts the raw string, you avoid constructing IP objects on the hot path yourself.
Config-driven network registry. Keep CIDRs in config/*.php rather than hard-coding them. That lets ops change the allow-list without a deploy, and lets you unit-test the same list through Range::parse() in isolation. If you need a reusable service, wrap it — e.g. App\Networks\NetworkRegistry that parses each range once and memoizes the Range objects.
Storing and sorting addresses. Persist the getLong() value (as a string column if you care about IPv6) alongside the textual IP. Numeric form gives you correct sort order and lets you do BETWEEN lookups for "which subnet does this IP fall into" queries without recomputing ranges in PHP. Keep the original string too; you will want it for logs and abuse reports.
Blacklist / per-range iteration. Range is iterable, which is tempting for building scan lists, but treat it as a bounded tool: iterating a /24 (256 addresses) is fine, iterating a /8 (16 million) is not. If you need to walk large space, iterate subnets and process them in chunks rather than expanding to individual IPs.
Classification for logging and abuse handling. isPrivate(), isPublic(), and version checks are cheap and useful in an access-log pipeline — flag loopback/RFC1918 traffic differently from public traffic, and reject version mismatches (an IPv4-only endpoint receiving an IPv6 literal). Combine with filter_var($ip, FILTER_VALIDATE_IP) if you just need boolean validity and don't want to catch constructor exceptions.
Testable integration. Bind a thin adapter around Range in the container and swap it in tests; the underlying classes are static-friendly (Range::parse) and easy to construct with fixed values, so pure unit tests against contains() cover most of the risk.
IPv6 path actually exercises your big-integer extension.int; check that ext-gmp (or whichever backend the package expects) is installed in every environment, including CI and the production image. A missing extension often surfaces as wrong comparisons rather than a clean error.getLong() for IPv4. On 32-bit PHP builds the numeric value can go signed/negative. Prefer 64-bit runtimes, and always cast to string before storing.Range::parse() raise exceptions (InvalidArgumentException and friends) on malformed input. Validate or try/catch at the boundary — never let a client-supplied X-Forwarded-For reach new IP() unguarded.192.168.1.5/24 describes a subnet whose first/last addresses are the network/broadcast host values. Confirm the package's normalization behavior before relying on getFirstIP() matching what you stored in config; test the off-by-one at the edges explicitly.IpTools will not clash with Symfony's IpUtils unless you alias — but you will be writing your own Rule/Rule::in extension or middleware wrapper.contains() is your friend, iteration is not. Re-check large ranges by nesting smaller CIDRs instead of expanding; this keeps memory bounded and makes the code read like the network design.IP/Range only if you need domain-specific behavior (custom __toString, extra classification helpers). For most apps the better seam is a wrapper service around Range::parse() so you can swap the implementation later without touching call sites.composer.json, keep a thin wrapper around the two public APIs, and add regression tests for the exact CIDRs your app trusts so a future replacement is a one-file change.How can I help you explore Laravel packages today?