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

markrogoyski / ipv4-subnet-calculator-php / 21015766054

15 Jan 2026 01:01AM UTC coverage: 97.956% (+0.1%) from 97.831%
21015766054

push

github

markrogoyski
Update CHANGELOG for v4.4.0 release.

623 of 636 relevant lines covered (97.96%)

83.72 hits per line

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

99.5
/src/SubnetCalculator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace IPv4;
6

7
/**
8
 * IPv4 subnet calculator for CIDR operations, network planning, and IP address management.
9
 *
10
 * Given an IP address and CIDR prefix (e.g., 192.168.1.100/24), calculates network properties
11
 * including subnet masks, network/host portions, IP ranges, broadcast addresses, and all IPs
12
 * in the subnet. Provides data in multiple formats: dotted quads, hex, binary, integer, and array.
13
 *
14
 * Key capabilities:
15
 *  - Network calculations (subnet mask, wildcard, broadcast, usable ranges)
16
 *  - IP address type detection (private, public, loopback, multicast, etc.)
17
 *  - Network analysis (overlap detection, containment checking)
18
 *  - Subnet operations (splitting, navigation, exclusion)
19
 *  - Reports in multiple formats (array, JSON, string, STDOUT)
20
 *
21
 * Special handling per RFC 3021:
22
 *  - /32: Single host network (min/max host same as IP address)
23
 *  - /31: Point-to-point link (both addresses usable, no broadcast/network addresses)
24
 *
25
 * @link https://datatracker.ietf.org/doc/html/rfc3021 RFC 3021 - Using 31-Bit Prefixes on IPv4 Point-to-Point Links
26
 */
27
class SubnetCalculator implements \JsonSerializable
28
{
29
    /** @var string IP address as dotted quads: xxx.xxx.xxx.xxx */
30
    private $ipAddress;
31

32
    /** @var int CIDR network size */
33
    private $networkSize;
34

35
    /** @var string[] of four elements containing the four quads of the IP address */
36
    private $quads = [];
37

38
    /** @var int Subnet mask in format used for subnet calculations */
39
    private $subnetMask;
40

41
    /** @var SubnetReportInterface */
42
    private $report;
43

44
    private const FORMAT_QUADS  = '%d';
45
    private const FORMAT_HEX    = '%02X';
46
    private const FORMAT_BINARY = '%08b';
47

48
    /**
49
     * Constructor - Takes IP address and network size, validates inputs, and assigns class attributes.
50
     * For example: 192.168.1.120/24 would be $ip = 192.168.1.120 $network_size = 24
51
     *
52
     * @param string                     $ipAddress   IP address in dotted quad notation.
53
     * @param int                        $networkSize CIDR network size.
54
     * @param SubnetReportInterface|null $report
55
     */
56
    public function __construct(string $ipAddress, int $networkSize, ?SubnetReportInterface $report = null)
57
    {
58
        $this->validateInputs($ipAddress, $networkSize);
1,488✔
59

60
        $this->ipAddress   = $ipAddress;
1,477✔
61
        $this->networkSize = $networkSize;
1,477✔
62
        $this->quads       = \explode('.', $ipAddress);
1,477✔
63
        $this->subnetMask  = $this->calculateSubnetMask($networkSize);
1,477✔
64
        $this->report      = $report ?? new SubnetReport();
1,477✔
65
    }
1,477✔
66

67
    /* **************** *
68
     * PUBLIC INTERFACE
69
     * **************** */
70

71
    /**
72
     * Get IP address as dotted quads: xxx.xxx.xxx.xxx
73
     *
74
     * @return string IP address as dotted quads.
75
     */
76
    public function getIPAddress(): string
77
    {
78
        return $this->ipAddress;
97✔
79
    }
80

81
    /**
82
     * Get IP address as array of quads: [xxx, xxx, xxx, xxx]
83
     *
84
     * @return string[]
85
     */
86
    public function getIPAddressQuads(): array
87
    {
88
        return $this->quads;
3✔
89
    }
90

91
    /**
92
     * Get IP address as hexadecimal
93
     *
94
     * @return string IP address in hex
95
     */
96
    public function getIPAddressHex(): string
97
    {
98
        return $this->ipAddressCalculation(self::FORMAT_HEX);
68✔
99
    }
100

101
    /**
102
     * Get IP address as binary
103
     *
104
     * @return string IP address in binary
105
     */
106
    public function getIPAddressBinary(): string
107
    {
108
        return $this->ipAddressCalculation(self::FORMAT_BINARY);
68✔
109
    }
110

111
    /**
112
     * Get the IP address as an integer
113
     *
114
     * @return int
115
     */
116
    public function getIPAddressInteger(): int
117
    {
118
        return $this->convertIpToInt($this->ipAddress);
213✔
119
    }
120

121
    /**
122
     * Get network size
123
     *
124
     * @return int network size
125
     */
126
    public function getNetworkSize(): int
127
    {
128
        return $this->networkSize;
204✔
129
    }
130

131
    /**
132
     * Get the CIDR notation of the subnet: IP Address/Network size.
133
     * Example: 192.168.0.15/24
134
     *
135
     * @return string
136
     */
137
    public function getCidrNotation(): string
138
    {
139
        return $this->ipAddress . '/' . $this->networkSize;
120✔
140
    }
141

142
    /**
143
     * Get the number of IP addresses in the network
144
     *
145
     * @return int Number of IP addresses
146
     */
147
    public function getNumberIPAddresses(): int
148
    {
149
        return $this->getNumberIPAddressesOfNetworkSize($this->networkSize);
748✔
150
    }
151

152
    /**
153
     * Get the number of addressable hosts in the network
154
     *
155
     * For most networks, this is the total IP count minus 2 (network and broadcast addresses).
156
     * Special cases per RFC 3021:
157
     *  - /32: Returns 1 (single host network)
158
     *  - /31: Returns 2 (point-to-point link where both addresses are usable)
159
     *
160
     * @return int Number of IP addresses that are addressable
161
     */
162
    public function getNumberAddressableHosts(): int
163
    {
164
        if ($this->networkSize == 32) {
171✔
165
            return 1;
25✔
166
        }
167
        if ($this->networkSize == 31) {
164✔
168
            return 2;
23✔
169
        }
170

171
        return ($this->getNumberIPAddresses() - 2);
141✔
172
    }
173

174
    /**
175
     * Get range of IP addresses in the network
176
     *
177
     * @return string[] containing start and end of IP address range. IP addresses in dotted quad notation.
178
     */
179
    public function getIPAddressRange(): array
180
    {
181
        return [$this->getNetworkPortion(), $this->getBroadcastAddress()];
374✔
182
    }
183

184
    /**
185
     * Get range of IP addresses in the network
186
     *
187
     * @return string[] containing start and end of IP address range. IP addresses in dotted quad notation.
188
     */
189
    public function getAddressableHostRange(): array
190
    {
191
        return [$this->getMinHost(), $this->getMaxHost()];
38✔
192
    }
193

194
    /**
195
     * Get the broadcast IP address
196
     *
197
     * @return string IP address as dotted quads
198
     */
199
    public function getBroadcastAddress(): string
200
    {
201
        $network_quads       = $this->getNetworkPortionQuads();
459✔
202
        $number_ip_addresses = $this->getNumberIPAddresses();
459✔
203

204
        $network_range_quads = [
205
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[0] & ($this->subnetMask >> 24)) + ((($number_ip_addresses - 1) >> 24) & 0xFF)),
459✔
206
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[1] & ($this->subnetMask >> 16)) + ((($number_ip_addresses - 1) >> 16) & 0xFF)),
459✔
207
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[2] & ($this->subnetMask >>  8)) + ((($number_ip_addresses - 1) >>  8) & 0xFF)),
459✔
208
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[3] & ($this->subnetMask >>  0)) + ((($number_ip_addresses - 1) >>  0) & 0xFF)),
459✔
209
        ];
210

211
        return \implode('.', $network_range_quads);
459✔
212
    }
213

214
    /**
215
     * Get minimum host IP address as dotted quads: xxx.xxx.xxx.xxx
216
     *
217
     * For most networks, this is the network address + 1.
218
     * Special cases:
219
     *  - /32: Returns the IP address itself (single host)
220
     *  - /31: Returns the network portion (lower IP of the point-to-point pair per RFC 3021)
221
     *
222
     * @return string min host as dotted quads
223
     */
224
    public function getMinHost(): string
225
    {
226
        if ($this->networkSize === 32) {
141✔
227
            return $this->ipAddress;
20✔
228
        }
229
        if ($this->networkSize === 31) {
139✔
230
            return $this->getNetworkPortion();
30✔
231
        }
232
        return $this->minHostCalculation(self::FORMAT_QUADS, '.');
109✔
233
    }
234

235
    /**
236
     * Get minimum host IP address as array of quads: [xxx, xxx, xxx, xxx]
237
     *
238
     * @return string[] min host portion as dotted quads.
239
     */
240
    public function getMinHostQuads(): array
241
    {
242
        if ($this->networkSize === 32) {
17✔
243
            return $this->quads;
1✔
244
        }
245
        if ($this->networkSize === 31) {
16✔
246
            return $this->getNetworkPortionQuads();
7✔
247
        }
248
        return \explode('.', $this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
249
    }
250

251
    /**
252
     * Get minimum host IP address as hex
253
     *
254
     * @return string min host portion as hex
255
     */
256
    public function getMinHostHex(): string
257
    {
258
        if ($this->networkSize === 32) {
17✔
259
            return \implode('', \array_map(
1✔
260
                function ($quad) {
261
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
262
                },
1✔
263
                $this->quads
1✔
264
            ));
265
        }
266
        if ($this->networkSize === 31) {
16✔
267
            return $this->getNetworkPortionHex();
7✔
268
        }
269
        return $this->minHostCalculation(self::FORMAT_HEX);
9✔
270
    }
271

272
    /**
273
     * Get minimum host IP address as binary
274
     *
275
     * @return string min host portion as binary
276
     */
277
    public function getMinHostBinary(): string
278
    {
279
        if ($this->networkSize === 32) {
17✔
280
            return \implode('', \array_map(
1✔
281
                function ($quad) {
282
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
283
                },
1✔
284
                $this->quads
1✔
285
            ));
286
        }
287
        if ($this->networkSize === 31) {
16✔
288
            return $this->getNetworkPortionBinary();
7✔
289
        }
290
        return $this->minHostCalculation(self::FORMAT_BINARY);
9✔
291
    }
292

293
    /**
294
     * Get minimum host IP address as an Integer
295
     *
296
     * @return int min host portion as integer
297
     */
298
    public function getMinHostInteger(): int
299
    {
300
        if ($this->networkSize === 32) {
17✔
301
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
302
        }
303
        if ($this->networkSize === 31) {
16✔
304
            return $this->getNetworkPortionInteger();
7✔
305
        }
306
        return $this->convertIpToInt($this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
307
    }
308

309
    /**
310
     * Get maximum host IP address as dotted quads: xxx.xxx.xxx.xxx
311
     *
312
     * For most networks, this is the broadcast address - 1.
313
     * Special cases:
314
     *  - /32: Returns the IP address itself (single host)
315
     *  - /31: Returns the broadcast address (higher IP of the point-to-point pair per RFC 3021)
316
     *
317
     * @return string max host as dotted quads.
318
     */
319
    public function getMaxHost(): string
320
    {
321
        if ($this->networkSize === 32) {
141✔
322
            return $this->ipAddress;
20✔
323
        }
324
        if ($this->networkSize === 31) {
139✔
325
            return $this->getBroadcastAddress();
30✔
326
        }
327
        return $this->maxHostCalculation(self::FORMAT_QUADS, '.');
109✔
328
    }
329

330
    /**
331
     * Get maximum host IP address as array of quads: [xxx, xxx, xxx, xxx]
332
     *
333
     * @return string[] max host portion as dotted quads
334
     */
335
    public function getMaxHostQuads(): array
336
    {
337
        if ($this->networkSize === 32) {
17✔
338
            return $this->quads;
1✔
339
        }
340
        if ($this->networkSize === 31) {
16✔
341
            return \explode('.', $this->getBroadcastAddress());
7✔
342
        }
343
        return \explode('.', $this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
344
    }
345

346
    /**
347
     * Get maximum host IP address as hex
348
     *
349
     * @return string max host portion as hex
350
     */
351
    public function getMaxHostHex(): string
352
    {
353
        if ($this->networkSize === 32) {
17✔
354
            return \implode('', \array_map(
1✔
355
                function ($quad) {
356
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
357
                },
1✔
358
                $this->quads
1✔
359
            ));
360
        }
361
        if ($this->networkSize === 31) {
16✔
362
            return \implode('', \array_map(
7✔
363
                function ($quad) {
364
                    return \sprintf(self::FORMAT_HEX, $quad);
7✔
365
                },
7✔
366
                \explode('.', $this->getBroadcastAddress())
7✔
367
            ));
368
        }
369
        return $this->maxHostCalculation(self::FORMAT_HEX);
9✔
370
    }
371

372
    /**
373
     * Get maximum host IP address as binary
374
     *
375
     * @return string max host portion as binary
376
     */
377
    public function getMaxHostBinary(): string
378
    {
379
        if ($this->networkSize === 32) {
17✔
380
            return \implode('', \array_map(
1✔
381
                function ($quad) {
382
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
383
                },
1✔
384
                $this->quads
1✔
385
            ));
386
        }
387
        if ($this->networkSize === 31) {
16✔
388
            return \implode('', \array_map(
7✔
389
                function ($quad) {
390
                    return \sprintf(self::FORMAT_BINARY, $quad);
7✔
391
                },
7✔
392
                \explode('.', $this->getBroadcastAddress())
7✔
393
            ));
394
        }
395
        return $this->maxHostCalculation(self::FORMAT_BINARY);
9✔
396
    }
397

398
    /**
399
     * Get maximum host IP address as an Integer
400
     *
401
     * @return int max host portion as integer
402
     */
403
    public function getMaxHostInteger(): int
404
    {
405
        if ($this->networkSize === 32) {
17✔
406
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
407
        }
408
        if ($this->networkSize === 31) {
16✔
409
            return $this->convertIpToInt($this->getBroadcastAddress());
7✔
410
        }
411
        return $this->convertIpToInt($this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
412
    }
413

414
    /**
415
     * Get subnet mask as dotted quads: xxx.xxx.xxx.xxx
416
     *
417
     * @return string subnet mask as dotted quads
418
     */
419
    public function getSubnetMask(): string
420
    {
421
        return $this->subnetCalculation(self::FORMAT_QUADS, '.');
110✔
422
    }
423

424
    /**
425
     * Get subnet mask as array of quads: [xxx, xxx, xxx, xxx]
426
     *
427
     * @return string[] of four elements containing the four quads of the subnet mask.
428
     */
429
    public function getSubnetMaskQuads(): array
430
    {
431
        return \explode('.', $this->subnetCalculation(self::FORMAT_QUADS, '.'));
32✔
432
    }
433

434
    /**
435
     * Get subnet mask as hexadecimal
436
     *
437
     * @return string subnet mask in hex
438
     */
439
    public function getSubnetMaskHex(): string
440
    {
441
        return $this->subnetCalculation(self::FORMAT_HEX);
97✔
442
    }
443

444
    /**
445
     * Get subnet mask as binary
446
     *
447
     * @return string subnet mask in binary
448
     */
449
    public function getSubnetMaskBinary(): string
450
    {
451
        return $this->subnetCalculation(self::FORMAT_BINARY);
97✔
452
    }
453

454
    /**
455
     * Get subnet mask as an integer
456
     *
457
     * @return int
458
     */
459
    public function getSubnetMaskInteger(): int
460
    {
461
        return $this->convertIpToInt($this->subnetCalculation(self::FORMAT_QUADS, '.'));
103✔
462
    }
463

464
    /**
465
     * Get wildcard mask as dotted quads: xxx.xxx.xxx.xxx
466
     *
467
     * The wildcard mask is the inverse of the subnet mask, commonly used in
468
     * Cisco ACLs (Access Control Lists) and OSPF network statements.
469
     * For a /24 (subnet mask 255.255.255.0), the wildcard mask is 0.0.0.255.
470
     *
471
     * @see https://www.cisco.com/c/en/us/support/docs/security/ios-firewall/23602-confaccesslists.html Cisco IOS ACL Configuration Guide
472
     *
473
     * @return string wildcard mask as dotted quads
474
     */
475
    public function getWildcardMask(): string
476
    {
477
        return $this->wildcardCalculation(self::FORMAT_QUADS, '.');
87✔
478
    }
479

480
    /**
481
     * Get wildcard mask as array of quads: [xxx, xxx, xxx, xxx]
482
     *
483
     * @return string[] of four elements containing the four quads of the wildcard mask.
484
     */
485
    public function getWildcardMaskQuads(): array
486
    {
487
        return \explode('.', $this->wildcardCalculation(self::FORMAT_QUADS, '.'));
12✔
488
    }
489

490
    /**
491
     * Get wildcard mask as hexadecimal
492
     *
493
     * @return string wildcard mask in hex
494
     */
495
    public function getWildcardMaskHex(): string
496
    {
497
        return $this->wildcardCalculation(self::FORMAT_HEX);
74✔
498
    }
499

500
    /**
501
     * Get wildcard mask as binary
502
     *
503
     * @return string wildcard mask in binary
504
     */
505
    public function getWildcardMaskBinary(): string
506
    {
507
        return $this->wildcardCalculation(self::FORMAT_BINARY);
74✔
508
    }
509

510
    /**
511
     * Get wildcard mask as an integer
512
     *
513
     * @return int
514
     */
515
    public function getWildcardMaskInteger(): int
516
    {
517
        return $this->convertIpToInt($this->wildcardCalculation(self::FORMAT_QUADS, '.'));
82✔
518
    }
519

520
    /**
521
     * Split the network into smaller networks
522
     *
523
     * Divides this subnet into multiple smaller subnets of the specified prefix length.
524
     * The split is based on the actual network boundaries, not the input IP address.
525
     * For example, splitting 192.168.1.100/24 into /26s will produce the same result
526
     * as splitting 192.168.1.0/24 into /26s (four /26 subnets starting at .0, .64, .128, .192).
527
     *
528
     * @param int $networkSize The new prefix length (must be larger than current prefix)
529
     *
530
     * @return SubnetCalculator[] Array of subnet objects representing the split networks
531
     *
532
     * @throws \RuntimeException If networkSize is not larger than current prefix or exceeds 32
533
     */
534
    public function split(int $networkSize): array
535
    {
536
        if ($networkSize <= $this->networkSize) {
37✔
537
            throw new \RuntimeException('New networkSize must be larger than the base networkSize.');
6✔
538
        }
539

540
        if ($networkSize > 32) {
31✔
541
            throw new \RuntimeException('New networkSize must be smaller than the maximum networkSize.');
8✔
542
        }
543

544
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
23✔
545

546
        $addressCount = $this->getNumberIPAddressesOfNetworkSize($networkSize);
23✔
547

548
        $ranges = [];
23✔
549
        for ($ip = $startIp; $ip <= $endIp; $ip += $addressCount) {
23✔
550
            $ranges[] = new SubnetCalculator($this->convertIpToDottedQuad($ip), $networkSize);
23✔
551
        }
552

553
        return $ranges;
23✔
554
    }
555

556
    /**
557
     * Get network portion of IP address as dotted quads: xxx.xxx.xxx.xxx
558
     *
559
     * @return string network portion as dotted quads
560
     */
561
    public function getNetworkPortion(): string
562
    {
563
        return $this->networkCalculation(self::FORMAT_QUADS, '.');
535✔
564
    }
565

566
    /**
567
     * Get network portion as array of quads: [xxx, xxx, xxx, xxx]
568
     *
569
     * @return string[] of four elements containing the four quads of the network portion
570
     */
571
    public function getNetworkPortionQuads(): array
572
    {
573
        return \explode('.', $this->networkCalculation(self::FORMAT_QUADS, '.'));
594✔
574
    }
575

576
    /**
577
     * Get network portion of IP address as hexadecimal
578
     *
579
     * @return string network portion in hex
580
     */
581
    public function getNetworkPortionHex(): string
582
    {
583
        return $this->networkCalculation(self::FORMAT_HEX);
104✔
584
    }
585

586
    /**
587
     * Get network portion of IP address as binary
588
     *
589
     * @return string network portion in binary
590
     */
591
    public function getNetworkPortionBinary(): string
592
    {
593
        return $this->networkCalculation(self::FORMAT_BINARY);
104✔
594
    }
595

596
    /**
597
     * Get network portion of IP address as an integer
598
     *
599
     * @return int
600
     */
601
    public function getNetworkPortionInteger(): int
602
    {
603
        return $this->convertIpToInt($this->networkCalculation(self::FORMAT_QUADS, '.'));
215✔
604
    }
605

606
    /**
607
     * Get host portion of IP address as dotted quads: xxx.xxx.xxx.xxx
608
     *
609
     * @return string host portion as dotted quads
610
     */
611
    public function getHostPortion(): string
612
    {
613
        return $this->hostCalculation(self::FORMAT_QUADS, '.');
97✔
614
    }
615

616
    /**
617
     * Get host portion as array of quads: [xxx, xxx, xxx, xxx]
618
     *
619
     * @return string[] of four elements containing the four quads of the host portion
620
     */
621
    public function getHostPortionQuads(): array
622
    {
623
        return \explode('.', $this->hostCalculation(self::FORMAT_QUADS, '.'));
32✔
624
    }
625

626
    /**
627
     * Get host portion of IP address as hexadecimal
628
     *
629
     * @return string host portion in hex
630
     */
631
    public function getHostPortionHex(): string
632
    {
633
        return $this->hostCalculation(self::FORMAT_HEX);
97✔
634
    }
635

636
    /**
637
     * Get host portion of IP address as binary
638
     *
639
     * @return string host portion in binary
640
     */
641
    public function getHostPortionBinary(): string
642
    {
643
        return $this->hostCalculation(self::FORMAT_BINARY);
97✔
644
    }
645

646
    /**
647
     * Get host portion of IP address as an integer
648
     *
649
     * @return int
650
     */
651
    public function getHostPortionInteger(): int
652
    {
653
        return $this->convertIpToInt($this->hostCalculation(self::FORMAT_QUADS, '.'));
97✔
654
    }
655

656
    /**
657
     * Get all IP addresses
658
     *
659
     * @return \Generator|string[]|false[]
660
     */
661
    public function getAllIPAddresses(): \Generator
662
    {
663
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
47✔
664

665
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
46✔
666
            yield $this->convertIpToDottedQuad($ip);
46✔
667
        }
668
    }
46✔
669

670
    /**
671
     * Get all host IP addresses
672
     * Removes broadcast and network address if they exist.
673
     *
674
     * @return \Generator|string[]|false[]
675
     *
676
     * @throws \RuntimeException if there is an error in the IP address range calculation
677
     */
678
    public function getAllHostIPAddresses(): \Generator
679
    {
680
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
39✔
681

682
        if ($this->getNetworkSize() < 31) {
38✔
683
            $startIp += 1;
28✔
684
            $endIp   -= 1;
28✔
685
        }
686

687
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
38✔
688
            yield $this->convertIpToDottedQuad($ip);
38✔
689
        }
690
    }
38✔
691

692
    /**
693
     * Is the IP address in the subnet?
694
     *
695
     * @param string $ipAddressString
696
     *
697
     * @return bool
698
     */
699
    public function isIPAddressInSubnet(string $ipAddressString): bool
700
    {
701
        $ipAddress = \ip2long($ipAddressString);
120✔
702
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
120✔
703

704
        return $ipAddress >= $startIp && $ipAddress <= $endIp;
120✔
705
    }
706

707
    /**
708
     * Check if this subnet overlaps with another subnet.
709
     *
710
     * Two subnets overlap if they share any IP addresses.
711
     * This is useful for network planning and conflict prevention,
712
     * including firewall rule validation and routing table conflict detection.
713
     *
714
     * @param SubnetCalculator $other The other subnet to compare against
715
     *
716
     * @return bool True if the subnets share any IP addresses
717
     */
718
    public function overlaps(SubnetCalculator $other): bool
719
    {
720
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
84✔
721
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
84✔
722

723
        // Two ranges overlap if one starts before the other ends and vice versa
724
        return $thisStart <= $otherEnd && $otherStart <= $thisEnd;
84✔
725
    }
726

727
    /**
728
     * Check if this subnet fully contains another subnet.
729
     *
730
     * A subnet contains another if all IP addresses in the contained subnet
731
     * are also within this subnet's range.
732
     *
733
     * @param SubnetCalculator $other The subnet to check for containment
734
     *
735
     * @return bool True if this subnet fully contains the other subnet
736
     */
737
    public function contains(SubnetCalculator $other): bool
738
    {
739
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
86✔
740
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
86✔
741

742
        // This subnet contains other if other's entire range is within this range
743
        return $thisStart <= $otherStart && $thisEnd >= $otherEnd;
86✔
744
    }
745

746
    /**
747
     * Check if this subnet is fully contained within another subnet.
748
     *
749
     * This is the inverse of contains(): $a->isContainedIn($b) === $b->contains($a)
750
     *
751
     * @param SubnetCalculator $other The subnet to check if this subnet is within
752
     *
753
     * @return bool True if this subnet is fully contained within the other subnet
754
     */
755
    public function isContainedIn(SubnetCalculator $other): bool
756
    {
757
        return $other->contains($this);
21✔
758
    }
759

760
    /* ****************************** *
761
     * EXCLUDE/DIFFERENCE OPERATIONS
762
     * ****************************** */
763

764
    /**
765
     * Exclude a subnet from this subnet.
766
     *
767
     * Returns an array of subnets representing the remainder after removing
768
     * the excluded subnet. Useful for carving out reserved ranges.
769
     *
770
     * The result is the minimal set of CIDR blocks covering the remaining space.
771
     * If the subnets don't overlap, returns this subnet unchanged.
772
     * If the excluded subnet fully contains this subnet, returns an empty array.
773
     *
774
     * @param SubnetCalculator $exclude Subnet to exclude
775
     *
776
     * @return SubnetCalculator[] Remaining subnets (empty if fully excluded)
777
     */
778
    public function exclude(SubnetCalculator $exclude): array
779
    {
780
        // If no overlap, return this subnet unchanged
781
        if (!$this->overlaps($exclude)) {
55✔
782
            return [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
11✔
783
        }
784

785
        // If exclude fully contains this subnet, nothing remains
786
        if ($exclude->contains($this)) {
50✔
787
            return [];
6✔
788
        }
789

790
        // If this subnet fully contains exclude, we need to split
791
        // Recursively split this subnet in half until we isolate the excluded portion
792
        return $this->excludeRecursive($exclude);
46✔
793
    }
794

795
    /**
796
     * Exclude multiple subnets from this subnet.
797
     *
798
     * Applies multiple exclusions sequentially. The result is the set of subnets
799
     * remaining after all exclusions are applied.
800
     *
801
     * @param SubnetCalculator[] $excludes Subnets to exclude
802
     *
803
     * @return SubnetCalculator[] Remaining subnets
804
     */
805
    public function excludeAll(array $excludes): array
806
    {
807
        if (empty($excludes)) {
9✔
808
            return [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
1✔
809
        }
810

811
        // Start with this subnet
812
        $remaining = [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
8✔
813

814
        // Apply each exclusion to all remaining subnets
815
        foreach ($excludes as $exclude) {
8✔
816
            $newRemaining = [];
8✔
817
            foreach ($remaining as $subnet) {
8✔
818
                $afterExclude = $subnet->exclude($exclude);
8✔
819
                foreach ($afterExclude as $s) {
8✔
820
                    $newRemaining[] = $s;
8✔
821
                }
822
            }
823
            $remaining = $newRemaining;
8✔
824
        }
825

826
        return $remaining;
8✔
827
    }
828

829
    /**
830
     * Recursively exclude a subnet by splitting into halves.
831
     *
832
     * This method splits the current subnet into two halves and recursively
833
     * processes each half. Halves that don't overlap with the exclusion are
834
     * kept as-is. Halves that are fully contained by the exclusion are discarded.
835
     * Halves that partially overlap are split further.
836
     *
837
     * @param SubnetCalculator $exclude Subnet to exclude
838
     *
839
     * @return SubnetCalculator[] Remaining subnets
840
     */
841
    private function excludeRecursive(SubnetCalculator $exclude): array
842
    {
843
        // Can't split smaller than /32
844
        if ($this->networkSize >= 32) {
46✔
845
            // This /32 overlaps with exclude, so it's excluded
846
            return [];
×
847
        }
848

849
        // Split into two halves
850
        $newPrefix = $this->networkSize + 1;
46✔
851
        $firstHalf = new SubnetCalculator($this->getNetworkPortion(), $newPrefix);
46✔
852
        $secondHalfStart = $this->getNetworkPortionInteger() + ($this->getNumberIPAddresses() / 2);
46✔
853
        // Handle signed/unsigned conversion
854
        $secondHalfIp = $this->convertIpToDottedQuad((int) $secondHalfStart);
46✔
855
        $secondHalf = new SubnetCalculator($secondHalfIp, $newPrefix);
46✔
856

857
        $result = [];
46✔
858

859
        // Process first half
860
        if (!$firstHalf->overlaps($exclude)) {
46✔
861
            // No overlap, keep the whole half
862
            $result[] = $firstHalf;
23✔
863
        } elseif ($exclude->contains($firstHalf)) {
35✔
864
            // Fully excluded, discard
865
        } else {
866
            // Partial overlap, recurse
867
            $subResult = $firstHalf->excludeRecursive($exclude);
24✔
868
            foreach ($subResult as $s) {
24✔
869
                $result[] = $s;
24✔
870
            }
871
        }
872

873
        // Process second half
874
        if (!$secondHalf->overlaps($exclude)) {
46✔
875
            // No overlap, keep the whole half
876
            $result[] = $secondHalf;
35✔
877
        } elseif ($exclude->contains($secondHalf)) {
23✔
878
            // Fully excluded, discard
879
        } else {
880
            // Partial overlap, recurse
881
            $subResult = $secondHalf->excludeRecursive($exclude);
10✔
882
            foreach ($subResult as $s) {
10✔
883
                $result[] = $s;
10✔
884
            }
885
        }
886

887
        return $result;
46✔
888
    }
889

890
    /* ****************************** *
891
     * ADJACENT SUBNET NAVIGATION
892
     * ****************************** */
893

894
    /**
895
     * Get the next subnet of the same size.
896
     *
897
     * Returns a new SubnetCalculator representing the subnet immediately following
898
     * this one in the IP address space, using the same network size (CIDR prefix).
899
     *
900
     * Useful for sequential IP allocation and network expansion planning.
901
     *
902
     * @return SubnetCalculator The next adjacent subnet
903
     *
904
     * @throws \RuntimeException If the next subnet would exceed the valid IPv4 range (255.255.255.255)
905
     */
906
    public function getNextSubnet(): SubnetCalculator
907
    {
908
        $addressCount = $this->getNumberIPAddresses();
33✔
909
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
910

911
        // Calculate next subnet start as unsigned integer
912
        $nextNetworkStartUnsigned = (int) \sprintf('%u', $currentNetworkStart) + $addressCount;
33✔
913

914
        // Check if we would exceed the valid IPv4 range (max is 4294967295 = 255.255.255.255)
915
        // The next subnet start must be within valid range, AND the entire subnet must fit
916
        $maxValidIp = 4294967295; // 0xFFFFFFFF
33✔
917
        if ($nextNetworkStartUnsigned > $maxValidIp || ($nextNetworkStartUnsigned + $addressCount - 1) > $maxValidIp) {
33✔
918
            throw new \RuntimeException('Next subnet would exceed valid IPv4 address range.');
3✔
919
        }
920

921
        // Convert back to signed int for long2ip
922
        $nextNetworkStart = $nextNetworkStartUnsigned > 2147483647
30✔
923
            ? $nextNetworkStartUnsigned - 4294967296
21✔
924
            : $nextNetworkStartUnsigned;
30✔
925

926
        $nextIp = $this->convertIpToDottedQuad((int) $nextNetworkStart);
30✔
927

928
        return new SubnetCalculator($nextIp, $this->networkSize);
30✔
929
    }
930

931
    /**
932
     * Get the previous subnet of the same size.
933
     *
934
     * Returns a new SubnetCalculator representing the subnet immediately preceding
935
     * this one in the IP address space, using the same network size (CIDR prefix).
936
     *
937
     * Useful for navigating backward through allocated IP ranges.
938
     *
939
     * @return SubnetCalculator The previous adjacent subnet
940
     *
941
     * @throws \RuntimeException If the previous subnet would be below 0.0.0.0
942
     */
943
    public function getPreviousSubnet(): SubnetCalculator
944
    {
945
        $addressCount = $this->getNumberIPAddresses();
33✔
946
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
947

948
        // Convert to unsigned for calculation
949
        $currentNetworkStartUnsigned = (int) \sprintf('%u', $currentNetworkStart);
33✔
950

951
        // Check if we would go below 0.0.0.0
952
        if ($currentNetworkStartUnsigned < $addressCount) {
33✔
953
            throw new \RuntimeException('Previous subnet would be below valid IPv4 address range (0.0.0.0).');
3✔
954
        }
955

956
        $previousNetworkStartUnsigned = $currentNetworkStartUnsigned - $addressCount;
31✔
957

958
        // Convert back to signed int for long2ip
959
        $previousNetworkStart = $previousNetworkStartUnsigned > 2147483647
31✔
960
            ? $previousNetworkStartUnsigned - 4294967296
16✔
961
            : $previousNetworkStartUnsigned;
31✔
962

963
        $previousIp = $this->convertIpToDottedQuad((int) $previousNetworkStart);
31✔
964

965
        return new SubnetCalculator($previousIp, $this->networkSize);
31✔
966
    }
967

968
    /**
969
     * Get multiple adjacent subnets.
970
     *
971
     * Returns an array of SubnetCalculator objects representing adjacent subnets
972
     * of the same size. Positive count returns subnets forward (higher IPs),
973
     * negative count returns subnets backward (lower IPs).
974
     *
975
     * Useful for bulk allocation planning or viewing a range of subnets.
976
     *
977
     * @param int $count Number of subnets to return (positive = forward, negative = backward)
978
     *
979
     * @return SubnetCalculator[] Array of adjacent subnets
980
     *
981
     * @throws \RuntimeException If any requested subnet would exceed valid IPv4 range
982
     */
983
    public function getAdjacentSubnets(int $count): array
984
    {
985
        if ($count === 0) {
16✔
986
            return [];
1✔
987
        }
988

989
        $subnets = [];
15✔
990
        $current = $this;
15✔
991

992
        if ($count > 0) {
15✔
993
            // Forward direction
994
            for ($i = 0; $i < $count; $i++) {
8✔
995
                $current = $current->getNextSubnet();
8✔
996
                $subnets[] = $current;
7✔
997
            }
998
        } else {
999
            // Backward direction
1000
            for ($i = 0; $i > $count; $i--) {
7✔
1001
                $current = $current->getPreviousSubnet();
7✔
1002
                $subnets[] = $current;
7✔
1003
            }
1004
        }
1005

1006
        return $subnets;
13✔
1007
    }
1008

1009
    /* ******************************************* *
1010
     * PRIVATE/RESERVED IP RANGE DETECTION METHODS
1011
     * ******************************************* */
1012

1013
    /**
1014
     * Check if the IP address is in a private range (RFC 1918).
1015
     *
1016
     * Private address ranges:
1017
     *   - 10.0.0.0/8     (10.0.0.0 - 10.255.255.255)
1018
     *   - 172.16.0.0/12  (172.16.0.0 - 172.31.255.255)
1019
     *   - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
1020
     *
1021
     * @link https://datatracker.ietf.org/doc/html/rfc1918 RFC 1918 - Address Allocation for Private Internets
1022
     *
1023
     * @return bool True if the IP address is in a private range
1024
     */
1025
    public function isPrivate(): bool
1026
    {
1027
        $ip = $this->getIPAddressInteger();
134✔
1028

1029
        // 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
1030
        if ($this->isInRange($ip, 0x0A000000, 0x0AFFFFFF)) {
134✔
1031
            return true;
29✔
1032
        }
1033

1034
        // 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
1035
        if ($this->isInRange($ip, 0xAC100000, 0xAC1FFFFF)) {
106✔
1036
            return true;
14✔
1037
        }
1038

1039
        // 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
1040
        if ($this->isInRange($ip, 0xC0A80000, 0xC0A8FFFF)) {
92✔
1041
            return true;
43✔
1042
        }
1043

1044
        return false;
52✔
1045
    }
1046

1047
    /**
1048
     * Check if the IP address is publicly routable.
1049
     *
1050
     * An IP is public if it is not in any of the special-purpose address ranges:
1051
     * private, loopback, link-local, multicast, CGN, documentation, benchmarking,
1052
     * reserved, limited broadcast, or "this" network.
1053
     *
1054
     * @return bool True if the IP address is publicly routable
1055
     */
1056
    public function isPublic(): bool
1057
    {
1058
        return !$this->isPrivate()
28✔
1059
            && !$this->isLoopback()
28✔
1060
            && !$this->isLinkLocal()
28✔
1061
            && !$this->isMulticast()
28✔
1062
            && !$this->isCarrierGradeNat()
28✔
1063
            && !$this->isDocumentation()
28✔
1064
            && !$this->isBenchmarking()
28✔
1065
            && !$this->isReserved()
28✔
1066
            && !$this->isThisNetwork();
28✔
1067
    }
1068

1069
    /**
1070
     * Check if the IP address is in the loopback range (127.0.0.0/8).
1071
     *
1072
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1073
     *
1074
     * @return bool True if the IP address is in the loopback range
1075
     */
1076
    public function isLoopback(): bool
1077
    {
1078
        $ip = $this->getIPAddressInteger();
54✔
1079

1080
        // 127.0.0.0/8: 127.0.0.0 - 127.255.255.255
1081
        return $this->isInRange($ip, 0x7F000000, 0x7FFFFFFF);
54✔
1082
    }
1083

1084
    /**
1085
     * Check if the IP address is link-local (169.254.0.0/16).
1086
     *
1087
     * Link-local addresses are used for automatic private IP addressing (APIPA)
1088
     * when DHCP is not available.
1089
     *
1090
     * @link https://datatracker.ietf.org/doc/html/rfc3927 RFC 3927 - Dynamic Configuration of IPv4 Link-Local Addresses
1091
     *
1092
     * @return bool True if the IP address is link-local
1093
     */
1094
    public function isLinkLocal(): bool
1095
    {
1096
        $ip = $this->getIPAddressInteger();
49✔
1097

1098
        // 169.254.0.0/16: 169.254.0.0 - 169.254.255.255
1099
        return $this->isInRange($ip, 0xA9FE0000, 0xA9FEFFFF);
49✔
1100
    }
1101

1102
    /**
1103
     * Check if the IP address is multicast (224.0.0.0/4).
1104
     *
1105
     * @link https://datatracker.ietf.org/doc/html/rfc5771 RFC 5771 - IANA Guidelines for IPv4 Multicast Address Assignments
1106
     *
1107
     * @return bool True if the IP address is multicast
1108
     */
1109
    public function isMulticast(): bool
1110
    {
1111
        $ip = $this->getIPAddressInteger();
39✔
1112

1113
        // 224.0.0.0/4: 224.0.0.0 - 239.255.255.255
1114
        return $this->isInRange($ip, 0xE0000000, 0xEFFFFFFF);
39✔
1115
    }
1116

1117
    /**
1118
     * Check if the IP address is in Carrier-Grade NAT range (100.64.0.0/10).
1119
     *
1120
     * Also known as Shared Address Space, used by ISPs for CGN deployments.
1121
     *
1122
     * @link https://datatracker.ietf.org/doc/html/rfc6598 RFC 6598 - IANA-Reserved IPv4 Prefix for Shared Address Space
1123
     *
1124
     * @return bool True if the IP address is in the CGN range
1125
     */
1126
    public function isCarrierGradeNat(): bool
1127
    {
1128
        $ip = $this->getIPAddressInteger();
43✔
1129

1130
        // 100.64.0.0/10: 100.64.0.0 - 100.127.255.255
1131
        return $this->isInRange($ip, 0x64400000, 0x647FFFFF);
43✔
1132
    }
1133

1134
    /**
1135
     * Check if the IP address is reserved for documentation (RFC 5737).
1136
     *
1137
     * Documentation ranges (TEST-NET-1, TEST-NET-2, TEST-NET-3):
1138
     *   - 192.0.2.0/24   (TEST-NET-1)
1139
     *   - 198.51.100.0/24 (TEST-NET-2)
1140
     *   - 203.0.113.0/24  (TEST-NET-3)
1141
     *
1142
     * @link https://datatracker.ietf.org/doc/html/rfc5737 RFC 5737 - IPv4 Address Blocks Reserved for Documentation
1143
     *
1144
     * @return bool True if the IP address is reserved for documentation
1145
     */
1146
    public function isDocumentation(): bool
1147
    {
1148
        $ip = $this->getIPAddressInteger();
48✔
1149

1150
        // 192.0.2.0/24 (TEST-NET-1): 192.0.2.0 - 192.0.2.255
1151
        if ($this->isInRange($ip, 0xC0000200, 0xC00002FF)) {
48✔
1152
            return true;
6✔
1153
        }
1154

1155
        // 198.51.100.0/24 (TEST-NET-2): 198.51.100.0 - 198.51.100.255
1156
        if ($this->isInRange($ip, 0xC6336400, 0xC63364FF)) {
42✔
1157
            return true;
4✔
1158
        }
1159

1160
        // 203.0.113.0/24 (TEST-NET-3): 203.0.113.0 - 203.0.113.255
1161
        if ($this->isInRange($ip, 0xCB007100, 0xCB0071FF)) {
38✔
1162
            return true;
4✔
1163
        }
1164

1165
        return false;
34✔
1166
    }
1167

1168
    /**
1169
     * Check if the IP address is reserved for benchmarking (198.18.0.0/15).
1170
     *
1171
     * @link https://datatracker.ietf.org/doc/html/rfc2544 RFC 2544 - Benchmarking Methodology for Network Interconnect Devices
1172
     *
1173
     * @return bool True if the IP address is reserved for benchmarking
1174
     */
1175
    public function isBenchmarking(): bool
1176
    {
1177
        $ip = $this->getIPAddressInteger();
35✔
1178

1179
        // 198.18.0.0/15: 198.18.0.0 - 198.19.255.255
1180
        return $this->isInRange($ip, 0xC6120000, 0xC613FFFF);
35✔
1181
    }
1182

1183
    /**
1184
     * Check if the IP address is reserved for future use (240.0.0.0/4).
1185
     *
1186
     * Note: This includes 255.255.255.255 (limited broadcast), which can be
1187
     * separately identified using isLimitedBroadcast().
1188
     *
1189
     * @link https://datatracker.ietf.org/doc/html/rfc1112 RFC 1112 - Host Extensions for IP Multicasting
1190
     *
1191
     * @return bool True if the IP address is reserved for future use
1192
     */
1193
    public function isReserved(): bool
1194
    {
1195
        $ip = $this->getIPAddressInteger();
27✔
1196

1197
        // 240.0.0.0/4: 240.0.0.0 - 255.255.255.255
1198
        return $this->isInRange($ip, 0xF0000000, 0xFFFFFFFF);
27✔
1199
    }
1200

1201
    /**
1202
     * Check if the IP address is the broadcast address (255.255.255.255/32).
1203
     *
1204
     * @link https://datatracker.ietf.org/doc/html/rfc919 RFC 919 - Broadcasting Internet Datagrams
1205
     *
1206
     * @return bool True if the IP address is the limited broadcast address
1207
     */
1208
    public function isLimitedBroadcast(): bool
1209
    {
1210
        return $this->ipAddress === '255.255.255.255';
13✔
1211
    }
1212

1213
    /**
1214
     * Check if the IP address is in the "this" network range (0.0.0.0/8).
1215
     *
1216
     * Addresses in this range represent "this host on this network" and are
1217
     * only valid as source addresses.
1218
     *
1219
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1220
     *
1221
     * @return bool True if the IP address is in the "this" network range
1222
     */
1223
    public function isThisNetwork(): bool
1224
    {
1225
        $ip = $this->getIPAddressInteger();
104✔
1226

1227
        // 0.0.0.0/8: 0.0.0.0 - 0.255.255.255
1228
        return $this->isInRange($ip, 0x00000000, 0x00FFFFFF);
104✔
1229
    }
1230

1231
    /**
1232
     * Get the address type classification.
1233
     *
1234
     * Returns a string identifying the type of address. The order of checks matters:
1235
     * more specific types (like limited-broadcast) are checked before broader types
1236
     * (like reserved) that would also match.
1237
     *
1238
     * @return string Address type: 'private', 'public', 'loopback', 'link-local',
1239
     *                'multicast', 'carrier-grade-nat', 'documentation', 'benchmarking',
1240
     *                'reserved', 'limited-broadcast', 'this-network'
1241
     */
1242
    public function getAddressType(): string
1243
    {
1244
        // Check specific types first, then broader types
1245
        if ($this->isThisNetwork()) {
89✔
1246
            return 'this-network';
2✔
1247
        }
1248
        if ($this->isPrivate()) {
87✔
1249
            return 'private';
68✔
1250
        }
1251
        if ($this->isLoopback()) {
21✔
1252
            return 'loopback';
2✔
1253
        }
1254
        if ($this->isLinkLocal()) {
19✔
1255
            return 'link-local';
2✔
1256
        }
1257
        if ($this->isCarrierGradeNat()) {
17✔
1258
            return 'carrier-grade-nat';
2✔
1259
        }
1260
        if ($this->isDocumentation()) {
15✔
1261
            return 'documentation';
3✔
1262
        }
1263
        if ($this->isBenchmarking()) {
12✔
1264
            return 'benchmarking';
2✔
1265
        }
1266
        if ($this->isMulticast()) {
10✔
1267
            return 'multicast';
2✔
1268
        }
1269
        // Check limited broadcast before reserved (since it's a subset)
1270
        if ($this->isLimitedBroadcast()) {
8✔
1271
            return 'limited-broadcast';
1✔
1272
        }
1273
        if ($this->isReserved()) {
7✔
1274
            return 'reserved';
2✔
1275
        }
1276

1277
        return 'public';
5✔
1278
    }
1279

1280
    /**
1281
     * Check if an IP integer is within a given range (inclusive).
1282
     *
1283
     * @param int $ip    IP address as integer
1284
     * @param int $start Start of range as integer
1285
     * @param int $end   End of range as integer
1286
     *
1287
     * @return bool True if the IP is within the range
1288
     */
1289
    private function isInRange(int $ip, int $start, int $end): bool
1290
    {
1291
        // Handle PHP's signed integer representation for high IP addresses
1292
        // Convert to unsigned for comparison using sprintf
1293
        $ipUnsigned    = \sprintf('%u', $ip);
210✔
1294
        $startUnsigned = \sprintf('%u', $start);
210✔
1295
        $endUnsigned   = \sprintf('%u', $end);
210✔
1296

1297
        return $ipUnsigned >= $startUnsigned && $ipUnsigned <= $endUnsigned;
210✔
1298
    }
1299

1300
    /* ****************************************** *
1301
     * NETWORK CLASS INFORMATION (LEGACY) METHODS
1302
     * ****************************************** */
1303

1304
    /**
1305
     * Get the legacy network class.
1306
     *
1307
     * Returns the classful network class based on the first octet of the IP address.
1308
     * While classful networking is obsolete (RFC 4632 established CIDR), it's still
1309
     * referenced in education, certifications, and some legacy systems.
1310
     *
1311
     * Network class definitions (RFC 791):
1312
     *   Class A: 0-127   (leading bit 0)     - Note: 0 is "this network", 127 is loopback
1313
     *   Class B: 128-191 (leading bits 10)
1314
     *   Class C: 192-223 (leading bits 110)
1315
     *   Class D: 224-239 (leading bits 1110) - Multicast
1316
     *   Class E: 240-255 (leading bits 1111) - Reserved for future use
1317
     *
1318
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1319
     * @link https://datatracker.ietf.org/doc/html/rfc4632 RFC 4632 - CIDR (obsoletes classful routing)
1320
     *
1321
     * @return string 'A', 'B', 'C', 'D' (multicast), or 'E' (reserved)
1322
     */
1323
    public function getNetworkClass(): string
1324
    {
1325
        $firstOctet = (int) $this->quads[0];
153✔
1326

1327
        // Class A: 0-127 (includes 0.x.x.x "this network" and 127.x.x.x loopback)
1328
        if ($firstOctet <= 127) {
153✔
1329
            return 'A';
52✔
1330
        }
1331

1332
        // Class B: 128-191
1333
        if ($firstOctet <= 191) {
103✔
1334
            return 'B';
31✔
1335
        }
1336

1337
        // Class C: 192-223
1338
        if ($firstOctet <= 223) {
76✔
1339
            return 'C';
57✔
1340
        }
1341

1342
        // Class D: 224-239 (Multicast)
1343
        if ($firstOctet <= 239) {
20✔
1344
            return 'D';
11✔
1345
        }
1346

1347
        // Class E: 240-255 (Reserved)
1348
        return 'E';
10✔
1349
    }
1350

1351
    /**
1352
     * Get the default classful mask for this IP.
1353
     *
1354
     * Returns the default subnet mask for the IP's network class. Classes D and E
1355
     * (multicast and reserved) do not have a default mask and return null.
1356
     *
1357
     * Default masks:
1358
     *   Class A: 255.0.0.0 (/8)
1359
     *   Class B: 255.255.0.0 (/16)
1360
     *   Class C: 255.255.255.0 (/24)
1361
     *   Class D/E: null (no default mask)
1362
     *
1363
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1364
     *
1365
     * @return string|null Dotted quad mask (e.g., "255.0.0.0" for Class A), or null for D/E
1366
     */
1367
    public function getDefaultClassMask(): ?string
1368
    {
1369
        $class = $this->getNetworkClass();
84✔
1370

1371
        switch ($class) {
84✔
1372
            case 'A':
84✔
1373
                return '255.0.0.0';
29✔
1374
            case 'B':
55✔
1375
                return '255.255.0.0';
14✔
1376
            case 'C':
43✔
1377
                return '255.255.255.0';
39✔
1378
            default:
1379
                // Class D (multicast) and E (reserved) have no default mask
1380
                return null;
4✔
1381
        }
1382
    }
1383

1384
    /**
1385
     * Get the default classful prefix.
1386
     *
1387
     * Returns the default CIDR prefix for the IP's network class. Classes D and E
1388
     * (multicast and reserved) do not have a default prefix and return null.
1389
     *
1390
     * Default prefixes:
1391
     *   Class A: 8
1392
     *   Class B: 16
1393
     *   Class C: 24
1394
     *   Class D/E: null (no default prefix)
1395
     *
1396
     * @link https://datatracker.ietf.org/doc/html/rfc791 RFC 791 - Internet Protocol
1397
     *
1398
     * @return int|null CIDR prefix (8, 16, 24), or null for D/E
1399
     */
1400
    public function getDefaultClassPrefix(): ?int
1401
    {
1402
        $class = $this->getNetworkClass();
105✔
1403

1404
        switch ($class) {
105✔
1405
            case 'A':
105✔
1406
                return 8;
35✔
1407
            case 'B':
71✔
1408
                return 16;
20✔
1409
            case 'C':
54✔
1410
                return 24;
46✔
1411
            default:
1412
                // Class D (multicast) and E (reserved) have no default prefix
1413
                return null;
8✔
1414
        }
1415
    }
1416

1417
    /**
1418
     * Check if the current subnet uses the classful default mask.
1419
     *
1420
     * Returns true if the network size matches the default classful prefix for
1421
     * this IP's class. Classes D and E always return false as they have no
1422
     * default mask.
1423
     *
1424
     * Examples:
1425
     *   10.0.0.0/8 → true (Class A with default /8)
1426
     *   10.0.0.0/24 → false (Class A but subnetted to /24)
1427
     *   172.16.0.0/16 → true (Class B with default /16)
1428
     *   172.16.0.0/12 → false (Class B but supernetted to /12)
1429
     *   224.0.0.1/32 → false (Class D, no default)
1430
     *
1431
     * @return bool True if the subnet uses the classful default mask
1432
     */
1433
    public function isClassful(): bool
1434
    {
1435
        $defaultPrefix = $this->getDefaultClassPrefix();
91✔
1436

1437
        // Class D and E have no default prefix, so they can never be classful
1438
        if ($defaultPrefix === null) {
91✔
1439
            return false;
4✔
1440
        }
1441

1442
        return $this->networkSize === $defaultPrefix;
87✔
1443
    }
1444

1445
    /* ************************** *
1446
     * UTILIZATION STATISTICS
1447
     * ************************** */
1448

1449
    /**
1450
     * Get the percentage of addresses that are usable hosts.
1451
     *
1452
     * Calculates (usable hosts / total IP addresses) * 100.
1453
     *
1454
     * Special cases per RFC 3021:
1455
     *   - /31: 100% (2 usable of 2 total - no network/broadcast overhead)
1456
     *   - /32: 100% (1 usable of 1 total - single host)
1457
     *
1458
     * Helpful for capacity planning and choosing optimal subnet sizes.
1459
     *
1460
     * @return float Percentage (0.0 to 100.0)
1461
     */
1462
    public function getUsableHostPercentage(): float
1463
    {
1464
        $totalAddresses = $this->getNumberIPAddresses();
9✔
1465
        $usableHosts = $this->getNumberAddressableHosts();
9✔
1466

1467
        return ($usableHosts / $totalAddresses) * 100.0;
9✔
1468
    }
1469

1470
    /**
1471
     * Get the number of addresses not usable as hosts.
1472
     *
1473
     * For most networks, this is 2 (network address + broadcast address).
1474
     * Special cases per RFC 3021:
1475
     *   - /31: 0 (both addresses are usable - point-to-point link)
1476
     *   - /32: 0 (single host network)
1477
     *
1478
     * @return int Number of unusable addresses
1479
     */
1480
    public function getUnusableAddressCount(): int
1481
    {
1482
        $totalAddresses = $this->getNumberIPAddresses();
7✔
1483
        $usableHosts = $this->getNumberAddressableHosts();
7✔
1484

1485
        return $totalAddresses - $usableHosts;
7✔
1486
    }
1487

1488
    /**
1489
     * Calculate efficiency for a given host requirement.
1490
     *
1491
     * Returns what percentage of usable addresses would be utilized
1492
     * if the specified number of hosts were deployed in this subnet.
1493
     *
1494
     * Values > 100% indicate the subnet is too small to accommodate
1495
     * the required number of hosts.
1496
     *
1497
     * Helpful for evaluating subnet sizing decisions.
1498
     *
1499
     * @param int $requiredHosts Number of hosts needed (must be >= 0)
1500
     *
1501
     * @return float Percentage (0.0 to 100.0+), or > 100 if insufficient
1502
     *
1503
     * @throws \InvalidArgumentException If $requiredHosts is negative
1504
     */
1505
    public function getUtilizationForHosts(int $requiredHosts): float
1506
    {
1507
        if ($requiredHosts < 0) {
13✔
1508
            throw new \InvalidArgumentException('Required hosts cannot be negative.');
1✔
1509
        }
1510

1511
        if ($requiredHosts === 0) {
12✔
1512
            return 0.0;
2✔
1513
        }
1514

1515
        $usableHosts = $this->getNumberAddressableHosts();
10✔
1516

1517
        return ($requiredHosts / $usableHosts) * 100.0;
10✔
1518
    }
1519

1520
    /**
1521
     * Get wasted addresses for a given host requirement.
1522
     *
1523
     * Returns the number of usable addresses that would remain unused
1524
     * if the specified number of hosts were deployed in this subnet.
1525
     *
1526
     * A negative value indicates the subnet is too small (insufficient capacity).
1527
     *
1528
     * Helpful for capacity planning and minimizing IP address waste.
1529
     *
1530
     * @param int $requiredHosts Number of hosts needed (must be >= 0)
1531
     *
1532
     * @return int Number of unused usable addresses (negative if insufficient)
1533
     *
1534
     * @throws \InvalidArgumentException If $requiredHosts is negative
1535
     */
1536
    public function getWastedAddresses(int $requiredHosts): int
1537
    {
1538
        if ($requiredHosts < 0) {
13✔
1539
            throw new \InvalidArgumentException('Required hosts cannot be negative.');
1✔
1540
        }
1541

1542
        $usableHosts = $this->getNumberAddressableHosts();
12✔
1543

1544
        return $usableHosts - $requiredHosts;
12✔
1545
    }
1546

1547
    /**
1548
     * Get the IPv4 Arpa Domain
1549
     *
1550
     * Reverse DNS lookups for IPv4 addresses use the special domain in-addr.arpa.
1551
     * In this domain, an IPv4 address is represented as a concatenated sequence of four decimal numbers,
1552
     * separated by dots, to which is appended the second level domain suffix .in-addr.arpa.
1553
     *
1554
     * The four decimal numbers are obtained by splitting the 32-bit IPv4 address into four octets and converting
1555
     * each octet into a decimal number. These decimal numbers are then concatenated in the order:
1556
     * least significant octet first (leftmost), to most significant octet last (rightmost).
1557
     * It is important to note that this is the reverse order to the usual dotted-decimal convention for writing
1558
     * IPv4 addresses in textual form.
1559
     *
1560
     * Ex: to do a reverse lookup of the IP address 8.8.4.4 the PTR record for the domain name 4.4.8.8.in-addr.arpa would be looked up.
1561
     *
1562
     * @link https://en.wikipedia.org/wiki/Reverse_DNS_lookup
1563
     *
1564
     * @return string
1565
     */
1566
    public function getIPv4ArpaDomain(): string
1567
    {
1568
        $reverseQuads = \implode('.', \array_reverse($this->quads));
73✔
1569
        return $reverseQuads . '.in-addr.arpa';
73✔
1570
    }
1571

1572
    /**
1573
     * Get subnet calculations as an associated array
1574
     *
1575
     * @return mixed[] of subnet calculations
1576
     */
1577
    public function getSubnetArrayReport(): array
1578
    {
1579
        return $this->report->createArrayReport($this);
1✔
1580
    }
1581

1582
    /**
1583
     * Get subnet calculations as a JSON string
1584
     *
1585
     * @return string JSON string of subnet calculations
1586
     *
1587
     * @throws \RuntimeException if there is a JSON encode error
1588
     */
1589
    public function getSubnetJsonReport(): string
1590
    {
1591
        $json = $this->report->createJsonReport($this);
2✔
1592

1593
        if ($json === false) {
2✔
1594
            throw new \RuntimeException('JSON report failure: ' . json_last_error_msg());
1✔
1595
        }
1596

1597
        return $json;
1✔
1598
    }
1599

1600
    /**
1601
     * Print a report of subnet calculations
1602
     */
1603
    public function printSubnetReport(): void
1604
    {
1605
        $this->report->printReport($this);
1✔
1606
    }
1✔
1607

1608
    /**
1609
     * Print a report of subnet calculations
1610
     *
1611
     * @return string Subnet Calculator report
1612
     */
1613
    public function getPrintableReport(): string
1614
    {
1615
        return $this->report->createPrintableReport($this);
1✔
1616
    }
1617

1618
    /**
1619
     * String representation of a report of subnet calculations
1620
     *
1621
     * @return string
1622
     */
1623
    public function __toString(): string
1624
    {
1625
        return $this->report->createPrintableReport($this);
58✔
1626
    }
1627

1628
    /* ************** *
1629
     * PHP INTERFACES
1630
     * ************** */
1631

1632
    /**
1633
     * \JsonSerializable interface
1634
     *
1635
     * @return mixed[]
1636
     */
1637
    public function jsonSerialize(): array
1638
    {
1639
        return $this->report->createArrayReport($this);
1✔
1640
    }
1641

1642
    /* ********************** *
1643
     * PRIVATE IMPLEMENTATION
1644
     * ********************** */
1645

1646
    /**
1647
     * Calculate subnet mask
1648
     *
1649
     * @param  int $networkSize
1650
     *
1651
     * @return int
1652
     */
1653
    private function calculateSubnetMask(int $networkSize): int
1654
    {
1655
        return 0xFFFFFFFF << (32 - $networkSize);
1,477✔
1656
    }
1657

1658
    /**
1659
     * Calculate IP address for formatting
1660
     *
1661
     * @param string $format    sprintf format to determine if decimal, hex or binary
1662
     * @param string $separator implode separator for formatting quads vs hex and binary
1663
     *
1664
     * @return string formatted IP address
1665
     */
1666
    private function ipAddressCalculation(string $format, string $separator = ''): string
1667
    {
1668
        return \implode($separator, array_map(
71✔
1669
            function ($quad) use ($format) {
1670
                return \sprintf($format, $quad);
71✔
1671
            },
71✔
1672
            $this->quads
71✔
1673
        ));
1674
    }
1675

1676
    /**
1677
     * Subnet calculation
1678
     *
1679
     * @param string $format    sprintf format to determine if decimal, hex or binary
1680
     * @param string $separator implode separator for formatting quads vs hex and binary
1681
     *
1682
     * @return string subnet
1683
     */
1684
    private function subnetCalculation(string $format, string $separator = ''): string
1685
    {
1686
        $maskQuads = [
1687
            \sprintf($format, ($this->subnetMask >> 24) & 0xFF),
116✔
1688
            \sprintf($format, ($this->subnetMask >> 16) & 0xFF),
116✔
1689
            \sprintf($format, ($this->subnetMask >>  8) & 0xFF),
116✔
1690
            \sprintf($format, ($this->subnetMask >>  0) & 0xFF),
116✔
1691
        ];
1692

1693
        return implode($separator, $maskQuads);
116✔
1694
    }
1695

1696
    /**
1697
     * Wildcard mask calculation
1698
     *
1699
     * The wildcard mask is the bitwise inverse of the subnet mask.
1700
     *
1701
     * @param string $format    sprintf format to determine if decimal, hex or binary
1702
     * @param string $separator implode separator for formatting quads vs hex and binary
1703
     *
1704
     * @return string wildcard mask
1705
     */
1706
    private function wildcardCalculation(string $format, string $separator = ''): string
1707
    {
1708
        $wildcardMask = ~$this->subnetMask;
124✔
1709
        $maskQuads = [
1710
            \sprintf($format, ($wildcardMask >> 24) & 0xFF),
124✔
1711
            \sprintf($format, ($wildcardMask >> 16) & 0xFF),
124✔
1712
            \sprintf($format, ($wildcardMask >>  8) & 0xFF),
124✔
1713
            \sprintf($format, ($wildcardMask >>  0) & 0xFF),
124✔
1714
        ];
1715

1716
        return implode($separator, $maskQuads);
124✔
1717
    }
1718

1719
    /**
1720
     * Calculate network portion for formatting
1721
     *
1722
     * @param string $format    sprintf format to determine if decimal, hex or binary
1723
     * @param string $separator implode separator for formatting quads vs hex and binary
1724
     *
1725
     * @return string formatted subnet mask
1726
     */
1727
    private function networkCalculation(string $format, string $separator = ''): string
1728
    {
1729
        $networkQuads = [
1730
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
732✔
1731
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
732✔
1732
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
732✔
1733
            \sprintf($format, (int) $this->quads[3] & ($this->subnetMask >>  0)),
732✔
1734
        ];
1735

1736
        return implode($separator, $networkQuads);
732✔
1737
    }
1738

1739
    /**
1740
     * Calculate host portion for formatting
1741
     *
1742
     * @param string $format    sprintf format to determine if decimal, hex or binary
1743
     * @param string $separator implode separator for formatting quads vs hex and binary
1744
     *
1745
     * @return string formatted subnet mask
1746
     */
1747
    private function hostCalculation(string $format, string $separator = ''): string
1748
    {
1749
        $networkQuads = [
1750
            \sprintf($format, (int) $this->quads[0] & ~($this->subnetMask >> 24)),
97✔
1751
            \sprintf($format, (int) $this->quads[1] & ~($this->subnetMask >> 16)),
97✔
1752
            \sprintf($format, (int) $this->quads[2] & ~($this->subnetMask >>  8)),
97✔
1753
            \sprintf($format, (int) $this->quads[3] & ~($this->subnetMask >>  0)),
97✔
1754
        ];
1755

1756
        return implode($separator, $networkQuads);
97✔
1757
    }
1758

1759
    /**
1760
     * Calculate min host for formatting
1761
     *
1762
     * @param string $format    sprintf format to determine if decimal, hex or binary
1763
     * @param string $separator implode separator for formatting quads vs hex and binary
1764
     *
1765
     * @return string formatted min host
1766
     */
1767
    private function minHostCalculation(string $format, string $separator = ''): string
1768
    {
1769
        $networkQuads = [
1770
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
145✔
1771
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
145✔
1772
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
145✔
1773
            \sprintf($format, ((int) $this->quads[3] & ($this->subnetMask >> 0)) + 1),
145✔
1774
        ];
1775

1776
        return implode($separator, $networkQuads);
145✔
1777
    }
1778

1779
    /**
1780
     * Calculate max host for formatting
1781
     *
1782
     * @param string $format    sprintf format to determine if decimal, hex or binary
1783
     * @param string $separator implode separator for formatting quads vs hex and binary
1784
     *
1785
     * @return string formatted max host
1786
     */
1787
    private function maxHostCalculation(string $format, string $separator = ''): string
1788
    {
1789
        $networkQuads      = $this->getNetworkPortionQuads();
145✔
1790
        $numberIpAddresses = $this->getNumberIPAddresses();
145✔
1791

1792
        $network_range_quads = [
1793
            \sprintf($format, ((int) $networkQuads[0] & ($this->subnetMask >> 24)) + ((($numberIpAddresses - 1) >> 24) & 0xFF)),
145✔
1794
            \sprintf($format, ((int) $networkQuads[1] & ($this->subnetMask >> 16)) + ((($numberIpAddresses - 1) >> 16) & 0xFF)),
145✔
1795
            \sprintf($format, ((int) $networkQuads[2] & ($this->subnetMask >>  8)) + ((($numberIpAddresses - 1) >>  8) & 0xFF)),
145✔
1796
            \sprintf($format, ((int) $networkQuads[3] & ($this->subnetMask >>  0)) + ((($numberIpAddresses - 1) >>  0) & 0xFE)),
145✔
1797
        ];
1798

1799
        return implode($separator, $network_range_quads);
145✔
1800
    }
1801

1802
    /**
1803
     * Validate IP address and network
1804
     *
1805
     * @param string $ipAddress   IP address in dotted quads format
1806
     * @param int    $networkSize Network size
1807
     *
1808
     * @throws \UnexpectedValueException IP or network size not valid
1809
     */
1810
    private function validateInputs(string $ipAddress, int $networkSize): void
1811
    {
1812
        if (!\filter_var($ipAddress, FILTER_VALIDATE_IP)) {
1,488✔
1813
            throw new \UnexpectedValueException("IP address $ipAddress not valid.");
13✔
1814
        }
1815
        if (($networkSize < 1) || ($networkSize > 32)) {
1,481✔
1816
            throw new \UnexpectedValueException("Network size $networkSize not valid.");
16✔
1817
        }
1818
    }
1,477✔
1819

1820
    /**
1821
     * Get the start and end of the IP address range as ints
1822
     *
1823
     * @return int[] [start IP, end IP]
1824
     */
1825
    private function getIPAddressRangeAsInts(): array
1826
    {
1827
        [$startIp, $endIp] = $this->getIPAddressRange();
335✔
1828
        $startIp = $this->convertIpToInt($startIp);
335✔
1829
        $endIp   = $this->convertIpToInt($endIp);
333✔
1830

1831
        return [$startIp, $endIp];
333✔
1832
    }
1833

1834
    /**
1835
     * Get the number of IP addresses in the given network size
1836
     *
1837
     * @param int $networkSize
1838
     *
1839
     * @return int Number of IP addresses
1840
     */
1841
    private function getNumberIPAddressesOfNetworkSize($networkSize): int
1842
    {
1843
        return 1 << (32 - $networkSize);
748✔
1844
    }
1845

1846

1847
    /**
1848
     * Convert a dotted-quad IP address to an integer
1849
     *
1850
     * @param string $ipAddress Dotted-quad IP address
1851
     *
1852
     * @return int Integer representation of an IP address
1853
     */
1854
    private function convertIpToInt(string $ipAddress): int
1855
    {
1856
        $ipAsInt = \ip2long($ipAddress);
705✔
1857
        if ($ipAsInt === false) {
705✔
1858
            throw new \RuntimeException('Invalid IP address string. Could not convert dotted-quad string address to an integer: ' . $ipAddress);
3✔
1859
        }
1860
        return $ipAsInt;
702✔
1861
    }
1862

1863
    /**
1864
     * Convert an integer IP address to a dotted-quad IP string
1865
     *
1866
     * @param int $ipAsInt Integer representation of an IP address
1867
     *
1868
     * @return string Dotted-quad IP address
1869
     */
1870
    private function convertIpToDottedQuad(int $ipAsInt): string
1871
    {
1872
        $ipDottedQuad = \long2ip($ipAsInt);
213✔
1873
        if ($ipDottedQuad == false) {
213✔
1874
            // @codeCoverageIgnore
1875
            throw new \RuntimeException('Invalid IP address integer. Could not convert integer address to dotted-quad string: ' . $ipAsInt);
×
1876
        }
1877
        return $ipDottedQuad;
213✔
1878
    }
1879
}
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