Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.09% covered (success)
97.09%
100 / 103
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AlphaBrowse
97.09% covered (success)
97.09%
100 / 103
66.67% covered (warning)
66.67%
4 / 6
30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 setOptions
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getFromRecord
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getFromSearch
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
 summarizeBrowseDetails
91.30% covered (success)
91.30%
21 / 23
0.00% covered (danger)
0.00%
0 / 1
9.05
 buildChannelFromRecord
97.87% covered (success)
97.87%
46 / 47
0.00% covered (danger)
0.00%
0 / 1
4
1<?php
2
3/**
4 * Alphabrowse channel provider.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2016, 2022.
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\Record\Router as RecordRouter;
35use VuFind\RecordDriver\AbstractBase as RecordDriver;
36use VuFind\Search\Base\Results;
37use VuFindSearch\Command\AlphabeticBrowseCommand;
38use VuFindSearch\Command\RetrieveBatchCommand;
39use VuFindSearch\Command\RetrieveCommand;
40use VuFindSearch\ParamBag;
41
42use function count;
43use function is_object;
44
45/**
46 * Alphabrowse channel provider.
47 *
48 * @category VuFind
49 * @package  Channels
50 * @author   Demian Katz <demian.katz@villanova.edu>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org/wiki/development Wiki
53 */
54class AlphaBrowse extends AbstractChannelProvider implements TranslatorAwareInterface
55{
56    use \VuFind\I18n\Translator\TranslatorAwareTrait;
57
58    /**
59     * Number of results to include in each channel.
60     *
61     * @var int
62     */
63    protected $channelSize;
64
65    /**
66     * Maximum number of records to examine for similar results.
67     *
68     * @var int
69     */
70    protected $maxRecordsToExamine;
71
72    /**
73     * Search service
74     *
75     * @var \VuFindSearch\Service
76     */
77    protected $searchService;
78
79    /**
80     * URL helper
81     *
82     * @var Url
83     */
84    protected $url;
85
86    /**
87     * Record router
88     *
89     * @var RecordRouter
90     */
91    protected $recordRouter;
92
93    /**
94     * Browse index to search
95     *
96     * @var string
97     */
98    protected $browseIndex;
99
100    /**
101     * Solr field to use for search seed
102     *
103     * @var string
104     */
105    protected $solrField;
106
107    /**
108     * How many rows to show before the selected value
109     *
110     * @var int
111     */
112    protected $rowsBefore;
113
114    /**
115     * The search backend to query
116     *
117     * @var string
118     */
119    protected $source;
120
121    /**
122     * Constructor
123     *
124     * @param \VuFindSearch\Service $search  Search service
125     * @param Url                   $url     URL helper
126     * @param RecordRouter          $router  Record router
127     * @param array                 $options Settings (optional)
128     */
129    public function __construct(
130        \VuFindSearch\Service $search,
131        Url $url,
132        RecordRouter $router,
133        array $options = []
134    ) {
135        $this->searchService = $search;
136        $this->url = $url;
137        $this->recordRouter = $router;
138        $this->setOptions($options);
139    }
140
141    /**
142     * Set the options for the provider.
143     *
144     * @param array $options Options
145     *
146     * @return void
147     */
148    public function setOptions(array $options)
149    {
150        $this->channelSize = $options['channelSize'] ?? 20;
151        $this->maxRecordsToExamine = $options['maxRecordsToExamine'] ?? 2;
152        $this->browseIndex = $options['browseIndex'] ?? 'lcc';
153        $this->solrField = $options['solrField'] ?? 'callnumber-raw';
154        $this->rowsBefore = $options['rows_before'] ?? 10;
155        $this->source = $options['source'] ?? 'Solr';
156    }
157
158    /**
159     * Return channel information derived from a record driver object.
160     *
161     * @param RecordDriver $driver       Record driver
162     * @param string       $channelToken Token identifying a single specific channel
163     * to load (if omitted, all channels will be loaded)
164     *
165     * @return array
166     */
167    public function getFromRecord(RecordDriver $driver, $channelToken = null)
168    {
169        // If we have a token and it doesn't match the record driver, we can't
170        // fetch any results!
171        if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
172            return [];
173        }
174        $channel = $this->buildChannelFromRecord($driver);
175        return (count($channel['contents']) > 0) ? [$channel] : [];
176    }
177
178    /**
179     * Return channel information derived from a search results object.
180     *
181     * @param Results $results      Search results
182     * @param string  $channelToken Token identifying a single specific channel
183     * to load (if omitted, all channels will be loaded)
184     *
185     * @return array
186     */
187    public function getFromSearch(Results $results, $channelToken = null)
188    {
189        $driver = null;
190        $channels = [];
191        foreach ($results->getResults() as $driver) {
192            // If we have a token and it doesn't match the current driver, skip
193            // that driver.
194            if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
195                continue;
196            }
197            $channel = (count($channels) < $this->maxRecordsToExamine)
198                ? $this->buildChannelFromRecord($driver)
199                : $this->buildChannelFromRecord($driver, true);
200            if (isset($channel['token']) || count($channel['contents']) > 0) {
201                $channels[] = $channel;
202            }
203        }
204        // If the search results did not include the object we were looking for,
205        // we need to fetch it from the search service:
206        if (empty($channels) && is_object($driver) && $channelToken !== null) {
207            $command = new RetrieveCommand(
208                $driver->getSourceIdentifier(),
209                $channelToken
210            );
211            $driver = $this->searchService->invoke($command)->getResult()->first();
212            if ($driver) {
213                $channels[] = $this->buildChannelFromRecord($driver);
214            }
215        }
216        return $channels;
217    }
218
219    /**
220     * Given details from alphabeticBrowse(), create channel contents.
221     *
222     * @param array $details Details from alphabetic browse index
223     *
224     * @return array
225     */
226    protected function summarizeBrowseDetails($details)
227    {
228        $ids = $results = [];
229        if (isset($details['Browse']['items'])) {
230            foreach ($details['Browse']['items'] as $item) {
231                if (!isset($item['extras']['title'][0][0])) {
232                    continue;
233                }
234                // Collect a list of IDs in the result set while we create it:
235                $ids[] = $id = $item['extras']['id'][0][0];
236                $results[] = [
237                    'title' => $item['extras']['title'][0][0],
238                    'source' => $this->source,
239                    'thumbnail' => false, // TODO: better thumbnails!
240                    'id' => $id,
241                ];
242            }
243        }
244        // If we have a cover router and a non-empty ID list, look up thumbnails:
245        if ($this->coverRouter && !empty($ids)) {
246            $command = new RetrieveBatchCommand($this->source, $ids);
247            $records = $this->searchService->invoke($command)->getResult();
248            $thumbs = [];
249            // First map record drivers to an ID => thumb array...
250            foreach ($records as $record) {
251                $thumbs[$record->getUniqueId()] = $this->coverRouter
252                    ->getUrl($record, 'medium');
253            }
254            // Now apply the thumbnails to the existing result set...
255            foreach ($results as $i => $current) {
256                if (isset($thumbs[$current['id']])) {
257                    $results[$i]['thumbnail'] = $thumbs[$current['id']];
258                }
259            }
260        }
261        return $results;
262    }
263
264    /**
265     * Add a new filter to an existing search results object to populate a
266     * channel.
267     *
268     * @param RecordDriver $driver    Record driver
269     * @param bool         $tokenOnly Create full channel (false) or return a
270     * token for future loading (true)?
271     *
272     * @return array
273     */
274    protected function buildChannelFromRecord(
275        RecordDriver $driver,
276        $tokenOnly = false
277    ) {
278        $retVal = [
279            'title' => $this->translate(
280                'nearby_items',
281                ['%%title%%' => $driver->getBreadcrumb()]
282            ),
283            'providerId' => $this->providerId,
284            'links' => [],
285        ];
286        $raw = $driver->getRawData();
287        $from = isset($raw[$this->solrField]) ? (array)$raw[$this->solrField] : null;
288        if (empty($from[0])) {
289            // If there is no "from" value to look up, skip this so we don't
290            //generate a token that retrieves nothing later!
291            $retVal['contents'] = [];
292        } elseif ($tokenOnly) {
293            $retVal['token'] = $driver->getUniqueID();
294        } else {
295            $command = new AlphabeticBrowseCommand(
296                $this->source,
297                $this->browseIndex,
298                // If we got this far, we can safely assume that $from[0] is set
299                $from[0],
300                0,
301                $this->channelSize,
302                new ParamBag(['extras' => 'title:author:isbn:id']),
303                -$this->rowsBefore
304            );
305            $details = $this->searchService->invoke($command)->getResult();
306            $retVal['contents'] = $this->summarizeBrowseDetails($details);
307            $route = $this->recordRouter->getRouteDetails($driver);
308            $retVal['links'][] = [
309                'label' => 'View Record',
310                'icon' => 'fa-file-text-o',
311                'url' => $this->url
312                    ->fromRoute($route['route'], $route['params']),
313            ];
314            $retVal['links'][] = [
315                'label' => 'channel_expand',
316                'icon' => 'fa-search-plus',
317                'url' => $this->url->fromRoute('channels-record')
318                    . '?id=' . urlencode($driver->getUniqueID())
319                    . '&source=' . urlencode($driver->getSourceIdentifier()),
320            ];
321            $retVal['links'][] = [
322                'label' => 'channel_browse',
323                'icon' => 'fa-list',
324                'url' => $this->url->fromRoute('alphabrowse-home')
325                    . '?source=' . urlencode($this->browseIndex)
326                    . '&from=' . $from[0],
327            ];
328        }
329        return $retVal;
330    }
331}