Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.91% covered (success)
90.91%
70 / 77
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Backend
90.91% covered (success)
90.91%
70 / 77
80.00% covered (warning)
80.00%
8 / 10
25.47
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
2
 search
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 retrieve
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
 retrieveBatch
76.92% covered (warning)
76.92%
20 / 26
0.00% covered (danger)
0.00%
0 / 1
5.31
 setQueryBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryBuilder
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRecordCollectionFactory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getConnector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createRecordCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 paramBagToSummonQuery
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * Summon backend.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
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  Search
25 * @author   David Maus <maus@hab.de>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org
28 */
29
30namespace VuFindSearch\Backend\Summon;
31
32use SerialsSolutions\Summon\Laminas as Connector;
33use SerialsSolutions_Summon_Exception as SummonException;
34use SerialsSolutions_Summon_Query as SummonQuery;
35use VuFind\Exception\RecordMissing as RecordMissingException;
36use VuFindSearch\Backend\AbstractBackend;
37use VuFindSearch\Backend\Exception\BackendException;
38use VuFindSearch\Feature\RetrieveBatchInterface;
39use VuFindSearch\ParamBag;
40use VuFindSearch\Query\AbstractQuery;
41use VuFindSearch\Response\RecordCollectionFactoryInterface;
42use VuFindSearch\Response\RecordCollectionInterface;
43
44use function count;
45use function in_array;
46
47/**
48 * Summon backend.
49 *
50 * @category VuFind
51 * @package  Search
52 * @author   David Maus <maus@hab.de>
53 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
54 * @link     https://vufind.org
55 */
56class Backend extends AbstractBackend implements RetrieveBatchInterface
57{
58    /**
59     * Connector.
60     *
61     * @var Connector
62     */
63    protected $connector;
64
65    /**
66     * Query builder.
67     *
68     * @var QueryBuilder
69     */
70    protected $queryBuilder = null;
71
72    /**
73     * Constructor.
74     *
75     * @param Connector                        $connector Summon connector
76     * @param RecordCollectionFactoryInterface $factory   Record collection factory
77     * (null for default)
78     *
79     * @return void
80     */
81    public function __construct(
82        Connector $connector,
83        RecordCollectionFactoryInterface $factory = null
84    ) {
85        if (null !== $factory) {
86            $this->setRecordCollectionFactory($factory);
87        }
88        $this->connector    = $connector;
89        $this->identifier   = null;
90    }
91
92    /**
93     * Perform a search and return record collection.
94     *
95     * @param AbstractQuery $query  Search query
96     * @param int           $offset Search offset
97     * @param int           $limit  Search limit
98     * @param ParamBag      $params Search backend parameters
99     *
100     * @return RecordCollectionInterface
101     */
102    public function search(
103        AbstractQuery $query,
104        $offset,
105        $limit,
106        ParamBag $params = null
107    ) {
108        $baseParams = $this->getQueryBuilder()->build($query);
109        if (null !== $params) {
110            $baseParams->mergeWith($params);
111        }
112        $baseParams->set('pageSize', $limit);
113        $page = $limit > 0 ? floor($offset / $limit) + 1 : 1;
114        $baseParams->set('pageNumber', $page);
115
116        $summonQuery = $this->paramBagToSummonQuery($baseParams);
117        try {
118            $response = $this->connector->query($summonQuery);
119        } catch (SummonException $e) {
120            throw new BackendException(
121                $e->getMessage(),
122                $e->getCode(),
123                $e
124            );
125        }
126        $collection = $this->createRecordCollection($response);
127        $this->injectSourceIdentifier($collection);
128        return $collection;
129    }
130
131    /**
132     * Retrieve a single document.
133     *
134     * @param string   $id     Document identifier
135     * @param ParamBag $params Search backend parameters
136     *
137     * @return RecordCollectionInterface
138     * @throws RecordMissingException
139     */
140    public function retrieve($id, ParamBag $params = null)
141    {
142        $finalParams = $params ?: new ParamBag();
143        // We normally look up by ID, but we occasionally need to use bookmarks:
144        $idType = $finalParams->get('summonIdType')[0] ?? Connector::IDENTIFIER_ID;
145        try {
146            $response = $this->connector->getRecord($id, false, $idType);
147        } catch (SummonException $e) {
148            throw new BackendException(
149                $e->getMessage(),
150                $e->getCode(),
151                $e
152            );
153        }
154        if (empty($response['documents'])) {
155            throw new RecordMissingException('Record does not exist.');
156        }
157        $collection = $this->createRecordCollection($response);
158        $this->injectSourceIdentifier($collection);
159        return $collection;
160    }
161
162    /**
163     * Retrieve a batch of documents.
164     *
165     * @param array    $ids    Array of document identifiers
166     * @param ParamBag $params Search backend parameters
167     *
168     * @return RecordCollectionInterface
169     */
170    public function retrieveBatch($ids, ParamBag $params = null)
171    {
172        // Load 50 records at a time; this is the limit for Summon.
173        $pageSize = 50;
174
175        // Retrieve records a page at a time:
176        $results = false;
177        while (count($ids) > 0) {
178            $currentPage = array_splice($ids, 0, $pageSize, []);
179            $query = new SummonQuery(
180                null,
181                [
182                    'idsToFetch' => $currentPage,
183                    'pageNumber' => 1,
184                    'pageSize' => $pageSize,
185                ]
186            );
187            try {
188                $batch = $this->connector->query($query);
189            } catch (SummonException $e) {
190                throw new BackendException(
191                    $e->getMessage(),
192                    $e->getCode(),
193                    $e
194                );
195            }
196            $next = $this->createRecordCollection($batch);
197            if (!$results) {
198                $results = $next;
199            } else {
200                foreach ($next->getRecords() as $record) {
201                    $results->add($record);
202                }
203            }
204        }
205        $this->injectSourceIdentifier($results);
206        return $results;
207    }
208
209    /**
210     * Set the query builder.
211     *
212     * @param QueryBuilder $queryBuilder Query builder
213     *
214     * @return void
215     */
216    public function setQueryBuilder(QueryBuilder $queryBuilder)
217    {
218        $this->queryBuilder = $queryBuilder;
219    }
220
221    /**
222     * Return query builder.
223     *
224     * Lazy loads an empty QueryBuilder if none was set.
225     *
226     * @return QueryBuilder
227     */
228    public function getQueryBuilder()
229    {
230        if (!$this->queryBuilder) {
231            $this->queryBuilder = new QueryBuilder();
232        }
233        return $this->queryBuilder;
234    }
235
236    /**
237     * Return the record collection factory.
238     *
239     * Lazy loads a generic collection factory.
240     *
241     * @return RecordCollectionFactoryInterface
242     */
243    public function getRecordCollectionFactory()
244    {
245        if ($this->collectionFactory === null) {
246            $this->collectionFactory = new Response\RecordCollectionFactory();
247        }
248        return $this->collectionFactory;
249    }
250
251    /**
252     * Return the Summon connector.
253     *
254     * @return Connector
255     */
256    public function getConnector()
257    {
258        return $this->connector;
259    }
260
261    /// Internal API
262
263    /**
264     * Create record collection.
265     *
266     * @param array $records Records to process
267     *
268     * @return RecordCollectionInterface
269     */
270    protected function createRecordCollection($records)
271    {
272        return $this->getRecordCollectionFactory()->factory($records);
273    }
274
275    /**
276     * Convert a ParamBag to a Summon query object.
277     *
278     * @param ParamBag $params ParamBag to convert
279     *
280     * @return SummonQuery
281     */
282    protected function paramBagToSummonQuery(ParamBag $params)
283    {
284        $params = $params->getArrayCopy();
285
286        // Extract the query:
287        $query = $params['query'][0] ?? null;
288        unset($params['query']);
289
290        // Convert the options:
291        $options = [];
292        // Most parameters need to be flattened from array format, but a few
293        // should remain as arrays:
294        $arraySettings = ['facets', 'filters', 'groupFilters', 'rangeFilters'];
295        foreach ($params as $key => $param) {
296            $options[$key] = in_array($key, $arraySettings) ? $param : $param[0];
297        }
298
299        return new SummonQuery($query, $options);
300    }
301}