Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.93% covered (warning)
81.93%
68 / 83
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Databases
81.93% covered (warning)
81.93%
68 / 83
50.00% covered (danger)
50.00%
4 / 8
38.04
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
 setConfig
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
6.02
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResults
68.57% covered (warning)
68.57%
24 / 35
0.00% covered (danger)
0.00%
0 / 1
21.98
 getDatabases
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getLibGuidesDatabases
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 getLinkToAllDatabases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Databases Recommendations Module
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2023.
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   Maccabee Levine <msl321@lehigh.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development:plugins:recommendation_modules Wiki
28 */
29
30namespace VuFind\Recommend;
31
32use Laminas\Cache\Storage\StorageInterface as CacheAdapter;
33
34use function count;
35use function intval;
36use function is_callable;
37use function strlen;
38
39/**
40 * Databases Recommendations Module
41 *
42 * This class displays a list of external links to the research databases represented
43 * by EDS or similar results.  (Unlike the EDS ContentProvider facet that would narrow
44 * down the results within VuFind.)
45 *
46 * @category VuFind
47 * @package  Recommendations
48 * @author   Maccabee Levine <msl321@lehigh.edu>
49 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
50 * @link     https://vufind.org/wiki/development:plugins:recommendation_modules Wiki
51 */
52class Databases implements RecommendInterface, \Laminas\Log\LoggerAwareInterface
53{
54    use \VuFind\Cache\CacheTrait;
55    use \VuFind\Log\LoggerAwareTrait;
56
57    /**
58     * Results object
59     *
60     * @var \VuFind\Search\Base\Results
61     */
62    protected $results;
63
64    /**
65     * Configuration manager
66     *
67     * @var ConfigManager
68     */
69    protected $configManager;
70
71    /**
72     * Number of results to show
73     *
74     * @var int
75     */
76    protected $limit = 5;
77
78    /**
79     * The result facet with the list of databases.  Each value in the
80     * array is a level of the facet hierarchy.
81     *
82     * @var array
83     */
84    protected $resultFacet = [];
85
86    /**
87     * For each database facet, the key to the database name.
88     *
89     * @var string
90     */
91    protected $resultFacetNameKey = 'value';
92
93    /**
94     * Databases listed in configuration file
95     *
96     * @var array
97     */
98    protected $configFileDatabases = [];
99
100    /**
101     * Configuration of whether to use the query string as a match point
102     *
103     * @var bool
104     */
105    protected $useQuery = true;
106
107    /**
108     * Minimum string length of a query to use as a match point
109     *
110     * @var bool
111     */
112    protected $useQueryMinLength = 3;
113
114    /**
115     * Configuration of whether to use LibGuides as a data source
116     *
117     * @var bool
118     */
119    protected $useLibGuides = false;
120
121    /**
122     * Configuration of whether to match on the alt_names field in LibGuides
123     * in addition to the primary name
124     *
125     * @var bool
126     */
127    protected $useLibGuidesAlternateNames = true;
128
129    /**
130     * URL to a list of all available databases, for display in the results list,
131     * or false to omit.
132     */
133    protected $linkToAllDatabases = false;
134
135    /**
136     * Callable for LibGuides connector
137     *
138     * @var callable
139     */
140    protected $libGuidesGetter;
141
142    /**
143     * Constructor
144     *
145     * @param \VuFind\Config\PluginManager $configManager   Config PluginManager
146     * @param callable                     $libGuidesGetter Getter for LibGuides API connection
147     * @param CacheAdapter                 $cache           Object cache
148     */
149    public function __construct(
150        \VuFind\Config\PluginManager $configManager,
151        callable $libGuidesGetter,
152        CacheAdapter $cache
153    ) {
154        $this->configManager = $configManager;
155        $this->libGuidesGetter = $libGuidesGetter;
156        $this->setCacheStorage($cache);
157    }
158
159    /**
160     * Store the configuration of the recommendation module.
161     *
162     * @param string $settings Settings from searches.ini.
163     *
164     * @return void
165     */
166    public function setConfig($settings)
167    {
168        // Only change settings from current values if they are defined in $settings or .ini
169
170        $settings = explode(':', $settings);
171        $this->limit
172            = (isset($settings[0]) && is_numeric($settings[0]) && $settings[0] > 0)
173            ? intval($settings[0]) : $this->limit;
174        $databasesConfigFile = $settings[1] ?? 'EDS';
175
176        $databasesConfig = $this->configManager->get($databasesConfigFile)->Databases;
177        if (!$databasesConfig) {
178            throw new \Exception("Databases config file $databasesConfigFile must have section 'Databases'.");
179        }
180        $this->configFileDatabases = $databasesConfig->url?->toArray()
181            ?? $this->configFileDatabases;
182        array_walk($this->configFileDatabases, function (&$value, $name) {
183            $value = [
184                'name' => $name,
185                'url' => $value,
186            ];
187        });
188
189        $this->resultFacet = $databasesConfig->resultFacet?->toArray() ?? $this->resultFacet;
190        $this->resultFacetNameKey = $databasesConfig->resultFacetNameKey
191            ?? $this->resultFacetNameKey;
192
193        $this->useQuery = $databasesConfig->useQuery ?? $this->useQuery;
194        $this->useQueryMinLength = $databasesConfig->useQueryMinLength
195            ?? $this->useQueryMinLength;
196
197        $this->useLibGuides = $databasesConfig->useLibGuides ?? $this->useLibGuides;
198        if ($this->useLibGuides) {
199            // Cache the data related to profiles for up to 10 minutes:
200            $libGuidesApiConfig = $this->configManager->get('LibGuidesAPI');
201            $this->cacheLifetime = intval($libGuidesApiConfig->GetAZ->cache_lifetime ?? 600);
202
203            $this->useLibGuidesAlternateNames = $databasesConfig->useLibGuidesAlternateNames
204                ?? $this->useLibGuidesAlternateNames;
205
206            $this->linkToAllDatabases = $databasesConfig->linkToAllDatabases
207                ?? $this->linkToAllDatabases;
208        }
209    }
210
211    /**
212     * Called before the Search Results object performs its main search
213     * (specifically, in response to \VuFind\Search\SearchRunner::EVENT_CONFIGURED).
214     * This method is responsible for setting search parameters needed by the
215     * recommendation module and for reading any existing search parameters that may
216     * be needed.
217     *
218     * @param \VuFind\Search\Base\Params $params  Search parameter object
219     * @param \Laminas\Stdlib\Parameters $request Parameter object representing user
220     * request.
221     *
222     * @return void
223     *
224     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
225     */
226    public function init($params, $request)
227    {
228        // No action needed.
229    }
230
231    /**
232     * Called after the Search Results object has performed its main search. This
233     * may be used to extract necessary information from the Search Results object
234     * or to perform completely unrelated processing.
235     *
236     * @param \VuFind\Search\Base\Results $results Search results object
237     *
238     * @return void
239     */
240    public function process($results)
241    {
242        $this->results = $results;
243    }
244
245    /**
246     * Get terms related to the query.
247     *
248     * @return array
249     */
250    public function getResults()
251    {
252        if (count($this->resultFacet) < 1) {
253            $this->logError('At least one facet key is required.');
254            return [];
255        }
256
257        $resultDatabasesTopFacet = array_shift($this->resultFacet);
258        try {
259            $resultDatabases =
260                $this->results->getFacetList([$resultDatabasesTopFacet => null])[$resultDatabasesTopFacet];
261            while (count($this->resultFacet) && $resultDatabases) {
262                $resultDatabases = $resultDatabases[array_shift($this->resultFacet)];
263            }
264        } catch (\Exception $ex) {
265            $this->logError('Error using configured facets to find list of result databases.');
266            return [];
267        }
268        $nameToDatabase = $this->getDatabases();
269
270        // Array of url => [name, url].  Key by URL so that the same database (under alternate
271        // names) is not duplicated.
272        $databases = [];
273
274        // Add databases from search query
275        if ($this->useQuery) {
276            $queryObject = $this->results->getParams()->getQuery();
277            $query = is_callable([$queryObject, 'getString'])
278                ? strtolower($queryObject->getString())
279                : '';
280            if (strlen($query) >= $this->useQueryMinLength) {
281                foreach ($nameToDatabase as $name => $databaseInfo) {
282                    if (str_contains(strtolower($name), $query)) {
283                        $databases[$databaseInfo['url']] = $databaseInfo;
284                    }
285                    if (count($databases) >= $this->limit) {
286                        return $databases;
287                    }
288                }
289            }
290        }
291
292        // Add databases from result facets
293        foreach ($resultDatabases as $resultDatabase) {
294            try {
295                $name = $resultDatabase[$this->resultFacetNameKey];
296            } catch (\Exception $ex) {
297                $this->logError("Name key '$this->resultFacetNameKey' not found for database.");
298                continue;
299            }
300            $databaseInfo = $nameToDatabase[$name] ?? null;
301            if ($databaseInfo) {
302                $databases[$databaseInfo['url']] = $databaseInfo;
303            }
304            if (count($databases) >= $this->limit) {
305                return $databases;
306            }
307        }
308
309        return $databases;
310    }
311
312    /**
313     * Generate a combined list of databases from all enabled sources.
314     *
315     * @return An array mapping a database name to a sub-array with
316     * the url.
317     */
318    protected function getDatabases()
319    {
320        $databases = [];
321        if ($this->useLibGuides) {
322            $databases = $this->getLibGuidesDatabases();
323        }
324        $databases = array_merge($databases, $this->configFileDatabases);
325        return $databases;
326    }
327
328    /**
329     * Load or retrieve from the cache the list of LibGuides A-Z databases.
330     *
331     * @return array An array mapping a database name to an array
332     * representing the full object retrieved from the LibGuides /az API.
333     */
334    protected function getLibGuidesDatabases()
335    {
336        $nameToDatabase = $this->getCachedData('libGuidesAZ-nameToDatabase');
337        if (empty($nameToDatabase)) {
338            $libGuides = ($this->libGuidesGetter)();
339            $databases = $libGuides->getAZ();
340
341            $nameToDatabase = [];
342            foreach ($databases as $database) {
343                $nameToDatabase[$database->name] = (array)$database;
344                // The alt_names field is single-valued free text
345                if ($this->useLibGuidesAlternateNames && ($database->alt_names ?? false)) {
346                    $nameToDatabase[$database->alt_names] = (array)$database;
347                }
348            }
349
350            $this->putCachedData('libGuidesAZ-nameToDatabase', $nameToDatabase);
351        }
352        return $nameToDatabase;
353    }
354
355    /**
356     * Get a URL to a list of all available databases, if configured.
357     *
358     * @return string The URL, or null.
359     */
360    public function getLinkToAllDatabases()
361    {
362        return $this->linkToAllDatabases;
363    }
364}