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!=, orswitchconditions), values of different types might be equal.For
stringvalues, the empty string''is a special case, in particular the following results might be unexpected: