Complex classes like ApiOpenSearch 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 ApiOpenSearch, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 32 | class ApiOpenSearch extends ApiBase { |
||
| 33 | use SearchApi; |
||
| 34 | |||
| 35 | private $format = null; |
||
| 36 | private $fm = null; |
||
| 37 | |||
| 38 | /** @var array list of api allowed params */ |
||
| 39 | private $allowedParams = null; |
||
| 40 | |||
| 41 | /** |
||
| 42 | * Get the output format |
||
| 43 | * |
||
| 44 | * @return string |
||
| 45 | */ |
||
| 46 | protected function getFormat() { |
||
| 66 | |||
| 67 | public function getCustomPrinter() { |
||
| 83 | |||
| 84 | public function execute() { |
||
| 85 | $params = $this->extractRequestParams(); |
||
| 86 | $search = $params['search']; |
||
| 87 | $suggest = $params['suggest']; |
||
| 88 | $results = []; |
||
| 89 | if ( !$suggest || $this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) { |
||
| 90 | // Open search results may be stored for a very long time |
||
| 91 | $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) ); |
||
| 92 | $this->getMain()->setCacheMode( 'public' ); |
||
| 93 | $results = $this->search( $search, $params ); |
||
| 94 | |||
| 95 | // Allow hooks to populate extracts and images |
||
| 96 | Hooks::run( 'ApiOpenSearchSuggest', [ &$results ] ); |
||
| 97 | |||
| 98 | // Trim extracts, if necessary |
||
| 99 | $length = $this->getConfig()->get( 'OpenSearchDescriptionLength' ); |
||
| 100 | foreach ( $results as &$r ) { |
||
| 101 | if ( is_string( $r['extract'] ) && !$r['extract trimmed'] ) { |
||
| 102 | $r['extract'] = self::trimExtract( $r['extract'], $length ); |
||
| 103 | } |
||
| 104 | } |
||
| 105 | } |
||
| 106 | |||
| 107 | // Populate result object |
||
| 108 | $this->populateResult( $search, $results ); |
||
| 109 | } |
||
| 110 | |||
| 111 | /** |
||
| 112 | * Perform the search |
||
| 113 | * @param string $search the search query |
||
| 114 | * @param array $params api request params |
||
| 115 | * @return array search results. Keys are integers. |
||
| 116 | */ |
||
| 117 | private function search( $search, array $params ) { |
||
| 118 | $searchEngine = $this->buildSearchEngine( $params ); |
||
| 119 | $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); |
||
| 120 | $results = []; |
||
| 121 | |||
| 122 | if ( !$titles ) { |
||
| 123 | return $results; |
||
| 124 | } |
||
| 125 | |||
| 126 | // Special pages need unique integer ids in the return list, so we just |
||
| 127 | // assign them negative numbers because those won't clash with the |
||
| 128 | // always positive articleIds that non-special pages get. |
||
| 129 | $nextSpecialPageId = -1; |
||
| 130 | |||
| 131 | if ( $params['redirects'] === null ) { |
||
| 132 | // Backwards compatibility, don't resolve for JSON. |
||
| 133 | $resolveRedir = $this->getFormat() !== 'json'; |
||
| 134 | } else { |
||
| 135 | $resolveRedir = $params['redirects'] === 'resolve'; |
||
| 136 | } |
||
| 137 | |||
| 138 | if ( $resolveRedir ) { |
||
| 139 | // Query for redirects |
||
| 140 | $redirects = []; |
||
| 141 | $lb = new LinkBatch( $titles ); |
||
| 142 | if ( !$lb->isEmpty() ) { |
||
| 143 | $db = $this->getDB(); |
||
| 144 | $res = $db->select( |
||
| 145 | [ 'page', 'redirect' ], |
||
| 146 | [ 'page_namespace', 'page_title', 'rd_namespace', 'rd_title' ], |
||
| 147 | [ |
||
| 148 | 'rd_from = page_id', |
||
| 149 | 'rd_interwiki IS NULL OR rd_interwiki = ' . $db->addQuotes( '' ), |
||
| 150 | $lb->constructSet( 'page', $db ), |
||
| 151 | ], |
||
| 152 | __METHOD__ |
||
| 153 | ); |
||
| 154 | foreach ( $res as $row ) { |
||
|
|
|||
| 155 | $redirects[$row->page_namespace][$row->page_title] = |
||
| 156 | [ $row->rd_namespace, $row->rd_title ]; |
||
| 157 | } |
||
| 158 | } |
||
| 159 | |||
| 160 | // Bypass any redirects |
||
| 161 | $seen = []; |
||
| 162 | foreach ( $titles as $title ) { |
||
| 163 | $ns = $title->getNamespace(); |
||
| 164 | $dbkey = $title->getDBkey(); |
||
| 165 | $from = null; |
||
| 166 | if ( isset( $redirects[$ns][$dbkey] ) ) { |
||
| 167 | list( $ns, $dbkey ) = $redirects[$ns][$dbkey]; |
||
| 168 | $from = $title; |
||
| 169 | $title = Title::makeTitle( $ns, $dbkey ); |
||
| 170 | } |
||
| 171 | if ( !isset( $seen[$ns][$dbkey] ) ) { |
||
| 172 | $seen[$ns][$dbkey] = true; |
||
| 173 | $resultId = $title->getArticleID(); |
||
| 174 | if ( $resultId === 0 ) { |
||
| 175 | $resultId = $nextSpecialPageId; |
||
| 176 | $nextSpecialPageId -= 1; |
||
| 177 | } |
||
| 178 | $results[$resultId] = [ |
||
| 179 | 'title' => $title, |
||
| 180 | 'redirect from' => $from, |
||
| 181 | 'extract' => false, |
||
| 182 | 'extract trimmed' => false, |
||
| 183 | 'image' => false, |
||
| 184 | 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ), |
||
| 185 | ]; |
||
| 186 | } |
||
| 187 | } |
||
| 188 | } else { |
||
| 189 | foreach ( $titles as $title ) { |
||
| 190 | $resultId = $title->getArticleID(); |
||
| 191 | if ( $resultId === 0 ) { |
||
| 192 | $resultId = $nextSpecialPageId; |
||
| 193 | $nextSpecialPageId -= 1; |
||
| 194 | } |
||
| 195 | $results[$resultId] = [ |
||
| 196 | 'title' => $title, |
||
| 197 | 'redirect from' => null, |
||
| 198 | 'extract' => false, |
||
| 199 | 'extract trimmed' => false, |
||
| 200 | 'image' => false, |
||
| 201 | 'url' => wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ), |
||
| 202 | ]; |
||
| 203 | } |
||
| 204 | } |
||
| 205 | |||
| 206 | return $results; |
||
| 207 | } |
||
| 208 | |||
| 209 | /** |
||
| 210 | * @param string $search |
||
| 211 | * @param array &$results |
||
| 212 | */ |
||
| 213 | protected function populateResult( $search, &$results ) { |
||
| 270 | |||
| 271 | public function getAllowedParams() { |
||
| 272 | if ( $this->allowedParams !== null ) { |
||
| 273 | return $this->allowedParams; |
||
| 274 | } |
||
| 275 | $this->allowedParams = $this->buildCommonApiParams( false ) + [ |
||
| 276 | 'suggest' => false, |
||
| 277 | 'redirects' => [ |
||
| 278 | ApiBase::PARAM_TYPE => [ 'return', 'resolve' ], |
||
| 279 | ], |
||
| 280 | 'format' => [ |
||
| 281 | ApiBase::PARAM_DFLT => 'json', |
||
| 282 | ApiBase::PARAM_TYPE => [ 'json', 'jsonfm', 'xml', 'xmlfm' ], |
||
| 283 | ], |
||
| 284 | 'warningsaserror' => false, |
||
| 285 | ]; |
||
| 286 | |||
| 287 | // Use open search specific default limit |
||
| 288 | $this->allowedParams['limit'][ApiBase::PARAM_DFLT] = $this->getConfig()->get( |
||
| 289 | 'OpenSearchDefaultLimit' |
||
| 290 | ); |
||
| 291 | |||
| 292 | return $this->allowedParams; |
||
| 293 | } |
||
| 294 | |||
| 295 | public function getSearchProfileParams() { |
||
| 296 | return [ |
||
| 297 | 'profile' => [ |
||
| 298 | 'profile-type' => SearchEngine::COMPLETION_PROFILE_TYPE, |
||
| 299 | 'help-message' => 'apihelp-query+prefixsearch-param-profile' |
||
| 300 | ], |
||
| 301 | ]; |
||
| 302 | } |
||
| 303 | |||
| 304 | protected function getExamplesMessages() { |
||
| 305 | return [ |
||
| 306 | 'action=opensearch&search=Te' |
||
| 307 | => 'apihelp-opensearch-example-te', |
||
| 308 | ]; |
||
| 309 | } |
||
| 310 | |||
| 311 | public function getHelpUrls() { |
||
| 312 | return 'https://www.mediawiki.org/wiki/API:Opensearch'; |
||
| 313 | } |
||
| 314 | |||
| 315 | /** |
||
| 316 | * Trim an extract to a sensible length. |
||
| 317 | * |
||
| 318 | * Adapted from Extension:OpenSearchXml, which adapted it from |
||
| 319 | * Extension:ActiveAbstract. |
||
| 320 | * |
||
| 321 | * @param string $text |
||
| 322 | * @param int $length Target length; actual result will continue to the end of a sentence. |
||
| 323 | * @return string |
||
| 324 | */ |
||
| 325 | public static function trimExtract( $text, $length ) { |
||
| 349 | |||
| 350 | /** |
||
| 351 | * Fetch the template for a type. |
||
| 352 | * |
||
| 353 | * @param string $type MIME type |
||
| 354 | * @return string |
||
| 355 | * @throws MWException |
||
| 356 | */ |
||
| 357 | public static function getOpenSearchTemplate( $type ) { |
||
| 383 | } |
||
| 384 | |||
| 385 | class ApiOpenSearchFormatJson extends ApiFormatJson { |
||
| 386 | private $warningsAsError = false; |
||
| 387 | |||
| 419 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.