Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
SimilarItems
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
5 / 5
18
100.00% covered (success)
100.00%
1 / 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%
2 / 2
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%
23 / 23
100.00% covered (success)
100.00%
1 / 1
10
 buildChannelFromRecord
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * "Similar items" 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\RetrieveCommand;
38use VuFindSearch\Command\SimilarCommand;
39
40use function count;
41use function is_object;
42
43/**
44 * "Similar items" channel provider.
45 *
46 * @category VuFind
47 * @package  Channels
48 * @author   Demian Katz <demian.katz@villanova.edu>
49 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
50 * @link     https://vufind.org/wiki/development Wiki
51 */
52class SimilarItems extends AbstractChannelProvider implements TranslatorAwareInterface
53{
54    use \VuFind\I18n\Translator\TranslatorAwareTrait;
55
56    /**
57     * Number of results to include in each channel.
58     *
59     * @var int
60     */
61    protected $channelSize;
62
63    /**
64     * Maximum number of records to examine for similar results.
65     *
66     * @var int
67     */
68    protected $maxRecordsToExamine;
69
70    /**
71     * Search service
72     *
73     * @var \VuFindSearch\Service
74     */
75    protected $searchService;
76
77    /**
78     * URL helper
79     *
80     * @var Url
81     */
82    protected $url;
83
84    /**
85     * Record router
86     *
87     * @var RecordRouter
88     */
89    protected $recordRouter;
90
91    /**
92     * Constructor
93     *
94     * @param \VuFindSearch\Service $search  Search service
95     * @param Url                   $url     URL helper
96     * @param RecordRouter          $router  Record router
97     * @param array                 $options Settings (optional)
98     */
99    public function __construct(
100        \VuFindSearch\Service $search,
101        Url $url,
102        RecordRouter $router,
103        array $options = []
104    ) {
105        $this->searchService = $search;
106        $this->url = $url;
107        $this->recordRouter = $router;
108        $this->setOptions($options);
109    }
110
111    /**
112     * Set the options for the provider.
113     *
114     * @param array $options Options
115     *
116     * @return void
117     */
118    public function setOptions(array $options)
119    {
120        $this->channelSize = $options['channelSize'] ?? 20;
121        $this->maxRecordsToExamine = $options['maxRecordsToExamine'] ?? 2;
122    }
123
124    /**
125     * Return channel information derived from a record driver object.
126     *
127     * @param RecordDriver $driver       Record driver
128     * @param string       $channelToken Token identifying a single specific channel
129     * to load (if omitted, all channels will be loaded)
130     *
131     * @return array
132     */
133    public function getFromRecord(RecordDriver $driver, $channelToken = null)
134    {
135        // If we have a token and it doesn't match the record driver, we can't
136        // fetch any results!
137        if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
138            return [];
139        }
140        $channel = $this->buildChannelFromRecord($driver);
141        return (count($channel['contents']) > 0) ? [$channel] : [];
142    }
143
144    /**
145     * Return channel information derived from a search results object.
146     *
147     * @param Results $results      Search results
148     * @param string  $channelToken Token identifying a single specific channel
149     * to load (if omitted, all channels will be loaded)
150     *
151     * @return array
152     */
153    public function getFromSearch(Results $results, $channelToken = null)
154    {
155        $driver = null;
156        $channels = [];
157        foreach ($results->getResults() as $driver) {
158            // If we have a token and it doesn't match the current driver, skip
159            // that driver.
160            if ($channelToken !== null && $channelToken !== $driver->getUniqueID()) {
161                continue;
162            }
163            if (count($channels) < $this->maxRecordsToExamine) {
164                $channel = $this->buildChannelFromRecord($driver);
165                if (count($channel['contents']) > 0) {
166                    $channels[] = $channel;
167                }
168            } else {
169                $channels[] = $this->buildChannelFromRecord($driver, true);
170            }
171        }
172        // If the search results did not include the object we were looking for,
173        // we need to fetch it from the search service:
174        if (
175            empty($channels)
176            && is_object($driver ?? null)
177            && $channelToken !== null
178        ) {
179            $command = new RetrieveCommand(
180                $driver->getSourceIdentifier(),
181                $channelToken
182            );
183            $driver = $this->searchService->invoke(
184                $command
185            )->getResult()->first();
186            if ($driver) {
187                $channels[] = $this->buildChannelFromRecord($driver);
188            }
189        }
190        return $channels;
191    }
192
193    /**
194     * Add a new filter to an existing search results object to populate a
195     * channel.
196     *
197     * @param RecordDriver $driver    Record driver
198     * @param bool         $tokenOnly Create full channel (false) or return a
199     * token for future loading (true)?
200     *
201     * @return array
202     */
203    protected function buildChannelFromRecord(
204        RecordDriver $driver,
205        $tokenOnly = false
206    ) {
207        $heading = $this->translate('Similar Items');
208        $retVal = [
209            'title' => "{$heading}{$driver->getBreadcrumb()}",
210            'providerId' => $this->providerId,
211            'links' => [],
212        ];
213        if ($tokenOnly) {
214            $retVal['token'] = $driver->getUniqueID();
215        } else {
216            $params = new \VuFindSearch\ParamBag(['rows' => $this->channelSize]);
217            $command = new SimilarCommand(
218                $driver->getSourceIdentifier(),
219                $driver->getUniqueID(),
220                $params
221            );
222            $similar = $this->searchService->invoke($command)->getResult();
223            $retVal['contents'] = $this->summarizeRecordDrivers($similar);
224            $route = $this->recordRouter->getRouteDetails($driver);
225            $retVal['links'][] = [
226                'label' => 'View Record',
227                'icon' => 'fa-file-text-o',
228                'url' => $this->url
229                    ->fromRoute($route['route'], $route['params']),
230            ];
231            $retVal['links'][] = [
232                'label' => 'channel_expand',
233                'icon' => 'fa-search-plus',
234                'url' => $this->url->fromRoute('channels-record')
235                    . '?id=' . urlencode($driver->getUniqueID())
236                    . '&source=' . urlencode($driver->getSourceIdentifier()),
237            ];
238        }
239        return $retVal;
240    }
241}