• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

markrogoyski / ipv4-subnet-calculator-php / 22473410450

27 Feb 2026 04:56AM UTC coverage: 82.311% (-15.6%) from 97.956%
22473410450

push

github

markrogoyski
Changes for version 5.0.0.

577 of 701 new or added lines in 13 files covered. (82.31%)

577 of 701 relevant lines covered (82.31%)

71.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.7
/src/SubnetParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace IPv4;
6

7
use IPv4\Internal\IPv4;
8

9
/**
10
 * Parser for creating Subnet instances from various input formats.
11
 *
12
 * Provides methods for complex subnet creation scenarios:
13
 *  - From IP address and subnet mask string
14
 *  - From IP address range
15
 *  - From host count requirements
16
 *
17
 * For simple creation, use Subnet::fromCidr() or the Subnet constructor directly.
18
 */
19
final class SubnetParser
20
{
21
    /**
22
     * Create a Subnet from an IP address and subnet mask.
23
     *
24
     * @param string $ipAddress  IP address in dotted quad notation
25
     * @param string $subnetMask Subnet mask in dotted quad notation (e.g., "255.255.255.0")
26
     *
27
     * @return Subnet
28
     *
29
     * @throws \InvalidArgumentException If subnet mask is invalid or non-contiguous
30
     */
31
    public static function fromMask(string $ipAddress, string $subnetMask): Subnet
32
    {
33
        $networkSize = self::maskToNetworkSize($subnetMask);
26✔
34

35
        return new Subnet($ipAddress, $networkSize);
16✔
36
    }
37

38
    /**
39
     * Create a Subnet from an IP address range.
40
     *
41
     * The range must represent a valid CIDR block. The start IP must be the
42
     * network address and the end IP must be the broadcast address.
43
     *
44
     * @param string $startIp Start IP address (network address)
45
     * @param string $endIp   End IP address (broadcast address)
46
     *
47
     * @return Subnet
48
     *
49
     * @throws \InvalidArgumentException If range does not represent a valid CIDR block
50
     */
51
    public static function fromRange(string $startIp, string $endIp): Subnet
52
    {
53
        $startLong = self::validateAndConvertIp($startIp);
52✔
54
        $endLong = self::validateAndConvertIp($endIp);
51✔
55

56
        if ($startLong > $endLong) {
50✔
57
            throw new \InvalidArgumentException(
1✔
58
                "Start IP '{$startIp}' is greater than end IP '{$endIp}'"
1✔
59
            );
1✔
60
        }
61

62
        $rangeSize = $endLong - $startLong + 1;
49✔
63

64
        // Check if range size is a power of 2
65
        if (($rangeSize & ($rangeSize - 1)) !== 0) {
49✔
66
            throw new \InvalidArgumentException(
4✔
67
                "Range does not represent a valid CIDR block (size {$rangeSize} is not a power of 2)"
4✔
68
            );
4✔
69
        }
70

71
        $networkSize = 32 - (int) \log($rangeSize, 2);
45✔
72

73
        // Validate alignment
74
        $mask = self::calculateSubnetMaskInt($networkSize);
45✔
75
        if (($startLong & $mask) !== $startLong) {
45✔
76
            throw new \InvalidArgumentException(
1✔
77
                "Start IP '{$startIp}' is not a valid network address for a /{$networkSize} subnet"
1✔
78
            );
1✔
79
        }
80

81
        return new Subnet($startIp, $networkSize);
44✔
82
    }
83

84
    /**
85
     * Create a Subnet from an IP address and required host count.
86
     *
87
     * Returns the smallest subnet that can accommodate the specified number of hosts.
88
     *
89
     * @param string $ipAddress Base IP address
90
     * @param int    $hostCount Number of hosts required
91
     *
92
     * @return Subnet
93
     *
94
     * @throws \InvalidArgumentException If host count is invalid
95
     */
96
    public static function fromHostCount(string $ipAddress, int $hostCount): Subnet
97
    {
98
        if ($hostCount <= 0) {
16✔
99
            throw new \InvalidArgumentException(
2✔
100
                "Host count must be positive, got {$hostCount}"
2✔
101
            );
2✔
102
        }
103

104
        if ($hostCount > IPv4::MAX_HOSTS) {
14✔
105
            throw new \InvalidArgumentException(
1✔
106
                "Host count {$hostCount} exceeds maximum possible hosts in IPv4"
1✔
107
            );
1✔
108
        }
109

110
        $networkSize = self::calculateOptimalNetworkSize($hostCount);
13✔
111

112
        return new Subnet($ipAddress, $networkSize);
13✔
113
    }
114

115
    /**
116
     * Calculate the optimal CIDR prefix for a given host count.
117
     *
118
     * @param int $hostCount Number of hosts required
119
     *
120
     * @return int Optimal CIDR prefix
121
     *
122
     * @throws \InvalidArgumentException If host count is invalid
123
     */
124
    public static function optimalPrefixForHosts(int $hostCount): int
125
    {
126
        if ($hostCount <= 0) {
30✔
127
            throw new \InvalidArgumentException(
2✔
128
                "Host count must be positive, got {$hostCount}"
2✔
129
            );
2✔
130
        }
131

132
        if ($hostCount > IPv4::MAX_HOSTS) {
28✔
133
            throw new \InvalidArgumentException(
2✔
134
                "Host count {$hostCount} exceeds maximum possible hosts in IPv4"
2✔
135
            );
2✔
136
        }
137

138
        return self::calculateOptimalNetworkSize($hostCount);
26✔
139
    }
140

141
    /**
142
     * Convert a subnet mask to a network size.
143
     *
144
     * @param string $subnetMask
145
     *
146
     * @return int
147
     */
148
    private static function maskToNetworkSize(string $subnetMask): int
149
    {
150
        $quads = \explode('.', $subnetMask);
26✔
151
        if (\count($quads) !== 4) {
26✔
152
            throw new \InvalidArgumentException(
2✔
153
                "Invalid subnet mask format: '{$subnetMask}'"
2✔
154
            );
2✔
155
        }
156

157
        $maskInt = 0;
24✔
158
        foreach ($quads as $quad) {
24✔
159
            if (!\ctype_digit($quad)) {
24✔
160
                throw new \InvalidArgumentException(
4✔
161
                    "Invalid subnet mask: non-numeric octet in '{$subnetMask}'"
4✔
162
                );
4✔
163
            }
164
            $octet = (int) $quad;
21✔
165
            if ($octet < 0 || $octet > 255) {
21✔
166
                throw new \InvalidArgumentException(
1✔
167
                    "Invalid subnet mask: octet out of range in '{$subnetMask}'"
1✔
168
                );
1✔
169
            }
170
            $maskInt = ($maskInt << 8) | $octet;
20✔
171
        }
172

173
        if ($maskInt === 0) {
19✔
174
            return 0;
2✔
175
        }
176

177
        // Validate contiguous mask
178
        $inverted = ~$maskInt & 0xFF_FF_FF_FF;
17✔
179
        if (($inverted & ($inverted + 1)) !== 0) {
17✔
180
            throw new \InvalidArgumentException(
3✔
181
                "Invalid subnet mask: non-contiguous mask '{$subnetMask}'"
3✔
182
            );
3✔
183
        }
184

185
        // Count 1 bits
186
        $networkSize = 0;
14✔
187
        $tempMask = $maskInt;
14✔
188
        while ($tempMask & 0x80000000) {
14✔
189
            $networkSize++;
14✔
190
            $tempMask <<= 1;
14✔
191
            $tempMask &= 0xFF_FF_FF_FF;
14✔
192
        }
193

194
        return $networkSize;
14✔
195
    }
196

197
    /**
198
     * Validate and convert an IP address to integer.
199
     *
200
     * @param string $ipAddress
201
     *
202
     * @return int
203
     */
204
    private static function validateAndConvertIp(string $ipAddress): int
205
    {
206
        if (!\filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
52✔
207
            throw new \InvalidArgumentException(
2✔
208
                "Invalid IP address: '{$ipAddress}'"
2✔
209
            );
2✔
210
        }
211

212
        $long = \ip2long($ipAddress);
51✔
213
        if ($long === false) {
51✔
NEW
214
            throw new \InvalidArgumentException(
×
NEW
215
                "Invalid IP address: '{$ipAddress}'"
×
NEW
216
            );
×
217
        }
218

219
        return $long;
51✔
220
    }
221

222
    /**
223
     * Calculate subnet mask as integer.
224
     *
225
     * @param int $networkSize
226
     *
227
     * @return int
228
     */
229
    private static function calculateSubnetMaskInt(int $networkSize): int
230
    {
231
        if ($networkSize === 0) {
45✔
232
            return 0;
2✔
233
        }
234
        return (0xFF_FF_FF_FF << (32 - $networkSize)) & 0xFF_FF_FF_FF;
43✔
235
    }
236

237
    /**
238
     * Calculate optimal network size for host count.
239
     *
240
     * @param int $hostCount
241
     *
242
     * @return int
243
     */
244
    private static function calculateOptimalNetworkSize(int $hostCount): int
245
    {
246
        if ($hostCount === 1) {
39✔
247
            return 32;
2✔
248
        }
249

250
        if ($hostCount === 2) {
37✔
251
            return 31;
2✔
252
        }
253

254
        $totalAddressesNeeded = $hostCount + 2;
35✔
255
        $bitsNeeded = (int) \ceil(\log($totalAddressesNeeded, 2));
35✔
256
        $networkSize = 32 - $bitsNeeded;
35✔
257

258
        if ($networkSize < 0) {
35✔
NEW
259
            $networkSize = 0;
×
260
        }
261

262
        return $networkSize;
35✔
263
    }
264
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc