Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.77% covered (warning)
57.77%
119 / 206
39.53% covered (danger)
39.53%
17 / 43
CRAP
0.00% covered (danger)
0.00%
0 / 1
Results
57.77% covered (warning)
57.77%
119 / 206
39.53% covered (danger)
39.53%
17 / 43
588.51
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __clone
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUrlQueryHelperOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUrlQuery
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 setHelper
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 performAndProcessSearch
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getFacetList
n/a
0 / 0
n/a
0 / 0
0
 performSearch
n/a
0 / 0
n/a
0 / 0
0
 getSpellingSuggestions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResultTotal
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 overrideStartRecord
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStartRecord
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 getEndRecord
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getResults
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getErrors
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getBackendId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSavedSearch
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getNotificationFrequency
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 updateSaveStatus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 startQueryTimer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 stopQueryTimer
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getQuerySpeed
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getStartTime
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPaginator
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getRawSuggestions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getScores
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMaxScore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtraData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExtraData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 minify
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 deminify
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getRecommendations
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setRecommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 translate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getUrlQueryHelperFactory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setUrlQueryHelperFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHierarchicalFacetHelper
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFullFieldFacets
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 getExtraSearchBackendDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildFacetList
94.55% covered (success)
94.55%
52 / 55
0.00% covered (danger)
0.00%
0 / 1
12.02
1<?php
2
3/**
4 * Abstract results search model.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Search_Base
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Page
28 */
29
30namespace VuFind\Search\Base;
31
32use Laminas\Paginator\Paginator;
33use VuFind\Record\Loader;
34use VuFind\Search\Factory\UrlQueryHelperFactory;
35use VuFindSearch\Service as SearchService;
36
37use function call_user_func_array;
38use function count;
39use function func_get_args;
40use function get_class;
41use function in_array;
42use function is_callable;
43use function is_object;
44
45/**
46 * Abstract results search model.
47 *
48 * This abstract class defines the results methods for modeling a search in VuFind.
49 *
50 * @category VuFind
51 * @package  Search_Base
52 * @author   Demian Katz <demian.katz@villanova.edu>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org Main Page
55 */
56abstract class Results
57{
58    /**
59     * Search parameters
60     *
61     * @var Params
62     */
63    protected $params;
64
65    /**
66     * Total number of results available
67     *
68     * @var int
69     */
70    protected $resultTotal = null;
71
72    /**
73     * Search backend identifier.
74     *
75     * @var string
76     */
77    protected $backendId;
78
79    /**
80     * Override (only for use in very rare cases)
81     *
82     * @var int
83     */
84    protected $startRecordOverride = null;
85
86    /**
87     * Array of results (represented as Record Driver objects) retrieved on latest
88     * search
89     *
90     * @var array
91     */
92    protected $results = null;
93
94    /**
95     * Any errors reported by the search backend
96     *
97     * @var array
98     */
99    protected $errors = null;
100
101    /**
102     * An ID number for saving/retrieving search
103     *
104     * @var int
105     */
106    protected $searchId = null;
107
108    /**
109     * Is this a user-saved search?
110     *
111     * @var bool
112     */
113    protected $savedSearch = null;
114
115    /**
116     * How frequently will a user be notified about this search (0 = never)?
117     *
118     * @var int
119     */
120    protected $notificationFrequency = null;
121
122    /**
123     * Query start time
124     *
125     * @var float
126     */
127    protected $queryStartTime = null;
128
129    /**
130     * Query end time
131     *
132     * @var float
133     */
134    protected $queryEndTime = null;
135
136    /**
137     * Query time (total)
138     *
139     * @var float
140     */
141    protected $queryTime = null;
142
143    /**
144     * Helper objects
145     *
146     * @var array
147     */
148    protected $helpers = [];
149
150    /**
151     * Spelling suggestions
152     *
153     * @var array
154     */
155    protected $suggestions = null;
156
157    /**
158     * Recommendations
159     *
160     * @var array
161     */
162    protected $recommend = [];
163
164    /**
165     * Search service.
166     *
167     * @var SearchService
168     */
169    protected $searchService;
170
171    /**
172     * Record loader
173     *
174     * @var Loader
175     */
176    protected $recordLoader;
177
178    /**
179     * URL query helper factory
180     *
181     * @var UrlQueryHelperFactory
182     */
183    protected $urlQueryHelperFactory = null;
184
185    /**
186     * Hierarchical facet helper
187     *
188     * @var HierarchicalFacetHelperInterface
189     */
190    protected $hierarchicalFacetHelper = null;
191
192    /**
193     * Extra search details.
194     *
195     * @var ?array
196     */
197    protected $extraSearchBackendDetails = null;
198
199    /**
200     * Constructor
201     *
202     * @param \VuFind\Search\Base\Params $params        Object representing user
203     * search parameters.
204     * @param SearchService              $searchService Search service
205     * @param Loader                     $recordLoader  Record loader
206     */
207    public function __construct(
208        Params $params,
209        SearchService $searchService,
210        Loader $recordLoader
211    ) {
212        $this->setParams($params);
213        $this->searchService = $searchService;
214        $this->recordLoader = $recordLoader;
215    }
216
217    /**
218     * Copy constructor
219     *
220     * @return void
221     */
222    public function __clone()
223    {
224        if (is_object($this->params)) {
225            $this->params = clone $this->params;
226        }
227        $this->helpers = [];
228    }
229
230    /**
231     * Get the search parameters object.
232     *
233     * @return \VuFind\Search\Base\Params
234     */
235    public function getParams()
236    {
237        return $this->params;
238    }
239
240    /**
241     * Set the search parameters object.
242     *
243     * @param \VuFind\Search\Base\Params $params Parameters to set
244     *
245     * @return void
246     */
247    public function setParams($params)
248    {
249        $this->params = $params;
250    }
251
252    /**
253     * Get the search options object.
254     *
255     * @return \VuFind\Search\Base\Options
256     */
257    public function getOptions()
258    {
259        return $this->getParams()->getOptions();
260    }
261
262    /**
263     * Options for UrlQueryHelper
264     *
265     * @return array
266     */
267    protected function getUrlQueryHelperOptions()
268    {
269        return [];
270    }
271
272    /**
273     * Get the URL helper for this object.
274     *
275     * @return \VuFind\Search\UrlQueryHelper
276     */
277    public function getUrlQuery()
278    {
279        // Set up URL helper:
280        if (!isset($this->helpers['urlQuery'])) {
281            $factory = $this->getUrlQueryHelperFactory();
282            $this->helpers['urlQuery'] = $factory->fromParams(
283                $this->getParams(),
284                $this->getUrlQueryHelperOptions()
285            );
286        }
287        return $this->helpers['urlQuery'];
288    }
289
290    /**
291     * Override a helper object.
292     *
293     * @param string $key   Name of helper to set
294     * @param object $value Helper object
295     *
296     * @return void
297     */
298    public function setHelper($key, $value)
299    {
300        $this->helpers[$key] = $value;
301    }
302
303    /**
304     * Actually execute the search.
305     *
306     * @return void
307     */
308    public function performAndProcessSearch()
309    {
310        // Initialize variables to defaults (to ensure they don't stay null
311        // and cause unnecessary repeat processing):
312        // The value of -1 indicates that resultTotal is not available.
313        $this->resultTotal = -1;
314        $this->results = [];
315        $this->suggestions = [];
316        $this->errors = [];
317
318        // Run the search:
319        $this->startQueryTimer();
320        $this->performSearch();
321        $this->stopQueryTimer();
322    }
323
324    /**
325     * Returns the stored list of facets for the last search
326     *
327     * @param array $filter Array of field => on-screen description listing
328     * all of the desired facet fields; set to null to get all configured values.
329     *
330     * @return array        Facets data arrays
331     */
332    abstract public function getFacetList($filter = null);
333
334    /**
335     * Abstract support method for performAndProcessSearch -- perform a search based
336     * on the parameters passed to the object. This method is responsible for
337     * filling in all of the key class properties: results, resultTotal, etc.
338     *
339     * @return void
340     */
341    abstract protected function performSearch();
342
343    /**
344     * Get spelling suggestion information.
345     *
346     * @return array
347     */
348    public function getSpellingSuggestions()
349    {
350        // Not supported by default:
351        return [];
352    }
353
354    /**
355     * Get total count of records in the result set (not just current page).
356     *
357     * @return int
358     */
359    public function getResultTotal()
360    {
361        if (null === $this->resultTotal) {
362            $this->performAndProcessSearch();
363        }
364        return $this->resultTotal;
365    }
366
367    /**
368     * Manually override the start record number.
369     *
370     * @param int $rec Record number to use.
371     *
372     * @return void
373     */
374    public function overrideStartRecord($rec)
375    {
376        $this->startRecordOverride = $rec;
377    }
378
379    /**
380     * Get record number for start of range represented by current result set.
381     *
382     * @return int
383     */
384    public function getStartRecord()
385    {
386        if (null !== $this->startRecordOverride) {
387            return $this->startRecordOverride;
388        }
389        $params = $this->getParams();
390        $page = $params->getPage();
391        $pageLimit = $params->getLimit();
392        $resultLimit = $this->getOptions()->getVisibleSearchResultLimit();
393        if ($resultLimit > -1 && $resultLimit < $page * $pageLimit) {
394            $page = ceil($resultLimit / $pageLimit);
395        }
396        return (($page - 1) * $pageLimit) + 1;
397    }
398
399    /**
400     * Get record number for end of range represented by current result set.
401     *
402     * @return int
403     */
404    public function getEndRecord()
405    {
406        $total = $this->getResultTotal();
407        $params = $this->getParams();
408        $page = $params->getPage();
409        $pageLimit = $params->getLimit();
410        $resultLimit = $this->getOptions()->getVisibleSearchResultLimit();
411
412        if ($resultLimit > -1 && $resultLimit < ($page * $pageLimit)) {
413            $record = $resultLimit;
414        } else {
415            $record = $page * $pageLimit;
416        }
417        // If the end of the current page runs past the last record, use total
418        // results; otherwise use the last record on this page:
419        return ($record > $total) ? $total : $record;
420    }
421
422    /**
423     * Basic 'getter' for search results.
424     *
425     * @return array
426     */
427    public function getResults()
428    {
429        if (null === $this->results) {
430            $this->performAndProcessSearch();
431        }
432        return $this->results;
433    }
434
435    /**
436     * Basic 'getter' for errors.
437     *
438     * @return array
439     */
440    public function getErrors()
441    {
442        if (null === $this->errors) {
443            $this->performAndProcessSearch();
444        }
445        return $this->errors;
446    }
447
448    /**
449     * Basic 'getter' of search backend identifier.
450     *
451     * @return string
452     */
453    public function getBackendId()
454    {
455        return $this->backendId;
456    }
457
458    /**
459     * Basic 'getter' for ID of saved search.
460     *
461     * @return int
462     */
463    public function getSearchId()
464    {
465        return $this->searchId;
466    }
467
468    /**
469     * Is the current search saved in the database?
470     *
471     * @return bool
472     */
473    public function isSavedSearch()
474    {
475        // This data is not available until the search has been saved; blow up if somebody
476        // tries to get data that is not yet available.
477        if (null === $this->savedSearch) {
478            throw new \Exception(
479                'Cannot retrieve save status before updateSaveStatus is called.'
480            );
481        }
482        return $this->savedSearch;
483    }
484
485    /**
486     * How frequently (in days) will the current user be notified about updates to
487     * these search results (0 = never)?
488     *
489     * @return int
490     * @throws \Exception
491     */
492    public function getNotificationFrequency(): int
493    {
494        // This data is not available until the search has been saved; blow up if somebody
495        // tries to get data that is not yet available.
496        if (null === $this->notificationFrequency) {
497            throw new \Exception(
498                'Cannot retrieve notification frequency before updateSaveStatus is called.'
499            );
500        }
501        return $this->notificationFrequency;
502    }
503
504    /**
505     * Given a database row corresponding to the current search object,
506     * mark whether this search is saved and what its database ID is.
507     *
508     * @param SearchEntityInterface $row Relevant database row.
509     *
510     * @return void
511     */
512    public function updateSaveStatus($row)
513    {
514        $this->searchId = $row->getId();
515        $this->savedSearch = $row->getSaved();
516        $this->notificationFrequency = $this->savedSearch ? $row->getNotificationFrequency() : 0;
517    }
518
519    /**
520     * Start the timer to figure out how long a query takes. Complements
521     * stopQueryTimer().
522     *
523     * @return void
524     */
525    protected function startQueryTimer()
526    {
527        // Get time before the query
528        $time = explode(' ', microtime());
529        $this->queryStartTime = $time[1] + $time[0];
530    }
531
532    /**
533     * End the timer to figure out how long a query takes. Complements
534     * startQueryTimer().
535     *
536     * @return void
537     */
538    protected function stopQueryTimer()
539    {
540        $time = explode(' ', microtime());
541        $this->queryEndTime = $time[1] + $time[0];
542        $this->queryTime = $this->queryEndTime - $this->queryStartTime;
543    }
544
545    /**
546     * Basic 'getter' for query speed.
547     *
548     * @return float
549     */
550    public function getQuerySpeed()
551    {
552        if (null === $this->queryTime) {
553            $this->performAndProcessSearch();
554        }
555        return $this->queryTime;
556    }
557
558    /**
559     * Basic 'getter' for query start time.
560     *
561     * @return float
562     */
563    public function getStartTime()
564    {
565        if (null === $this->queryStartTime) {
566            $this->performAndProcessSearch();
567        }
568        return $this->queryStartTime;
569    }
570
571    /**
572     * Get a paginator for the result set.
573     *
574     * @return Paginator
575     */
576    public function getPaginator()
577    {
578        // If there is a limit on how many pages are accessible,
579        // apply that limit now:
580        $max = $this->getOptions()->getVisibleSearchResultLimit();
581        $total = $this->getResultTotal();
582        if ($max > 0 && $total > $max) {
583            $total = $max;
584        }
585
586        // Build the standard paginator control:
587        $nullAdapter = "Laminas\Paginator\Adapter\NullFill";
588        $paginator = new Paginator(new $nullAdapter($total));
589        $paginator->setCurrentPageNumber($this->getParams()->getPage())
590            ->setItemCountPerPage($this->getParams()->getLimit())
591            ->setPageRange(11);
592        return $paginator;
593    }
594
595    /**
596     * Basic 'getter' for suggestion list.
597     *
598     * @return array
599     */
600    public function getRawSuggestions()
601    {
602        if (null === $this->suggestions) {
603            $this->performAndProcessSearch();
604        }
605        return $this->suggestions;
606    }
607
608    /**
609     * Get the scores of the results
610     *
611     * @return array
612     */
613    public function getScores()
614    {
615        // Not implemented in the base class
616        return [];
617    }
618
619    /**
620     * Getting the highest relevance of all the results
621     *
622     * @return ?float
623     */
624    public function getMaxScore()
625    {
626        // Not implemented in the base class
627        return null;
628    }
629
630    /**
631     * Get extra data for the search.
632     *
633     * Extra data can be used to store local implementation-specific information.
634     * Contents must be serializable. It is recommended to make the array as small
635     * as possible.
636     *
637     * @return array
638     */
639    public function getExtraData(): array
640    {
641        // Not implemented in the base class
642        return [];
643    }
644
645    /**
646     * Set extra data for the search.
647     *
648     * @param array $data Extra data
649     *
650     * @return void
651     */
652    public function setExtraData(array $data): void
653    {
654        // Not implemented in the base class
655        if (!empty($data)) {
656            error_log(get_class($this) . ': Extra data passed but not handled');
657        }
658    }
659
660    /**
661     * Add settings to a minified object.
662     *
663     * @param \VuFind\Search\Minified $minified Minified Search Object
664     *
665     * @return void
666     */
667    public function minify(&$minified): void
668    {
669        $minified->id = $this->getSearchId();
670        $minified->i  = $this->getStartTime();
671        $minified->s  = $this->getQuerySpeed();
672        $minified->r  = $this->getResultTotal();
673        $minified->ex = $this->getExtraData();
674
675        $this->getParams()->minify($minified);
676    }
677
678    /**
679     * Restore settings from a minified object found in the database.
680     *
681     * @param \VuFind\Search\Minified $minified Minified Search Object
682     *
683     * @return void
684     */
685    public function deminify($minified)
686    {
687        $this->searchId = $minified->id;
688        $this->queryStartTime = $minified->i;
689        $this->queryTime = $minified->s;
690        $this->resultTotal = $minified->r;
691        $this->setExtraData($minified->ex);
692
693        $this->getParams()->deminify($minified);
694    }
695
696    /**
697     * Get an array of recommendation objects for augmenting the results display.
698     *
699     * @param string $location Name of location to use as a filter (null to get
700     * associative array of all locations); legal non-null values: 'top', 'side'
701     *
702     * @return array
703     */
704    public function getRecommendations($location = 'top')
705    {
706        if (null === $location) {
707            return $this->recommend;
708        }
709        return $this->recommend[$location] ?? [];
710    }
711
712    /**
713     * Set the recommendation objects (see \VuFind\Search\RecommendListener).
714     *
715     * @param array $recommend Recommendations
716     *
717     * @return void
718     */
719    public function setRecommendations($recommend)
720    {
721        $this->recommend = $recommend;
722    }
723
724    /**
725     * Return search service.
726     *
727     * @return SearchService
728     *
729     * @todo May better error handling, throw a custom exception if search service
730     * not present
731     */
732    protected function getSearchService()
733    {
734        return $this->searchService;
735    }
736
737    /**
738     * Translate a string if a translator is available (proxies method in Options).
739     *
740     * @return string
741     */
742    public function translate()
743    {
744        return call_user_func_array(
745            [$this->getOptions(), 'translate'],
746            func_get_args()
747        );
748    }
749
750    /**
751     * Get URL query helper factory
752     *
753     * @return UrlQueryHelperFactory
754     */
755    protected function getUrlQueryHelperFactory()
756    {
757        if (null === $this->urlQueryHelperFactory) {
758            $this->urlQueryHelperFactory = new UrlQueryHelperFactory();
759        }
760        return $this->urlQueryHelperFactory;
761    }
762
763    /**
764     * Set URL query helper factory
765     *
766     * @param UrlQueryHelperFactory $factory UrlQueryHelperFactory object
767     *
768     * @return void
769     */
770    public function setUrlQueryHelperFactory(UrlQueryHelperFactory $factory)
771    {
772        $this->urlQueryHelperFactory = $factory;
773    }
774
775    /**
776     * Set hierarchical facet helper
777     *
778     * @param HierarchicalFacetHelperInterface $helper Hierarchical facet helper
779     *
780     * @return void
781     */
782    public function setHierarchicalFacetHelper(
783        HierarchicalFacetHelperInterface $helper
784    ) {
785        $this->hierarchicalFacetHelper = $helper;
786    }
787
788    /**
789     * Get complete facet counts for several index fields
790     *
791     * @param array  $facetfields  name of the Solr fields to return facets for
792     * @param bool   $removeFilter Clear existing filters from selected fields (true)
793     * or retain them (false)?
794     * @param int    $limit        A limit for the number of facets returned, this
795     * may be useful for very large amounts of facets that can break the JSON parse
796     * method because of PHP out of memory exceptions (default = -1, no limit).
797     * @param string $facetSort    A facet sort value to use (null to retain current)
798     *
799     * @return array an array with the facet values for each index field
800     */
801    public function getFullFieldFacets(
802        $facetfields,
803        $removeFilter = true,
804        $limit = -1,
805        $facetSort = null
806    ) {
807        if (!method_exists($this, 'getPartialFieldFacets')) {
808            throw new \Exception('getPartialFieldFacets not implemented');
809        }
810        $page = 1;
811        $facets = [];
812        do {
813            $facetpage = $this->getPartialFieldFacets(
814                $facetfields,
815                $removeFilter,
816                $limit,
817                $facetSort,
818                $page
819            );
820            $nextfields = [];
821            foreach ($facetfields as $field) {
822                if (!empty($facetpage[$field]['data']['list'])) {
823                    if (!isset($facets[$field])) {
824                        $facets[$field] = $facetpage[$field];
825                        $facets[$field]['more'] = false;
826                    } else {
827                        $facets[$field]['data']['list'] = array_merge(
828                            $facets[$field]['data']['list'],
829                            $facetpage[$field]['data']['list']
830                        );
831                    }
832                    if ($facetpage[$field]['more'] !== false) {
833                        $nextfields[] = $field;
834                    }
835                }
836            }
837            $facetfields = $nextfields;
838            $page++;
839        } while ($limit == -1 && !empty($facetfields));
840        return $facets;
841    }
842
843    /**
844     * Get the extra search details
845     *
846     * @return ?array
847     */
848    public function getExtraSearchBackendDetails()
849    {
850        return $this->extraSearchBackendDetails;
851    }
852
853    /**
854     * A helper method that converts the list of facets for the last search from
855     * RecordCollection's facet list.
856     *
857     * @param array $facetList Facet list
858     * @param array $filter    Array of field => on-screen description listing
859     * all of the desired facet fields; set to null to get all configured values.
860     *
861     * @return array Facets data arrays
862     */
863    protected function buildFacetList(array $facetList, array $filter = null): array
864    {
865        // If there is no filter, we'll use all facets as the filter:
866        if (null === $filter) {
867            $filter = $this->getParams()->getFacetConfig();
868        }
869
870        // Start building the facet list:
871        $result = [];
872
873        // Loop through every field returned by the result set
874        $translatedFacets = $this->getOptions()->getTranslatedFacets();
875        $hierarchicalFacets
876            = is_callable([$this->getOptions(), 'getHierarchicalFacets'])
877            ? $this->getOptions()->getHierarchicalFacets()
878            : [];
879        $hierarchicalFacetSortSettings
880            = is_callable([$this->getOptions(), 'getHierarchicalFacetSortSettings'])
881            ? $this->getOptions()->getHierarchicalFacetSortSettings()
882            : [];
883
884        foreach (array_keys($filter) as $field) {
885            $data = $facetList[$field] ?? [];
886            // Skip empty arrays:
887            if (count($data) < 1) {
888                continue;
889            }
890            // Initialize the settings for the current field
891            $result[$field] = [
892                'label' => $filter[$field],
893                'list' => [],
894            ];
895            // Should we translate values for the current facet?
896            $translate = in_array($field, $translatedFacets);
897            $hierarchical = in_array($field, $hierarchicalFacets);
898            $operator = $this->getParams()->getFacetOperator($field);
899            $resultList = [];
900            // Loop through values:
901            foreach ($data as $value => $count) {
902                $displayText = $this->getParams()
903                    ->getFacetValueRawDisplayText($field, $value);
904                if ($hierarchical) {
905                    if (!$this->hierarchicalFacetHelper) {
906                        throw new \Exception(
907                            get_class($this)
908                            . ': hierarchical facet helper unavailable'
909                        );
910                    }
911                    $displayText = $this->hierarchicalFacetHelper
912                        ->formatDisplayText($displayText);
913                }
914                $displayText = $translate
915                    ? $this->getParams()->translateFacetValue($field, $displayText)
916                    : $displayText;
917                $isApplied = $this->getParams()->hasFilter("$field:" . $value)
918                    || $this->getParams()->hasFilter("~$field:" . $value);
919
920                // Store the collected values:
921                $resultList[] = compact(
922                    'value',
923                    'displayText',
924                    'count',
925                    'operator',
926                    'isApplied'
927                );
928            }
929
930            if ($hierarchical) {
931                $sort = $hierarchicalFacetSortSettings[$field]
932                    ?? $hierarchicalFacetSortSettings['*'] ?? 'count';
933                $this->hierarchicalFacetHelper->sortFacetList($resultList, $sort);
934
935                $resultList
936                    = $this->hierarchicalFacetHelper->buildFacetArray($field, $resultList);
937            }
938
939            $result[$field]['list'] = $resultList;
940        }
941        return $result;
942    }
943}