Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.86% covered (warning)
75.86%
44 / 58
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SolrDefault
75.86% covered (warning)
75.86%
44 / 58
61.54% covered (warning)
61.54%
8 / 13
56.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getFirstIndexed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHighlightDetails
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHighlightDetails
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRawAuthorHighlights
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getSnippetCaption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHighlightedSnippet
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
12
 getHighlightedTitle
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 attachSearchService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildRecordCount
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
13.26
 getContainerRecordID
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getWorkKeys
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 explainEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Default model for Solr records -- used when a more specific model based on
5 * the record_format field cannot be found.
6 *
7 * PHP version 8
8 *
9 * Copyright (C) Villanova University 2010, 2022.
10 * Copyright (C) The National Library of Finland 2019.
11 *
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License version 2,
14 * as published by the Free Software Foundation.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with this program; if not, write to the Free Software
23 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
24 *
25 * @category VuFind
26 * @package  RecordDrivers
27 * @author   Demian Katz <demian.katz@villanova.edu>
28 * @author   Ere Maijala <ere.maijala@helsinki.fi>
29 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
30 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
31 */
32
33namespace VuFind\RecordDriver;
34
35use VuFindSearch\Command\SearchCommand;
36
37use function count;
38use function in_array;
39use function is_array;
40
41/**
42 * Default model for Solr records -- used when a more specific model based on
43 * the record_format field cannot be found.
44 *
45 * This should be used as the base class for all Solr-based record models.
46 *
47 * @category VuFind
48 * @package  RecordDrivers
49 * @author   Demian Katz <demian.katz@villanova.edu>
50 * @author   Ere Maijala <ere.maijala@helsinki.fi>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org/wiki/development:plugins:record_drivers Wiki
53 *
54 * @SuppressWarnings(PHPMD.ExcessivePublicCount)
55 */
56class SolrDefault extends DefaultRecord implements
57    Feature\PreviousUniqueIdInterface,
58    Feature\VersionAwareInterface
59{
60    use Feature\HierarchyAwareTrait;
61    use Feature\PreviousUniqueIdTrait;
62    use Feature\VersionAwareTrait;
63
64    /**
65     * These Solr fields should be used for snippets if available (listed in order
66     * of preference).
67     *
68     * @var array
69     */
70    protected $preferredSnippetFields = [
71        'contents', 'topic',
72    ];
73
74    /**
75     * These Solr fields should NEVER be used for snippets. (We exclude author
76     * and title because they are already covered by displayed fields; we exclude
77     * spelling because it contains lots of fields jammed together and may cause
78     * glitchy output; we exclude ID because random numbers are not helpful).
79     *
80     * @var array
81     */
82    protected $forbiddenSnippetFields = [
83        'author', 'title', 'title_short', 'title_full',
84        'title_full_unstemmed', 'title_auth', 'title_sub', 'spelling', 'id',
85        'ctrlnum', 'author_variant', 'author2_variant', 'fullrecord',
86        'work_keys_str_mv',
87    ];
88
89    /**
90     * These are captions corresponding with Solr fields for use when displaying
91     * snippets.
92     *
93     * @var array
94     */
95    protected $snippetCaptions = [];
96
97    /**
98     * Should we include snippets in search results?
99     *
100     * @var bool
101     */
102    protected $snippet = false;
103
104    /**
105     * Highlighting details
106     *
107     * @var array
108     */
109    protected $highlightDetails = [];
110
111    /**
112     * Should we use hierarchy fields for simple container-child records linking?
113     *
114     * @var bool
115     */
116    protected $containerLinking = false;
117
118    /**
119     * Search results plugin manager
120     *
121     * @var \VuFindSearch\Service
122     */
123    protected $searchService = null;
124
125    /**
126     * If the explain feature is enabled
127     *
128     * @var bool
129     */
130    protected $explainEnabled = false;
131
132    /**
133     * Constructor
134     *
135     * @param \Laminas\Config\Config $mainConfig     VuFind main configuration (omit
136     * for built-in defaults)
137     * @param \Laminas\Config\Config $recordConfig   Record-specific configuration
138     * file (omit to use $mainConfig as $recordConfig)
139     * @param \Laminas\Config\Config $searchSettings Search-specific configuration
140     * file
141     */
142    public function __construct(
143        $mainConfig = null,
144        $recordConfig = null,
145        $searchSettings = null
146    ) {
147        $this->setSourceIdentifiers('Solr');
148        // Load snippet settings:
149        $this->snippet = !isset($searchSettings->General->snippets)
150            ? false : $searchSettings->General->snippets;
151        if (
152            isset($searchSettings->Snippet_Captions)
153            && count($searchSettings->Snippet_Captions) > 0
154        ) {
155            foreach ($searchSettings->Snippet_Captions as $key => $value) {
156                $this->snippetCaptions[$key] = $value;
157            }
158        }
159        // Container-contents linking
160        $this->containerLinking
161            = !isset($mainConfig->Hierarchy->simpleContainerLinks)
162            ? false : $mainConfig->Hierarchy->simpleContainerLinks;
163
164        $this->explainEnabled = $searchSettings->Explain->enabled ?? false;
165
166        parent::__construct($mainConfig, $recordConfig, $searchSettings);
167    }
168
169    /**
170     * Get the date this record was first indexed (if set).
171     *
172     * @return string
173     */
174    public function getFirstIndexed()
175    {
176        return $this->fields['first_indexed'] ?? '';
177    }
178
179    /**
180     * Get highlighting details from the object.
181     *
182     * @return array
183     */
184    public function getHighlightDetails()
185    {
186        return $this->highlightDetails;
187    }
188
189    /**
190     * Add highlighting details to the object.
191     *
192     * @param array $details Details to add
193     *
194     * @return void
195     */
196    public function setHighlightDetails($details)
197    {
198        $this->highlightDetails = $details;
199    }
200
201    /**
202     * Get highlighted author data, if available.
203     *
204     * @return array
205     */
206    public function getRawAuthorHighlights()
207    {
208        // Don't check for highlighted values if highlighting is disabled:
209        return ($this->highlight && isset($this->highlightDetails['author']))
210            ? $this->highlightDetails['author'] : [];
211    }
212
213    /**
214     * Given a Solr field name, return an appropriate caption.
215     *
216     * @param string $field Solr field name
217     *
218     * @return mixed        Caption if found, false if none available.
219     */
220    public function getSnippetCaption($field)
221    {
222        return $this->snippetCaptions[$field] ?? false;
223    }
224
225    /**
226     * Pick one line from the highlighted text (if any) to use as a snippet.
227     *
228     * @return mixed False if no snippet found, otherwise associative array
229     * with 'snippet' and 'caption' keys.
230     */
231    public function getHighlightedSnippet()
232    {
233        // Only process snippets if the setting is enabled:
234        if ($this->snippet) {
235            // First check for preferred fields:
236            foreach ($this->preferredSnippetFields as $current) {
237                foreach ($this->highlightDetails[$current] ?? [] as $hl) {
238                    if (!empty($hl)) {
239                        return [
240                            'snippet' => $hl,
241                            'caption' => $this->getSnippetCaption($current),
242                        ];
243                    }
244                }
245            }
246
247            // No preferred field found, so try for a non-forbidden field:
248            if (
249                isset($this->highlightDetails)
250                && is_array($this->highlightDetails)
251            ) {
252                foreach ($this->highlightDetails as $key => $value) {
253                    if ($value && !in_array($key, $this->forbiddenSnippetFields)) {
254                        foreach ($value as $hl) {
255                            if (!empty($hl)) {
256                                return [
257                                    'snippet' => $hl,
258                                    'caption' => $this->getSnippetCaption($key),
259                                ];
260                            }
261                        }
262                    }
263                }
264            }
265        }
266
267        // If we got this far, no snippet was found:
268        return false;
269    }
270
271    /**
272     * Get a highlighted title string, if available.
273     *
274     * @return string
275     */
276    public function getHighlightedTitle()
277    {
278        // Don't check for highlighted values if highlighting is disabled:
279        if (!$this->highlight) {
280            return '';
281        }
282        return $this->highlightDetails['title'][0] ?? '';
283    }
284
285    /**
286     * Attach a Search Results Plugin Manager connection and related logic to
287     * the driver
288     *
289     * @param \VuFindSearch\Service $service Search Service Manager
290     *
291     * @return void
292     */
293    public function attachSearchService(\VuFindSearch\Service $service)
294    {
295        $this->searchService = $service;
296    }
297
298    /**
299     * Get the number of child records belonging to this record
300     *
301     * @return int Number of records
302     */
303    public function getChildRecordCount()
304    {
305        // Shortcut: if this record is not the top record, let's not find out the
306        // count. This assumes that contained records cannot contain more records.
307        if (
308            !$this->containerLinking
309            || empty($this->fields['is_hierarchy_id'])
310            || null === $this->searchService
311        ) {
312            return 0;
313        }
314
315        $safeId = addcslashes($this->fields['is_hierarchy_id'], '"');
316        $query = new \VuFindSearch\Query\Query(
317            'hierarchy_parent_id:"' . $safeId . '"'
318        );
319        // Disable highlighting for efficiency; not needed here:
320        $params = new \VuFindSearch\ParamBag(['hl' => ['false']]);
321        $command = new SearchCommand($this->sourceIdentifier, $query, 0, 0, $params);
322        return $this->searchService
323            ->invoke($command)->getResult()->getTotal();
324    }
325
326    /**
327     * Get the container record id.
328     *
329     * @return string Container record id (empty string if none)
330     */
331    public function getContainerRecordID()
332    {
333        return $this->containerLinking
334            && !empty($this->fields['hierarchy_parent_id'])
335            ? $this->fields['hierarchy_parent_id'][0] : '';
336    }
337
338    /**
339     * Get work identification keys
340     *
341     * @return array
342     */
343    public function getWorkKeys()
344    {
345        return $this->fields['work_keys_str_mv'] ?? [];
346    }
347
348    /**
349     * Get if the explain features is enabled.
350     *
351     * @return bool
352     */
353    public function explainEnabled()
354    {
355        return $this->explainEnabled;
356    }
357}