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 Request 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 Request, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | class Request |
||
15 | { |
||
16 | /// @todo: do these need to be public? |
||
17 | public $payload; |
||
18 | public $methodname; |
||
19 | public $params = array(); |
||
20 | public $debug = 0; |
||
21 | public $content_type = 'text/xml'; |
||
22 | |||
23 | // holds data while parsing the response. NB: Not a full Response object |
||
24 | protected $httpResponse = array(); |
||
25 | |||
26 | /** |
||
27 | * @param string $methodName the name of the method to invoke |
||
28 | * @param Value[] $params array of parameters to be passed to the method (NB: Value objects, not plain php values) |
||
29 | */ |
||
30 | 602 | public function __construct($methodName, $params = array()) |
|
31 | { |
||
32 | 602 | $this->methodname = $methodName; |
|
33 | 602 | foreach ($params as $param) { |
|
34 | 429 | $this->addParam($param); |
|
35 | } |
||
36 | 602 | } |
|
37 | |||
38 | 543 | public function xml_header($charsetEncoding = '') |
|
46 | |||
47 | 543 | public function xml_footer() |
|
51 | |||
52 | 543 | public function createPayload($charsetEncoding = '') |
|
53 | { |
||
54 | 543 | if ($charsetEncoding != '') { |
|
55 | 56 | $this->content_type = 'text/xml; charset=' . $charsetEncoding; |
|
56 | } else { |
||
57 | 487 | $this->content_type = 'text/xml'; |
|
58 | } |
||
59 | 543 | $this->payload = $this->xml_header($charsetEncoding); |
|
60 | 543 | $this->payload .= '<methodName>' . Charset::instance()->encodeEntities( |
|
61 | 543 | $this->methodname, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</methodName>\n"; |
|
62 | 543 | $this->payload .= "<params>\n"; |
|
63 | 543 | foreach ($this->params as $p) { |
|
64 | 486 | $this->payload .= "<param>\n" . $p->serialize($charsetEncoding) . |
|
65 | 486 | "</param>\n"; |
|
66 | } |
||
67 | 543 | $this->payload .= "</params>\n"; |
|
68 | 543 | $this->payload .= $this->xml_footer(); |
|
69 | 543 | } |
|
70 | |||
71 | /** |
||
72 | * Gets/sets the xmlrpc method to be invoked. |
||
73 | * |
||
74 | * @param string $methodName the method to be set (leave empty not to set it) |
||
75 | * |
||
76 | * @return string the method that will be invoked |
||
77 | */ |
||
78 | 38 | public function method($methodName = '') |
|
79 | { |
||
80 | 38 | if ($methodName != '') { |
|
81 | $this->methodname = $methodName; |
||
82 | } |
||
83 | |||
84 | 38 | return $this->methodname; |
|
85 | } |
||
86 | |||
87 | /** |
||
88 | * Returns xml representation of the message. XML prologue included. |
||
89 | * |
||
90 | * @param string $charsetEncoding |
||
91 | * |
||
92 | * @return string the xml representation of the message, xml prologue included |
||
93 | */ |
||
94 | 1 | public function serialize($charsetEncoding = '') |
|
100 | |||
101 | /** |
||
102 | * Add a parameter to the list of parameters to be used upon method invocation. |
||
103 | * |
||
104 | * Checks that $params is actually a Value object and not a plain php value. |
||
105 | * |
||
106 | * @param Value $param |
||
107 | * |
||
108 | * @return boolean false on failure |
||
109 | */ |
||
110 | 486 | public function addParam($param) |
|
121 | |||
122 | /** |
||
123 | * Returns the nth parameter in the request. The index zero-based. |
||
124 | * |
||
125 | * @param integer $i the index of the parameter to fetch (zero based) |
||
126 | * |
||
127 | * @return Value the i-th parameter |
||
128 | */ |
||
129 | 38 | public function getParam($i) |
|
133 | |||
134 | /** |
||
135 | * Returns the number of parameters in the message. |
||
136 | * |
||
137 | * @return integer the number of parameters currently set |
||
138 | */ |
||
139 | 38 | public function getNumParams() |
|
143 | |||
144 | /** |
||
145 | * Given an open file handle, read all data available and parse it as an xmlrpc response. |
||
146 | * |
||
147 | * NB: the file handle is not closed by this function. |
||
148 | * NNB: might have trouble in rare cases to work on network streams, as we check for a read of 0 bytes instead of |
||
149 | * feof($fp). But since checking for feof(null) returns false, we would risk an infinite loop in that case, |
||
150 | * because we cannot trust the caller to give us a valid pointer to an open file... |
||
151 | * |
||
152 | * @param resource $fp stream pointer |
||
153 | * |
||
154 | * @return Response |
||
155 | * |
||
156 | * @todo add 2nd & 3rd param to be passed to ParseResponse() ??? |
||
157 | */ |
||
158 | public function parseResponseFile($fp) |
||
159 | { |
||
160 | $ipd = ''; |
||
161 | while ($data = fread($fp, 32768)) { |
||
162 | $ipd .= $data; |
||
163 | } |
||
164 | return $this->parseResponse($ipd); |
||
165 | } |
||
166 | |||
167 | /** |
||
168 | * Parse the xmlrpc response contained in the string $data and return a Response object. |
||
169 | * |
||
170 | * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding. |
||
171 | * |
||
172 | * @param string $data the xmlrpc response, possibly including http headers |
||
173 | * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and |
||
174 | * consequent decoding |
||
175 | * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or |
||
176 | * 'phpvals' |
||
177 | * |
||
178 | * @return Response |
||
179 | */ |
||
180 | 510 | public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals') |
|
181 | { |
||
182 | 510 | if ($this->debug) { |
|
183 | 462 | Logger::instance()->debugMessage("---GOT---\n$data\n---END---"); |
|
184 | } |
||
185 | |||
186 | 510 | $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array()); |
|
187 | |||
188 | 510 | View Code Duplication | if ($data == '') { |
|
|||
189 | error_log('XML-RPC: ' . __METHOD__ . ': no response received from server.'); |
||
190 | return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']); |
||
191 | } |
||
192 | |||
193 | // parse the HTTP headers of the response, if present, and separate them from data |
||
194 | 510 | if (substr($data, 0, 4) == 'HTTP') { |
|
195 | 498 | $httpParser = new Http(); |
|
196 | try { |
||
197 | 498 | $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug); |
|
198 | 495 | } catch(\Exception $e) { |
|
199 | 495 | $r = new Response(0, $e->getCode(), $e->getMessage()); |
|
200 | // failed processing of HTTP response headers |
||
201 | // save into response obj the full payload received, for debugging |
||
202 | 495 | $r->raw_data = $data; |
|
203 | |||
204 | 495 | return $r; |
|
205 | } |
||
206 | } |
||
207 | |||
208 | // be tolerant of extra whitespace in response body |
||
209 | 15 | $data = trim($data); |
|
210 | |||
211 | /// @todo return an error msg if $data=='' ? |
||
212 | |||
213 | // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts) |
||
214 | // idea from Luca Mariano <[email protected]> originally in PEARified version of the lib |
||
215 | 15 | $pos = strrpos($data, '</methodResponse>'); |
|
216 | 15 | if ($pos !== false) { |
|
217 | 15 | $data = substr($data, 0, $pos + 17); |
|
218 | } |
||
219 | |||
220 | // try to 'guestimate' the character encoding of the received response |
||
221 | 15 | $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data); |
|
222 | |||
223 | 15 | if ($this->debug) { |
|
224 | 15 | $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); |
|
225 | 15 | if ($start) { |
|
226 | $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); |
||
227 | $end = strpos($data, '-->', $start); |
||
228 | $comments = substr($data, $start, $end - $start); |
||
229 | Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" . |
||
230 | str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding); |
||
231 | } |
||
232 | } |
||
233 | |||
234 | // if user wants back raw xml, give it to him |
||
235 | 15 | if ($returnType == 'xml') { |
|
236 | 1 | $r = new Response($data, 0, '', 'xml'); |
|
237 | 1 | $r->hdrs = $this->httpResponse['headers']; |
|
238 | 1 | $r->_cookies = $this->httpResponse['cookies']; |
|
239 | 1 | $r->raw_data = $this->httpResponse['raw_data']; |
|
240 | |||
241 | 1 | return $r; |
|
242 | } |
||
243 | |||
244 | 14 | View Code Duplication | if ($respEncoding != '') { |
245 | |||
246 | // Since parsing will fail if charset is not specified in the xml prologue, |
||
247 | // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that... |
||
248 | // The following code might be better for mb_string enabled installs, but |
||
249 | // makes the lib about 200% slower... |
||
250 | //if (!is_valid_charset($respEncoding, array('UTF-8'))) |
||
251 | 14 | if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) { |
|
252 | 1 | if ($respEncoding == 'ISO-8859-1') { |
|
253 | 1 | $data = utf8_encode($data); |
|
254 | } else { |
||
255 | if (extension_loaded('mbstring')) { |
||
256 | $data = mb_convert_encoding($data, 'UTF-8', $respEncoding); |
||
257 | } else { |
||
258 | error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding); |
||
259 | } |
||
260 | } |
||
261 | } |
||
262 | } |
||
263 | |||
264 | 14 | $parser = xml_parser_create(); |
|
265 | 14 | xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true); |
|
266 | // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell |
||
267 | // the xml parser to give us back data in the expected charset. |
||
268 | // What if internal encoding is not in one of the 3 allowed? |
||
269 | // we use the broadest one, ie. utf8 |
||
270 | // This allows to send data which is native in various charset, |
||
271 | // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding |
||
272 | 14 | View Code Duplication | if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) { |
273 | xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); |
||
274 | } else { |
||
275 | 14 | xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding); |
|
276 | } |
||
277 | |||
278 | 14 | $xmlRpcParser = new XMLParser(); |
|
279 | 14 | xml_set_object($parser, $xmlRpcParser); |
|
280 | |||
281 | 14 | if ($returnType == 'phpvals') { |
|
282 | 3 | xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast'); |
|
283 | } else { |
||
284 | 11 | xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee'); |
|
285 | } |
||
286 | |||
287 | 14 | xml_set_character_data_handler($parser, 'xmlrpc_cd'); |
|
288 | 14 | xml_set_default_handler($parser, 'xmlrpc_dh'); |
|
289 | |||
290 | // first error check: xml not well formed |
||
291 | 14 | if (!xml_parse($parser, $data, 1)) { |
|
292 | // thanks to Peter Kocks <[email protected]> |
||
293 | 1 | if ((xml_get_current_line_number($parser)) == 1) { |
|
294 | $errStr = 'XML error at line 1, check URL'; |
||
295 | } else { |
||
296 | 1 | $errStr = sprintf('XML error: %s at line %d, column %d', |
|
297 | 1 | xml_error_string(xml_get_error_code($parser)), |
|
298 | 1 | xml_get_current_line_number($parser), xml_get_current_column_number($parser)); |
|
299 | } |
||
300 | 1 | error_log($errStr); |
|
301 | 1 | $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $errStr); |
|
302 | 1 | xml_parser_free($parser); |
|
303 | 1 | if ($this->debug) { |
|
304 | 1 | print $errStr; |
|
305 | } |
||
306 | 1 | $r->hdrs = $this->httpResponse['headers']; |
|
307 | 1 | $r->_cookies = $this->httpResponse['cookies']; |
|
308 | 1 | $r->raw_data = $this->httpResponse['raw_data']; |
|
309 | |||
310 | 1 | return $r; |
|
311 | } |
||
312 | 14 | xml_parser_free($parser); |
|
313 | // second error check: xml well formed but not xml-rpc compliant |
||
314 | 14 | if ($xmlRpcParser->_xh['isf'] > 1) { |
|
315 | 4 | if ($this->debug) { |
|
316 | /// @todo echo something for user? |
||
317 | } |
||
318 | |||
319 | 4 | $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], |
|
320 | 4 | PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']); |
|
321 | } |
||
322 | // third error check: parsing of the response has somehow gone boink. |
||
323 | // NB: shall we omit this check, since we trust the parsing code? |
||
324 | 11 | elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) { |
|
325 | // something odd has happened |
||
326 | // and it's time to generate a client side error |
||
327 | // indicating something odd went on |
||
328 | $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], |
||
329 | PhpXmlRpc::$xmlrpcstr['invalid_return']); |
||
330 | } else { |
||
331 | 11 | if ($this->debug > 1) { |
|
332 | Logger::instance()->debugMessage( |
||
333 | "---PARSED---\n".var_export($xmlRpcParser->_xh['value'], true)."\n---END---" |
||
334 | ); |
||
335 | } |
||
336 | |||
337 | // note that using =& will raise an error if $xmlRpcParser->_xh['st'] does not generate an object. |
||
338 | 11 | $v = &$xmlRpcParser->_xh['value']; |
|
339 | |||
340 | 11 | if ($xmlRpcParser->_xh['isf']) { |
|
341 | /// @todo we should test here if server sent an int and a string, and/or coerce them into such... |
||
342 | 1 | if ($returnType == 'xmlrpcvals') { |
|
343 | 1 | $errNo_v = $v['faultCode']; |
|
344 | 1 | $errStr_v = $v['faultString']; |
|
345 | 1 | $errNo = $errNo_v->scalarval(); |
|
346 | 1 | $errStr = $errStr_v->scalarval(); |
|
347 | } else { |
||
348 | $errNo = $v['faultCode']; |
||
349 | $errStr = $v['faultString']; |
||
350 | } |
||
351 | |||
352 | 1 | if ($errNo == 0) { |
|
353 | // FAULT returned, errno needs to reflect that |
||
354 | $errNo = -1; |
||
355 | } |
||
356 | |||
357 | 1 | $r = new Response(0, $errNo, $errStr); |
|
358 | } else { |
||
359 | 10 | $r = new Response($v, 0, '', $returnType); |
|
360 | } |
||
361 | } |
||
362 | |||
363 | 14 | $r->hdrs = $this->httpResponse['headers']; |
|
364 | 14 | $r->_cookies = $this->httpResponse['cookies']; |
|
365 | 14 | $r->raw_data = $this->httpResponse['raw_data']; |
|
366 | |||
367 | 14 | return $r; |
|
368 | } |
||
369 | |||
370 | /** |
||
371 | * Kept the old name even if Request class was renamed, for compatibility. |
||
372 | * |
||
373 | * @return string |
||
374 | */ |
||
375 | public function kindOf() |
||
376 | { |
||
377 | return 'msg'; |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * Enables/disables the echoing to screen of the xmlrpc responses received. |
||
382 | * |
||
383 | * @param integer $level values 0, 1, 2 are supported |
||
384 | */ |
||
385 | 601 | public function setDebug($level) |
|
389 | } |
||
390 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.