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 MediawikiApi 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 MediawikiApi, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class MediawikiApi implements MediawikiApiInterface, LoggerAwareInterface { |
||
28 | |||
29 | /** |
||
30 | * @var ClientInterface|null Should be accessed through getClient |
||
31 | */ |
||
32 | private $client = null; |
||
33 | |||
34 | /** |
||
35 | * @var bool|string |
||
36 | */ |
||
37 | private $isLoggedIn; |
||
38 | |||
39 | /** |
||
40 | * @var MediawikiSession |
||
41 | */ |
||
42 | private $session; |
||
43 | |||
44 | /** |
||
45 | * @var string |
||
46 | */ |
||
47 | private $version; |
||
48 | |||
49 | /** |
||
50 | * @var LoggerInterface |
||
51 | */ |
||
52 | private $logger; |
||
53 | |||
54 | /** |
||
55 | * @var string |
||
56 | */ |
||
57 | private $apiUrl; |
||
58 | |||
59 | /** |
||
60 | * @since 2.0 |
||
61 | * |
||
62 | * @param string $apiEndpoint e.g. https://en.wikipedia.org/w/api.php |
||
63 | * |
||
64 | * @return self returns a MediawikiApi instance using $apiEndpoint |
||
65 | */ |
||
66 | public static function newFromApiEndpoint( $apiEndpoint ) { |
||
69 | |||
70 | /** |
||
71 | * Create a new MediawikiApi object from a URL to any page in a MediaWiki website. |
||
72 | * |
||
73 | * @since 2.0 |
||
74 | * @see https://en.wikipedia.org/wiki/Really_Simple_Discovery |
||
75 | * |
||
76 | * @param string $url e.g. https://en.wikipedia.org OR https://de.wikipedia.org/wiki/Berlin |
||
77 | * @return self returns a MediawikiApi instance using the apiEndpoint provided by the RSD |
||
78 | * file accessible on all Mediawiki pages |
||
79 | * @throws RsdException If the RSD URL could not be found in the page's HTML. |
||
80 | */ |
||
81 | 3 | public static function newFromPage( $url ) { |
|
113 | |||
114 | /** |
||
115 | * @param string $apiUrl The API Url |
||
116 | * @param ClientInterface|null $client Guzzle Client |
||
117 | * @param MediawikiSession|null $session Inject a custom session here |
||
118 | */ |
||
119 | 25 | public function __construct( $apiUrl, ClientInterface $client = null, |
|
134 | |||
135 | /** |
||
136 | * Get the API URL (the URL to which API requests are sent, usually ending in api.php). |
||
137 | * This is useful if you've created this object via MediawikiApi::newFromPage(). |
||
138 | * |
||
139 | * @since 2.3 |
||
140 | * |
||
141 | * @return string The API URL. |
||
142 | */ |
||
143 | public function getApiUrl() { |
||
146 | |||
147 | /** |
||
148 | * @return ClientInterface |
||
149 | */ |
||
150 | 21 | private function getClient() { |
|
158 | |||
159 | /** |
||
160 | * Sets a logger instance on the object |
||
161 | * |
||
162 | * @since 1.1 |
||
163 | * |
||
164 | * @param LoggerInterface $logger The new Logger object. |
||
165 | * |
||
166 | * @return null |
||
167 | */ |
||
168 | 1 | public function setLogger( LoggerInterface $logger ) { |
|
172 | |||
173 | /** |
||
174 | * @since 2.0 |
||
175 | * |
||
176 | * @param Request $request The GET request to send. |
||
177 | * |
||
178 | * @return PromiseInterface |
||
179 | * Normally promising an array, though can be mixed (json_decode result) |
||
180 | * Can throw UsageExceptions or RejectionExceptions |
||
181 | */ |
||
182 | 1 | View Code Duplication | public function getRequestAsync( Request $request ) { |
|
|||
183 | 1 | $promise = $this->getClient()->requestAsync( |
|
184 | 1 | 'GET', |
|
185 | 1 | $this->apiUrl, |
|
186 | 1 | $this->getClientRequestOptions( $request, 'query' ) |
|
187 | ); |
||
188 | |||
189 | return $promise->then( function ( ResponseInterface $response ) { |
||
190 | 1 | return call_user_func( [ $this, 'decodeResponse' ], $response ); |
|
191 | 1 | } ); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * @since 2.0 |
||
196 | * |
||
197 | * @param Request $request The POST request to send. |
||
198 | * |
||
199 | * @return PromiseInterface |
||
200 | * Normally promising an array, though can be mixed (json_decode result) |
||
201 | * Can throw UsageExceptions or RejectionExceptions |
||
202 | */ |
||
203 | 1 | View Code Duplication | public function postRequestAsync( Request $request ) { |
204 | 1 | $promise = $this->getClient()->requestAsync( |
|
205 | 1 | 'POST', |
|
206 | 1 | $this->apiUrl, |
|
207 | 1 | $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) |
|
208 | ); |
||
209 | |||
210 | return $promise->then( function ( ResponseInterface $response ) { |
||
211 | 1 | return call_user_func( [ $this, 'decodeResponse' ], $response ); |
|
212 | 1 | } ); |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * @since 0.2 |
||
217 | * |
||
218 | * @param Request $request The GET request to send. |
||
219 | * |
||
220 | * @return mixed Normally an array |
||
221 | */ |
||
222 | 9 | View Code Duplication | public function getRequest( Request $request ) { |
223 | 9 | $response = $this->getClient()->request( |
|
224 | 9 | 'GET', |
|
225 | 9 | $this->apiUrl, |
|
226 | 9 | $this->getClientRequestOptions( $request, 'query' ) |
|
227 | ); |
||
228 | |||
229 | 9 | return $this->decodeResponse( $response ); |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * @since 0.2 |
||
234 | * |
||
235 | * @param Request $request The POST request to send. |
||
236 | * |
||
237 | * @return mixed Normally an array |
||
238 | */ |
||
239 | 10 | View Code Duplication | public function postRequest( Request $request ) { |
240 | 10 | $response = $this->getClient()->request( |
|
241 | 10 | 'POST', |
|
242 | 10 | $this->apiUrl, |
|
243 | 10 | $this->getClientRequestOptions( $request, $this->getPostRequestEncoding( $request ) ) |
|
244 | ); |
||
245 | |||
246 | 10 | return $this->decodeResponse( $response ); |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * @param ResponseInterface $response |
||
251 | * |
||
252 | * @return mixed |
||
253 | * @throws UsageException |
||
254 | */ |
||
255 | 21 | private function decodeResponse( ResponseInterface $response ) { |
|
263 | |||
264 | /** |
||
265 | * @param Request $request |
||
266 | * |
||
267 | * @return string |
||
268 | */ |
||
269 | 9 | private function getPostRequestEncoding( Request $request ) { |
|
280 | |||
281 | /** |
||
282 | * @param Request $request |
||
283 | * @param string $paramsKey either 'query' or 'multipart' |
||
284 | * |
||
285 | * @throws RequestException |
||
286 | * |
||
287 | * @return array as needed by ClientInterface::get and ClientInterface::post |
||
288 | */ |
||
289 | 21 | private function getClientRequestOptions( Request $request, $paramsKey ) { |
|
300 | |||
301 | /** |
||
302 | * Turn the normal key-value array of request parameters into a multipart array where each |
||
303 | * parameter is a new array with a 'name' and 'contents' elements (and optionally more, if the |
||
304 | * request is a MultipartRequest). |
||
305 | * |
||
306 | * @param Request $request The request to which the parameters belong. |
||
307 | * @param string[] $params The existing parameters. Not the same as $request->getParams(). |
||
308 | * |
||
309 | * @return array |
||
310 | */ |
||
311 | 1 | private function encodeMultipartParams( Request $request, $params ) { |
|
332 | |||
333 | /** |
||
334 | * @return array |
||
335 | */ |
||
336 | 17 | private function getDefaultHeaders() { |
|
341 | |||
342 | 17 | private function getUserAgent() { |
|
349 | |||
350 | /** |
||
351 | * @param array $result |
||
352 | */ |
||
353 | 18 | private function logWarnings( $result ) { |
|
370 | |||
371 | /** |
||
372 | * @param array $array Array response to look for warning in. |
||
373 | * |
||
374 | * @return bool Whether any warning has been logged or not. |
||
375 | */ |
||
376 | 17 | protected function logWarning( $array ) { |
|
399 | |||
400 | /** |
||
401 | * @param array $result |
||
402 | * |
||
403 | * @throws UsageException |
||
404 | */ |
||
405 | 17 | private function throwUsageExceptions( $result ) { |
|
414 | |||
415 | /** |
||
416 | * @since 0.1 |
||
417 | * |
||
418 | * @return bool|string false or the name of the current user |
||
419 | */ |
||
420 | 17 | public function isLoggedin() { |
|
423 | |||
424 | /** |
||
425 | * @since 0.1 |
||
426 | * |
||
427 | * @param ApiUser $apiUser The ApiUser to log in as. |
||
428 | * |
||
429 | * @throws UsageException |
||
430 | * @return bool success |
||
431 | */ |
||
432 | 2 | public function login( ApiUser $apiUser ) { |
|
450 | |||
451 | /** |
||
452 | * @param ApiUser $apiUser |
||
453 | * |
||
454 | * @return string[] |
||
455 | */ |
||
456 | 2 | private function getLoginParams( ApiUser $apiUser ) { |
|
467 | |||
468 | /** |
||
469 | * @param array $result |
||
470 | * |
||
471 | * @throws UsageException |
||
472 | */ |
||
473 | 1 | private function throwLoginUsageException( $result ) { |
|
484 | |||
485 | /** |
||
486 | * @since 0.1 |
||
487 | * |
||
488 | * @return bool success |
||
489 | */ |
||
490 | 2 | public function logout() { |
|
502 | |||
503 | /** |
||
504 | * @since 0.1 |
||
505 | * |
||
506 | * @param string $type The token type to get. |
||
507 | * |
||
508 | 2 | * @return string |
|
509 | 2 | */ |
|
510 | public function getToken( $type = 'csrf' ) { |
||
513 | |||
514 | /** |
||
515 | * Clear all tokens stored by the API. |
||
516 | * |
||
517 | 1 | * @since 0.1 |
|
518 | 1 | */ |
|
519 | 1 | public function clearTokens() { |
|
522 | |||
523 | /** |
||
524 | 4 | * @return string |
|
525 | 4 | */ |
|
526 | 4 | public function getVersion() { |
|
541 | |||
542 | } |
||
543 |
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.