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 SpecialSearch 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 SpecialSearch, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
32 | class SpecialSearch extends SpecialPage { |
||
33 | /** |
||
34 | * Current search profile. Search profile is just a name that identifies |
||
35 | * the active search tab on the search page (content, discussions...) |
||
36 | * For users tt replaces the set of enabled namespaces from the query |
||
37 | * string when applicable. Extensions can add new profiles with hooks |
||
38 | * with custom search options just for that profile. |
||
39 | * @var null|string |
||
40 | */ |
||
41 | protected $profile; |
||
42 | |||
43 | /** @var SearchEngine Search engine */ |
||
44 | protected $searchEngine; |
||
45 | |||
46 | /** @var string Search engine type, if not default */ |
||
47 | protected $searchEngineType; |
||
48 | |||
49 | /** @var array For links */ |
||
50 | protected $extraParams = []; |
||
51 | |||
52 | /** |
||
53 | * @var string The prefix url parameter. Set on the searcher and the |
||
54 | * is expected to treat it as prefix filter on titles. |
||
55 | */ |
||
56 | protected $mPrefix; |
||
57 | |||
58 | /** |
||
59 | * @var int |
||
60 | */ |
||
61 | protected $limit, $offset; |
||
|
|||
62 | |||
63 | /** |
||
64 | * @var array |
||
65 | */ |
||
66 | protected $namespaces; |
||
67 | |||
68 | /** |
||
69 | * @var string |
||
70 | */ |
||
71 | protected $fulltext; |
||
72 | |||
73 | /** |
||
74 | * @var bool |
||
75 | */ |
||
76 | protected $runSuggestion = true; |
||
77 | |||
78 | /** |
||
79 | * Names of the wikis, in format: Interwiki prefix -> caption |
||
80 | * @var array |
||
81 | */ |
||
82 | protected $customCaptions; |
||
83 | |||
84 | /** |
||
85 | * Search engine configurations. |
||
86 | * @var SearchEngineConfig |
||
87 | */ |
||
88 | protected $searchConfig; |
||
89 | |||
90 | const NAMESPACES_CURRENT = 'sense'; |
||
91 | |||
92 | public function __construct() { |
||
96 | |||
97 | /** |
||
98 | * Entry point |
||
99 | * |
||
100 | * @param string $par |
||
101 | */ |
||
102 | public function execute( $par ) { |
||
103 | $request = $this->getRequest(); |
||
104 | |||
105 | // Fetch the search term |
||
106 | $search = str_replace( "\n", " ", $request->getText( 'search' ) ); |
||
107 | |||
108 | // Historically search terms have been accepted not only in the search query |
||
109 | // parameter, but also as part of the primary url. This can have PII implications |
||
110 | // in releasing page view data. As such issue a 301 redirect to the correct |
||
111 | // URL. |
||
112 | if ( strlen( $par ) && !strlen( $search ) ) { |
||
113 | $query = $request->getValues(); |
||
114 | unset( $query['title'] ); |
||
115 | // Strip underscores from title parameter; most of the time we'll want |
||
116 | // text form here. But don't strip underscores from actual text params! |
||
117 | $query['search'] = str_replace( '_', ' ', $par ); |
||
118 | $this->getOutput()->redirect( $this->getPageTitle()->getFullURL( $query ), 301 ); |
||
119 | return; |
||
120 | } |
||
121 | |||
122 | $this->setHeaders(); |
||
123 | $this->outputHeader(); |
||
124 | $out = $this->getOutput(); |
||
125 | $out->allowClickjacking(); |
||
126 | $out->addModuleStyles( [ |
||
127 | 'mediawiki.special', 'mediawiki.special.search.styles', 'mediawiki.ui', 'mediawiki.ui.button', |
||
128 | 'mediawiki.ui.input', 'mediawiki.widgets.SearchInputWidget.styles', |
||
129 | ] ); |
||
130 | $this->addHelpLink( 'Help:Searching' ); |
||
131 | |||
132 | $this->load(); |
||
133 | if ( !is_null( $request->getVal( 'nsRemember' ) ) ) { |
||
134 | $this->saveNamespaces(); |
||
135 | // Remove the token from the URL to prevent the user from inadvertently |
||
136 | // exposing it (e.g. by pasting it into a public wiki page) or undoing |
||
137 | // later settings changes (e.g. by reloading the page). |
||
138 | $query = $request->getValues(); |
||
139 | unset( $query['title'], $query['nsRemember'] ); |
||
140 | $out->redirect( $this->getPageTitle()->getFullURL( $query ) ); |
||
141 | return; |
||
142 | } |
||
143 | |||
144 | $out->addJsConfigVars( [ 'searchTerm' => $search ] ); |
||
145 | $this->searchEngineType = $request->getVal( 'srbackend' ); |
||
146 | |||
147 | if ( $request->getVal( 'fulltext' ) |
||
148 | || !is_null( $request->getVal( 'offset' ) ) |
||
149 | ) { |
||
150 | $this->showResults( $search ); |
||
151 | } else { |
||
152 | $this->goResult( $search ); |
||
153 | } |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Set up basic search parameters from the request and user settings. |
||
158 | * |
||
159 | * @see tests/phpunit/includes/specials/SpecialSearchTest.php |
||
160 | */ |
||
161 | public function load() { |
||
207 | |||
208 | /** |
||
209 | * If an exact title match can be found, jump straight ahead to it. |
||
210 | * |
||
211 | * @param string $term |
||
212 | */ |
||
213 | public function goResult( $term ) { |
||
214 | $this->setupPage( $term ); |
||
215 | # Try to go to page as entered. |
||
216 | $title = Title::newFromText( $term ); |
||
217 | # If the string cannot be used to create a title |
||
218 | if ( is_null( $title ) ) { |
||
219 | $this->showResults( $term ); |
||
220 | |||
221 | return; |
||
222 | } |
||
223 | # If there's an exact or very near match, jump right there. |
||
224 | $title = $this->getSearchEngine() |
||
225 | ->getNearMatcher( $this->getConfig() )->getNearMatch( $term ); |
||
226 | |||
227 | if ( !is_null( $title ) && |
||
228 | Hooks::run( 'SpecialSearchGoResult', [ $term, $title, &$url ] ) |
||
229 | ) { |
||
230 | if ( $url === null ) { |
||
231 | $url = $title->getFullURL(); |
||
232 | } |
||
233 | $this->getOutput()->redirect( $url ); |
||
234 | |||
235 | return; |
||
236 | } |
||
237 | $this->showResults( $term ); |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * @param string $term |
||
242 | */ |
||
243 | public function showResults( $term ) { |
||
244 | global $wgContLang; |
||
245 | |||
246 | $search = $this->getSearchEngine(); |
||
247 | $search->setFeatureData( 'rewrite', $this->runSuggestion ); |
||
248 | $search->setLimitOffset( $this->limit, $this->offset ); |
||
249 | $search->setNamespaces( $this->namespaces ); |
||
250 | $search->prefix = $this->mPrefix; |
||
251 | $term = $search->transformSearchTerm( $term ); |
||
252 | |||
253 | Hooks::run( 'SpecialSearchSetupEngine', [ $this, $this->profile, $search ] ); |
||
254 | |||
255 | $this->setupPage( $term ); |
||
256 | |||
257 | $out = $this->getOutput(); |
||
258 | |||
259 | if ( $this->getConfig()->get( 'DisableTextSearch' ) ) { |
||
260 | $searchFowardUrl = $this->getConfig()->get( 'SearchForwardUrl' ); |
||
261 | if ( $searchFowardUrl ) { |
||
262 | $url = str_replace( '$1', urlencode( $term ), $searchFowardUrl ); |
||
263 | $out->redirect( $url ); |
||
264 | } else { |
||
265 | $out->addHTML( |
||
266 | Xml::openElement( 'fieldset' ) . |
||
267 | Xml::element( 'legend', null, $this->msg( 'search-external' )->text() ) . |
||
268 | Xml::element( |
||
269 | 'p', |
||
270 | [ 'class' => 'mw-searchdisabled' ], |
||
271 | $this->msg( 'searchdisabled' )->text() |
||
272 | ) . |
||
273 | $this->msg( 'googlesearch' )->rawParams( |
||
274 | htmlspecialchars( $term ), |
||
275 | 'UTF-8', |
||
276 | $this->msg( 'searchbutton' )->escaped() |
||
277 | )->text() . |
||
278 | Xml::closeElement( 'fieldset' ) |
||
279 | ); |
||
280 | } |
||
281 | |||
282 | return; |
||
283 | } |
||
284 | |||
285 | $title = Title::newFromText( $term ); |
||
286 | $showSuggestion = $title === null || !$title->isKnown(); |
||
287 | $search->setShowSuggestion( $showSuggestion ); |
||
288 | |||
289 | // fetch search results |
||
290 | $rewritten = $search->replacePrefixes( $term ); |
||
291 | |||
292 | $titleMatches = $search->searchTitle( $rewritten ); |
||
293 | $textMatches = $search->searchText( $rewritten ); |
||
294 | |||
295 | $textStatus = null; |
||
296 | if ( $textMatches instanceof Status ) { |
||
297 | $textStatus = $textMatches; |
||
298 | $textMatches = null; |
||
299 | } |
||
300 | |||
301 | // did you mean... suggestions |
||
302 | $didYouMeanHtml = ''; |
||
303 | if ( $showSuggestion && $textMatches && !$textStatus ) { |
||
304 | if ( $textMatches->hasRewrittenQuery() ) { |
||
305 | $didYouMeanHtml = $this->getDidYouMeanRewrittenHtml( $term, $textMatches ); |
||
306 | } elseif ( $textMatches->hasSuggestion() ) { |
||
307 | $didYouMeanHtml = $this->getDidYouMeanHtml( $textMatches ); |
||
308 | } |
||
309 | } |
||
310 | |||
311 | if ( !Hooks::run( 'SpecialSearchResultsPrepend', [ $this, $out, $term ] ) ) { |
||
312 | # Hook requested termination |
||
313 | return; |
||
314 | } |
||
315 | |||
316 | // start rendering the page |
||
317 | $out->addHTML( |
||
318 | Xml::openElement( |
||
319 | 'form', |
||
320 | [ |
||
321 | 'id' => ( $this->isPowerSearch() ? 'powersearch' : 'search' ), |
||
322 | 'method' => 'get', |
||
323 | 'action' => wfScript(), |
||
324 | ] |
||
325 | ) |
||
326 | ); |
||
327 | |||
328 | // Get number of results |
||
329 | $titleMatchesNum = $textMatchesNum = $numTitleMatches = $numTextMatches = 0; |
||
330 | if ( $titleMatches ) { |
||
331 | $titleMatchesNum = $titleMatches->numRows(); |
||
332 | $numTitleMatches = $titleMatches->getTotalHits(); |
||
333 | } |
||
334 | if ( $textMatches ) { |
||
335 | $textMatchesNum = $textMatches->numRows(); |
||
336 | $numTextMatches = $textMatches->getTotalHits(); |
||
337 | } |
||
338 | $num = $titleMatchesNum + $textMatchesNum; |
||
339 | $totalRes = $numTitleMatches + $numTextMatches; |
||
340 | |||
341 | $out->enableOOUI(); |
||
342 | $out->addHTML( |
||
343 | # This is an awful awful ID name. It's not a table, but we |
||
344 | # named it poorly from when this was a table so now we're |
||
345 | # stuck with it |
||
346 | Xml::openElement( 'div', [ 'id' => 'mw-search-top-table' ] ) . |
||
347 | $this->shortDialog( $term, $num, $totalRes ) . |
||
348 | Xml::closeElement( 'div' ) . |
||
349 | $this->searchProfileTabs( $term ) . |
||
350 | $this->searchOptions( $term ) . |
||
351 | Xml::closeElement( 'form' ) . |
||
352 | $didYouMeanHtml |
||
353 | ); |
||
354 | |||
355 | $filePrefix = $wgContLang->getFormattedNsText( NS_FILE ) . ':'; |
||
356 | if ( trim( $term ) === '' || $filePrefix === trim( $term ) ) { |
||
357 | // Empty query -- straight view of search form |
||
358 | return; |
||
359 | } |
||
360 | |||
361 | $out->addHTML( "<div class='searchresults'>" ); |
||
362 | |||
363 | // prev/next links |
||
364 | $prevnext = null; |
||
365 | if ( $num || $this->offset ) { |
||
366 | // Show the create link ahead |
||
367 | $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); |
||
368 | if ( $totalRes > $this->limit || $this->offset ) { |
||
369 | if ( $this->searchEngineType !== null ) { |
||
370 | $this->setExtraParam( 'srbackend', $this->searchEngineType ); |
||
371 | } |
||
372 | $prevnext = $this->getLanguage()->viewPrevNext( |
||
373 | $this->getPageTitle(), |
||
374 | $this->offset, |
||
375 | $this->limit, |
||
376 | $this->powerSearchOptions() + [ 'search' => $term ], |
||
377 | $this->limit + $this->offset >= $totalRes |
||
378 | ); |
||
379 | } |
||
380 | } |
||
381 | Hooks::run( 'SpecialSearchResults', [ $term, &$titleMatches, &$textMatches ] ); |
||
382 | |||
383 | $out->parserOptions()->setEditSection( false ); |
||
384 | if ( $titleMatches ) { |
||
385 | if ( $numTitleMatches > 0 ) { |
||
386 | $out->wrapWikiMsg( "==$1==\n", 'titlematches' ); |
||
387 | $out->addHTML( $this->showMatches( $titleMatches ) ); |
||
388 | } |
||
389 | $titleMatches->free(); |
||
390 | } |
||
391 | if ( $textMatches && !$textStatus ) { |
||
392 | // output appropriate heading |
||
393 | if ( $numTextMatches > 0 && $numTitleMatches > 0 ) { |
||
394 | $out->addHTML( '<div class="mw-search-visualclear"></div>' ); |
||
395 | // if no title matches the heading is redundant |
||
396 | $out->wrapWikiMsg( "==$1==\n", 'textmatches' ); |
||
397 | } |
||
398 | |||
399 | // show results |
||
400 | if ( $numTextMatches > 0 ) { |
||
401 | $search->augmentSearchResults( $textMatches ); |
||
402 | $out->addHTML( $this->showMatches( $textMatches ) ); |
||
403 | } |
||
404 | |||
405 | // show secondary interwiki results if any |
||
406 | if ( $textMatches->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) ) { |
||
407 | $out->addHTML( $this->showInterwiki( $textMatches->getInterwikiResults( |
||
408 | SearchResultSet::SECONDARY_RESULTS ), $term ) ); |
||
409 | } |
||
410 | } |
||
411 | |||
412 | $hasOtherResults = $textMatches && |
||
413 | $textMatches->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ); |
||
414 | |||
415 | if ( $num === 0 ) { |
||
416 | if ( $textStatus ) { |
||
417 | $out->addHTML( '<div class="error">' . |
||
418 | $textStatus->getMessage( 'search-error' ) . '</div>' ); |
||
419 | } else { |
||
420 | if ( !$this->offset ) { |
||
421 | // If we have an offset the create link was rendered earlier in this function. |
||
422 | // This class needs a good de-spaghettification, but for now this will |
||
423 | // do the job. |
||
424 | $this->showCreateLink( $title, $num, $titleMatches, $textMatches ); |
||
425 | } |
||
426 | $out->wrapWikiMsg( "<p class=\"mw-search-nonefound\">\n$1</p>", |
||
427 | [ $hasOtherResults ? 'search-nonefound-thiswiki' : 'search-nonefound', |
||
428 | wfEscapeWikiText( $term ) |
||
429 | ] ); |
||
430 | } |
||
431 | } |
||
432 | |||
433 | if ( $hasOtherResults ) { |
||
434 | foreach ( $textMatches->getInterwikiResults( SearchResultSet::INLINE_RESULTS ) |
||
435 | as $interwiki => $interwikiResult ) { |
||
436 | if ( $interwikiResult instanceof Status || $interwikiResult->numRows() == 0 ) { |
||
437 | // ignore bad interwikis for now |
||
438 | continue; |
||
439 | } |
||
440 | // TODO: wiki header |
||
441 | $out->addHTML( $this->showMatches( $interwikiResult, $interwiki ) ); |
||
442 | } |
||
443 | } |
||
444 | |||
445 | if ( $textMatches ) { |
||
446 | $textMatches->free(); |
||
447 | } |
||
448 | |||
449 | $out->addHTML( '<div class="mw-search-visualclear"></div>' ); |
||
450 | |||
451 | if ( $prevnext ) { |
||
452 | $out->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" ); |
||
453 | } |
||
454 | |||
455 | $out->addHTML( "</div>" ); |
||
456 | |||
457 | Hooks::run( 'SpecialSearchResultsAppend', [ $this, $out, $term ] ); |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * Produce wiki header for interwiki results |
||
462 | * @param string $interwiki Interwiki name |
||
463 | * @param SearchResultSet $interwikiResult The result set |
||
464 | * @return string |
||
465 | */ |
||
466 | protected function interwikiHeader( $interwiki, $interwikiResult ) { |
||
471 | |||
472 | /** |
||
473 | * Generates HTML shown to the user when we have a suggestion about a query |
||
474 | * that might give more results than their current query. |
||
475 | */ |
||
476 | protected function getDidYouMeanHtml( SearchResultSet $textMatches ) { |
||
500 | |||
501 | /** |
||
502 | * Generates HTML shown to user when their query has been internally rewritten, |
||
503 | * and the results of the rewritten query are being returned. |
||
504 | * |
||
505 | * @param string $term The users search input |
||
506 | * @param SearchResultSet $textMatches The response to the users initial search request |
||
507 | * @return string HTML linking the user to their original $term query, and the one |
||
508 | * suggested by $textMatches. |
||
509 | */ |
||
510 | protected function getDidYouMeanRewrittenHtml( $term, SearchResultSet $textMatches ) { |
||
544 | |||
545 | /** |
||
546 | * @param Title $title |
||
547 | * @param int $num The number of search results found |
||
548 | * @param null|SearchResultSet $titleMatches Results from title search |
||
549 | * @param null|SearchResultSet $textMatches Results from text search |
||
550 | */ |
||
551 | protected function showCreateLink( $title, $num, $titleMatches, $textMatches ) { |
||
593 | |||
594 | /** |
||
595 | * @param string $term |
||
596 | */ |
||
597 | protected function setupPage( $term ) { |
||
609 | |||
610 | /** |
||
611 | * Return true if current search is a power (advanced) search |
||
612 | * |
||
613 | * @return bool |
||
614 | */ |
||
615 | protected function isPowerSearch() { |
||
618 | |||
619 | /** |
||
620 | * Extract "power search" namespace settings from the request object, |
||
621 | * returning a list of index numbers to search. |
||
622 | * |
||
623 | * @param WebRequest $request |
||
624 | * @return array |
||
625 | */ |
||
626 | protected function powerSearch( &$request ) { |
||
636 | |||
637 | /** |
||
638 | * Reconstruct the 'power search' options for links |
||
639 | * |
||
640 | * @return array |
||
641 | */ |
||
642 | protected function powerSearchOptions() { |
||
654 | |||
655 | /** |
||
656 | * Save namespace preferences when we're supposed to |
||
657 | * |
||
658 | * @return bool Whether we wrote something |
||
659 | */ |
||
660 | protected function saveNamespaces() { |
||
691 | |||
692 | /** |
||
693 | * Show whole set of results |
||
694 | * |
||
695 | * @param SearchResultSet $matches |
||
696 | * @param string $interwiki Interwiki name |
||
697 | * |
||
698 | * @return string |
||
699 | */ |
||
700 | protected function showMatches( $matches, $interwiki = null ) { |
||
724 | |||
725 | /** |
||
726 | * Format a single hit result |
||
727 | * |
||
728 | * @param SearchResult $result |
||
729 | * @param array $terms Terms to highlight |
||
730 | * @param int $position Position within the search results, including offset. |
||
731 | * |
||
732 | * @return string |
||
733 | */ |
||
734 | protected function showHit( SearchResult $result, $terms, $position ) { |
||
885 | |||
886 | /** |
||
887 | * Extract custom captions from search-interwiki-custom message |
||
888 | */ |
||
889 | protected function getCustomCaptions() { |
||
902 | |||
903 | /** |
||
904 | * Show results from other wikis |
||
905 | * |
||
906 | * @param SearchResultSet|array $matches |
||
907 | * @param string $query |
||
908 | * |
||
909 | * @return string |
||
910 | */ |
||
911 | protected function showInterwiki( $matches, $query ) { |
||
943 | |||
944 | /** |
||
945 | * Show single interwiki link |
||
946 | * |
||
947 | * @param SearchResult $result |
||
948 | * @param string $lastInterwiki |
||
949 | * @param string $query |
||
950 | * |
||
951 | * @return string |
||
952 | */ |
||
953 | protected function showInterwikiHit( $result, $lastInterwiki, $query ) { |
||
1017 | |||
1018 | /** |
||
1019 | * Generates the power search box at [[Special:Search]] |
||
1020 | * |
||
1021 | * @param string $term Search term |
||
1022 | * @param array $opts |
||
1023 | * @return string HTML form |
||
1024 | */ |
||
1025 | protected function powerSearchBox( $term, $opts ) { |
||
1107 | |||
1108 | /** |
||
1109 | * @return array |
||
1110 | */ |
||
1111 | protected function getSearchProfiles() { |
||
1152 | |||
1153 | /** |
||
1154 | * @param string $term |
||
1155 | * @return string |
||
1156 | */ |
||
1157 | protected function searchProfileTabs( $term ) { |
||
1202 | |||
1203 | /** |
||
1204 | * @param string $term Search term |
||
1205 | * @return string |
||
1206 | */ |
||
1207 | protected function searchOptions( $term ) { |
||
1222 | |||
1223 | /** |
||
1224 | * @param string $term |
||
1225 | * @param int $resultsShown |
||
1226 | * @param int $totalNum |
||
1227 | * @return string |
||
1228 | */ |
||
1229 | protected function shortDialog( $term, $resultsShown, $totalNum ) { |
||
1264 | |||
1265 | /** |
||
1266 | * Make a search link with some target namespaces |
||
1267 | * |
||
1268 | * @param string $term |
||
1269 | * @param array $namespaces Ignored |
||
1270 | * @param string $label Link's text |
||
1271 | * @param string $tooltip Link's tooltip |
||
1272 | * @param array $params Query string parameters |
||
1273 | * @return string HTML fragment |
||
1274 | */ |
||
1275 | protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params = [] ) { |
||
1298 | |||
1299 | /** |
||
1300 | * Check if query starts with image: prefix |
||
1301 | * |
||
1302 | * @param string $term The string to check |
||
1303 | * @return bool |
||
1304 | */ |
||
1305 | protected function startsWithImage( $term ) { |
||
1315 | |||
1316 | /** |
||
1317 | * @since 1.18 |
||
1318 | * |
||
1319 | * @return SearchEngine |
||
1320 | */ |
||
1321 | public function getSearchEngine() { |
||
1330 | |||
1331 | /** |
||
1332 | * Current search profile. |
||
1333 | * @return null|string |
||
1334 | */ |
||
1335 | function getProfile() { |
||
1338 | |||
1339 | /** |
||
1340 | * Current namespaces. |
||
1341 | * @return array |
||
1342 | */ |
||
1343 | function getNamespaces() { |
||
1346 | |||
1347 | /** |
||
1348 | * Users of hook SpecialSearchSetupEngine can use this to |
||
1349 | * add more params to links to not lose selection when |
||
1350 | * user navigates search results. |
||
1351 | * @since 1.18 |
||
1352 | * |
||
1353 | * @param string $key |
||
1354 | * @param mixed $value |
||
1355 | */ |
||
1356 | public function setExtraParam( $key, $value ) { |
||
1359 | |||
1360 | protected function getGroupName() { |
||
1363 | } |
||
1364 |
Only declaring a single property per statement allows you to later on add doc comments more easily.
It is also recommended by PSR2, so it is a common style that many people expect.