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

markrogoyski / ipv4-subnet-calculator-php / 20985512917

14 Jan 2026 07:04AM UTC coverage: 97.956% (+0.9%) from 97.05%
20985512917

push

github

markrogoyski
Remove unnecessary code block.

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
 * Network calculator for subnet mask and other classless (CIDR) network information.
9
 *
10
 * Given an IP address and CIDR network size, it calculates the following information:
11
 *   - IP address network subnet masks, network and host portions, and provides aggregated reports.
12
 *   - Subnet mask
13
 *   - Network portion
14
 *   - Host portion
15
 *   - Number of IP addresses in the network
16
 *   - Number of addressable hosts in the network
17
 *   - IP address range
18
 *   - Broadcast address
19
 *   - Min and max host
20
 *   - All IP addresses
21
 *   - IPv4 ARPA Domain
22
 * Provides each data in dotted quads, hexadecimal, and binary formats, as well as array of quads.
23
 *
24
 * Aggregated network calculation reports:
25
 *  - Associative array
26
 *  - JSON
27
 *  - String
28
 *  - Printed to STDOUT
29
 *
30
 * Special handling for /31 and /32 networks:
31
 *  - /32: Single host network. Min and max host are the same as the IP address.
32
 *  - /31: Point-to-point link per RFC 3021. Both addresses are usable hosts (no reserved
33
 *         network or broadcast addresses). Min host is the lower IP, max host is the higher IP.
34
 *
35
 * @link https://datatracker.ietf.org/doc/html/rfc3021 RFC 3021 - Using 31-Bit Prefixes on IPv4 Point-to-Point Links
36
 */
37
class SubnetCalculator implements \JsonSerializable
38
{
39
    /** @var string IP address as dotted quads: xxx.xxx.xxx.xxx */
40
    private $ipAddress;
41

42
    /** @var int CIDR network size */
43
    private $networkSize;
44

45
    /** @var string[] of four elements containing the four quads of the IP address */
46
    private $quads = [];
47

48
    /** @var int Subnet mask in format used for subnet calculations */
49
    private $subnetMask;
50

51
    /** @var SubnetReportInterface */
52
    private $report;
53

54
    private const FORMAT_QUADS  = '%d';
55
    private const FORMAT_HEX    = '%02X';
56
    private const FORMAT_BINARY = '%08b';
57

58
    /**
59
     * Constructor - Takes IP address and network size, validates inputs, and assigns class attributes.
60
     * For example: 192.168.1.120/24 would be $ip = 192.168.1.120 $network_size = 24
61
     *
62
     * @param string                     $ipAddress   IP address in dotted quad notation.
63
     * @param int                        $networkSize CIDR network size.
64
     * @param SubnetReportInterface|null $report
65
     */
66
    public function __construct(string $ipAddress, int $networkSize, ?SubnetReportInterface $report = null)
67
    {
68
        $this->validateInputs($ipAddress, $networkSize);
1,488✔
69

70
        $this->ipAddress   = $ipAddress;
1,477✔
71
        $this->networkSize = $networkSize;
1,477✔
72
        $this->quads       = \explode('.', $ipAddress);
1,477✔
73
        $this->subnetMask  = $this->calculateSubnetMask($networkSize);
1,477✔
74
        $this->report      = $report ?? new SubnetReport();
1,477✔
75
    }
1,477✔
76

77
    /* **************** *
78
     * PUBLIC INTERFACE
79
     * **************** */
80

81
    /**
82
     * Get IP address as dotted quads: xxx.xxx.xxx.xxx
83
     *
84
     * @return string IP address as dotted quads.
85
     */
86
    public function getIPAddress(): string
87
    {
88
        return $this->ipAddress;
97✔
89
    }
90

91
    /**
92
     * Get IP address as array of quads: [xxx, xxx, xxx, xxx]
93
     *
94
     * @return string[]
95
     */
96
    public function getIPAddressQuads(): array
97
    {
98
        return $this->quads;
3✔
99
    }
100

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

111
    /**
112
     * Get IP address as binary
113
     *
114
     * @return string IP address in binary
115
     */
116
    public function getIPAddressBinary(): string
117
    {
118
        return $this->ipAddressCalculation(self::FORMAT_BINARY);
68✔
119
    }
120

121
    /**
122
     * Get the IP address as an integer
123
     *
124
     * @return int
125
     */
126
    public function getIPAddressInteger(): int
127
    {
128
        return $this->convertIpToInt($this->ipAddress);
213✔
129
    }
130

131
    /**
132
     * Get network size
133
     *
134
     * @return int network size
135
     */
136
    public function getNetworkSize(): int
137
    {
138
        return $this->networkSize;
204✔
139
    }
140

141
    /**
142
     * Get the CIDR notation of the subnet: IP Address/Network size.
143
     * Example: 192.168.0.15/24
144
     *
145
     * @return string
146
     */
147
    public function getCidrNotation(): string
148
    {
149
        return $this->ipAddress . '/' . $this->networkSize;
120✔
150
    }
151

152
    /**
153
     * Get the number of IP addresses in the network
154
     *
155
     * @return int Number of IP addresses
156
     */
157
    public function getNumberIPAddresses(): int
158
    {
159
        return $this->getNumberIPAddressesOfNetworkSize($this->networkSize);
748✔
160
    }
161

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

181
        return ($this->getNumberIPAddresses() - 2);
141✔
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 getIPAddressRange(): array
190
    {
191
        return [$this->getNetworkPortion(), $this->getBroadcastAddress()];
374✔
192
    }
193

194
    /**
195
     * Get range of IP addresses in the network
196
     *
197
     * @return string[] containing start and end of IP address range. IP addresses in dotted quad notation.
198
     */
199
    public function getAddressableHostRange(): array
200
    {
201
        return [$this->getMinHost(), $this->getMaxHost()];
38✔
202
    }
203

204
    /**
205
     * Get the broadcast IP address
206
     *
207
     * @return string IP address as dotted quads
208
     */
209
    public function getBroadcastAddress(): string
210
    {
211
        $network_quads       = $this->getNetworkPortionQuads();
459✔
212
        $number_ip_addresses = $this->getNumberIPAddresses();
459✔
213

214
        $network_range_quads = [
215
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[0] & ($this->subnetMask >> 24)) + ((($number_ip_addresses - 1) >> 24) & 0xFF)),
459✔
216
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[1] & ($this->subnetMask >> 16)) + ((($number_ip_addresses - 1) >> 16) & 0xFF)),
459✔
217
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[2] & ($this->subnetMask >>  8)) + ((($number_ip_addresses - 1) >>  8) & 0xFF)),
459✔
218
            \sprintf(self::FORMAT_QUADS, ((int) $network_quads[3] & ($this->subnetMask >>  0)) + ((($number_ip_addresses - 1) >>  0) & 0xFF)),
459✔
219
        ];
220

221
        return \implode('.', $network_range_quads);
459✔
222
    }
223

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

245
    /**
246
     * Get minimum host IP address as array of quads: [xxx, xxx, xxx, xxx]
247
     *
248
     * @return string[] min host portion as dotted quads.
249
     */
250
    public function getMinHostQuads(): array
251
    {
252
        if ($this->networkSize === 32) {
17✔
253
            return $this->quads;
1✔
254
        }
255
        if ($this->networkSize === 31) {
16✔
256
            return $this->getNetworkPortionQuads();
7✔
257
        }
258
        return \explode('.', $this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
259
    }
260

261
    /**
262
     * Get minimum host IP address as hex
263
     *
264
     * @return string min host portion as hex
265
     */
266
    public function getMinHostHex(): string
267
    {
268
        if ($this->networkSize === 32) {
17✔
269
            return \implode('', \array_map(
1✔
270
                function ($quad) {
271
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
272
                },
1✔
273
                $this->quads
1✔
274
            ));
275
        }
276
        if ($this->networkSize === 31) {
16✔
277
            return $this->getNetworkPortionHex();
7✔
278
        }
279
        return $this->minHostCalculation(self::FORMAT_HEX);
9✔
280
    }
281

282
    /**
283
     * Get minimum host IP address as binary
284
     *
285
     * @return string min host portion as binary
286
     */
287
    public function getMinHostBinary(): string
288
    {
289
        if ($this->networkSize === 32) {
17✔
290
            return \implode('', \array_map(
1✔
291
                function ($quad) {
292
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
293
                },
1✔
294
                $this->quads
1✔
295
            ));
296
        }
297
        if ($this->networkSize === 31) {
16✔
298
            return $this->getNetworkPortionBinary();
7✔
299
        }
300
        return $this->minHostCalculation(self::FORMAT_BINARY);
9✔
301
    }
302

303
    /**
304
     * Get minimum host IP address as an Integer
305
     *
306
     * @return int min host portion as integer
307
     */
308
    public function getMinHostInteger(): int
309
    {
310
        if ($this->networkSize === 32) {
17✔
311
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
312
        }
313
        if ($this->networkSize === 31) {
16✔
314
            return $this->getNetworkPortionInteger();
7✔
315
        }
316
        return $this->convertIpToInt($this->minHostCalculation(self::FORMAT_QUADS, '.'));
9✔
317
    }
318

319
    /**
320
     * Get maximum host IP address as dotted quads: xxx.xxx.xxx.xxx
321
     *
322
     * For most networks, this is the broadcast address - 1.
323
     * Special cases:
324
     *  - /32: Returns the IP address itself (single host)
325
     *  - /31: Returns the broadcast address (higher IP of the point-to-point pair per RFC 3021)
326
     *
327
     * @return string max host as dotted quads.
328
     */
329
    public function getMaxHost(): string
330
    {
331
        if ($this->networkSize === 32) {
141✔
332
            return $this->ipAddress;
20✔
333
        }
334
        if ($this->networkSize === 31) {
139✔
335
            return $this->getBroadcastAddress();
30✔
336
        }
337
        return $this->maxHostCalculation(self::FORMAT_QUADS, '.');
109✔
338
    }
339

340
    /**
341
     * Get maximum host IP address as array of quads: [xxx, xxx, xxx, xxx]
342
     *
343
     * @return string[] max host portion as dotted quads
344
     */
345
    public function getMaxHostQuads(): array
346
    {
347
        if ($this->networkSize === 32) {
17✔
348
            return $this->quads;
1✔
349
        }
350
        if ($this->networkSize === 31) {
16✔
351
            return \explode('.', $this->getBroadcastAddress());
7✔
352
        }
353
        return \explode('.', $this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
354
    }
355

356
    /**
357
     * Get maximum host IP address as hex
358
     *
359
     * @return string max host portion as hex
360
     */
361
    public function getMaxHostHex(): string
362
    {
363
        if ($this->networkSize === 32) {
17✔
364
            return \implode('', \array_map(
1✔
365
                function ($quad) {
366
                    return \sprintf(self::FORMAT_HEX, $quad);
1✔
367
                },
1✔
368
                $this->quads
1✔
369
            ));
370
        }
371
        if ($this->networkSize === 31) {
16✔
372
            return \implode('', \array_map(
7✔
373
                function ($quad) {
374
                    return \sprintf(self::FORMAT_HEX, $quad);
7✔
375
                },
7✔
376
                \explode('.', $this->getBroadcastAddress())
7✔
377
            ));
378
        }
379
        return $this->maxHostCalculation(self::FORMAT_HEX);
9✔
380
    }
381

382
    /**
383
     * Get maximum host IP address as binary
384
     *
385
     * @return string max host portion as binary
386
     */
387
    public function getMaxHostBinary(): string
388
    {
389
        if ($this->networkSize === 32) {
17✔
390
            return \implode('', \array_map(
1✔
391
                function ($quad) {
392
                    return \sprintf(self::FORMAT_BINARY, $quad);
1✔
393
                },
1✔
394
                $this->quads
1✔
395
            ));
396
        }
397
        if ($this->networkSize === 31) {
16✔
398
            return \implode('', \array_map(
7✔
399
                function ($quad) {
400
                    return \sprintf(self::FORMAT_BINARY, $quad);
7✔
401
                },
7✔
402
                \explode('.', $this->getBroadcastAddress())
7✔
403
            ));
404
        }
405
        return $this->maxHostCalculation(self::FORMAT_BINARY);
9✔
406
    }
407

408
    /**
409
     * Get maximum host IP address as an Integer
410
     *
411
     * @return int max host portion as integer
412
     */
413
    public function getMaxHostInteger(): int
414
    {
415
        if ($this->networkSize === 32) {
17✔
416
            return $this->convertIpToInt(\implode('.', $this->quads));
1✔
417
        }
418
        if ($this->networkSize === 31) {
16✔
419
            return $this->convertIpToInt($this->getBroadcastAddress());
7✔
420
        }
421
        return $this->convertIpToInt($this->maxHostCalculation(self::FORMAT_QUADS, '.'));
9✔
422
    }
423

424
    /**
425
     * Get subnet mask as dotted quads: xxx.xxx.xxx.xxx
426
     *
427
     * @return string subnet mask as dotted quads
428
     */
429
    public function getSubnetMask(): string
430
    {
431
        return $this->subnetCalculation(self::FORMAT_QUADS, '.');
110✔
432
    }
433

434
    /**
435
     * Get subnet mask as array of quads: [xxx, xxx, xxx, xxx]
436
     *
437
     * @return string[] of four elements containing the four quads of the subnet mask.
438
     */
439
    public function getSubnetMaskQuads(): array
440
    {
441
        return \explode('.', $this->subnetCalculation(self::FORMAT_QUADS, '.'));
32✔
442
    }
443

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

454
    /**
455
     * Get subnet mask as binary
456
     *
457
     * @return string subnet mask in binary
458
     */
459
    public function getSubnetMaskBinary(): string
460
    {
461
        return $this->subnetCalculation(self::FORMAT_BINARY);
97✔
462
    }
463

464
    /**
465
     * Get subnet mask as an integer
466
     *
467
     * @return int
468
     */
469
    public function getSubnetMaskInteger(): int
470
    {
471
        return $this->convertIpToInt($this->subnetCalculation(self::FORMAT_QUADS, '.'));
103✔
472
    }
473

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

490
    /**
491
     * Get wildcard mask as array of quads: [xxx, xxx, xxx, xxx]
492
     *
493
     * @return string[] of four elements containing the four quads of the wildcard mask.
494
     */
495
    public function getWildcardMaskQuads(): array
496
    {
497
        return \explode('.', $this->wildcardCalculation(self::FORMAT_QUADS, '.'));
12✔
498
    }
499

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

510
    /**
511
     * Get wildcard mask as binary
512
     *
513
     * @return string wildcard mask in binary
514
     */
515
    public function getWildcardMaskBinary(): string
516
    {
517
        return $this->wildcardCalculation(self::FORMAT_BINARY);
74✔
518
    }
519

520
    /**
521
     * Get wildcard mask as an integer
522
     *
523
     * @return int
524
     */
525
    public function getWildcardMaskInteger(): int
526
    {
527
        return $this->convertIpToInt($this->wildcardCalculation(self::FORMAT_QUADS, '.'));
82✔
528
    }
529

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

550
        if ($networkSize > 32) {
31✔
551
            throw new \RuntimeException('New networkSize must be smaller than the maximum networkSize.');
8✔
552
        }
553

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

556
        $addressCount = $this->getNumberIPAddressesOfNetworkSize($networkSize);
23✔
557

558
        $ranges = [];
23✔
559
        for ($ip = $startIp; $ip <= $endIp; $ip += $addressCount) {
23✔
560
            $ranges[] = new SubnetCalculator($this->convertIpToDottedQuad($ip), $networkSize);
23✔
561
        }
562

563
        return $ranges;
23✔
564
    }
565

566
    /**
567
     * Get network portion of IP address as dotted quads: xxx.xxx.xxx.xxx
568
     *
569
     * @return string network portion as dotted quads
570
     */
571
    public function getNetworkPortion(): string
572
    {
573
        return $this->networkCalculation(self::FORMAT_QUADS, '.');
535✔
574
    }
575

576
    /**
577
     * Get network portion as array of quads: [xxx, xxx, xxx, xxx]
578
     *
579
     * @return string[] of four elements containing the four quads of the network portion
580
     */
581
    public function getNetworkPortionQuads(): array
582
    {
583
        return \explode('.', $this->networkCalculation(self::FORMAT_QUADS, '.'));
594✔
584
    }
585

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

596
    /**
597
     * Get network portion of IP address as binary
598
     *
599
     * @return string network portion in binary
600
     */
601
    public function getNetworkPortionBinary(): string
602
    {
603
        return $this->networkCalculation(self::FORMAT_BINARY);
104✔
604
    }
605

606
    /**
607
     * Get network portion of IP address as an integer
608
     *
609
     * @return int
610
     */
611
    public function getNetworkPortionInteger(): int
612
    {
613
        return $this->convertIpToInt($this->networkCalculation(self::FORMAT_QUADS, '.'));
215✔
614
    }
615

616
    /**
617
     * Get host portion of IP address as dotted quads: xxx.xxx.xxx.xxx
618
     *
619
     * @return string host portion as dotted quads
620
     */
621
    public function getHostPortion(): string
622
    {
623
        return $this->hostCalculation(self::FORMAT_QUADS, '.');
97✔
624
    }
625

626
    /**
627
     * Get host portion as array of quads: [xxx, xxx, xxx, xxx]
628
     *
629
     * @return string[] of four elements containing the four quads of the host portion
630
     */
631
    public function getHostPortionQuads(): array
632
    {
633
        return \explode('.', $this->hostCalculation(self::FORMAT_QUADS, '.'));
32✔
634
    }
635

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

646
    /**
647
     * Get host portion of IP address as binary
648
     *
649
     * @return string host portion in binary
650
     */
651
    public function getHostPortionBinary(): string
652
    {
653
        return $this->hostCalculation(self::FORMAT_BINARY);
97✔
654
    }
655

656
    /**
657
     * Get host portion of IP address as an integer
658
     *
659
     * @return int
660
     */
661
    public function getHostPortionInteger(): int
662
    {
663
        return $this->convertIpToInt($this->hostCalculation(self::FORMAT_QUADS, '.'));
97✔
664
    }
665

666
    /**
667
     * Get all IP addresses
668
     *
669
     * @return \Generator|string[]|false[]
670
     */
671
    public function getAllIPAddresses(): \Generator
672
    {
673
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
47✔
674

675
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
46✔
676
            yield $this->convertIpToDottedQuad($ip);
46✔
677
        }
678
    }
46✔
679

680
    /**
681
     * Get all host IP addresses
682
     * Removes broadcast and network address if they exist.
683
     *
684
     * @return \Generator|string[]|false[]
685
     *
686
     * @throws \RuntimeException if there is an error in the IP address range calculation
687
     */
688
    public function getAllHostIPAddresses(): \Generator
689
    {
690
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
39✔
691

692
        if ($this->getNetworkSize() < 31) {
38✔
693
            $startIp += 1;
28✔
694
            $endIp   -= 1;
28✔
695
        }
696

697
        for ($ip = $startIp; $ip <= $endIp; $ip++) {
38✔
698
            yield $this->convertIpToDottedQuad($ip);
38✔
699
        }
700
    }
38✔
701

702
    /**
703
     * Is the IP address in the subnet?
704
     *
705
     * @param string $ipAddressString
706
     *
707
     * @return bool
708
     */
709
    public function isIPAddressInSubnet(string $ipAddressString): bool
710
    {
711
        $ipAddress = \ip2long($ipAddressString);
120✔
712
        [$startIp, $endIp] = $this->getIPAddressRangeAsInts();
120✔
713

714
        return $ipAddress >= $startIp && $ipAddress <= $endIp;
120✔
715
    }
716

717
    /**
718
     * Check if this subnet overlaps with another subnet.
719
     *
720
     * Two subnets overlap if they share any IP addresses.
721
     * This is useful for network planning and conflict prevention,
722
     * including firewall rule validation and routing table conflict detection.
723
     *
724
     * @param SubnetCalculator $other The other subnet to compare against
725
     *
726
     * @return bool True if the subnets share any IP addresses
727
     */
728
    public function overlaps(SubnetCalculator $other): bool
729
    {
730
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
84✔
731
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
84✔
732

733
        // Two ranges overlap if one starts before the other ends and vice versa
734
        return $thisStart <= $otherEnd && $otherStart <= $thisEnd;
84✔
735
    }
736

737
    /**
738
     * Check if this subnet fully contains another subnet.
739
     *
740
     * A subnet contains another if all IP addresses in the contained subnet
741
     * are also within this subnet's range.
742
     *
743
     * @param SubnetCalculator $other The subnet to check for containment
744
     *
745
     * @return bool True if this subnet fully contains the other subnet
746
     */
747
    public function contains(SubnetCalculator $other): bool
748
    {
749
        [$thisStart, $thisEnd] = $this->getIPAddressRangeAsInts();
86✔
750
        [$otherStart, $otherEnd] = $other->getIPAddressRangeAsInts();
86✔
751

752
        // This subnet contains other if other's entire range is within this range
753
        return $thisStart <= $otherStart && $thisEnd >= $otherEnd;
86✔
754
    }
755

756
    /**
757
     * Check if this subnet is fully contained within another subnet.
758
     *
759
     * This is the inverse of contains(): $a->isContainedIn($b) === $b->contains($a)
760
     *
761
     * @param SubnetCalculator $other The subnet to check if this subnet is within
762
     *
763
     * @return bool True if this subnet is fully contained within the other subnet
764
     */
765
    public function isContainedIn(SubnetCalculator $other): bool
766
    {
767
        return $other->contains($this);
21✔
768
    }
769

770
    /* ****************************** *
771
     * EXCLUDE/DIFFERENCE OPERATIONS
772
     * ****************************** */
773

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

795
        // If exclude fully contains this subnet, nothing remains
796
        if ($exclude->contains($this)) {
50✔
797
            return [];
6✔
798
        }
799

800
        // If this subnet fully contains exclude, we need to split
801
        // Recursively split this subnet in half until we isolate the excluded portion
802
        return $this->excludeRecursive($exclude);
46✔
803
    }
804

805
    /**
806
     * Exclude multiple subnets from this subnet.
807
     *
808
     * Applies multiple exclusions sequentially. The result is the set of subnets
809
     * remaining after all exclusions are applied.
810
     *
811
     * @param SubnetCalculator[] $excludes Subnets to exclude
812
     *
813
     * @return SubnetCalculator[] Remaining subnets
814
     */
815
    public function excludeAll(array $excludes): array
816
    {
817
        if (empty($excludes)) {
9✔
818
            return [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
1✔
819
        }
820

821
        // Start with this subnet
822
        $remaining = [new SubnetCalculator($this->getNetworkPortion(), $this->networkSize)];
8✔
823

824
        // Apply each exclusion to all remaining subnets
825
        foreach ($excludes as $exclude) {
8✔
826
            $newRemaining = [];
8✔
827
            foreach ($remaining as $subnet) {
8✔
828
                $afterExclude = $subnet->exclude($exclude);
8✔
829
                foreach ($afterExclude as $s) {
8✔
830
                    $newRemaining[] = $s;
8✔
831
                }
832
            }
833
            $remaining = $newRemaining;
8✔
834
        }
835

836
        return $remaining;
8✔
837
    }
838

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

859
        // Split into two halves
860
        $newPrefix = $this->networkSize + 1;
46✔
861
        $firstHalf = new SubnetCalculator($this->getNetworkPortion(), $newPrefix);
46✔
862
        $secondHalfStart = $this->getNetworkPortionInteger() + ($this->getNumberIPAddresses() / 2);
46✔
863
        // Handle signed/unsigned conversion
864
        $secondHalfIp = $this->convertIpToDottedQuad((int) $secondHalfStart);
46✔
865
        $secondHalf = new SubnetCalculator($secondHalfIp, $newPrefix);
46✔
866

867
        $result = [];
46✔
868

869
        // Process first half
870
        if (!$firstHalf->overlaps($exclude)) {
46✔
871
            // No overlap, keep the whole half
872
            $result[] = $firstHalf;
23✔
873
        } elseif ($exclude->contains($firstHalf)) {
35✔
874
            // Fully excluded, discard
875
        } else {
876
            // Partial overlap, recurse
877
            $subResult = $firstHalf->excludeRecursive($exclude);
24✔
878
            foreach ($subResult as $s) {
24✔
879
                $result[] = $s;
24✔
880
            }
881
        }
882

883
        // Process second half
884
        if (!$secondHalf->overlaps($exclude)) {
46✔
885
            // No overlap, keep the whole half
886
            $result[] = $secondHalf;
35✔
887
        } elseif ($exclude->contains($secondHalf)) {
23✔
888
            // Fully excluded, discard
889
        } else {
890
            // Partial overlap, recurse
891
            $subResult = $secondHalf->excludeRecursive($exclude);
10✔
892
            foreach ($subResult as $s) {
10✔
893
                $result[] = $s;
10✔
894
            }
895
        }
896

897
        return $result;
46✔
898
    }
899

900
    /* ****************************** *
901
     * ADJACENT SUBNET NAVIGATION
902
     * ****************************** */
903

904
    /**
905
     * Get the next subnet of the same size.
906
     *
907
     * Returns a new SubnetCalculator representing the subnet immediately following
908
     * this one in the IP address space, using the same network size (CIDR prefix).
909
     *
910
     * Useful for sequential IP allocation and network expansion planning.
911
     *
912
     * @return SubnetCalculator The next adjacent subnet
913
     *
914
     * @throws \RuntimeException If the next subnet would exceed the valid IPv4 range (255.255.255.255)
915
     */
916
    public function getNextSubnet(): SubnetCalculator
917
    {
918
        $addressCount = $this->getNumberIPAddresses();
33✔
919
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
920

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

924
        // Check if we would exceed the valid IPv4 range (max is 4294967295 = 255.255.255.255)
925
        // The next subnet start must be within valid range, AND the entire subnet must fit
926
        $maxValidIp = 4294967295; // 0xFFFFFFFF
33✔
927
        if ($nextNetworkStartUnsigned > $maxValidIp || ($nextNetworkStartUnsigned + $addressCount - 1) > $maxValidIp) {
33✔
928
            throw new \RuntimeException('Next subnet would exceed valid IPv4 address range.');
3✔
929
        }
930

931
        // Convert back to signed int for long2ip
932
        $nextNetworkStart = $nextNetworkStartUnsigned > 2147483647
30✔
933
            ? $nextNetworkStartUnsigned - 4294967296
21✔
934
            : $nextNetworkStartUnsigned;
30✔
935

936
        $nextIp = $this->convertIpToDottedQuad((int) $nextNetworkStart);
30✔
937

938
        return new SubnetCalculator($nextIp, $this->networkSize);
30✔
939
    }
940

941
    /**
942
     * Get the previous subnet of the same size.
943
     *
944
     * Returns a new SubnetCalculator representing the subnet immediately preceding
945
     * this one in the IP address space, using the same network size (CIDR prefix).
946
     *
947
     * Useful for navigating backward through allocated IP ranges.
948
     *
949
     * @return SubnetCalculator The previous adjacent subnet
950
     *
951
     * @throws \RuntimeException If the previous subnet would be below 0.0.0.0
952
     */
953
    public function getPreviousSubnet(): SubnetCalculator
954
    {
955
        $addressCount = $this->getNumberIPAddresses();
33✔
956
        $currentNetworkStart = $this->getNetworkPortionInteger();
33✔
957

958
        // Convert to unsigned for calculation
959
        $currentNetworkStartUnsigned = (int) \sprintf('%u', $currentNetworkStart);
33✔
960

961
        // Check if we would go below 0.0.0.0
962
        if ($currentNetworkStartUnsigned < $addressCount) {
33✔
963
            throw new \RuntimeException('Previous subnet would be below valid IPv4 address range (0.0.0.0).');
3✔
964
        }
965

966
        $previousNetworkStartUnsigned = $currentNetworkStartUnsigned - $addressCount;
31✔
967

968
        // Convert back to signed int for long2ip
969
        $previousNetworkStart = $previousNetworkStartUnsigned > 2147483647
31✔
970
            ? $previousNetworkStartUnsigned - 4294967296
16✔
971
            : $previousNetworkStartUnsigned;
31✔
972

973
        $previousIp = $this->convertIpToDottedQuad((int) $previousNetworkStart);
31✔
974

975
        return new SubnetCalculator($previousIp, $this->networkSize);
31✔
976
    }
977

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

999
        $subnets = [];
15✔
1000
        $current = $this;
15✔
1001

1002
        if ($count > 0) {
15✔
1003
            // Forward direction
1004
            for ($i = 0; $i < $count; $i++) {
8✔
1005
                $current = $current->getNextSubnet();
8✔
1006
                $subnets[] = $current;
7✔
1007
            }
1008
        } else {
1009
            // Backward direction
1010
            for ($i = 0; $i > $count; $i--) {
7✔
1011
                $current = $current->getPreviousSubnet();
7✔
1012
                $subnets[] = $current;
7✔
1013
            }
1014
        }
1015

1016
        return $subnets;
13✔
1017
    }
1018

1019
    /* ******************************************* *
1020
     * PRIVATE/RESERVED IP RANGE DETECTION METHODS
1021
     * ******************************************* */
1022

1023
    /**
1024
     * Check if the IP address is in a private range (RFC 1918).
1025
     *
1026
     * Private address ranges:
1027
     *   - 10.0.0.0/8     (10.0.0.0 - 10.255.255.255)
1028
     *   - 172.16.0.0/12  (172.16.0.0 - 172.31.255.255)
1029
     *   - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
1030
     *
1031
     * @link https://datatracker.ietf.org/doc/html/rfc1918 RFC 1918 - Address Allocation for Private Internets
1032
     *
1033
     * @return bool True if the IP address is in a private range
1034
     */
1035
    public function isPrivate(): bool
1036
    {
1037
        $ip = $this->getIPAddressInteger();
134✔
1038

1039
        // 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
1040
        if ($this->isInRange($ip, 0x0A000000, 0x0AFFFFFF)) {
134✔
1041
            return true;
29✔
1042
        }
1043

1044
        // 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
1045
        if ($this->isInRange($ip, 0xAC100000, 0xAC1FFFFF)) {
106✔
1046
            return true;
14✔
1047
        }
1048

1049
        // 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
1050
        if ($this->isInRange($ip, 0xC0A80000, 0xC0A8FFFF)) {
92✔
1051
            return true;
43✔
1052
        }
1053

1054
        return false;
52✔
1055
    }
1056

1057
    /**
1058
     * Check if the IP address is publicly routable.
1059
     *
1060
     * An IP is public if it is not in any of the special-purpose address ranges:
1061
     * private, loopback, link-local, multicast, CGN, documentation, benchmarking,
1062
     * reserved, limited broadcast, or "this" network.
1063
     *
1064
     * @return bool True if the IP address is publicly routable
1065
     */
1066
    public function isPublic(): bool
1067
    {
1068
        return !$this->isPrivate()
28✔
1069
            && !$this->isLoopback()
28✔
1070
            && !$this->isLinkLocal()
28✔
1071
            && !$this->isMulticast()
28✔
1072
            && !$this->isCarrierGradeNat()
28✔
1073
            && !$this->isDocumentation()
28✔
1074
            && !$this->isBenchmarking()
28✔
1075
            && !$this->isReserved()
28✔
1076
            && !$this->isThisNetwork();
28✔
1077
    }
1078

1079
    /**
1080
     * Check if the IP address is in the loopback range (127.0.0.0/8).
1081
     *
1082
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1083
     *
1084
     * @return bool True if the IP address is in the loopback range
1085
     */
1086
    public function isLoopback(): bool
1087
    {
1088
        $ip = $this->getIPAddressInteger();
54✔
1089

1090
        // 127.0.0.0/8: 127.0.0.0 - 127.255.255.255
1091
        return $this->isInRange($ip, 0x7F000000, 0x7FFFFFFF);
54✔
1092
    }
1093

1094
    /**
1095
     * Check if the IP address is link-local (169.254.0.0/16).
1096
     *
1097
     * Link-local addresses are used for automatic private IP addressing (APIPA)
1098
     * when DHCP is not available.
1099
     *
1100
     * @link https://datatracker.ietf.org/doc/html/rfc3927 RFC 3927 - Dynamic Configuration of IPv4 Link-Local Addresses
1101
     *
1102
     * @return bool True if the IP address is link-local
1103
     */
1104
    public function isLinkLocal(): bool
1105
    {
1106
        $ip = $this->getIPAddressInteger();
49✔
1107

1108
        // 169.254.0.0/16: 169.254.0.0 - 169.254.255.255
1109
        return $this->isInRange($ip, 0xA9FE0000, 0xA9FEFFFF);
49✔
1110
    }
1111

1112
    /**
1113
     * Check if the IP address is multicast (224.0.0.0/4).
1114
     *
1115
     * @link https://datatracker.ietf.org/doc/html/rfc5771 RFC 5771 - IANA Guidelines for IPv4 Multicast Address Assignments
1116
     *
1117
     * @return bool True if the IP address is multicast
1118
     */
1119
    public function isMulticast(): bool
1120
    {
1121
        $ip = $this->getIPAddressInteger();
39✔
1122

1123
        // 224.0.0.0/4: 224.0.0.0 - 239.255.255.255
1124
        return $this->isInRange($ip, 0xE0000000, 0xEFFFFFFF);
39✔
1125
    }
1126

1127
    /**
1128
     * Check if the IP address is in Carrier-Grade NAT range (100.64.0.0/10).
1129
     *
1130
     * Also known as Shared Address Space, used by ISPs for CGN deployments.
1131
     *
1132
     * @link https://datatracker.ietf.org/doc/html/rfc6598 RFC 6598 - IANA-Reserved IPv4 Prefix for Shared Address Space
1133
     *
1134
     * @return bool True if the IP address is in the CGN range
1135
     */
1136
    public function isCarrierGradeNat(): bool
1137
    {
1138
        $ip = $this->getIPAddressInteger();
43✔
1139

1140
        // 100.64.0.0/10: 100.64.0.0 - 100.127.255.255
1141
        return $this->isInRange($ip, 0x64400000, 0x647FFFFF);
43✔
1142
    }
1143

1144
    /**
1145
     * Check if the IP address is reserved for documentation (RFC 5737).
1146
     *
1147
     * Documentation ranges (TEST-NET-1, TEST-NET-2, TEST-NET-3):
1148
     *   - 192.0.2.0/24   (TEST-NET-1)
1149
     *   - 198.51.100.0/24 (TEST-NET-2)
1150
     *   - 203.0.113.0/24  (TEST-NET-3)
1151
     *
1152
     * @link https://datatracker.ietf.org/doc/html/rfc5737 RFC 5737 - IPv4 Address Blocks Reserved for Documentation
1153
     *
1154
     * @return bool True if the IP address is reserved for documentation
1155
     */
1156
    public function isDocumentation(): bool
1157
    {
1158
        $ip = $this->getIPAddressInteger();
48✔
1159

1160
        // 192.0.2.0/24 (TEST-NET-1): 192.0.2.0 - 192.0.2.255
1161
        if ($this->isInRange($ip, 0xC0000200, 0xC00002FF)) {
48✔
1162
            return true;
6✔
1163
        }
1164

1165
        // 198.51.100.0/24 (TEST-NET-2): 198.51.100.0 - 198.51.100.255
1166
        if ($this->isInRange($ip, 0xC6336400, 0xC63364FF)) {
42✔
1167
            return true;
4✔
1168
        }
1169

1170
        // 203.0.113.0/24 (TEST-NET-3): 203.0.113.0 - 203.0.113.255
1171
        if ($this->isInRange($ip, 0xCB007100, 0xCB0071FF)) {
38✔
1172
            return true;
4✔
1173
        }
1174

1175
        return false;
34✔
1176
    }
1177

1178
    /**
1179
     * Check if the IP address is reserved for benchmarking (198.18.0.0/15).
1180
     *
1181
     * @link https://datatracker.ietf.org/doc/html/rfc2544 RFC 2544 - Benchmarking Methodology for Network Interconnect Devices
1182
     *
1183
     * @return bool True if the IP address is reserved for benchmarking
1184
     */
1185
    public function isBenchmarking(): bool
1186
    {
1187
        $ip = $this->getIPAddressInteger();
35✔
1188

1189
        // 198.18.0.0/15: 198.18.0.0 - 198.19.255.255
1190
        return $this->isInRange($ip, 0xC6120000, 0xC613FFFF);
35✔
1191
    }
1192

1193
    /**
1194
     * Check if the IP address is reserved for future use (240.0.0.0/4).
1195
     *
1196
     * Note: This includes 255.255.255.255 (limited broadcast), which can be
1197
     * separately identified using isLimitedBroadcast().
1198
     *
1199
     * @link https://datatracker.ietf.org/doc/html/rfc1112 RFC 1112 - Host Extensions for IP Multicasting
1200
     *
1201
     * @return bool True if the IP address is reserved for future use
1202
     */
1203
    public function isReserved(): bool
1204
    {
1205
        $ip = $this->getIPAddressInteger();
27✔
1206

1207
        // 240.0.0.0/4: 240.0.0.0 - 255.255.255.255
1208
        return $this->isInRange($ip, 0xF0000000, 0xFFFFFFFF);
27✔
1209
    }
1210

1211
    /**
1212
     * Check if the IP address is the broadcast address (255.255.255.255/32).
1213
     *
1214
     * @link https://datatracker.ietf.org/doc/html/rfc919 RFC 919 - Broadcasting Internet Datagrams
1215
     *
1216
     * @return bool True if the IP address is the limited broadcast address
1217
     */
1218
    public function isLimitedBroadcast(): bool
1219
    {
1220
        return $this->ipAddress === '255.255.255.255';
13✔
1221
    }
1222

1223
    /**
1224
     * Check if the IP address is in the "this" network range (0.0.0.0/8).
1225
     *
1226
     * Addresses in this range represent "this host on this network" and are
1227
     * only valid as source addresses.
1228
     *
1229
     * @link https://datatracker.ietf.org/doc/html/rfc1122 RFC 1122 - Requirements for Internet Hosts
1230
     *
1231
     * @return bool True if the IP address is in the "this" network range
1232
     */
1233
    public function isThisNetwork(): bool
1234
    {
1235
        $ip = $this->getIPAddressInteger();
104✔
1236

1237
        // 0.0.0.0/8: 0.0.0.0 - 0.255.255.255
1238
        return $this->isInRange($ip, 0x00000000, 0x00FFFFFF);
104✔
1239
    }
1240

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

1287
        return 'public';
5✔
1288
    }
1289

1290
    /**
1291
     * Check if an IP integer is within a given range (inclusive).
1292
     *
1293
     * @param int $ip    IP address as integer
1294
     * @param int $start Start of range as integer
1295
     * @param int $end   End of range as integer
1296
     *
1297
     * @return bool True if the IP is within the range
1298
     */
1299
    private function isInRange(int $ip, int $start, int $end): bool
1300
    {
1301
        // Handle PHP's signed integer representation for high IP addresses
1302
        // Convert to unsigned for comparison using sprintf
1303
        $ipUnsigned    = \sprintf('%u', $ip);
210✔
1304
        $startUnsigned = \sprintf('%u', $start);
210✔
1305
        $endUnsigned   = \sprintf('%u', $end);
210✔
1306

1307
        return $ipUnsigned >= $startUnsigned && $ipUnsigned <= $endUnsigned;
210✔
1308
    }
1309

1310
    /* ****************************************** *
1311
     * NETWORK CLASS INFORMATION (LEGACY) METHODS
1312
     * ****************************************** */
1313

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

1337
        // Class A: 0-127 (includes 0.x.x.x "this network" and 127.x.x.x loopback)
1338
        if ($firstOctet <= 127) {
153✔
1339
            return 'A';
52✔
1340
        }
1341

1342
        // Class B: 128-191
1343
        if ($firstOctet <= 191) {
103✔
1344
            return 'B';
31✔
1345
        }
1346

1347
        // Class C: 192-223
1348
        if ($firstOctet <= 223) {
76✔
1349
            return 'C';
57✔
1350
        }
1351

1352
        // Class D: 224-239 (Multicast)
1353
        if ($firstOctet <= 239) {
20✔
1354
            return 'D';
11✔
1355
        }
1356

1357
        // Class E: 240-255 (Reserved)
1358
        return 'E';
10✔
1359
    }
1360

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

1381
        switch ($class) {
84✔
1382
            case 'A':
84✔
1383
                return '255.0.0.0';
29✔
1384
            case 'B':
55✔
1385
                return '255.255.0.0';
14✔
1386
            case 'C':
43✔
1387
                return '255.255.255.0';
39✔
1388
            default:
1389
                // Class D (multicast) and E (reserved) have no default mask
1390
                return null;
4✔
1391
        }
1392
    }
1393

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

1414
        switch ($class) {
105✔
1415
            case 'A':
105✔
1416
                return 8;
35✔
1417
            case 'B':
71✔
1418
                return 16;
20✔
1419
            case 'C':
54✔
1420
                return 24;
46✔
1421
            default:
1422
                // Class D (multicast) and E (reserved) have no default prefix
1423
                return null;
8✔
1424
        }
1425
    }
1426

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

1447
        // Class D and E have no default prefix, so they can never be classful
1448
        if ($defaultPrefix === null) {
91✔
1449
            return false;
4✔
1450
        }
1451

1452
        return $this->networkSize === $defaultPrefix;
87✔
1453
    }
1454

1455
    /* ************************** *
1456
     * UTILIZATION STATISTICS
1457
     * ************************** */
1458

1459
    /**
1460
     * Get the percentage of addresses that are usable hosts.
1461
     *
1462
     * Calculates (usable hosts / total IP addresses) * 100.
1463
     *
1464
     * Special cases per RFC 3021:
1465
     *   - /31: 100% (2 usable of 2 total - no network/broadcast overhead)
1466
     *   - /32: 100% (1 usable of 1 total - single host)
1467
     *
1468
     * Helpful for capacity planning and choosing optimal subnet sizes.
1469
     *
1470
     * @return float Percentage (0.0 to 100.0)
1471
     */
1472
    public function getUsableHostPercentage(): float
1473
    {
1474
        $totalAddresses = $this->getNumberIPAddresses();
9✔
1475
        $usableHosts = $this->getNumberAddressableHosts();
9✔
1476

1477
        return ($usableHosts / $totalAddresses) * 100.0;
9✔
1478
    }
1479

1480
    /**
1481
     * Get the number of addresses not usable as hosts.
1482
     *
1483
     * For most networks, this is 2 (network address + broadcast address).
1484
     * Special cases per RFC 3021:
1485
     *   - /31: 0 (both addresses are usable - point-to-point link)
1486
     *   - /32: 0 (single host network)
1487
     *
1488
     * @return int Number of unusable addresses
1489
     */
1490
    public function getUnusableAddressCount(): int
1491
    {
1492
        $totalAddresses = $this->getNumberIPAddresses();
7✔
1493
        $usableHosts = $this->getNumberAddressableHosts();
7✔
1494

1495
        return $totalAddresses - $usableHosts;
7✔
1496
    }
1497

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

1521
        if ($requiredHosts === 0) {
12✔
1522
            return 0.0;
2✔
1523
        }
1524

1525
        $usableHosts = $this->getNumberAddressableHosts();
10✔
1526

1527
        return ($requiredHosts / $usableHosts) * 100.0;
10✔
1528
    }
1529

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

1552
        $usableHosts = $this->getNumberAddressableHosts();
12✔
1553

1554
        return $usableHosts - $requiredHosts;
12✔
1555
    }
1556

1557
    /**
1558
     * Get the IPv4 Arpa Domain
1559
     *
1560
     * Reverse DNS lookups for IPv4 addresses use the special domain in-addr.arpa.
1561
     * In this domain, an IPv4 address is represented as a concatenated sequence of four decimal numbers,
1562
     * separated by dots, to which is appended the second level domain suffix .in-addr.arpa.
1563
     *
1564
     * The four decimal numbers are obtained by splitting the 32-bit IPv4 address into four octets and converting
1565
     * each octet into a decimal number. These decimal numbers are then concatenated in the order:
1566
     * least significant octet first (leftmost), to most significant octet last (rightmost).
1567
     * It is important to note that this is the reverse order to the usual dotted-decimal convention for writing
1568
     * IPv4 addresses in textual form.
1569
     *
1570
     * 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.
1571
     *
1572
     * @link https://en.wikipedia.org/wiki/Reverse_DNS_lookup
1573
     *
1574
     * @return string
1575
     */
1576
    public function getIPv4ArpaDomain(): string
1577
    {
1578
        $reverseQuads = \implode('.', \array_reverse($this->quads));
73✔
1579
        return $reverseQuads . '.in-addr.arpa';
73✔
1580
    }
1581

1582
    /**
1583
     * Get subnet calculations as an associated array
1584
     *
1585
     * @return mixed[] of subnet calculations
1586
     */
1587
    public function getSubnetArrayReport(): array
1588
    {
1589
        return $this->report->createArrayReport($this);
1✔
1590
    }
1591

1592
    /**
1593
     * Get subnet calculations as a JSON string
1594
     *
1595
     * @return string JSON string of subnet calculations
1596
     *
1597
     * @throws \RuntimeException if there is a JSON encode error
1598
     */
1599
    public function getSubnetJsonReport(): string
1600
    {
1601
        $json = $this->report->createJsonReport($this);
2✔
1602

1603
        if ($json === false) {
2✔
1604
            throw new \RuntimeException('JSON report failure: ' . json_last_error_msg());
1✔
1605
        }
1606

1607
        return $json;
1✔
1608
    }
1609

1610
    /**
1611
     * Print a report of subnet calculations
1612
     */
1613
    public function printSubnetReport(): void
1614
    {
1615
        $this->report->printReport($this);
1✔
1616
    }
1✔
1617

1618
    /**
1619
     * Print a report of subnet calculations
1620
     *
1621
     * @return string Subnet Calculator report
1622
     */
1623
    public function getPrintableReport(): string
1624
    {
1625
        return $this->report->createPrintableReport($this);
1✔
1626
    }
1627

1628
    /**
1629
     * String representation of a report of subnet calculations
1630
     *
1631
     * @return string
1632
     */
1633
    public function __toString(): string
1634
    {
1635
        return $this->report->createPrintableReport($this);
58✔
1636
    }
1637

1638
    /* ************** *
1639
     * PHP INTERFACES
1640
     * ************** */
1641

1642
    /**
1643
     * \JsonSerializable interface
1644
     *
1645
     * @return mixed[]
1646
     */
1647
    public function jsonSerialize(): array
1648
    {
1649
        return $this->report->createArrayReport($this);
1✔
1650
    }
1651

1652
    /* ********************** *
1653
     * PRIVATE IMPLEMENTATION
1654
     * ********************** */
1655

1656
    /**
1657
     * Calculate subnet mask
1658
     *
1659
     * @param  int $networkSize
1660
     *
1661
     * @return int
1662
     */
1663
    private function calculateSubnetMask(int $networkSize): int
1664
    {
1665
        return 0xFFFFFFFF << (32 - $networkSize);
1,477✔
1666
    }
1667

1668
    /**
1669
     * Calculate IP address for formatting
1670
     *
1671
     * @param string $format    sprintf format to determine if decimal, hex or binary
1672
     * @param string $separator implode separator for formatting quads vs hex and binary
1673
     *
1674
     * @return string formatted IP address
1675
     */
1676
    private function ipAddressCalculation(string $format, string $separator = ''): string
1677
    {
1678
        return \implode($separator, array_map(
71✔
1679
            function ($quad) use ($format) {
1680
                return \sprintf($format, $quad);
71✔
1681
            },
71✔
1682
            $this->quads
71✔
1683
        ));
1684
    }
1685

1686
    /**
1687
     * Subnet calculation
1688
     *
1689
     * @param string $format    sprintf format to determine if decimal, hex or binary
1690
     * @param string $separator implode separator for formatting quads vs hex and binary
1691
     *
1692
     * @return string subnet
1693
     */
1694
    private function subnetCalculation(string $format, string $separator = ''): string
1695
    {
1696
        $maskQuads = [
1697
            \sprintf($format, ($this->subnetMask >> 24) & 0xFF),
116✔
1698
            \sprintf($format, ($this->subnetMask >> 16) & 0xFF),
116✔
1699
            \sprintf($format, ($this->subnetMask >>  8) & 0xFF),
116✔
1700
            \sprintf($format, ($this->subnetMask >>  0) & 0xFF),
116✔
1701
        ];
1702

1703
        return implode($separator, $maskQuads);
116✔
1704
    }
1705

1706
    /**
1707
     * Wildcard mask calculation
1708
     *
1709
     * The wildcard mask is the bitwise inverse of the subnet mask.
1710
     *
1711
     * @param string $format    sprintf format to determine if decimal, hex or binary
1712
     * @param string $separator implode separator for formatting quads vs hex and binary
1713
     *
1714
     * @return string wildcard mask
1715
     */
1716
    private function wildcardCalculation(string $format, string $separator = ''): string
1717
    {
1718
        $wildcardMask = ~$this->subnetMask;
124✔
1719
        $maskQuads = [
1720
            \sprintf($format, ($wildcardMask >> 24) & 0xFF),
124✔
1721
            \sprintf($format, ($wildcardMask >> 16) & 0xFF),
124✔
1722
            \sprintf($format, ($wildcardMask >>  8) & 0xFF),
124✔
1723
            \sprintf($format, ($wildcardMask >>  0) & 0xFF),
124✔
1724
        ];
1725

1726
        return implode($separator, $maskQuads);
124✔
1727
    }
1728

1729
    /**
1730
     * Calculate network portion for formatting
1731
     *
1732
     * @param string $format    sprintf format to determine if decimal, hex or binary
1733
     * @param string $separator implode separator for formatting quads vs hex and binary
1734
     *
1735
     * @return string formatted subnet mask
1736
     */
1737
    private function networkCalculation(string $format, string $separator = ''): string
1738
    {
1739
        $networkQuads = [
1740
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
732✔
1741
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
732✔
1742
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
732✔
1743
            \sprintf($format, (int) $this->quads[3] & ($this->subnetMask >>  0)),
732✔
1744
        ];
1745

1746
        return implode($separator, $networkQuads);
732✔
1747
    }
1748

1749
    /**
1750
     * Calculate host portion for formatting
1751
     *
1752
     * @param string $format    sprintf format to determine if decimal, hex or binary
1753
     * @param string $separator implode separator for formatting quads vs hex and binary
1754
     *
1755
     * @return string formatted subnet mask
1756
     */
1757
    private function hostCalculation(string $format, string $separator = ''): string
1758
    {
1759
        $networkQuads = [
1760
            \sprintf($format, (int) $this->quads[0] & ~($this->subnetMask >> 24)),
97✔
1761
            \sprintf($format, (int) $this->quads[1] & ~($this->subnetMask >> 16)),
97✔
1762
            \sprintf($format, (int) $this->quads[2] & ~($this->subnetMask >>  8)),
97✔
1763
            \sprintf($format, (int) $this->quads[3] & ~($this->subnetMask >>  0)),
97✔
1764
        ];
1765

1766
        return implode($separator, $networkQuads);
97✔
1767
    }
1768

1769
    /**
1770
     * Calculate min host for formatting
1771
     *
1772
     * @param string $format    sprintf format to determine if decimal, hex or binary
1773
     * @param string $separator implode separator for formatting quads vs hex and binary
1774
     *
1775
     * @return string formatted min host
1776
     */
1777
    private function minHostCalculation(string $format, string $separator = ''): string
1778
    {
1779
        $networkQuads = [
1780
            \sprintf($format, (int) $this->quads[0] & ($this->subnetMask >> 24)),
145✔
1781
            \sprintf($format, (int) $this->quads[1] & ($this->subnetMask >> 16)),
145✔
1782
            \sprintf($format, (int) $this->quads[2] & ($this->subnetMask >>  8)),
145✔
1783
            \sprintf($format, ((int) $this->quads[3] & ($this->subnetMask >> 0)) + 1),
145✔
1784
        ];
1785

1786
        return implode($separator, $networkQuads);
145✔
1787
    }
1788

1789
    /**
1790
     * Calculate max host for formatting
1791
     *
1792
     * @param string $format    sprintf format to determine if decimal, hex or binary
1793
     * @param string $separator implode separator for formatting quads vs hex and binary
1794
     *
1795
     * @return string formatted max host
1796
     */
1797
    private function maxHostCalculation(string $format, string $separator = ''): string
1798
    {
1799
        $networkQuads      = $this->getNetworkPortionQuads();
145✔
1800
        $numberIpAddresses = $this->getNumberIPAddresses();
145✔
1801

1802
        $network_range_quads = [
1803
            \sprintf($format, ((int) $networkQuads[0] & ($this->subnetMask >> 24)) + ((($numberIpAddresses - 1) >> 24) & 0xFF)),
145✔
1804
            \sprintf($format, ((int) $networkQuads[1] & ($this->subnetMask >> 16)) + ((($numberIpAddresses - 1) >> 16) & 0xFF)),
145✔
1805
            \sprintf($format, ((int) $networkQuads[2] & ($this->subnetMask >>  8)) + ((($numberIpAddresses - 1) >>  8) & 0xFF)),
145✔
1806
            \sprintf($format, ((int) $networkQuads[3] & ($this->subnetMask >>  0)) + ((($numberIpAddresses - 1) >>  0) & 0xFE)),
145✔
1807
        ];
1808

1809
        return implode($separator, $network_range_quads);
145✔
1810
    }
1811

1812
    /**
1813
     * Validate IP address and network
1814
     *
1815
     * @param string $ipAddress   IP address in dotted quads format
1816
     * @param int    $networkSize Network size
1817
     *
1818
     * @throws \UnexpectedValueException IP or network size not valid
1819
     */
1820
    private function validateInputs(string $ipAddress, int $networkSize): void
1821
    {
1822
        if (!\filter_var($ipAddress, FILTER_VALIDATE_IP)) {
1,488✔
1823
            throw new \UnexpectedValueException("IP address $ipAddress not valid.");
13✔
1824
        }
1825
        if (($networkSize < 1) || ($networkSize > 32)) {
1,481✔
1826
            throw new \UnexpectedValueException("Network size $networkSize not valid.");
16✔
1827
        }
1828
    }
1,477✔
1829

1830
    /**
1831
     * Get the start and end of the IP address range as ints
1832
     *
1833
     * @return int[] [start IP, end IP]
1834
     */
1835
    private function getIPAddressRangeAsInts(): array
1836
    {
1837
        [$startIp, $endIp] = $this->getIPAddressRange();
335✔
1838
        $startIp = $this->convertIpToInt($startIp);
335✔
1839
        $endIp   = $this->convertIpToInt($endIp);
333✔
1840

1841
        return [$startIp, $endIp];
333✔
1842
    }
1843

1844
    /**
1845
     * Get the number of IP addresses in the given network size
1846
     *
1847
     * @param int $networkSize
1848
     *
1849
     * @return int Number of IP addresses
1850
     */
1851
    private function getNumberIPAddressesOfNetworkSize($networkSize): int
1852
    {
1853
        return 1 << (32 - $networkSize);
748✔
1854
    }
1855

1856

1857
    /**
1858
     * Convert a dotted-quad IP address to an integer
1859
     *
1860
     * @param string $ipAddress Dotted-quad IP address
1861
     *
1862
     * @return int Integer representation of an IP address
1863
     */
1864
    private function convertIpToInt(string $ipAddress): int
1865
    {
1866
        $ipAsInt = \ip2long($ipAddress);
705✔
1867
        if ($ipAsInt === false) {
705✔
1868
            throw new \RuntimeException('Invalid IP address string. Could not convert dotted-quad string address to an integer: ' . $ipAddress);
3✔
1869
        }
1870
        return $ipAsInt;
702✔
1871
    }
1872

1873
    /**
1874
     * Convert an integer IP address to a dotted-quad IP string
1875
     *
1876
     * @param int $ipAsInt Integer representation of an IP address
1877
     *
1878
     * @return string Dotted-quad IP address
1879
     */
1880
    private function convertIpToDottedQuad(int $ipAsInt): string
1881
    {
1882
        $ipDottedQuad = \long2ip($ipAsInt);
213✔
1883
        if ($ipDottedQuad == false) {
213✔
1884
            // @codeCoverageIgnore
1885
            throw new \RuntimeException('Invalid IP address integer. Could not convert integer address to dotted-quad string: ' . $ipAsInt);
×
1886
        }
1887
        return $ipDottedQuad;
213✔
1888
    }
1889
}
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