Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.39% covered (danger)
16.39%
10 / 61
10.53% covered (danger)
10.53%
2 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceTagsService
16.39% covered (danger)
16.39%
10 / 61
10.53% covered (danger)
10.53%
2 / 19
1282.62
0.00% covered (danger)
0.00%
0 / 1
 beginTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 commitTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rollBackTransaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceTagsPaginator
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 createEntity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createLink
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 deleteLinksByResourceTagsIdArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 destroyResourceTagsLinksForUser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
6
 destroyNonListResourceTagsLinksForUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 destroyAllListResourceTagsLinksForUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 destroyUserListLinks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getUniqueResources
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueTags
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueUsers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteResourceTags
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getAnonymousCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 assignAnonymousTags
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 changeResourceId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deduplicate
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 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
30namespace VuFind\Db\Service;
31
32use DateTime;
33use Laminas\Paginator\Paginator;
34use VuFind\Db\Entity\ResourceEntityInterface;
35use VuFind\Db\Entity\ResourceTagsEntityInterface;
36use VuFind\Db\Entity\TagsEntityInterface;
37use VuFind\Db\Entity\UserEntityInterface;
38use VuFind\Db\Entity\UserListEntityInterface;
39
40use 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 */
51class 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}