Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AMQPAbstractCollection 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 AMQPAbstractCollection, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
11 | abstract class AMQPAbstractCollection implements \Iterator |
||
12 | { |
||
13 | |||
14 | //protocol defines available field types and their corresponding symbols |
||
15 | const PROTOCOL_080 = AbstractChannel::PROTOCOL_080; |
||
16 | const PROTOCOL_091 = AbstractChannel::PROTOCOL_091; |
||
17 | const PROTOCOL_RBT = 'rabbit'; //pseudo proto |
||
18 | |||
19 | //Abstract data types |
||
20 | const T_INT_SHORTSHORT = 1; |
||
21 | const T_INT_SHORTSHORT_U = 2; |
||
22 | const T_INT_SHORT = 3; |
||
23 | const T_INT_SHORT_U = 4; |
||
24 | const T_INT_LONG = 5; |
||
25 | const T_INT_LONG_U = 6; |
||
26 | const T_INT_LONGLONG = 7; |
||
27 | const T_INT_LONGLONG_U = 8; |
||
28 | |||
29 | const T_DECIMAL = 9; |
||
30 | const T_TIMESTAMP = 10; |
||
31 | const T_VOID = 11; |
||
32 | |||
33 | const T_BOOL = 12; |
||
34 | |||
35 | const T_STRING_SHORT = 13; |
||
36 | const T_STRING_LONG = 14; |
||
37 | |||
38 | const T_ARRAY = 15; |
||
39 | const T_TABLE = 16; |
||
40 | |||
41 | /** |
||
42 | * @var string |
||
43 | */ |
||
44 | private static $_protocol = null; |
||
45 | |||
46 | /* |
||
47 | * Field types messy mess http://www.rabbitmq.com/amqp-0-9-1-errata.html#section_3 |
||
48 | * Default behaviour is to use rabbitMQ compatible field-set |
||
49 | * Define AMQP_STRICT_FLD_TYPES=true to use strict AMQP instead |
||
50 | */ |
||
51 | private static $_types_080 = array( |
||
52 | self::T_INT_LONG => 'I', |
||
53 | self::T_DECIMAL => 'D', |
||
54 | self::T_TIMESTAMP => 'T', |
||
55 | self::T_STRING_LONG => 'S', |
||
56 | self::T_TABLE => 'F' |
||
57 | ); |
||
58 | |||
59 | /** |
||
60 | * @var array |
||
61 | */ |
||
62 | private static $_types_091 = array( |
||
63 | self::T_INT_SHORTSHORT => 'b', |
||
64 | self::T_INT_SHORTSHORT_U => 'B', |
||
65 | self::T_INT_SHORT => 'U', |
||
66 | self::T_INT_SHORT_U => 'u', |
||
67 | self::T_INT_LONG => 'I', |
||
68 | self::T_INT_LONG_U => 'i', |
||
69 | self::T_INT_LONGLONG => 'L', |
||
70 | self::T_INT_LONGLONG_U => 'l', |
||
71 | self::T_DECIMAL => 'D', |
||
72 | self::T_TIMESTAMP => 'T', |
||
73 | self::T_VOID => 'V', |
||
74 | self::T_BOOL => 't', |
||
75 | self::T_STRING_SHORT => 's', |
||
76 | self::T_STRING_LONG => 'S', |
||
77 | self::T_ARRAY => 'A', |
||
78 | self::T_TABLE => 'F' |
||
79 | ); |
||
80 | |||
81 | /** |
||
82 | * @var array |
||
83 | */ |
||
84 | private static $_types_rabbit = array( |
||
85 | self::T_INT_SHORTSHORT => 'b', |
||
86 | self::T_INT_SHORT => 's', |
||
87 | self::T_INT_LONG => 'I', |
||
88 | self::T_INT_LONGLONG => 'l', |
||
89 | self::T_DECIMAL => 'D', |
||
90 | self::T_TIMESTAMP => 'T', |
||
91 | self::T_VOID => 'V', |
||
92 | self::T_BOOL => 't', |
||
93 | self::T_STRING_LONG => 'S', |
||
94 | self::T_ARRAY => 'A', |
||
95 | self::T_TABLE => 'F' |
||
96 | ); |
||
97 | |||
98 | /** |
||
99 | * @var array |
||
100 | */ |
||
101 | protected $data = array(); |
||
102 | |||
103 | 190 | public function __construct(array $data = null) |
|
104 | { |
||
105 | 190 | if (!empty($data)) { |
|
106 | 110 | $this->data = $this->encodeCollection($data); |
|
107 | 84 | } |
|
108 | 185 | } |
|
109 | |||
110 | /** |
||
111 | * @return int |
||
112 | */ |
||
113 | abstract public function getType(); |
||
114 | |||
115 | /** |
||
116 | * @param mixed $val |
||
117 | * @param int $type |
||
118 | * @param string $key |
||
119 | */ |
||
120 | 105 | final protected function setValue($val, $type = null, $key = null) |
|
121 | { |
||
122 | 105 | if ($val instanceof self) { |
|
123 | 35 | if ($type && ($type != $val->getType())) { |
|
|
|||
124 | 5 | throw new Exception\AMQPInvalidArgumentException( |
|
125 | 5 | 'Attempted to add instance of ' . get_class($val) . ' representing type [' . $val->getType() . '] as mismatching type [' . $type . ']' |
|
126 | 4 | ); |
|
127 | } |
||
128 | 30 | $type = $val->getType(); |
|
129 | 99 | } elseif ($type) { //ensuring data integrity and that all members are properly validated |
|
130 | switch ($type) { |
||
131 | 90 | case self::T_ARRAY: |
|
132 | 5 | throw new Exception\AMQPInvalidArgumentException('Arrays must be passed as AMQPArray instance'); |
|
133 | break; |
||
134 | 85 | case self::T_TABLE: |
|
135 | 5 | throw new Exception\AMQPInvalidArgumentException('Tables must be passed as AMQPTable instance'); |
|
136 | break; |
||
137 | 80 | case self::T_DECIMAL: |
|
138 | 5 | if (!($val instanceof AMQPDecimal)) { |
|
139 | 5 | throw new Exception\AMQPInvalidArgumentException('Decimal values must be instance of AMQPDecimal'); |
|
140 | } |
||
141 | break; |
||
142 | } |
||
143 | 60 | } |
|
144 | |||
145 | 85 | if ($type) { |
|
146 | 80 | self::checkDataTypeIsSupported($type, false); |
|
147 | 65 | $val = array($type, $val); |
|
148 | 52 | } else { |
|
149 | 5 | $val = $this->encodeValue($val); |
|
150 | } |
||
151 | |||
152 | 70 | if ($key === null) { |
|
153 | 25 | $this->data[] = $val; |
|
154 | 20 | } else { |
|
155 | 60 | $this->data[$key] = $val; |
|
156 | } |
||
157 | 70 | } |
|
158 | |||
159 | /** |
||
160 | * @return array |
||
161 | */ |
||
162 | 80 | final public function getNativeData() |
|
166 | |||
167 | /** |
||
168 | * @param array $val |
||
169 | * @return array |
||
170 | */ |
||
171 | 110 | final protected function encodeCollection(array $val) |
|
172 | { |
||
173 | 110 | foreach ($val as $k=>$v) { |
|
174 | 110 | $val[$k] = $this->encodeValue($v); |
|
175 | 84 | } |
|
176 | |||
177 | 105 | return $val; |
|
178 | } |
||
179 | |||
180 | /** |
||
181 | * @param array $val |
||
182 | * @return array |
||
183 | */ |
||
184 | 80 | final protected function decodeCollection(array $val) |
|
185 | { |
||
186 | 80 | foreach ($val as $k=>$v) { |
|
187 | 80 | $val[$k] = $this->decodeValue($v[1], $v[0]); |
|
188 | 64 | } |
|
189 | |||
190 | 80 | return $val; |
|
191 | } |
||
192 | |||
193 | /** |
||
194 | * @param mixed $val |
||
195 | * @return mixed |
||
196 | * @throws Exception\AMQPOutOfBoundsException |
||
197 | */ |
||
198 | 115 | protected function encodeValue($val) |
|
199 | { |
||
200 | 115 | if (is_string($val)) { |
|
201 | 105 | $val = $this->encodeString($val); |
|
202 | 110 | } elseif (is_float($val)) { |
|
203 | 40 | $val = $this->encodeFloat($val); |
|
204 | 90 | } elseif (is_int($val)) { |
|
205 | 75 | $val = $this->encodeInt($val); |
|
206 | 90 | } elseif (is_bool($val)) { |
|
207 | 75 | $val = $this->encodeBool($val); |
|
208 | 88 | } elseif (is_null($val)) { |
|
209 | 35 | $val = $this->encodeVoid(); |
|
210 | 64 | } elseif ($val instanceof \DateTime) { |
|
211 | $val = array(self::T_TIMESTAMP, $val->getTimestamp()); |
||
212 | 61 | } elseif ($val instanceof AMQPDecimal) { |
|
213 | $val = array(self::T_DECIMAL, $val); |
||
214 | 75 | } elseif ($val instanceof self) { |
|
215 | //avoid silent type correction of strictly typed values |
||
216 | self::checkDataTypeIsSupported($val->getType(), false); |
||
217 | $val = array($val->getType(), $val); |
||
218 | 75 | } elseif (is_array($val)) { |
|
219 | //AMQP specs says "Field names MUST start with a letter, '$' or '#'" |
||
220 | //so beware, some servers may raise an exception with 503 code in cases when indexed array is encoded as table |
||
221 | 70 | if (self::isProtocol(self::PROTOCOL_080)) { |
|
222 | //080 doesn't support arrays, forcing table |
||
223 | 15 | $val = array(self::T_TABLE, new AMQPTable($val)); |
|
224 | 67 | } elseif (empty($val) || (array_keys($val) === range(0, count($val) - 1))) { |
|
225 | 50 | $val = array(self::T_ARRAY, new AMQPArray($val)); |
|
226 | 40 | } else { |
|
227 | 50 | $val = array(self::T_TABLE, new AMQPTable($val)); |
|
228 | } |
||
229 | 56 | } else { |
|
230 | 5 | throw new Exception\AMQPOutOfBoundsException(sprintf('Encountered value of unsupported type: %s', gettype($val))); |
|
231 | } |
||
232 | |||
233 | 110 | return $val; |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * @param mixed $val |
||
238 | * @param integer $type |
||
239 | * @return array|bool|\DateTime|null |
||
240 | */ |
||
241 | 80 | protected function decodeValue($val, $type) |
|
242 | { |
||
243 | 80 | if ($val instanceof self) { |
|
244 | //covering arrays and tables |
||
245 | 50 | $val = $val->getNativeData(); |
|
246 | 40 | } else { |
|
247 | switch ($type) { |
||
248 | 75 | case self::T_BOOL: |
|
249 | 30 | $val = (bool) $val; |
|
250 | 30 | break; |
|
251 | 75 | case self::T_TIMESTAMP: |
|
252 | $val = \DateTime::createFromFormat('U', $val); |
||
253 | break; |
||
254 | 75 | case self::T_VOID: |
|
255 | 25 | $val = null; |
|
256 | 25 | break; |
|
257 | 70 | case self::T_ARRAY: |
|
258 | 70 | case self::T_TABLE: |
|
259 | throw new Exception\AMQPLogicException( |
||
260 | 'Encountered an array/table struct which is not an instance of AMQPCollection. ' . |
||
261 | 'This is considered a bug and should be fixed, please report' |
||
262 | ); |
||
263 | } |
||
264 | } |
||
265 | |||
266 | 80 | return $val; |
|
267 | } |
||
268 | |||
269 | /** |
||
270 | * @param string $val |
||
271 | * @return array |
||
272 | */ |
||
273 | 105 | protected function encodeString($val) |
|
274 | { |
||
275 | 105 | return array(self::T_STRING_LONG, $val); |
|
276 | } |
||
277 | |||
278 | /** |
||
279 | * @param int $val |
||
280 | * @return array |
||
281 | */ |
||
282 | 75 | protected function encodeInt($val) |
|
283 | { |
||
284 | 75 | if (($val >= -2147483648) && ($val <= 2147483647)) { |
|
285 | 75 | $ev = array(self::T_INT_LONG, $val); |
|
286 | 71 | } elseif (self::isProtocol(self::PROTOCOL_080)) { |
|
287 | //080 doesn't support longlong |
||
288 | 15 | $ev = $this->encodeString((string) $val); |
|
289 | 12 | } else { |
|
290 | 40 | $ev = array(self::T_INT_LONGLONG, $val); |
|
291 | } |
||
292 | |||
293 | 75 | return $ev; |
|
294 | } |
||
295 | |||
296 | /** |
||
297 | * @param float $val |
||
298 | * @return array |
||
299 | */ |
||
300 | 40 | protected function encodeFloat($val) |
|
301 | { |
||
302 | 40 | return static::encodeString((string) $val); |
|
303 | } |
||
304 | |||
305 | /** |
||
306 | * @param bool $val |
||
307 | * @return array |
||
308 | */ |
||
309 | 75 | protected function encodeBool($val) |
|
310 | { |
||
311 | 75 | $val = (bool) $val; |
|
312 | |||
313 | 75 | return self::isProtocol(self::PROTOCOL_080) ? array(self::T_INT_LONG, (int) $val) : array(self::T_BOOL, $val); |
|
314 | } |
||
315 | |||
316 | /** |
||
317 | * @return array |
||
318 | */ |
||
319 | 35 | protected function encodeVoid() |
|
320 | { |
||
321 | 35 | return self::isProtocol(self::PROTOCOL_080) ? $this->encodeString('') : array(self::T_VOID, null); |
|
322 | } |
||
323 | |||
324 | /** |
||
325 | * @return string |
||
326 | */ |
||
327 | 215 | final public static function getProtocol() |
|
328 | { |
||
329 | 215 | if (self::$_protocol === null) { |
|
330 | 5 | self::$_protocol = defined('AMQP_STRICT_FLD_TYPES') && AMQP_STRICT_FLD_TYPES ? |
|
331 | 4 | AbstractChannel::getProtocolVersion() : |
|
332 | 5 | self::PROTOCOL_RBT; |
|
333 | 4 | } |
|
334 | |||
335 | 215 | return self::$_protocol; |
|
336 | } |
||
337 | |||
338 | /** |
||
339 | * @param string $proto |
||
340 | * @return bool |
||
341 | */ |
||
342 | 85 | final public static function isProtocol($proto) |
|
343 | { |
||
344 | 85 | return self::getProtocol() == $proto; |
|
345 | } |
||
346 | |||
347 | /** |
||
348 | * @return array [dataTypeConstant => dataTypeSymbol] |
||
349 | */ |
||
350 | 185 | final public static function getSupportedDataTypes() |
|
351 | { |
||
352 | 185 | switch ($proto = self::getProtocol()) { |
|
353 | 185 | case self::PROTOCOL_080: |
|
354 | 10 | $types = self::$_types_080; |
|
355 | 10 | break; |
|
356 | 175 | case self::PROTOCOL_091: |
|
357 | 40 | $types = self::$_types_091; |
|
358 | 40 | break; |
|
359 | 140 | case self::PROTOCOL_RBT: |
|
360 | 140 | $types = self::$_types_rabbit; |
|
361 | 140 | break; |
|
362 | default: |
||
363 | throw new Exception\AMQPOutOfRangeException(sprintf('Unknown protocol: %s', $proto)); |
||
364 | 148 | } |
|
365 | |||
366 | 185 | return $types; |
|
367 | } |
||
368 | |||
369 | /** |
||
370 | * @param string $type |
||
371 | * @param bool $return Whether to return or raise AMQPOutOfRangeException |
||
372 | * @return boolean |
||
373 | */ |
||
374 | 80 | final public static function checkDataTypeIsSupported($type, $return = true) |
|
375 | { |
||
376 | try { |
||
377 | 80 | $supported = self::getSupportedDataTypes(); |
|
378 | 80 | if (!isset($supported[$type])) { |
|
379 | 15 | throw new Exception\AMQPOutOfRangeException(sprintf('AMQP-%s doesn\'t support data of type [%s]', self::getProtocol(), $type)); |
|
380 | } |
||
381 | 65 | return true; |
|
382 | |||
383 | 15 | } catch (Exception\AMQPOutOfRangeException $ex) { |
|
384 | 15 | if (!$return) { |
|
385 | 15 | throw $ex; |
|
386 | } |
||
387 | |||
388 | return false; |
||
389 | } |
||
390 | } |
||
391 | |||
392 | /** |
||
393 | * @param integer $type |
||
394 | * @return string |
||
395 | */ |
||
396 | 135 | View Code Duplication | final public static function getSymbolForDataType($type) |
397 | { |
||
398 | 135 | $types = self::getSupportedDataTypes(); |
|
399 | 135 | if (!isset($types[$type])) { |
|
400 | throw new Exception\AMQPOutOfRangeException(sprintf('AMQP-%s doesn\'t support data of type [%s]', self::getProtocol(), $type)); |
||
401 | } |
||
402 | |||
403 | 135 | return $types[$type]; |
|
404 | } |
||
405 | |||
406 | /** |
||
407 | * @param string $symbol |
||
408 | * @return integer |
||
409 | */ |
||
410 | 155 | View Code Duplication | final public static function getDataTypeForSymbol($symbol) |
411 | { |
||
412 | 155 | $symbols = array_flip(self::getSupportedDataTypes()); |
|
413 | 155 | if (!isset($symbols[$symbol])) { |
|
414 | 5 | throw new Exception\AMQPOutOfRangeException(sprintf('AMQP-%s doesn\'t define data of type [%s]', self::getProtocol(), $symbol)); |
|
415 | } |
||
416 | |||
417 | 150 | return $symbols[$symbol]; |
|
418 | } |
||
419 | |||
420 | 55 | public function current() |
|
421 | { |
||
422 | 55 | return current($this->data); |
|
423 | } |
||
424 | |||
425 | 25 | public function key() |
|
426 | { |
||
427 | 25 | return key($this->data); |
|
428 | } |
||
429 | |||
430 | 55 | public function next() |
|
431 | { |
||
432 | 55 | next($this->data); |
|
433 | 55 | } |
|
434 | |||
435 | 60 | public function rewind() |
|
436 | { |
||
437 | 60 | reset($this->data); |
|
438 | 60 | } |
|
439 | |||
440 | 60 | public function valid() |
|
444 | } |
||
445 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: