Convert SearchResultSet to typical iteration
[lhc/web/wiklou.git] / includes / search / SearchResultSet.php
1 <?php
2 /**
3 * Search result sets
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Search
22 */
23
24 /**
25 * @ingroup Search
26 */
27 class SearchResultSet implements IteratorAggregate {
28
29 /**
30 * Types of interwiki results
31 */
32 /**
33 * Results that are displayed only together with existing main wiki results
34 * @var int
35 */
36 const SECONDARY_RESULTS = 0;
37 /**
38 * Results that can displayed even if no existing main wiki results exist
39 * @var int
40 */
41 const INLINE_RESULTS = 1;
42
43 protected $containedSyntax = false;
44
45 /**
46 * Cache of titles.
47 * Lists titles of the result set, in the same order as results.
48 * @var Title[]
49 */
50 private $titles;
51
52 /**
53 * Cache of results - serialization of the result iterator
54 * as an array.
55 * @var SearchResult[]
56 */
57 protected $results;
58
59 /**
60 * Set of result's extra data, indexed per result id
61 * and then per data item name.
62 * The structure is:
63 * PAGE_ID => [ augmentor name => data, ... ]
64 * @var array[]
65 */
66 protected $extraData = [];
67
68 /** @var ArrayIterator|null Iterator supporting BC iteration methods */
69 private $bcIterator;
70
71 public function __construct( $containedSyntax = false ) {
72 if ( static::class === __CLASS__ ) {
73 // This class will eventually be abstract. SearchEngine implementations
74 // already have to extend this class anyways to provide the actual
75 // search results.
76 wfDeprecated( __METHOD__, 1.32 );
77 }
78 $this->containedSyntax = $containedSyntax;
79 }
80
81 /**
82 * Fetch an array of regular expression fragments for matching
83 * the search terms as parsed by this engine in a text extract.
84 * STUB
85 *
86 * @return array
87 */
88 function termMatches() {
89 return [];
90 }
91
92 function numRows() {
93 return 0;
94 }
95
96 /**
97 * Some search modes return a total hit count for the query
98 * in the entire article database. This may include pages
99 * in namespaces that would not be matched on the given
100 * settings.
101 *
102 * Return null if no total hits number is supported.
103 *
104 * @return int
105 */
106 function getTotalHits() {
107 return null;
108 }
109
110 /**
111 * Some search modes will run an alternative query that it thinks gives
112 * a better result than the provided search. Returns true if this has
113 * occured.
114 *
115 * @return bool
116 */
117 function hasRewrittenQuery() {
118 return false;
119 }
120
121 /**
122 * @return string|null The search the query was internally rewritten to,
123 * or null when the result of the original query was returned.
124 */
125 function getQueryAfterRewrite() {
126 return null;
127 }
128
129 /**
130 * @return string|null Same as self::getQueryAfterRewrite(), but in HTML
131 * and with changes highlighted. Null when the query was not rewritten.
132 */
133 function getQueryAfterRewriteSnippet() {
134 return null;
135 }
136
137 /**
138 * Some search modes return a suggested alternate term if there are
139 * no exact hits. Returns true if there is one on this set.
140 *
141 * @return bool
142 */
143 function hasSuggestion() {
144 return false;
145 }
146
147 /**
148 * @return string|null Suggested query, null if none
149 */
150 function getSuggestionQuery() {
151 return null;
152 }
153
154 /**
155 * @return string HTML highlighted suggested query, '' if none
156 */
157 function getSuggestionSnippet() {
158 return '';
159 }
160
161 /**
162 * Return a result set of hits on other (multiple) wikis associated with this one
163 *
164 * @param int $type
165 * @return SearchResultSet[]
166 */
167 function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
168 return null;
169 }
170
171 /**
172 * Check if there are results on other wikis
173 *
174 * @param int $type
175 * @return bool
176 */
177 function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
178 return false;
179 }
180
181 /**
182 * Fetches next search result, or false.
183 * @deprecated since 1.32; Use self::extractResults() or foreach
184 * @return SearchResult|false
185 */
186 public function next() {
187 wfDeprecated( __METHOD__, '1.32' );
188 $it = $this->bcIterator();
189 $searchResult = $it->current();
190 $it->next();
191 return $searchResult === null ? false : $searchResult;
192 }
193
194 /**
195 * Rewind result set back to beginning
196 * @deprecated since 1.32; Use self::extractResults() or foreach
197 */
198 public function rewind() {
199 wfDeprecated( __METHOD__, '1.32' );
200 $this->bcIterator()->rewind();
201 }
202
203 private function bcIterator() {
204 if ( $this->bcIterator === null ) {
205 $this->bcIterator = 'RECURSION';
206 $this->bcIterator = $this->getIterator();
207 } elseif ( $this->bcIterator === 'RECURSION' ) {
208 // Either next/rewind or extractResults must be implemented. This
209 // class was potentially instantiated directly. It should be
210 // abstract with abstract methods to enforce this but that's a
211 // breaking change...
212 wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
213 $this->bcIterator = new ArrayIterator( [] );
214 }
215 return $this->bcIterator;
216 }
217
218 /**
219 * Frees the result set, if applicable.
220 */
221 function free() {
222 // ...
223 }
224
225 /**
226 * Did the search contain search syntax? If so, Special:Search won't offer
227 * the user a link to a create a page named by the search string because the
228 * name would contain the search syntax.
229 * @return bool
230 */
231 public function searchContainedSyntax() {
232 return $this->containedSyntax;
233 }
234
235 /**
236 * Extract all the results in the result set as array.
237 * @return SearchResult[]
238 */
239 public function extractResults() {
240 if ( is_null( $this->results ) ) {
241 $this->results = [];
242 if ( $this->numRows() == 0 ) {
243 // Don't bother if we've got empty result
244 return $this->results;
245 }
246 $this->rewind();
247 while ( ( $result = $this->next() ) != false ) {
248 $this->results[] = $result;
249 }
250 $this->rewind();
251 }
252 return $this->results;
253 }
254
255 /**
256 * Extract all the titles in the result set.
257 * @return Title[]
258 */
259 public function extractTitles() {
260 if ( is_null( $this->titles ) ) {
261 if ( $this->numRows() == 0 ) {
262 // Don't bother if we've got empty result
263 $this->titles = [];
264 } else {
265 $this->titles = array_map(
266 function ( SearchResult $result ) {
267 return $result->getTitle();
268 },
269 $this->extractResults() );
270 }
271 }
272 return $this->titles;
273 }
274
275 /**
276 * Sets augmented data for result set.
277 * @param string $name Extra data item name
278 * @param array[] $data Extra data as PAGEID => data
279 */
280 public function setAugmentedData( $name, $data ) {
281 foreach ( $data as $id => $resultData ) {
282 $this->extraData[$id][$name] = $resultData;
283 }
284 }
285
286 /**
287 * Returns extra data for specific result and store it in SearchResult object.
288 * @param SearchResult $result
289 */
290 public function augmentResult( SearchResult $result ) {
291 $id = $result->getTitle()->getArticleID();
292 if ( $id === -1 ) {
293 return;
294 }
295 $result->setExtensionData( function () use ( $id ) {
296 if ( isset( $this->extraData[$id] ) ) {
297 return $this->extraData[$id];
298 } else {
299 return [];
300 }
301 } );
302 }
303
304 /**
305 * @return int|null The offset the current page starts at. Typically
306 * this should be null to allow the UI to decide on its own, but in
307 * special cases like interleaved AB tests specifying explicitly is
308 * necessary.
309 */
310 public function getOffset() {
311 return null;
312 }
313
314 final public function getIterator() {
315 return new ArrayIterator( $this->extractResults() );
316 }
317 }