SubMuncher   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 1
dl 0
loc 287
rs 8.3999
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
D consolidate() 0 31 9
C ip_range_to_subnet_array() 0 72 14
D consolidate_subnets() 0 115 18
A consolidate_verbose() 0 14 2
A consolidate_subnets_verbose() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like SubMuncher often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SubMuncher, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AndrewAndante\SubMuncher;
4
5
class SubMuncher
6
{
7
    /**
8
     * This class should not be instantiated.
9
     */
10
    private function __construct()
11
    {
12
    }
13
14
    /**
15
     * @param array $ipsArray
16
     * @param int $max max number of rules returned
17
     * @return array
18
     */
19
    public static function consolidate($ipsArray, $max = null)
20
    {
21
        $consolidatedSubnets = [];
22
        $subnetStart = null;
23
24
        $ips = array_unique($ipsArray);
25
        $sortedIPs = Util::sort_addresses($ips);
26
27
        foreach ($sortedIPs as $index => $ipv4) {
28
            // If not last and the next IP is the next sequential one, we are at the beginning of a subnet
29
            if (isset($sortedIPs[$index + 1]) && $sortedIPs[$index + 1] == Util::ip_after($ipv4)) {
30
                // if we've already started, just keep going, else kick one off
31
                $subnetStart = $subnetStart ?: $ipv4;
32
                // if not the first IP and the previous IP is sequential, we're at the end of a subnet
33
            } elseif (isset($sortedIPs[$index - 1]) && $subnetStart !== null) {
34
                $result = self::ip_range_to_subnet_array($subnetStart, $ipv4);
35
                $consolidatedSubnets = array_merge($consolidatedSubnets, $result);
36
                $subnetStart = null;
37
                // otherwise we are a lone /32, so add it straight in
38
            } else {
39
                $consolidatedSubnets[]= $ipv4.'/32';
40
                $subnetStart = null;
41
            }
42
        }
43
44
        if ($max === null || count($consolidatedSubnets) <= $max) {
45
            return $consolidatedSubnets;
46
        }
47
48
        return self::consolidate_subnets($consolidatedSubnets, $max);
49
    }
50
51
    /**
52
     * @param string $startip an IPv4 address
53
     * @param string $endip an IPv4 address
54
     *
55
     * @return string[] list of subnets that cover the ip range specified
56
     */
57
    public static function ip_range_to_subnet_array($startip, $endip)
58
    {
59
60
        if (!Util::is_ipaddr($startip) || !Util::is_ipaddr($endip)) {
61
            return [];
62
        }
63
64
        // Container for subnets within this range.
65
        $rangesubnets = [];
66
67
        // Figure out what the smallest subnet is that holds the number of IPs in the
68
        // given range.
69
        $cidr = Util::find_smallest_cidr(Util::ip_range_size($startip, $endip));
70
71
        // Loop here to reduce subnet size and retest as needed. We need to make sure
72
        // that the target subnet is wholly contained between $startip and $endip.
73
        for ($cidr; $cidr <= 32; $cidr++) {
74
            // Find the network and broadcast addresses for the subnet being tested.
75
            $targetsub_min = Util::gen_subnet($startip, $cidr);
76
            $targetsub_max = Util::gen_subnet_max($startip, $cidr);
77
78
            // Check best case where the range is exactly one subnet.
79
            if (($targetsub_min == $startip) && ($targetsub_max == $endip)) {
80
                // Hooray, the range is exactly this subnet!
81
                return ["{$startip}/{$cidr}"];
82
            }
83
84
            // These remaining scenarios will find a subnet that uses the largest
85
            // chunk possible of the range being tested, and leave the rest to be
86
            // tested recursively after the loop.
87
88
            // Check if the subnet begins with $startip and ends before $endip
89
            if (($targetsub_min == $startip) && Util::ip_less_than($targetsub_max, $endip)) {
90
                break;
91
            }
92
93
            // Check if the subnet ends at $endip and starts after $startip
94
            if (Util::ip_greater_than($targetsub_min, $startip) && ($targetsub_max == $endip)) {
95
                break;
96
            }
97
98
            // Check if the subnet is between $startip and $endip
99
            if (Util::ip_greater_than($targetsub_min, $startip) && Util::ip_less_than($targetsub_max, $endip)) {
100
                break;
101
            }
102
        }
103
104
        // Some logic that will recursively search from $startip to the first IP before
105
        // the start of the subnet we just found.
106
        // NOTE: This may never be hit, the way the above algo turned out, but is left
107
        // for completeness.
108
        if ($startip != $targetsub_min) {
109
            $rangesubnets = array_merge(
110
                $rangesubnets,
111
                self::ip_range_to_subnet_array($startip, Util::ip_before($targetsub_min))
0 ignored issues
show
Bug introduced by
The variable $targetsub_min does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
112
            );
113
        }
114
115
        // Add in the subnet we found before, to preserve ordering
116
        $rangesubnets[] = "{$targetsub_min}/{$cidr}";
117
118
        // And some more logic that will search after the subnet we found to fill in
119
        // to the end of the range.
120
        if ($endip != $targetsub_max) {
121
            $rangesubnets = array_merge(
122
                $rangesubnets,
123
                self::ip_range_to_subnet_array(Util::ip_after($targetsub_max), $endip)
0 ignored issues
show
Bug introduced by
The variable $targetsub_max does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
124
            );
125
        }
126
127
        return $rangesubnets;
128
    }
129
130
    /**
131
     * Function to figure out the least problematic subnets to combine based on
132
     * fewest additional IPs introduced. Then combines them as such, and runs
133
     * it back through the consolidator with one less subnet - until we have
134
     * reduced it down to the maximum number of rules
135
     *
136
     * @param array $subnetsArray array of cidrs
137
     * @param int $max
138
     *
139
     * @return array
140
     */
141
    public static function consolidate_subnets($subnetsArray, $max = null)
142
    {
143
144
        $subnetsArray = Util::sort_cidrs(array_unique($subnetsArray));
145
146
        do {
147
            $countSubnetsArray = count($subnetsArray);
148
            $newSubnetsArray = [];
149
            $subnetToMaskMap = [];
150
            $ipReductionBySubnet = [];
151
            reset($subnetsArray);
152
            do {
153
                $cidr = current($subnetsArray);
154
                list($currentIP, $currentMask) = explode('/', $cidr);
155
                $nextIP = null;
156
                $nextMask = null;
157
158
                if (next($subnetsArray) !== false) {
159
                    list($nextIP, $nextMask) = explode('/', current($subnetsArray));
160
                    prev($subnetsArray);
161
                } else {
162
                    end($subnetsArray);
163
                }
164
165
                $endIP = Util::gen_subnet_max($currentIP, $currentMask);
166
                while (isset($nextIP) && Util::ip_after($endIP) == $nextIP) {
167
                    $nextEndIP = Util::gen_subnet_max($nextIP, $nextMask);
168
                    $consolidated = self::ip_range_to_subnet_array($currentIP, $nextEndIP);
169
                    if (count($consolidated) == 1) {
170
                        $endIP = $nextEndIP;
171
                        list($currentIP, $currentMask) = explode('/', $consolidated[0]);
172
                        if (next($subnetsArray) !== false) {
173
                            list($nextIP, $nextMask) = explode('/', current($subnetsArray));
174
                        } else {
175
                            end($subnetsArray);
176
                            $nextIP = null;
177
                            $nextMask = null;
178
                        }
179
                    } else {
180
                        break;
181
                    }
182
                }
183
184
                $newSubnetsArray[] = $currentIP . '/' . $currentMask;
185
186
                $subnetToMaskMap[$currentIP] = [
187
                    'startIP' => $currentIP,
188
                    'endIP' => $endIP,
189
                    'mask' => $currentMask,
190
                    'next' => isset($nextIP) ? $nextIP : 'none',
191
                ];
192
193
                $toJoin = Util::get_single_subnet($currentIP, Util::gen_subnet_max($nextIP, $nextMask));
194
                if (!$toJoin) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $toJoin of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
195
                    continue;
196
                }
197
                list($joinIP, $joinMask) = explode('/', $toJoin);
198
                $diff = abs(Util::subnet_range_size($currentMask) - Util::subnet_range_size($joinMask));
199
200
                $ipReductionBySubnet[$joinIP] = [
201
                    'mask' => $joinMask,
202
                    'diff' => $diff,
203
                    'original' => $currentIP,
204
                ];
205
            } while (next($subnetsArray) !== false);
206
            $subnetsArray = $newSubnetsArray;
207
        } while (count($subnetsArray) !== $countSubnetsArray);
208
209
        // sort array by number of additional IPs introduced
210
        uasort($ipReductionBySubnet, function ($a, $b) {
211
            return $a['diff'] - $b['diff'];
212
        });
213
214
        $returnCIDRs = [];
215
        foreach ($subnetToMaskMap as $ip => $config) {
216
            $returnCIDRs[] = $ip.'/'.$config['mask'];
217
        }
218
219
        if ($max === null || count($returnCIDRs) <= $max) {
220
            return $returnCIDRs;
221
        }
222
223
        reset($ipReductionBySubnet);
224
        do {
225
            current($ipReductionBySubnet);
226
            $injectedIP = key($ipReductionBySubnet);
227
228
            $toUpdate = $ipReductionBySubnet[$injectedIP]['original'];
229
            if (isset($subnetToMaskMap[$toUpdate])) {
230
                $next = $subnetToMaskMap[$toUpdate]['next'];
231
232
                // remove the two subnets we've just mushed
233
                unset($subnetToMaskMap[$toUpdate]);
234
                unset($subnetToMaskMap[$next]);
235
236
                // chuck in the new one
237
                $subnetToMaskMap[$injectedIP] = [
238
                    'mask' => $ipReductionBySubnet[$injectedIP]['mask'],
239
                ];
240
241
                $returnCIDRs = [];
242
                foreach ($subnetToMaskMap as $ip => $config) {
243
                    $returnCIDRs[] = $ip . '/' . $config['mask'];
244
                }
245
246
                $returnCIDRs = Util::sort_cidrs($returnCIDRs);
247
            }
248
        } while (count($returnCIDRs) > $max && next($ipReductionBySubnet) !== false);
249
250
        if (count($returnCIDRs > $max)) {
251
            return self::consolidate_subnets($returnCIDRs, $max);
252
        }
253
254
        return $returnCIDRs;
255
    }
256
257
    /**
258
     * @param string[] $ipsArray
259
     * @param int|null $max
260
     * @return array
261
     */
262
    public static function consolidate_verbose($ipsArray, $max = null)
263
    {
264
        $consolidateResults = self::consolidate($ipsArray, $max);
265
        $totalIPs = [];
266
        foreach ($consolidateResults as $cidr) {
267
            $totalIPs = array_merge($totalIPs, Util::cidr_to_ips_array($cidr));
268
        }
269
270
        return [
271
            'consolidated_subnets' => $consolidateResults,
272
            'initial_IPs' => Util::sort_addresses($ipsArray),
273
            'total_IPs' => $totalIPs
274
        ];
275
    }
276
277
    /**
278
     * @param string[] $subnetsArray
279
     * @param int|null $max
280
     * @return array
281
     */
282
    public static function consolidate_subnets_verbose($subnetsArray, $max = null)
283
    {
284
        $ips = [];
285
        foreach ($subnetsArray as $subnet) {
286
            $ips = array_merge($ips, Util::cidr_to_ips_array($subnet));
287
        }
288
289
        return self::consolidate_verbose($ips, $max);
290
    }
291
}
292