Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
RandomCommand
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
5 / 5
12
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getArguments
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
8
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Return random records command.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 * Copyright (C) The National Library of Finland 2021.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2,
13 * as published by the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @category VuFind
25 * @package  Search
26 * @author   Luke O'Sullivan <l.osullivan@swansea.ac.uk>
27 * @author   Aleksi Peebles <aleksi.peebles@helsinki.fi>
28 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
29 * @link     https://vufind.org
30 */
31
32namespace VuFindSearch\Command;
33
34use VuFindSearch\Backend\BackendInterface;
35use VuFindSearch\Feature\RandomInterface;
36use VuFindSearch\ParamBag;
37use VuFindSearch\Query\QueryInterface;
38
39use function in_array;
40
41/**
42 * Return random records command.
43 *
44 * @category VuFind
45 * @package  Search
46 * @author   Luke O'Sullivan <l.osullivan@swansea.ac.uk>
47 * @author   Aleksi Peebles <aleksi.peebles@helsinki.fi>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
49 * @link     https://vufind.org
50 */
51class RandomCommand extends CallMethodCommand
52{
53    /**
54     * Search query.
55     *
56     * @var QueryInterface
57     */
58    protected $query;
59
60    /**
61     * Search limit.
62     *
63     * @var int
64     */
65    protected $limit;
66
67    /**
68     * RandomCommand constructor.
69     *
70     * @param string         $backendId Search backend identifier
71     * @param QueryInterface $query     Search query
72     * @param int            $limit     Search limit
73     * @param ?ParamBag      $params    Search backend parameters
74     */
75    public function __construct(
76        string $backendId,
77        QueryInterface $query,
78        int $limit,
79        ?ParamBag $params = null
80    ) {
81        $this->query = $query;
82        $this->limit = $limit;
83        parent::__construct(
84            $backendId,
85            RandomInterface::class,
86            'random',
87            $params
88        );
89    }
90
91    /**
92     * Return search backend interface method arguments.
93     *
94     * @return array
95     */
96    public function getArguments(): array
97    {
98        return [
99            $this->getQuery(),
100            $this->getLimit(),
101            $this->getSearchParameters(),
102        ];
103    }
104
105    /**
106     * Execute command on backend.
107     *
108     * @param BackendInterface $backend Backend
109     *
110     * @return CommandInterface Command instance for method chaining
111     */
112    public function execute(BackendInterface $backend): CommandInterface
113    {
114        // If the backend implements the RetrieveRandomInterface, we can load
115        // all the records at once.
116        if ($backend instanceof RandomInterface) {
117            return parent::execute($backend);
118        }
119
120        // Otherwise, we need to load them one at a time and aggregate them.
121
122        $query = $this->getQuery();
123        $limit = $this->getLimit();
124
125        // offset/limit of 0 - we don't need records, just count
126        $results = $backend->search($query, 0, 0, $this->params);
127        $total_records = $results->getTotal();
128
129        if (0 === $total_records) {
130            // Empty result? Send back as-is:
131            $response = $results;
132        } elseif ($total_records < $limit) {
133            // Result set smaller than limit? Get everything and shuffle:
134            $response = $backend->search($query, 0, $limit, $this->params);
135            $response->shuffle();
136        } else {
137            // Default case: retrieve n random records:
138            $response = false;
139            $retrievedIndexes = [];
140            for ($i = 0; $i < $limit; $i++) {
141                $nextIndex = rand(0, $total_records - 1);
142                while (in_array($nextIndex, $retrievedIndexes)) {
143                    // avoid duplicate records
144                    $nextIndex = rand(0, $total_records - 1);
145                }
146                $retrievedIndexes[] = $nextIndex;
147                $currentBatch = $backend->search(
148                    $query,
149                    $nextIndex,
150                    1,
151                    $this->params
152                );
153                if (!$response) {
154                    $response = $currentBatch;
155                } elseif ($record = $currentBatch->first()) {
156                    $response->add($record);
157                }
158            }
159        }
160
161        return $this->finalizeExecution($response);
162    }
163
164    /**
165     * Return search query.
166     *
167     * @return QueryInterface
168     */
169    public function getQuery(): QueryInterface
170    {
171        return $this->query;
172    }
173
174    /**
175     * Return search limit.
176     *
177     * @return int
178     */
179    public function getLimit(): int
180    {
181        return $this->limit;
182    }
183}