strict equality with provided IP address. * * CIDR block -> provided IP address is part of that block. * * Wildcard pattern -> static parts match the corresponding ones in provided IP address. * * @param string[] $groups * @throws InvalidIpFormatException */ public static function ipAddressMatchesGroups(string $ipAddress, array $groups): bool { $ip = IPv4::parseString($ipAddress); if ($ip === null) { throw InvalidIpFormatException::fromInvalidIp($ipAddress); } $ipAddressParts = explode('.', $ipAddress); return some($groups, function (string $group) use ($ip, $ipAddressParts): bool { $range = self::candidateToRange($group, $ipAddressParts); return $range !== null && $range->contains($ip); }); } /** * Convert a static IP, CIDR block or wildcard pattern into a Range object * * @param string[] $ipAddressParts */ private static function candidateToRange(string $candidate, array $ipAddressParts): ?RangeInterface { return str_contains($candidate, '*') ? self::parseValueWithWildcards($candidate, $ipAddressParts) : Factory::parseRangeString($candidate); } /** * Try to generate an IP range from a wildcard pattern. * Factory::parseRangeString can usually do this automatically, but only if wildcards are at the end. This also * covers cases where wildcards are in between. */ private static function parseValueWithWildcards(string $value, array $ipAddressParts): ?RangeInterface { $octets = explode('.', $value); $keys = array_keys($octets); // Replace wildcard parts with the corresponding ones from the remote address return Factory::parseRangeString( implode('.', array_map( fn (string $part, int $index) => $part === '*' ? $ipAddressParts[$index] : $part, $octets, $keys, )), ); } }