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 SpecialWatchlist 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 SpecialWatchlist, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
32 | class SpecialWatchlist extends ChangesListSpecialPage { |
||
33 | public function __construct( $page = 'Watchlist', $restriction = 'viewmywatchlist' ) { |
||
36 | |||
37 | public function doesWrites() { |
||
40 | |||
41 | /** |
||
42 | * Main execution point |
||
43 | * |
||
44 | * @param string $subpage |
||
45 | */ |
||
46 | function execute( $subpage ) { |
||
90 | |||
91 | /** |
||
92 | * Return an array of subpages that this special page will accept. |
||
93 | * |
||
94 | * @see also SpecialEditWatchlist::getSubpagesForPrefixSearch |
||
95 | * @return string[] subpages |
||
96 | */ |
||
97 | public function getSubpagesForPrefixSearch() { |
||
104 | |||
105 | /** |
||
106 | * Get a FormOptions object containing the default options |
||
107 | * |
||
108 | * @return FormOptions |
||
109 | */ |
||
110 | public function getDefaultOptions() { |
||
131 | |||
132 | /** |
||
133 | * Get custom show/hide filters |
||
134 | * |
||
135 | * @return array Map of filter URL param names to properties (msg/default) |
||
136 | */ |
||
137 | View Code Duplication | protected function getCustomFilters() { |
|
145 | |||
146 | /** |
||
147 | * Fetch values for a FormOptions object from the WebRequest associated with this instance. |
||
148 | * |
||
149 | * Maps old pre-1.23 request parameters Watchlist used to use (different from Recentchanges' ones) |
||
150 | * to the current ones. |
||
151 | * |
||
152 | * @param FormOptions $opts |
||
153 | * @return FormOptions |
||
154 | */ |
||
155 | protected function fetchOptionsFromRequest( $opts ) { |
||
180 | |||
181 | /** |
||
182 | * Return an array of conditions depending of options set in $opts |
||
183 | * |
||
184 | * @param FormOptions $opts |
||
185 | * @return array |
||
186 | */ |
||
187 | public function buildMainQueryConds( FormOptions $opts ) { |
||
199 | |||
200 | /** |
||
201 | * Process the query |
||
202 | * |
||
203 | * @param array $conds |
||
204 | * @param FormOptions $opts |
||
205 | * @return bool|ResultWrapper Result or false (for Recentchangeslinked only) |
||
206 | */ |
||
207 | public function doMainQuery( $conds, $opts ) { |
||
208 | $dbr = $this->getDB(); |
||
209 | $user = $this->getUser(); |
||
210 | |||
211 | # Toggle watchlist content (all recent edits or just the latest) |
||
212 | if ( $opts['extended'] ) { |
||
213 | $limitWatchlist = $user->getIntOption( 'wllimit' ); |
||
214 | $usePage = false; |
||
215 | } else { |
||
216 | # Top log Ids for a page are not stored |
||
217 | $nonRevisionTypes = [ RC_LOG ]; |
||
218 | Hooks::run( 'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] ); |
||
219 | if ( $nonRevisionTypes ) { |
||
220 | $conds[] = $dbr->makeList( |
||
221 | [ |
||
222 | 'rc_this_oldid=page_latest', |
||
223 | 'rc_type' => $nonRevisionTypes, |
||
224 | ], |
||
225 | LIST_OR |
||
226 | ); |
||
227 | } |
||
228 | $limitWatchlist = 0; |
||
229 | $usePage = true; |
||
230 | } |
||
231 | |||
232 | $tables = [ 'recentchanges', 'watchlist' ]; |
||
233 | $fields = RecentChange::selectFields(); |
||
234 | $query_options = [ 'ORDER BY' => 'rc_timestamp DESC' ]; |
||
235 | $join_conds = [ |
||
236 | 'watchlist' => [ |
||
237 | 'INNER JOIN', |
||
238 | [ |
||
239 | 'wl_user' => $user->getId(), |
||
240 | 'wl_namespace=rc_namespace', |
||
241 | 'wl_title=rc_title' |
||
242 | ], |
||
243 | ], |
||
244 | ]; |
||
245 | |||
246 | if ( $this->getConfig()->get( 'ShowUpdatedMarker' ) ) { |
||
247 | $fields[] = 'wl_notificationtimestamp'; |
||
248 | } |
||
249 | if ( $limitWatchlist ) { |
||
250 | $query_options['LIMIT'] = $limitWatchlist; |
||
251 | } |
||
252 | |||
253 | $rollbacker = $user->isAllowed( 'rollback' ); |
||
254 | View Code Duplication | if ( $usePage || $rollbacker ) { |
|
255 | $tables[] = 'page'; |
||
256 | $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ]; |
||
257 | if ( $rollbacker ) { |
||
258 | $fields[] = 'page_latest'; |
||
259 | } |
||
260 | } |
||
261 | |||
262 | // Log entries with DELETED_ACTION must not show up unless the user has |
||
263 | // the necessary rights. |
||
264 | if ( !$user->isAllowed( 'deletedhistory' ) ) { |
||
265 | $bitmask = LogPage::DELETED_ACTION; |
||
266 | } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
||
267 | $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; |
||
268 | } else { |
||
269 | $bitmask = 0; |
||
270 | } |
||
271 | View Code Duplication | if ( $bitmask ) { |
|
272 | $conds[] = $dbr->makeList( [ |
||
273 | 'rc_type != ' . RC_LOG, |
||
274 | $dbr->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", |
||
275 | ], LIST_OR ); |
||
276 | } |
||
277 | |||
278 | ChangeTags::modifyDisplayQuery( |
||
279 | $tables, |
||
280 | $fields, |
||
281 | $conds, |
||
282 | $join_conds, |
||
283 | $query_options, |
||
284 | '' |
||
285 | ); |
||
286 | |||
287 | $this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ); |
||
288 | |||
289 | return $dbr->select( |
||
290 | $tables, |
||
291 | $fields, |
||
292 | $conds, |
||
293 | __METHOD__, |
||
294 | $query_options, |
||
295 | $join_conds |
||
296 | ); |
||
297 | } |
||
298 | |||
299 | View Code Duplication | protected function runMainQueryHook( &$tables, &$fields, &$conds, &$query_options, |
|
309 | |||
310 | /** |
||
311 | * Return a IDatabase object for reading |
||
312 | * |
||
313 | * @return IDatabase |
||
314 | */ |
||
315 | protected function getDB() { |
||
318 | |||
319 | /** |
||
320 | * Output feed links. |
||
321 | */ |
||
322 | public function outputFeedLinks() { |
||
334 | |||
335 | /** |
||
336 | * Build and output the actual changes list. |
||
337 | * |
||
338 | * @param ResultWrapper $rows Database rows |
||
339 | * @param FormOptions $opts |
||
340 | */ |
||
341 | public function outputChangesList( $rows, $opts ) { |
||
413 | |||
414 | /** |
||
415 | * Set the text to be displayed above the changes |
||
416 | * |
||
417 | * @param FormOptions $opts |
||
418 | * @param int $numRows Number of rows in the result to show after this header |
||
419 | */ |
||
420 | public function doHeader( $opts, $numRows ) { |
||
421 | $user = $this->getUser(); |
||
422 | $out = $this->getOutput(); |
||
423 | |||
424 | // if the user wishes, that the watchlist is reloaded, whenever a filter changes, |
||
425 | // add the module for that |
||
426 | if ( $user->getBoolOption( 'watchlistreloadautomatically' ) ) { |
||
427 | $out->addModules( [ 'mediawiki.special.watchlist' ] ); |
||
428 | } |
||
429 | |||
430 | $out->addSubtitle( |
||
431 | $this->msg( 'watchlistfor2', $user->getName() ) |
||
432 | ->rawParams( SpecialEditWatchlist::buildTools( |
||
433 | $this->getLanguage(), |
||
434 | $this->getLinkRenderer() |
||
435 | ) ) |
||
436 | ); |
||
437 | |||
438 | $this->setTopText( $opts ); |
||
439 | |||
440 | $lang = $this->getLanguage(); |
||
441 | if ( $opts['days'] > 0 ) { |
||
442 | $days = $opts['days']; |
||
443 | } else { |
||
444 | $days = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ); |
||
445 | } |
||
446 | $timestamp = wfTimestampNow(); |
||
447 | $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params( |
||
448 | $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user ) |
||
449 | )->parse() . "<br />\n"; |
||
450 | |||
451 | $nondefaults = $opts->getChangedValues(); |
||
452 | $cutofflinks = $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts ); |
||
453 | |||
454 | # Spit out some control panel links |
||
455 | $filters = [ |
||
456 | 'hideminor' => 'wlshowhideminor', |
||
457 | 'hidebots' => 'wlshowhidebots', |
||
458 | 'hideanons' => 'wlshowhideanons', |
||
459 | 'hideliu' => 'wlshowhideliu', |
||
460 | 'hidemyself' => 'wlshowhidemine', |
||
461 | 'hidepatrolled' => 'wlshowhidepatr' |
||
462 | ]; |
||
463 | |||
464 | if ( $this->getConfig()->get( 'RCWatchCategoryMembership' ) ) { |
||
465 | $filters['hidecategorization'] = 'wlshowhidecategorization'; |
||
466 | } |
||
467 | |||
468 | foreach ( $this->getCustomFilters() as $key => $params ) { |
||
469 | $filters[$key] = $params['msg']; |
||
470 | } |
||
471 | // Disable some if needed |
||
472 | if ( !$user->useRCPatrol() ) { |
||
473 | unset( $filters['hidepatrolled'] ); |
||
474 | } |
||
475 | |||
476 | $links = []; |
||
477 | foreach ( $filters as $name => $msg ) { |
||
478 | $links[] = $this->showHideCheck( $nondefaults, $msg, $name, $opts[$name] ); |
||
479 | } |
||
480 | |||
481 | $hiddenFields = $nondefaults; |
||
482 | $hiddenFields['action'] = 'submit'; |
||
483 | unset( $hiddenFields['namespace'] ); |
||
484 | unset( $hiddenFields['invert'] ); |
||
485 | unset( $hiddenFields['associated'] ); |
||
486 | unset( $hiddenFields['days'] ); |
||
487 | foreach ( $filters as $key => $value ) { |
||
488 | unset( $hiddenFields[$key] ); |
||
489 | } |
||
490 | |||
491 | # Create output |
||
492 | $form = ''; |
||
493 | |||
494 | # Namespace filter and put the whole form together. |
||
495 | $form .= $wlInfo; |
||
496 | $form .= $cutofflinks; |
||
497 | $form .= $this->msg( 'watchlist-hide' ) . |
||
498 | $this->msg( 'colon-separator' )->escaped() . |
||
499 | implode( ' ', $links ); |
||
500 | $form .= "\n<br />\n"; |
||
501 | $form .= Html::namespaceSelector( |
||
502 | [ |
||
503 | 'selected' => $opts['namespace'], |
||
504 | 'all' => '', |
||
505 | 'label' => $this->msg( 'namespace' )->text() |
||
506 | ], [ |
||
507 | 'name' => 'namespace', |
||
508 | 'id' => 'namespace', |
||
509 | 'class' => 'namespaceselector', |
||
510 | ] |
||
511 | ) . "\n"; |
||
512 | $form .= '<span class="mw-input-with-label">' . Xml::checkLabel( |
||
513 | $this->msg( 'invert' )->text(), |
||
514 | 'invert', |
||
515 | 'nsinvert', |
||
516 | $opts['invert'], |
||
517 | [ 'title' => $this->msg( 'tooltip-invert' )->text() ] |
||
518 | ) . "</span>\n"; |
||
519 | $form .= '<span class="mw-input-with-label">' . Xml::checkLabel( |
||
520 | $this->msg( 'namespace_association' )->text(), |
||
521 | 'associated', |
||
522 | 'nsassociated', |
||
523 | $opts['associated'], |
||
524 | [ 'title' => $this->msg( 'tooltip-namespace_association' )->text() ] |
||
525 | ) . "</span>\n"; |
||
526 | $form .= Xml::submitButton( $this->msg( 'watchlist-submit' )->text() ) . "\n"; |
||
527 | foreach ( $hiddenFields as $key => $value ) { |
||
528 | $form .= Html::hidden( $key, $value ) . "\n"; |
||
529 | } |
||
530 | $form .= Xml::closeElement( 'fieldset' ) . "\n"; |
||
531 | $form .= Xml::closeElement( 'form' ) . "\n"; |
||
532 | $this->getOutput()->addHTML( $form ); |
||
533 | |||
534 | $this->setBottomText( $opts ); |
||
535 | } |
||
536 | |||
537 | function cutoffselector( $options ) { |
||
581 | |||
582 | function setTopText( FormOptions $opts ) { |
||
635 | |||
636 | protected function showHideCheck( $options, $message, $name, $value ) { |
||
646 | |||
647 | /** |
||
648 | * Count the number of paired items on a user's watchlist. |
||
649 | * The assumption made here is that when a subject page is watched a talk page is also watched. |
||
650 | * Hence the number of individual items is halved. |
||
651 | * |
||
652 | * @return int |
||
653 | */ |
||
654 | protected function countItems() { |
||
659 | } |
||
660 |
In PHP, under loose comparison (like
==
, or!=
, orswitch
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: