Complex classes like Ip 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 Ip, and based on these observations, apply Extract Interface, too.
1 | <?php namespace Zhuzhichao\IpLocationZh; |
||
5 | class Ip |
||
6 | { |
||
7 | const IPV4 = 1; |
||
8 | const IPV6 = 2; |
||
9 | |||
10 | private $file = NULL; |
||
11 | private $fileSize = 0; |
||
12 | private $nodeCount = 0; |
||
13 | private $nodeOffset = 0; |
||
14 | |||
15 | private $meta = []; |
||
16 | |||
17 | private $database = '20190703.ipdb'; |
||
18 | |||
19 | private static $reader = null; |
||
20 | |||
21 | private static $cached = []; |
||
22 | |||
23 | /** |
||
24 | * 查询 IP 信息 |
||
25 | * |
||
26 | * @param $ip |
||
27 | * @return mixed|string |
||
28 | */ |
||
29 | public static function find($ip) |
||
30 | { |
||
31 | if (empty($ip) === true) { |
||
32 | return 'N/A'; |
||
33 | } |
||
34 | |||
35 | if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === FALSE) { |
||
36 | throw new \InvalidArgumentException("The value \"$ip\" is not a valid IP address."); |
||
37 | } |
||
38 | |||
39 | $nip = gethostbyname($ip); |
||
40 | if (isset(self::$cached[$nip]) === true) { |
||
41 | return self::$cached[$nip]; |
||
42 | } |
||
43 | |||
44 | try { |
||
45 | $reader = self::init(); |
||
46 | $node = $reader->findNode($ip); |
||
47 | |||
48 | if ($node > 0) { |
||
49 | $data = $reader->resolve($node); |
||
50 | $values = explode("\t", $data); |
||
51 | $location = array_slice($values, $reader->meta['languages']['CN'], count($reader->meta['fields'])); |
||
52 | $location[] = ''; |
||
53 | $locationCode = self::getLocationCode($location); |
||
54 | $location[] = $locationCode; |
||
55 | self::$cached[$nip] = $location; |
||
56 | |||
57 | return self::$cached[$nip]; |
||
58 | } |
||
59 | } catch (\Exception $e) { |
||
60 | return $e->getMessage(); |
||
61 | } |
||
62 | |||
63 | return 'N/A'; |
||
64 | } |
||
65 | |||
66 | /** |
||
67 | * @param $ip |
||
68 | * @return int |
||
69 | * @throws \Exception |
||
70 | */ |
||
71 | private function findNode($ip) |
||
72 | { |
||
73 | static $v4offset = 0; |
||
74 | static $v6offsetCache = []; |
||
75 | |||
76 | $binary = inet_pton($ip); |
||
77 | $bitCount = strlen($binary) * 8; // 32 | 128 |
||
78 | $key = substr($binary, 0, 2); |
||
79 | $node = 0; |
||
80 | $index = 0; |
||
81 | if ($bitCount === 32) { |
||
82 | if ($v4offset === 0) { |
||
83 | for ($i = 0; $i < 96 && $node < $this->nodeCount; $i++) { |
||
84 | if ($i >= 80) { |
||
85 | $idx = 1; |
||
86 | } else { |
||
87 | $idx = 0; |
||
88 | } |
||
89 | $node = $this->readNode($node, $idx); |
||
90 | if ($node > $this->nodeCount) { |
||
91 | return 0; |
||
92 | } |
||
93 | } |
||
94 | $v4offset = $node; |
||
95 | } else { |
||
96 | $node = $v4offset; |
||
97 | } |
||
98 | } else { |
||
99 | if (isset($v6offsetCache[$key])) { |
||
100 | $index = 16; |
||
101 | $node = $v6offsetCache[$key]; |
||
102 | } |
||
103 | } |
||
104 | |||
105 | for ($i = $index; $i < $bitCount; $i++) { |
||
106 | if ($node >= $this->nodeCount) { |
||
107 | break; |
||
108 | } |
||
109 | |||
110 | $node = $this->readNode($node, 1 & ((0xFF & ord($binary[$i >> 3])) >> 7 - ($i % 8))); |
||
111 | |||
112 | if ($i == 15) { |
||
113 | $v6offsetCache[$key] = $node; |
||
114 | } |
||
115 | } |
||
116 | |||
117 | if ($node === $this->nodeCount) { |
||
118 | return 0; |
||
119 | } elseif ($node > $this->nodeCount) { |
||
120 | return $node; |
||
121 | } |
||
122 | |||
123 | throw new \Exception("find node failed"); |
||
124 | } |
||
125 | |||
126 | |||
127 | /** |
||
128 | * @param $node |
||
129 | * @param $index |
||
130 | * @return mixed |
||
131 | * @throws \Exception |
||
132 | */ |
||
133 | private function readNode($node, $index) |
||
134 | { |
||
135 | return unpack('N', $this->read($this->file, $node * 8 + $index * 4, 4))[1]; |
||
136 | } |
||
137 | |||
138 | |||
139 | /** |
||
140 | * @param $node |
||
141 | * @return mixed |
||
142 | * @throws \Exception |
||
143 | */ |
||
144 | private function resolve($node) |
||
145 | { |
||
146 | $resolved = $node - $this->nodeCount + $this->nodeCount * 8; |
||
147 | if ($resolved >= $this->fileSize) { |
||
148 | return NULL; |
||
149 | } |
||
150 | |||
151 | $bytes = $this->read($this->file, $resolved, 2); |
||
152 | $size = unpack('N', str_pad($bytes, 4, "\x00", STR_PAD_LEFT))[1]; |
||
153 | |||
154 | $resolved += 2; |
||
155 | |||
156 | return $this->read($this->file, $resolved, $size); |
||
157 | } |
||
158 | |||
159 | public function close() |
||
160 | { |
||
161 | if (is_resource($this->file) === TRUE) { |
||
162 | fclose($this->file); |
||
163 | } |
||
164 | } |
||
165 | |||
166 | /** |
||
167 | * @param $stream |
||
168 | * @param $offset |
||
169 | * @param $length |
||
170 | * @return bool|string |
||
171 | * @throws \Exception |
||
172 | */ |
||
173 | private function read($stream, $offset, $length) |
||
174 | { |
||
175 | if ($length > 0) { |
||
176 | if (fseek($stream, $offset + $this->nodeOffset) === 0) { |
||
177 | $value = fread($stream, $length); |
||
178 | if (strlen($value) === $length) { |
||
179 | return $value; |
||
180 | } |
||
181 | } |
||
182 | |||
183 | throw new \Exception("The Database file read bad data"); |
||
184 | } |
||
185 | |||
186 | return ''; |
||
187 | } |
||
188 | |||
189 | /** |
||
190 | * 初始化单例 |
||
191 | * |
||
192 | * @return Ip|null |
||
193 | * @throws Exception |
||
194 | */ |
||
195 | private static function init() |
||
196 | { |
||
197 | if (self::$reader) { |
||
198 | return self::$reader; |
||
199 | } |
||
200 | |||
201 | $reader = new self(); |
||
202 | $databaseSrc = __DIR__ . '/' . $reader->database; |
||
203 | if (is_readable($databaseSrc) === FALSE) { |
||
204 | throw new \InvalidArgumentException("The IP Database file \"{$databaseSrc}\" does not exist or is not readable."); |
||
205 | } |
||
206 | $reader->file = @fopen($databaseSrc, 'rb'); |
||
207 | if ($reader->file === FALSE) { |
||
208 | throw new \InvalidArgumentException("IP Database File opening \"{$databaseSrc}\"."); |
||
209 | } |
||
210 | $reader->fileSize = @filesize($databaseSrc); |
||
211 | if ($reader->fileSize === FALSE) { |
||
212 | throw new \UnexpectedValueException("Error determining the size of \"{$databaseSrc}\"."); |
||
213 | } |
||
214 | |||
215 | $metaLength = unpack('N', fread($reader->file, 4))[1]; |
||
216 | $text = fread($reader->file, $metaLength); |
||
217 | |||
218 | $reader->meta = (array)json_decode($text, true); |
||
219 | |||
220 | if (!isset($reader->meta['fields']) || !isset($reader->meta['languages'])) { |
||
221 | throw new \Exception('IP Database metadata error.'); |
||
222 | } |
||
223 | |||
224 | $fileSize = 4 + $metaLength + $reader->meta['total_size']; |
||
225 | if ($fileSize != $reader->fileSize) { |
||
226 | throw new \Exception('IP Database size error.'); |
||
227 | } |
||
228 | |||
229 | $reader->nodeCount = $reader->meta['node_count']; |
||
230 | $reader->nodeOffset = 4 + $metaLength; |
||
231 | |||
232 | self::$reader = $reader; |
||
233 | |||
234 | return $reader; |
||
235 | } |
||
236 | |||
237 | /** |
||
238 | * 获取城市的行政区划编码 |
||
239 | * |
||
240 | * @param $arr |
||
241 | * @return string |
||
242 | */ |
||
243 | private static function getLocationCode($arr) |
||
264 | |||
265 | /** |
||
266 | * 城市的行政区划信息 |
||
267 | * |
||
268 | * @return array |
||
269 | */ |
||
270 | public static function locations() |
||
400 | } |
||
401 |