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 SpecialWhatLinksHere 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 SpecialWhatLinksHere, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 29 | class SpecialWhatLinksHere extends IncludableSpecialPage { |
||
| 30 | /** @var FormOptions */ |
||
| 31 | protected $opts; |
||
| 32 | |||
| 33 | protected $selfTitle; |
||
| 34 | |||
| 35 | /** @var Title */ |
||
| 36 | protected $target; |
||
| 37 | |||
| 38 | protected $limits = [ 20, 50, 100, 250, 500 ]; |
||
| 39 | |||
| 40 | public function __construct() { |
||
| 43 | |||
| 44 | function execute( $par ) { |
||
| 98 | |||
| 99 | /** |
||
| 100 | * @param int $level Recursion level |
||
| 101 | * @param Title $target Target title |
||
| 102 | * @param int $limit Number of entries to display |
||
| 103 | * @param int $from Display from this article ID (default: 0) |
||
| 104 | * @param int $back Display from this article ID at backwards scrolling (default: 0) |
||
| 105 | */ |
||
| 106 | function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) { |
||
| 107 | $out = $this->getOutput(); |
||
| 108 | $dbr = wfGetDB( DB_SLAVE ); |
||
| 109 | |||
| 110 | $hidelinks = $this->opts->getValue( 'hidelinks' ); |
||
| 111 | $hideredirs = $this->opts->getValue( 'hideredirs' ); |
||
| 112 | $hidetrans = $this->opts->getValue( 'hidetrans' ); |
||
| 113 | $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' ); |
||
| 114 | |||
| 115 | $fetchlinks = ( !$hidelinks || !$hideredirs ); |
||
| 116 | |||
| 117 | // Build query conds in concert for all three tables... |
||
| 118 | $conds['pagelinks'] = [ |
||
|
|
|||
| 119 | 'pl_namespace' => $target->getNamespace(), |
||
| 120 | 'pl_title' => $target->getDBkey(), |
||
| 121 | ]; |
||
| 122 | $conds['templatelinks'] = [ |
||
| 123 | 'tl_namespace' => $target->getNamespace(), |
||
| 124 | 'tl_title' => $target->getDBkey(), |
||
| 125 | ]; |
||
| 126 | $conds['imagelinks'] = [ |
||
| 127 | 'il_to' => $target->getDBkey(), |
||
| 128 | ]; |
||
| 129 | |||
| 130 | $namespace = $this->opts->getValue( 'namespace' ); |
||
| 131 | $invert = $this->opts->getValue( 'invert' ); |
||
| 132 | $nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace ); |
||
| 133 | View Code Duplication | if ( is_int( $namespace ) ) { |
|
| 134 | $conds['pagelinks'][] = "pl_from_namespace $nsComparison"; |
||
| 135 | $conds['templatelinks'][] = "tl_from_namespace $nsComparison"; |
||
| 136 | $conds['imagelinks'][] = "il_from_namespace $nsComparison"; |
||
| 137 | } |
||
| 138 | |||
| 139 | View Code Duplication | if ( $from ) { |
|
| 140 | $conds['templatelinks'][] = "tl_from >= $from"; |
||
| 141 | $conds['pagelinks'][] = "pl_from >= $from"; |
||
| 142 | $conds['imagelinks'][] = "il_from >= $from"; |
||
| 143 | } |
||
| 144 | |||
| 145 | if ( $hideredirs ) { |
||
| 146 | $conds['pagelinks']['rd_from'] = null; |
||
| 147 | } elseif ( $hidelinks ) { |
||
| 148 | $conds['pagelinks'][] = 'rd_from is NOT NULL'; |
||
| 149 | } |
||
| 150 | |||
| 151 | $queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use ( |
||
| 152 | $conds, $target, $limit |
||
| 153 | ) { |
||
| 154 | // Read an extra row as an at-end check |
||
| 155 | $queryLimit = $limit + 1; |
||
| 156 | $on = [ |
||
| 157 | "rd_from = $fromCol", |
||
| 158 | 'rd_title' => $target->getDBkey(), |
||
| 159 | 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL' |
||
| 160 | ]; |
||
| 161 | $on['rd_namespace'] = $target->getNamespace(); |
||
| 162 | // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces |
||
| 163 | $subQuery = $dbr->selectSQLText( |
||
| 164 | [ $table, 'redirect', 'page' ], |
||
| 165 | [ $fromCol, 'rd_from' ], |
||
| 166 | $conds[$table], |
||
| 167 | __CLASS__ . '::showIndirectLinks', |
||
| 168 | // Force JOIN order per T106682 to avoid large filesorts |
||
| 169 | [ 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ], |
||
| 170 | [ |
||
| 171 | 'page' => [ 'INNER JOIN', "$fromCol = page_id" ], |
||
| 172 | 'redirect' => [ 'LEFT JOIN', $on ] |
||
| 173 | ] |
||
| 174 | ); |
||
| 175 | return $dbr->select( |
||
| 176 | [ 'page', 'temp_backlink_range' => "($subQuery)" ], |
||
| 177 | [ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ], |
||
| 178 | [], |
||
| 179 | __CLASS__ . '::showIndirectLinks', |
||
| 180 | [ 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ], |
||
| 181 | [ 'page' => [ 'INNER JOIN', "$fromCol = page_id" ] ] |
||
| 182 | ); |
||
| 183 | }; |
||
| 184 | |||
| 185 | if ( $fetchlinks ) { |
||
| 186 | $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' ); |
||
| 187 | } |
||
| 188 | |||
| 189 | if ( !$hidetrans ) { |
||
| 190 | $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' ); |
||
| 191 | } |
||
| 192 | |||
| 193 | if ( !$hideimages ) { |
||
| 194 | $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' ); |
||
| 195 | } |
||
| 196 | |||
| 197 | if ( ( !$fetchlinks || !$plRes->numRows() ) |
||
| 198 | && ( $hidetrans || !$tlRes->numRows() ) |
||
| 199 | && ( $hideimages || !$ilRes->numRows() ) |
||
| 200 | ) { |
||
| 201 | if ( 0 == $level ) { |
||
| 202 | if ( !$this->including() ) { |
||
| 203 | $out->addHTML( $this->whatlinkshereForm() ); |
||
| 204 | |||
| 205 | // Show filters only if there are links |
||
| 206 | if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) { |
||
| 207 | $out->addHTML( $this->getFilterPanel() ); |
||
| 208 | } |
||
| 209 | $errMsg = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere'; |
||
| 210 | $out->addWikiMsg( $errMsg, $this->target->getPrefixedText() ); |
||
| 211 | $out->setStatusCode( 404 ); |
||
| 212 | } |
||
| 213 | } |
||
| 214 | |||
| 215 | return; |
||
| 216 | } |
||
| 217 | |||
| 218 | // Read the rows into an array and remove duplicates |
||
| 219 | // templatelinks comes second so that the templatelinks row overwrites the |
||
| 220 | // pagelinks row, so we get (inclusion) rather than nothing |
||
| 221 | if ( $fetchlinks ) { |
||
| 222 | foreach ( $plRes as $row ) { |
||
| 223 | $row->is_template = 0; |
||
| 224 | $row->is_image = 0; |
||
| 225 | $rows[$row->page_id] = $row; |
||
| 226 | } |
||
| 227 | } |
||
| 228 | if ( !$hidetrans ) { |
||
| 229 | foreach ( $tlRes as $row ) { |
||
| 230 | $row->is_template = 1; |
||
| 231 | $row->is_image = 0; |
||
| 232 | $rows[$row->page_id] = $row; |
||
| 233 | } |
||
| 234 | } |
||
| 235 | if ( !$hideimages ) { |
||
| 236 | foreach ( $ilRes as $row ) { |
||
| 237 | $row->is_template = 0; |
||
| 238 | $row->is_image = 1; |
||
| 239 | $rows[$row->page_id] = $row; |
||
| 240 | } |
||
| 241 | } |
||
| 242 | |||
| 243 | // Sort by key and then change the keys to 0-based indices |
||
| 244 | ksort( $rows ); |
||
| 245 | $rows = array_values( $rows ); |
||
| 246 | |||
| 247 | $numRows = count( $rows ); |
||
| 248 | |||
| 249 | // Work out the start and end IDs, for prev/next links |
||
| 250 | if ( $numRows > $limit ) { |
||
| 251 | // More rows available after these ones |
||
| 252 | // Get the ID from the last row in the result set |
||
| 253 | $nextId = $rows[$limit]->page_id; |
||
| 254 | // Remove undisplayed rows |
||
| 255 | $rows = array_slice( $rows, 0, $limit ); |
||
| 256 | } else { |
||
| 257 | // No more rows after |
||
| 258 | $nextId = false; |
||
| 259 | } |
||
| 260 | $prevId = $from; |
||
| 261 | |||
| 262 | // use LinkBatch to make sure, that all required data (associated with Titles) |
||
| 263 | // is loaded in one query |
||
| 264 | $lb = new LinkBatch(); |
||
| 265 | foreach ( $rows as $row ) { |
||
| 266 | $lb->add( $row->page_namespace, $row->page_title ); |
||
| 267 | } |
||
| 268 | $lb->execute(); |
||
| 269 | |||
| 270 | if ( $level == 0 ) { |
||
| 271 | if ( !$this->including() ) { |
||
| 272 | $out->addHTML( $this->whatlinkshereForm() ); |
||
| 273 | $out->addHTML( $this->getFilterPanel() ); |
||
| 274 | $out->addWikiMsg( 'linkshere', $this->target->getPrefixedText() ); |
||
| 275 | |||
| 276 | $prevnext = $this->getPrevNext( $prevId, $nextId ); |
||
| 277 | $out->addHTML( $prevnext ); |
||
| 278 | } |
||
| 279 | } |
||
| 280 | $out->addHTML( $this->listStart( $level ) ); |
||
| 281 | foreach ( $rows as $row ) { |
||
| 282 | $nt = Title::makeTitle( $row->page_namespace, $row->page_title ); |
||
| 283 | |||
| 284 | if ( $row->rd_from && $level < 2 ) { |
||
| 285 | $out->addHTML( $this->listItem( $row, $nt, $target, true ) ); |
||
| 286 | $this->showIndirectLinks( |
||
| 287 | $level + 1, |
||
| 288 | $nt, |
||
| 289 | $this->getConfig()->get( 'MaxRedirectLinksRetrieved' ) |
||
| 290 | ); |
||
| 291 | $out->addHTML( Xml::closeElement( 'li' ) ); |
||
| 292 | } else { |
||
| 293 | $out->addHTML( $this->listItem( $row, $nt, $target ) ); |
||
| 294 | } |
||
| 295 | } |
||
| 296 | |||
| 297 | $out->addHTML( $this->listEnd() ); |
||
| 298 | |||
| 299 | if ( $level == 0 ) { |
||
| 300 | if ( !$this->including() ) { |
||
| 301 | $out->addHTML( $prevnext ); |
||
| 302 | } |
||
| 303 | } |
||
| 304 | } |
||
| 305 | |||
| 306 | protected function listStart( $level ) { |
||
| 309 | |||
| 310 | protected function listItem( $row, $nt, $target, $notClose = false ) { |
||
| 368 | |||
| 369 | protected function listEnd() { |
||
| 372 | |||
| 373 | protected function wlhLink( Title $target, $text, $editText ) { |
||
| 374 | static $title = null; |
||
| 375 | if ( $title === null ) { |
||
| 376 | $title = $this->getPageTitle(); |
||
| 377 | } |
||
| 378 | |||
| 379 | // always show a "<- Links" link |
||
| 380 | $links = [ |
||
| 381 | 'links' => Linker::linkKnown( |
||
| 382 | $title, |
||
| 383 | $text, |
||
| 384 | [], |
||
| 385 | [ 'target' => $target->getPrefixedText() ] |
||
| 386 | ), |
||
| 387 | ]; |
||
| 388 | |||
| 389 | // if the page is editable, add an edit link |
||
| 390 | View Code Duplication | if ( |
|
| 391 | // check user permissions |
||
| 392 | $this->getUser()->isAllowed( 'edit' ) && |
||
| 393 | // check, if the content model is editable through action=edit |
||
| 394 | ContentHandler::getForTitle( $target )->supportsDirectEditing() |
||
| 395 | ) { |
||
| 396 | $links['edit'] = Linker::linkKnown( |
||
| 397 | $target, |
||
| 398 | $editText, |
||
| 399 | [], |
||
| 400 | [ 'action' => 'edit' ] |
||
| 401 | ); |
||
| 402 | } |
||
| 403 | |||
| 404 | // build the links html |
||
| 405 | return $this->getLanguage()->pipeList( $links ); |
||
| 406 | } |
||
| 407 | |||
| 408 | function makeSelfLink( $text, $query ) { |
||
| 416 | |||
| 417 | function getPrevNext( $prevId, $nextId ) { |
||
| 446 | |||
| 447 | function whatlinkshereForm() { |
||
| 506 | |||
| 507 | /** |
||
| 508 | * Create filter panel |
||
| 509 | * |
||
| 510 | * @return string HTML fieldset and filter panel with the show/hide links |
||
| 511 | */ |
||
| 512 | function getFilterPanel() { |
||
| 541 | |||
| 542 | /** |
||
| 543 | * Return an array of subpages beginning with $search that this special page will accept. |
||
| 544 | * |
||
| 545 | * @param string $search Prefix to search for |
||
| 546 | * @param int $limit Maximum number of results to return (usually 10) |
||
| 547 | * @param int $offset Number of results to skip (usually 0) |
||
| 548 | * @return string[] Matching subpages |
||
| 549 | */ |
||
| 550 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
||
| 553 | |||
| 554 | protected function getGroupName() { |
||
| 557 | } |
||
| 558 |
Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.
Let’s take a look at an example:
As you can see in this example, the array
$myArrayis initialized the first time when the foreach loop is entered. You can also see that the value of thebarkey is only written conditionally; thus, its value might result from a previous iteration.This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.