Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchService
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 11
462
0.00% covered (danger)
0.00%
0 / 1
 createEntity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createAndPersistEntityWithChecksum
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 destroySession
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getSearchById
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchByIdAndOwner
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getSearches
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getScheduledSearches
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchesByChecksumAndOwner
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 cleanUpInvalidUserIds
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getSavedSearchesWithMissingChecksums
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteExpired
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Database service for search.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2024.
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  Database
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development:plugins:database_gateways Wiki
28 */
29
30namespace VuFind\Db\Service;
31
32use DateTime;
33use Exception;
34use VuFind\Db\Entity\SearchEntityInterface;
35use VuFind\Db\Entity\UserEntityInterface;
36use VuFind\Db\Table\DbTableAwareInterface;
37use VuFind\Db\Table\DbTableAwareTrait;
38
39use function count;
40
41/**
42 * Database service for search.
43 *
44 * @category VuFind
45 * @package  Database
46 * @author   Demian Katz <demian.katz@villanova.edu>
47 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
48 * @link     https://vufind.org/wiki/development:plugins:database_gateways Wiki
49 */
50class SearchService extends AbstractDbService implements
51    SearchServiceInterface,
52    Feature\DeleteExpiredInterface,
53    DbTableAwareInterface
54{
55    use DbTableAwareTrait;
56
57    /**
58     * Create a search entity.
59     *
60     * @return SearchEntityInterface
61     */
62    public function createEntity(): SearchEntityInterface
63    {
64        return $this->getDbTable('search')->createRow();
65    }
66
67    /**
68     * Create a search entity containing the specified checksum, persist it to the database,
69     * and return a fully populated object. Throw an exception if something goes wrong during
70     * the process.
71     *
72     * @param int $checksum Checksum
73     *
74     * @return SearchEntityInterface
75     * @throws Exception
76     */
77    public function createAndPersistEntityWithChecksum(int $checksum): SearchEntityInterface
78    {
79        $table = $this->getDbTable('search');
80        $table->insert(
81            [
82                'created' => date('Y-m-d H:i:s'),
83                'checksum' => $checksum,
84            ]
85        );
86        $lastInsert = $table->getLastInsertValue();
87        if (!($row = $this->getSearchById($lastInsert))) {
88            throw new Exception('Cannot find id ' . $lastInsert);
89        }
90        return $row;
91    }
92
93    /**
94     * Destroy unsaved searches belonging to the specified session/user.
95     *
96     * @param string                       $sessionId Session ID of current user.
97     * @param UserEntityInterface|int|null $userOrId  User entity or ID of current user (optional).
98     *
99     * @return void
100     */
101    public function destroySession(string $sessionId, UserEntityInterface|int|null $userOrId = null): void
102    {
103        $uid = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
104        $callback = function ($select) use ($sessionId, $uid) {
105            $select->where->equalTo('session_id', $sessionId)->and->equalTo('saved', 0);
106            if ($uid !== null) {
107                $select->where->OR
108                    ->equalTo('user_id', $uid)->and->equalTo('saved', 0);
109            }
110        };
111        $this->getDbTable('search')->delete($callback);
112    }
113
114    /**
115     * Get a SearchEntityInterface object by ID.
116     *
117     * @param int $id Search identifier
118     *
119     * @return ?SearchEntityInterface
120     */
121    public function getSearchById(int $id): ?SearchEntityInterface
122    {
123        return $this->getDbTable('search')->select(['id' => $id])->current();
124    }
125
126    /**
127     * Get a SearchEntityInterface object by ID and owner.
128     *
129     * @param int                          $id        Search identifier
130     * @param string                       $sessionId Session ID of current user.
131     * @param UserEntityInterface|int|null $userOrId  User entity or ID of current user (optional).
132     *
133     * @return ?SearchEntityInterface
134     */
135    public function getSearchByIdAndOwner(
136        int $id,
137        string $sessionId,
138        UserEntityInterface|int|null $userOrId
139    ): ?SearchEntityInterface {
140        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
141        $callback = function ($select) use ($id, $sessionId, $userId) {
142            $nest = $select->where
143                ->equalTo('id', $id)
144                ->and
145                ->nest
146                ->equalTo('session_id', $sessionId);
147            if (!empty($userId)) {
148                $nest->or->equalTo('user_id', $userId);
149            }
150        };
151        return $this->getDbTable('search')->select($callback)->current();
152    }
153
154    /**
155     * Get an array of rows for the specified user.
156     *
157     * @param string                       $sessionId Session ID of current user.
158     * @param UserEntityInterface|int|null $userOrId  User entity or ID of current user (optional).
159     *
160     * @return SearchEntityInterface[]
161     */
162    public function getSearches(string $sessionId, UserEntityInterface|int|null $userOrId = null): array
163    {
164        $uid = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
165        $callback = function ($select) use ($sessionId, $uid) {
166            $select->where->equalTo('session_id', $sessionId)->and->equalTo('saved', 0);
167            if ($uid !== null) {
168                $select->where->OR->equalTo('user_id', $uid);
169            }
170            $select->order('created');
171        };
172        return iterator_to_array($this->getDbTable('search')->select($callback));
173    }
174
175    /**
176     * Get scheduled searches.
177     *
178     * @return SearchEntityInterface[]
179     */
180    public function getScheduledSearches(): array
181    {
182        $callback = function ($select) {
183            $select->where->equalTo('saved', 1);
184            $select->where->greaterThan('notification_frequency', 0);
185            $select->order('user_id');
186        };
187        return iterator_to_array($this->getDbTable('search')->select($callback));
188    }
189
190    /**
191     * Retrieve all searches matching the specified checksum and belonging to the user specified by session or user
192     * entity/ID.
193     *
194     * @param int                          $checksum  Checksum to match
195     * @param string                       $sessionId Current session ID
196     * @param UserEntityInterface|int|null $userOrId  Entity or ID representing current user (optional).
197     *
198     * @return SearchEntityInterface[]
199     * @throws Exception
200     */
201    public function getSearchesByChecksumAndOwner(
202        int $checksum,
203        string $sessionId,
204        UserEntityInterface|int|null $userOrId = null
205    ): array {
206        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
207        $callback = function ($select) use ($checksum, $sessionId, $userId) {
208            $nest = $select->where
209                ->equalTo('checksum', $checksum)
210                ->and
211                ->nest
212                ->equalTo('session_id', $sessionId)->and->equalTo('saved', 0);
213            if (!empty($userId)) {
214                $nest->or->equalTo('user_id', $userId);
215            }
216        };
217        return iterator_to_array($this->getDbTable('search')->select($callback));
218    }
219
220    /**
221     * Set invalid user_id values in the table to null; return count of affected rows.
222     *
223     * @return int
224     */
225    public function cleanUpInvalidUserIds(): int
226    {
227        $searchTable = $this->getDbTable('search');
228        $allIds = $this->getDbTable('user')->getSql()->select()->columns(['id']);
229        $searchCallback = function ($select) use ($allIds) {
230            $select->where->isNotNull('user_id')->AND->notIn('user_id', $allIds);
231        };
232        $badRows = $searchTable->select($searchCallback);
233        $count = count($badRows);
234        if ($count > 0) {
235            $searchTable->update(['user_id' => null], $searchCallback);
236        }
237        return $count;
238    }
239
240    /**
241     * Get saved searches with missing checksums (used for cleaning up legacy data).
242     *
243     * @return SearchEntityInterface[]
244     */
245    public function getSavedSearchesWithMissingChecksums(): array
246    {
247        $searchWhere = ['checksum' => null, 'saved' => 1];
248        return iterator_to_array($this->getDbTable('search')->select($searchWhere));
249    }
250
251    /**
252     * Delete expired records. Allows setting a limit so that rows can be deleted in small batches.
253     *
254     * @param DateTime $dateLimit Date threshold of an "expired" record.
255     * @param ?int     $limit     Maximum number of rows to delete or null for no limit.
256     *
257     * @return int Number of rows deleted
258     */
259    public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int
260    {
261        return $this->getDbTable('search')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit);
262    }
263}