Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Facets
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 9
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setOptions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 configureSearchParams
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getFromRecord
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 getFromSearch
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
110
 getToken
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 buildChannel
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 buildChannelFromToken
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 buildChannelFromFacet
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Facet-driven channel provider.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2016.
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  Channels
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/wiki/development Wiki
28 */
29
30namespace VuFind\ChannelProvider;
31
32use Laminas\Mvc\Controller\Plugin\Url;
33use VuFind\I18n\Translator\TranslatorAwareInterface;
34use VuFind\RecordDriver\AbstractBase as RecordDriver;
35use VuFind\Search\Base\Params;
36use VuFind\Search\Base\Results;
37use VuFind\Search\Results\PluginManager as ResultsManager;
38
39use function count;
40
41/**
42 * Facet-driven channel provider.
43 *
44 * @category VuFind
45 * @package  Channels
46 * @author   Demian Katz <demian.katz@villanova.edu>
47 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
48 * @link     https://vufind.org/wiki/development Wiki
49 */
50class Facets extends AbstractChannelProvider implements TranslatorAwareInterface
51{
52    use \VuFind\I18n\Translator\TranslatorAwareTrait;
53
54    /**
55     * Facet fields to use (field name => description).
56     *
57     * @var array
58     */
59    protected $fields;
60
61    /**
62     * Maximum number of different fields to suggest in the channel list.
63     *
64     * @var int
65     */
66    protected $maxFieldsToSuggest;
67
68    /**
69     * Maximum number of values to suggest per field.
70     *
71     * @var int
72     */
73    protected $maxValuesToSuggestPerField;
74
75    /**
76     * Search results manager.
77     *
78     * @var ResultsManager
79     */
80    protected $resultsManager;
81
82    /**
83     * URL helper
84     *
85     * @var Url
86     */
87    protected $url;
88
89    /**
90     * Constructor
91     *
92     * @param ResultsManager $rm      Results manager
93     * @param Url            $url     URL helper
94     * @param array          $options Settings (optional)
95     */
96    public function __construct(ResultsManager $rm, Url $url, array $options = [])
97    {
98        $this->resultsManager = $rm;
99        $this->url = $url;
100        $this->setOptions($options);
101    }
102
103    /**
104     * Set the options for the provider.
105     *
106     * @param array $options Options
107     *
108     * @return void
109     */
110    public function setOptions(array $options)
111    {
112        $this->fields = $options['fields']
113            ?? ['topic_facet' => 'Topic', 'author_facet' => 'Author'];
114        $this->maxFieldsToSuggest = $options['maxFieldsToSuggest'] ?? 2;
115        $this->maxValuesToSuggestPerField
116            = $options['maxValuesToSuggestPerField'] ?? 2;
117    }
118
119    /**
120     * Hook to configure search parameters before executing search.
121     *
122     * @param Params $params Search parameters to adjust
123     *
124     * @return void
125     */
126    public function configureSearchParams(Params $params)
127    {
128        foreach ($this->fields as $field => $desc) {
129            $params->addFacet($field, $desc);
130        }
131    }
132
133    /**
134     * Return channel information derived from a record driver object.
135     *
136     * @param RecordDriver $driver       Record driver
137     * @param string       $channelToken Token identifying a single specific channel
138     * to load (if omitted, all channels will be loaded)
139     *
140     * @return array
141     */
142    public function getFromRecord(RecordDriver $driver, $channelToken = null)
143    {
144        $results = $this->resultsManager->get($driver->getSourceIdentifier());
145        if (null !== $channelToken) {
146            return [$this->buildChannelFromToken($results, $channelToken)];
147        }
148        $channels = [];
149        $fieldCount = 0;
150        $data = $driver->getRawData();
151        foreach (array_keys($this->fields) as $field) {
152            if (!isset($data[$field])) {
153                continue;
154            }
155            $currentValueCount = 0;
156            foreach ($data[$field] as $value) {
157                $current = [
158                    'value' => $value,
159                    'displayText' => $value,
160                ];
161                $tokenOnly = $fieldCount >= $this->maxFieldsToSuggest
162                    || $currentValueCount >= $this->maxValuesToSuggestPerField;
163                $channel = $this
164                    ->buildChannelFromFacet($results, $field, $current, $tokenOnly);
165                if ($tokenOnly || count($channel['contents']) > 0) {
166                    $channels[] = $channel;
167                    $currentValueCount++;
168                }
169            }
170            if ($currentValueCount > 0) {
171                $fieldCount++;
172            }
173        }
174        return $channels;
175    }
176
177    /**
178     * Return channel information derived from a search results object.
179     *
180     * @param Results $results      Search results
181     * @param string  $channelToken Token identifying a single specific channel
182     * to load (if omitted, all channels will be loaded)
183     *
184     * @return array
185     */
186    public function getFromSearch(Results $results, $channelToken = null)
187    {
188        if (null !== $channelToken) {
189            return [$this->buildChannelFromToken($results, $channelToken)];
190        }
191        $channels = [];
192        $fieldCount = 0;
193        $facetList = $results->getFacetList();
194        foreach (array_keys($this->fields) as $field) {
195            if (!isset($facetList[$field])) {
196                continue;
197            }
198            $currentValueCount = 0;
199            foreach ($facetList[$field]['list'] as $current) {
200                if (!$current['isApplied']) {
201                    $tokenOnly = $fieldCount >= $this->maxFieldsToSuggest
202                        || $currentValueCount >= $this->maxValuesToSuggestPerField;
203                    $channel = $this->buildChannelFromFacet(
204                        $results,
205                        $field,
206                        $current,
207                        $tokenOnly
208                    );
209                    if ($tokenOnly || count($channel['contents']) > 0) {
210                        $channels[] = $channel;
211                        $currentValueCount++;
212                    }
213                }
214            }
215            if ($currentValueCount > 0) {
216                $fieldCount++;
217            }
218        }
219        return $channels;
220    }
221
222    /**
223     * Turn a filter and title into a token.
224     *
225     * @param string $filter Filter to apply to Solr
226     * @param string $title  Channel title
227     *
228     * @return string
229     */
230    protected function getToken($filter, $title)
231    {
232        return str_replace('|', ' ', $title)    // make sure delimiter not in title
233            . '|' . $filter;
234    }
235
236    /**
237     * Add a new filter to an existing search results object to populate a
238     * channel.
239     *
240     * @param Results $results   Results object
241     * @param string  $filter    Filter to apply to Solr
242     * @param string  $title     Channel title
243     * @param bool    $tokenOnly Create full channel (false) or return a
244     * token for future loading (true)?
245     *
246     * @return array
247     */
248    protected function buildChannel(
249        Results $results,
250        $filter,
251        $title,
252        $tokenOnly = false
253    ) {
254        $retVal = [
255            'title' => $title,
256            'providerId' => $this->providerId,
257            'groupId' => current(explode(':', $filter)),
258            'token' => $this->getToken($filter, $title),
259            'links' => [],
260        ];
261        if ($tokenOnly) {
262            return $retVal;
263        }
264
265        $newResults = clone $results;
266        $params = $newResults->getParams();
267
268        // Determine the filter for the current channel, and add it:
269        $params->addFilter($filter);
270
271        $query = $newResults->getUrlQuery()->getParams(false);
272        $retVal['links'][] = [
273            'label' => 'channel_search',
274            'icon' => 'fa-list',
275            'url' => $this->url->fromRoute($params->getOptions()->getSearchAction())
276                . $query,
277        ];
278        $retVal['links'][] = [
279            'label' => 'channel_expand',
280            'icon' => 'fa-search-plus',
281            'url' => $this->url->fromRoute('channels-search')
282                . $query . '&source=' . urlencode($params->getSearchClassId()),
283        ];
284
285        // Run the search and convert the results into a channel:
286        $newResults->performAndProcessSearch();
287        $retVal['contents']
288            = $this->summarizeRecordDrivers($newResults->getResults());
289        return $retVal;
290    }
291
292    /**
293     * Call buildChannel using data from a token.
294     *
295     * @param Results $results Results object
296     * @param string  $token   Token to parse
297     *
298     * @return array
299     */
300    protected function buildChannelFromToken(Results $results, $token)
301    {
302        $parts = explode('|', $token, 2);
303        if (count($parts) < 2) {
304            return [];
305        }
306        return $this->buildChannel($results, $parts[1], $parts[0]);
307    }
308
309    /**
310     * Call buildChannel using data from facet results.
311     *
312     * @param Results $results   Results object
313     * @param string  $field     Field name (for filter)
314     * @param array   $value     Field value information (for filter)
315     * @param bool    $tokenOnly Create full channel (false) or return a
316     * token for future loading (true)?
317     *
318     * @return array
319     */
320    protected function buildChannelFromFacet(
321        Results $results,
322        $field,
323        $value,
324        $tokenOnly = false
325    ) {
326        return $this->buildChannel(
327            $results,
328            "$field:{$value['value']}",
329            $this->translate($this->fields[$field]) . "{$value['displayText']}",
330            $tokenOnly
331        );
332    }
333}