Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
16.39% |
10 / 61 |
|
10.53% |
2 / 19 |
CRAP | |
0.00% |
0 / 1 |
ResourceTagsService | |
16.39% |
10 / 61 |
|
10.53% |
2 / 19 |
1282.62 | |
0.00% |
0 / 1 |
beginTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
commitTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
rollBackTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getResourceTagsPaginator | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
createEntity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
createLink | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
110 | |||
deleteLinksByResourceTagsIdArray | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
destroyResourceTagsLinksForUser | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
6 | |||
destroyNonListResourceTagsLinksForUser | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
destroyAllListResourceTagsLinksForUser | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
destroyUserListLinks | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
getUniqueResources | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUniqueTags | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getUniqueUsers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
deleteResourceTags | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getAnonymousCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
assignAnonymousTags | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
changeResourceId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
deduplicate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Database service for resource_tags. |
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 | |
30 | namespace VuFind\Db\Service; |
31 | |
32 | use DateTime; |
33 | use Laminas\Paginator\Paginator; |
34 | use VuFind\Db\Entity\ResourceEntityInterface; |
35 | use VuFind\Db\Entity\ResourceTagsEntityInterface; |
36 | use VuFind\Db\Entity\TagsEntityInterface; |
37 | use VuFind\Db\Entity\UserEntityInterface; |
38 | use VuFind\Db\Entity\UserListEntityInterface; |
39 | |
40 | use function is_int; |
41 | |
42 | /** |
43 | * Database service for resource_tags. |
44 | * |
45 | * @category VuFind |
46 | * @package Database |
47 | * @author Demian Katz <demian.katz@villanova.edu> |
48 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
49 | * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki |
50 | */ |
51 | class ResourceTagsService extends AbstractDbService implements |
52 | ResourceTagsServiceInterface, |
53 | Feature\TransactionInterface, |
54 | \VuFind\Db\Table\DbTableAwareInterface |
55 | { |
56 | use \VuFind\Db\Table\DbTableAwareTrait; |
57 | |
58 | /** |
59 | * Begin a database transaction. |
60 | * |
61 | * @return void |
62 | * @throws Exception |
63 | */ |
64 | public function beginTransaction(): void |
65 | { |
66 | $this->getDbTable('ResourceTags')->beginTransaction(); |
67 | } |
68 | |
69 | /** |
70 | * Commit a database transaction. |
71 | * |
72 | * @return void |
73 | * @throws Exception |
74 | */ |
75 | public function commitTransaction(): void |
76 | { |
77 | $this->getDbTable('ResourceTags')->commitTransaction(); |
78 | } |
79 | |
80 | /** |
81 | * Roll back a database transaction. |
82 | * |
83 | * @return void |
84 | * @throws Exception |
85 | */ |
86 | public function rollBackTransaction(): void |
87 | { |
88 | $this->getDbTable('ResourceTags')->rollbackTransaction(); |
89 | } |
90 | |
91 | /** |
92 | * Get Resource Tags Paginator |
93 | * |
94 | * @param ?int $userId ID of user (null for any) |
95 | * @param ?int $resourceId ID of the resource (null for any) |
96 | * @param ?int $tagId ID of the tag (null for any) |
97 | * @param ?string $order The order in which to return the data |
98 | * @param ?int $page The page number to select |
99 | * @param int $limit The number of items to fetch |
100 | * @param bool $caseSensitiveTags Should we treat tags as case-sensitive? |
101 | * |
102 | * @return Paginator |
103 | */ |
104 | public function getResourceTagsPaginator( |
105 | ?int $userId = null, |
106 | ?int $resourceId = null, |
107 | ?int $tagId = null, |
108 | ?string $order = null, |
109 | ?int $page = null, |
110 | int $limit = 20, |
111 | bool $caseSensitiveTags = false |
112 | ): Paginator { |
113 | return $this->getDbTable('ResourceTags') |
114 | ->getResourceTags($userId, $resourceId, $tagId, $order, $page, $limit, $caseSensitiveTags); |
115 | } |
116 | |
117 | /** |
118 | * Create a ResourceTagsEntityInterface object. |
119 | * |
120 | * @return ResourceTagsEntityInterface |
121 | */ |
122 | public function createEntity(): ResourceTagsEntityInterface |
123 | { |
124 | return $this->getDbTable('ResourceTags')->createRow(); |
125 | } |
126 | |
127 | /** |
128 | * Create a resource_tags row linking the specified resources |
129 | * |
130 | * @param ResourceEntityInterface|int|null $resourceOrId Resource entity or ID to link up (optional) |
131 | * @param TagsEntityInterface|int $tagOrId Tag entity or ID to link up |
132 | * @param UserEntityInterface|int|null $userOrId User entity or ID creating link (optional but recommended) |
133 | * @param UserListEntityInterface|int|null $listOrId List entity or ID to link up (optional) |
134 | * @param ?DateTime $posted Posted date (optional -- omit for current) |
135 | * |
136 | * @return void |
137 | */ |
138 | public function createLink( |
139 | ResourceEntityInterface|int|null $resourceOrId, |
140 | TagsEntityInterface|int $tagOrId, |
141 | UserEntityInterface|int|null $userOrId = null, |
142 | UserListEntityInterface|int|null $listOrId = null, |
143 | ?DateTime $posted = null |
144 | ) { |
145 | $table = $this->getDbTable('ResourceTags'); |
146 | $resourceId = is_int($resourceOrId) ? $resourceOrId : $resourceOrId?->getId(); |
147 | $tagId = is_int($tagOrId) ? $tagOrId : $tagOrId->getId(); |
148 | $userId = is_int($userOrId) ? $userOrId : $userOrId?->getId(); |
149 | $listId = is_int($listOrId) ? $listOrId : $listOrId?->getId(); |
150 | |
151 | $callback = function ($select) use ($resourceId, $tagId, $userId, $listId) { |
152 | $select->where->equalTo('resource_id', $resourceId) |
153 | ->equalTo('tag_id', $tagId); |
154 | if (null !== $listId) { |
155 | $select->where->equalTo('list_id', $listId); |
156 | } else { |
157 | $select->where->isNull('list_id'); |
158 | } |
159 | if (null !== $userId) { |
160 | $select->where->equalTo('user_id', $userId); |
161 | } else { |
162 | $select->where->isNull('user_id'); |
163 | } |
164 | }; |
165 | $result = $table->select($callback)->current(); |
166 | |
167 | // Only create row if it does not already exist: |
168 | if (!$result) { |
169 | $result = $this->createEntity(); |
170 | $result->resource_id = $resourceId; |
171 | $result->tag_id = $tagId; |
172 | if (null !== $listId) { |
173 | $result->list_id = $listId; |
174 | } |
175 | if (null !== $userId) { |
176 | $result->user_id = $userId; |
177 | } |
178 | $result->setPosted($posted ?? new DateTime()); |
179 | $this->persistEntity($result); |
180 | } |
181 | } |
182 | |
183 | /** |
184 | * Remove links from the resource_tags table based on an array of IDs. |
185 | * |
186 | * @param string[] $ids Identifiers from resource_tags to delete. |
187 | * |
188 | * @return int Count of $ids |
189 | */ |
190 | public function deleteLinksByResourceTagsIdArray(array $ids): int |
191 | { |
192 | return $this->getDbTable('ResourceTags')->deleteByIdArray($ids); |
193 | } |
194 | |
195 | /** |
196 | * Unlink tag rows for the specified resource and user. |
197 | * |
198 | * @param int|int[]|null $resourceId ID (or array of IDs) of resource(s) to |
199 | * unlink (null for ALL matching resources) |
200 | * @param UserEntityInterface|int $userOrId ID or entity representing user |
201 | * @param UserListEntityInterface|int|null $listOrId ID of list to unlink (null for ALL matching tags) |
202 | * @param int|int[]|null $tagId ID or array of IDs of tag(s) to unlink (null |
203 | * for ALL matching tags) |
204 | * |
205 | * @return void |
206 | */ |
207 | public function destroyResourceTagsLinksForUser( |
208 | int|array|null $resourceId, |
209 | UserEntityInterface|int $userOrId, |
210 | UserListEntityInterface|int|null $listOrId = null, |
211 | int|array|null $tagId = null |
212 | ): void { |
213 | $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; |
214 | $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; |
215 | $callback = function ($select) use ($resourceId, $userId, $listId, $tagId) { |
216 | $select->where->equalTo('user_id', $userId); |
217 | if (null !== $resourceId) { |
218 | $select->where->in('resource_id', (array)$resourceId); |
219 | } |
220 | if (null !== $listId) { |
221 | $select->where->equalTo('list_id', $listId); |
222 | } |
223 | if (null !== $tagId) { |
224 | $select->where->in('tag_id', (array)$tagId); |
225 | } |
226 | }; |
227 | $this->getDbTable('ResourceTags')->delete($callback); |
228 | } |
229 | |
230 | /** |
231 | * Unlink tag rows that are not associated with a favorite list for the specified resource and user. |
232 | * |
233 | * @param int|int[]|null $resourceId ID (or array of IDs) of resource(s) to unlink (null for ALL matching |
234 | * resources) |
235 | * @param UserEntityInterface|int $userOrId ID or entity representing user |
236 | * @param int|int[]|null $tagId ID or array of IDs of tag(s) to unlink (null for ALL matching tags) |
237 | * |
238 | * @return void |
239 | */ |
240 | public function destroyNonListResourceTagsLinksForUser( |
241 | int|array|null $resourceId, |
242 | UserEntityInterface|int $userOrId, |
243 | int|array|null $tagId = null |
244 | ): void { |
245 | $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; |
246 | $callback = function ($select) use ($resourceId, $userId, $tagId) { |
247 | $select->where->equalTo('user_id', $userId); |
248 | if (null !== $resourceId) { |
249 | $select->where->in('resource_id', (array)$resourceId); |
250 | } |
251 | $select->where->isNull('list_id'); |
252 | if (null !== $tagId) { |
253 | $select->where->in('tag_id', (array)$tagId); |
254 | } |
255 | }; |
256 | $this->getDbTable('ResourceTags')->delete($callback); |
257 | } |
258 | |
259 | /** |
260 | * Unlink all tag rows associated with favorite lists for the specified resource and user. Tags added directly |
261 | * to records outside of favorites will not be impacted. |
262 | * |
263 | * @param int|int[]|null $resourceId ID (or array of IDs) of resource(s) to unlink (null for ALL matching |
264 | * resources) |
265 | * @param UserEntityInterface|int $userOrId ID or entity representing user |
266 | * @param int|int[]|null $tagId ID or array of IDs of tag(s) to unlink (null for ALL matching tags) |
267 | * |
268 | * @return void |
269 | */ |
270 | public function destroyAllListResourceTagsLinksForUser( |
271 | int|array|null $resourceId, |
272 | UserEntityInterface|int $userOrId, |
273 | int|array|null $tagId = null |
274 | ): void { |
275 | $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; |
276 | $callback = function ($select) use ($resourceId, $userId, $tagId) { |
277 | $select->where->equalTo('user_id', $userId); |
278 | if (null !== $resourceId) { |
279 | $select->where->in('resource_id', (array)$resourceId); |
280 | } |
281 | $select->where->isNotNull('list_id'); |
282 | if (null !== $tagId) { |
283 | $select->where->in('tag_id', (array)$tagId); |
284 | } |
285 | }; |
286 | $this->getDbTable('ResourceTags')->delete($callback); |
287 | } |
288 | |
289 | /** |
290 | * Unlink rows for the specified user list. This removes tags ON THE LIST ITSELF, not tags on |
291 | * resources within the list. |
292 | * |
293 | * @param UserListEntityInterface|int $listOrId ID or entity representing list |
294 | * @param UserEntityInterface|int $userOrId ID or entity representing user |
295 | * @param int|int[]|null $tagId ID or array of IDs of tag(s) to unlink (null for ALL matching tags) |
296 | * |
297 | * @return void |
298 | */ |
299 | public function destroyUserListLinks( |
300 | UserListEntityInterface|int $listOrId, |
301 | UserEntityInterface|int $userOrId, |
302 | int|array|null $tagId = null |
303 | ): void { |
304 | $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; |
305 | $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; |
306 | $callback = function ($select) use ($userId, $listId, $tagId) { |
307 | $select->where->equalTo('user_id', $userId); |
308 | // retrieve tags assigned to a user list and filter out user resource tags |
309 | // (resource_id is NULL for list tags). |
310 | $select->where->isNull('resource_id'); |
311 | $select->where->equalTo('list_id', $listId); |
312 | |
313 | if (null !== $tagId) { |
314 | $select->where->in('tag_id', (array)$tagId); |
315 | } |
316 | }; |
317 | $this->getDbTable('ResourceTags')->delete($callback); |
318 | } |
319 | |
320 | /** |
321 | * Gets unique tagged resources from the database. |
322 | * |
323 | * @param ?int $userId ID of user (null for any) |
324 | * @param ?int $resourceId ID of the resource (null for any) |
325 | * @param ?int $tagId ID of the tag (null for any) |
326 | * |
327 | * @return array[] |
328 | */ |
329 | public function getUniqueResources( |
330 | ?int $userId = null, |
331 | ?int $resourceId = null, |
332 | ?int $tagId = null |
333 | ): array { |
334 | return $this->getDbTable('ResourceTags')->getUniqueResources($userId, $resourceId, $tagId)->toArray(); |
335 | } |
336 | |
337 | /** |
338 | * Gets unique tags from the database. |
339 | * |
340 | * @param ?int $userId ID of user (null for any) |
341 | * @param ?int $resourceId ID of the resource (null for any) |
342 | * @param ?int $tagId ID of the tag (null for any) |
343 | * @param bool $caseSensitive Should we treat tags in a case-sensitive manner? |
344 | * |
345 | * @return array[] |
346 | */ |
347 | public function getUniqueTags( |
348 | ?int $userId = null, |
349 | ?int $resourceId = null, |
350 | ?int $tagId = null, |
351 | bool $caseSensitive = false |
352 | ): array { |
353 | return $this->getDbTable('ResourceTags')->getUniqueTags($userId, $resourceId, $tagId, $caseSensitive) |
354 | ->toArray(); |
355 | } |
356 | |
357 | /** |
358 | * Gets unique users from the database. |
359 | * |
360 | * @param ?int $userId ID of user (null for any) |
361 | * @param ?int $resourceId ID of the resource (null for any) |
362 | * @param ?int $tagId ID of the tag (null for any) |
363 | * |
364 | * @return array[] |
365 | */ |
366 | public function getUniqueUsers( |
367 | ?int $userId = null, |
368 | ?int $resourceId = null, |
369 | ?int $tagId = null |
370 | ): array { |
371 | return $this->getDbTable('ResourceTags')->getUniqueUsers($userId, $resourceId, $tagId)->toArray(); |
372 | } |
373 | |
374 | /** |
375 | * Delete resource tags rows matching specified filter(s). Return count of IDs deleted. |
376 | * |
377 | * @param ?int $userId ID of user (null for any) |
378 | * @param ?int $resourceId ID of the resource (null for any) |
379 | * @param ?int $tagId ID of the tag (null for any) |
380 | * |
381 | * @return int |
382 | */ |
383 | public function deleteResourceTags( |
384 | ?int $userId = null, |
385 | ?int $resourceId = null, |
386 | ?int $tagId = null |
387 | ): int { |
388 | $deleted = 0; |
389 | while (true) { |
390 | $nextBatch = $this->getResourceTagsPaginator($userId, $resourceId, $tagId); |
391 | if ($nextBatch->getTotalItemCount() < 1) { |
392 | return $deleted; |
393 | } |
394 | $ids = []; |
395 | foreach ($nextBatch as $row) { |
396 | $ids[] = $row['id']; |
397 | } |
398 | $deleted += $this->deleteLinksByResourceTagsIdArray($ids); |
399 | } |
400 | } |
401 | |
402 | /** |
403 | * Get count of anonymous tags |
404 | * |
405 | * @return int count |
406 | */ |
407 | public function getAnonymousCount(): int |
408 | { |
409 | return $this->getDbTable('ResourceTags')->getAnonymousCount(); |
410 | } |
411 | |
412 | /** |
413 | * Assign anonymous tags to the specified user. |
414 | * |
415 | * @param UserEntityInterface|int $userOrId User entity or ID to own anonymous tags. |
416 | * |
417 | * @return void |
418 | */ |
419 | public function assignAnonymousTags(UserEntityInterface|int $userOrId): void |
420 | { |
421 | $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; |
422 | $this->getDbTable('ResourceTags')->assignAnonymousTags($userId); |
423 | } |
424 | |
425 | /** |
426 | * Change all matching rows to use the new resource ID instead of the old one (called when an ID changes). |
427 | * |
428 | * @param int $old Original resource ID |
429 | * @param int $new New resource ID |
430 | * |
431 | * @return void |
432 | */ |
433 | public function changeResourceId(int $old, int $new): void |
434 | { |
435 | $this->getDbTable('ResourceTags')->update(['resource_id' => $new], ['resource_id' => $old]); |
436 | } |
437 | |
438 | /** |
439 | * Deduplicate rows (sometimes necessary after merging foreign key IDs). |
440 | * |
441 | * @return void |
442 | */ |
443 | public function deduplicate(): void |
444 | { |
445 | $this->getDbTable('ResourceTags')->deduplicate(); |
446 | } |
447 | } |