Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthorityRecommend
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 12
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setConfig
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 performSearch
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 fuzzyCompare
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addUseForHeadings
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 addSeeAlsoReferences
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 isModeActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 process
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 getHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResults
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * AuthorityRecommend Recommendations Module
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2012.
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  Recommendations
25 * @author   Lutz Biedinger <vufind-tech@lists.sourceforge.net>
26 * @author   Ronan McHugh <vufind-tech@lists.sourceforge.net>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org Main Page
29 */
30
31namespace VuFind\Recommend;
32
33use Laminas\Stdlib\Parameters;
34use VuFindSearch\Backend\Exception\RequestErrorException;
35
36use function count;
37use function intval;
38
39/**
40 * AuthorityRecommend Module
41 *
42 * This class provides recommendations based on Authority records.
43 * i.e. searches for a pseudonym will provide the user with a link
44 * to the official name (according to the Authority index)
45 *
46 * Originally developed at the National Library of Ireland by Lutz
47 * Biedinger and Ronan McHugh.
48 *
49 * @category VuFind
50 * @package  Recommendations
51 * @author   Lutz Biedinger <vufind-tech@lists.sourceforge.net>
52 * @author   Ronan McHugh <vufind-tech@lists.sourceforge.net>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org Main Page
55 */
56class AuthorityRecommend implements RecommendInterface
57{
58    /**
59     * User search query
60     *
61     * @var string
62     */
63    protected $lookfor;
64
65    /**
66     * Configured filters for authority searches
67     *
68     * @var array
69     */
70    protected $filters = [];
71
72    /**
73     * Maximum number of results that will be accompanied by recommendations (set
74     * to 0 for no limit).
75     *
76     * @var int
77     */
78    protected $resultLimit = 0;
79
80    /**
81     * Current user search
82     *
83     * @var \VuFind\Search\Base\Results
84     */
85    protected $results;
86
87    /**
88     * Generated recommendations
89     *
90     * @var array
91     */
92    protected $recommendations = [];
93
94    /**
95     * Results plugin manager
96     *
97     * @var \VuFind\Search\Results\PluginManager
98     */
99    protected $resultsManager;
100
101    /**
102     * Which lookup mode(s) to use.
103     *
104     * @var string
105     */
106    protected $mode = '*';
107
108    /**
109     * Header to use in the user interface.
110     *
111     * @var string
112     */
113    protected $header = 'See also';
114
115    /**
116     * Constructor
117     *
118     * @param \VuFind\Search\Results\PluginManager $results Results plugin manager
119     */
120    public function __construct(\VuFind\Search\Results\PluginManager $results)
121    {
122        $this->resultsManager = $results;
123    }
124
125    /**
126     * Store the configuration of the recommendation module.
127     *
128     * @param string $settings Settings from searches.ini.
129     *
130     * @return void
131     */
132    public function setConfig($settings)
133    {
134        $params = explode(':', $settings);
135        for ($i = 0; $i < count($params); $i += 2) {
136            if (isset($params[$i + 1])) {
137                if ($params[$i] == '__resultlimit__') {
138                    $this->resultLimit = intval($params[$i + 1]);
139                } elseif ($params[$i] == '__mode__') {
140                    $this->mode = strtolower($params[$i + 1]);
141                } elseif ($params[$i] == '__header__') {
142                    $this->header = $params[$i + 1];
143                } else {
144                    $this->filters[] = $params[$i] . ':' . $params[$i + 1];
145                }
146            }
147        }
148    }
149
150    /**
151     * Called before the Search Results object performs its main search
152     * (specifically, in response to \VuFind\Search\SearchRunner::EVENT_CONFIGURED).
153     * This method is responsible for setting search parameters needed by the
154     * recommendation module and for reading any existing search parameters that may
155     * be needed.
156     *
157     * @param \VuFind\Search\Base\Params $params  Search parameter object
158     * @param Parameters                 $request Parameter object representing user
159     * request.
160     *
161     * @return void
162     *
163     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
164     */
165    public function init($params, $request)
166    {
167        // Save user search query:
168        $this->lookfor = $request->get('lookfor');
169    }
170
171    /**
172     * Perform a search of the authority index.
173     *
174     * @param array $params Array of request parameters.
175     *
176     * @return array
177     */
178    protected function performSearch($params)
179    {
180        // Initialise and process search (ignore Solr errors -- no reason to fail
181        // just because search syntax is not compatible with Authority core):
182        try {
183            $authResults = $this->resultsManager->get('SolrAuth');
184            $authParams = $authResults->getParams();
185            $authParams->initFromRequest(new Parameters($params));
186            foreach ($this->filters as $filter) {
187                $authParams->addHiddenFilter($filter);
188            }
189            return $authResults->getResults();
190        } catch (RequestErrorException $e) {
191            return [];
192        }
193    }
194
195    /**
196     * Return true if $a and $b are similar enough to represent the same heading.
197     *
198     * @param string $a First string to compare
199     * @param string $b Second string to compare
200     *
201     * @return bool
202     */
203    protected function fuzzyCompare($a, $b)
204    {
205        $normalize = function ($str) {
206            return trim(strtolower(preg_replace('/\W/', '', $str)));
207        };
208        return $normalize($a) == $normalize($b);
209    }
210
211    /**
212     * Add main headings from records that match search terms on use_for/see_also.
213     *
214     * @return void
215     */
216    protected function addUseForHeadings()
217    {
218        // Build an advanced search request that prevents Solr from retrieving
219        // records that would already have been retrieved by a search of the biblio
220        // core, i.e. it only returns results where $lookfor IS found in in the
221        // "Heading" search and IS NOT found in the "MainHeading" search defined
222        // in authsearchspecs.yaml.
223        $params = [
224            'join' => 'AND',
225            'bool0' => ['AND'],
226            'lookfor0' => [$this->lookfor],
227            'type0' => ['Heading'],
228            'bool1' => ['NOT'],
229            'lookfor1' => [$this->lookfor],
230            'type1' => ['MainHeading'],
231        ];
232
233        // loop through records and assign id and headings to separate arrays defined
234        // above
235        foreach ($this->performSearch($params) as $result) {
236            $this->recommendations[] = $result->getBreadcrumb();
237        }
238    }
239
240    /**
241     * Add "see also" headings from records that match search terms on main heading.
242     *
243     * @return void
244     */
245    protected function addSeeAlsoReferences()
246    {
247        // Build a simple "MainHeading" search.
248        $params = [
249            'lookfor' => [$this->lookfor],
250            'type' => ['MainHeading'],
251        ];
252
253        // loop through records and assign id and headings to separate arrays defined
254        // above
255        foreach ($this->performSearch($params) as $result) {
256            foreach ($result->getSeeAlso() as $seeAlso) {
257                // check for duplicates before adding record to recordSet
258                if (!$this->fuzzyCompare($seeAlso, $this->lookfor)) {
259                    $this->recommendations[] = $seeAlso;
260                }
261            }
262        }
263    }
264
265    /**
266     * Is the specified mode configured to be active?
267     *
268     * @param string $mode Mode to check
269     *
270     * @return bool
271     */
272    protected function isModeActive($mode)
273    {
274        return $this->mode === '*' || str_contains($this->mode, $mode);
275    }
276
277    /**
278     * Called after the Search Results object has performed its main search. This
279     * may be used to extract necessary information from the Search Results object
280     * or to perform completely unrelated processing.
281     *
282     * @param \VuFind\Search\Base\Results $results Search results object
283     *
284     * @return void
285     */
286    public function process($results)
287    {
288        $this->results = $results;
289
290        // empty searches such as New Items will return blank
291        if ($this->lookfor == null) {
292            return;
293        }
294
295        // function will return blank on Advanced Search
296        if ($results->getParams()->getSearchType() == 'advanced') {
297            return;
298        }
299
300        // check result limit before proceeding...
301        if (
302            $this->resultLimit > 0
303            && $this->resultLimit < $results->getResultTotal()
304        ) {
305            return;
306        }
307
308        // see if we can add main headings matching use_for/see_also fields...
309        if ($this->isModeActive('usefor')) {
310            $this->addUseForHeadings();
311        }
312
313        // see if we can add see-also references associated with main headings...
314        if ($this->isModeActive('seealso')) {
315            $this->addSeeAlsoReferences();
316        }
317    }
318
319    /**
320     * Get the header to display in the user interface.
321     *
322     * @return string
323     */
324    public function getHeader()
325    {
326        return $this->header;
327    }
328
329    /**
330     * Get recommendations (for use in the view).
331     *
332     * @return array
333     */
334    public function getRecommendations()
335    {
336        return array_unique($this->recommendations);
337    }
338
339    /**
340     * Get results stored in the object.
341     *
342     * @return \VuFind\Search\Base\Results
343     */
344    public function getResults()
345    {
346        return $this->results;
347    }
348}