Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.88% covered (danger)
5.88%
3 / 51
6.67% covered (danger)
6.67%
1 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
TagService
5.88% covered (danger)
5.88%
3 / 51
6.67% covered (danger)
6.67%
1 / 15
681.63
0.00% covered (danger)
0.00%
0 / 1
 getStatistics
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNonListTagsFuzzilyMatchingString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTagsByText
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTagByText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getResourcesMatchingTagQuery
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getTagBrowseList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordTags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRecordTagsFromFavorites
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getRecordTagsNotInFavorites
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getDuplicateTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserTagsFromFavorites
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getListTags
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 deleteOrphanedTags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTagById
0.00% covered (danger)
0.00%
0 / 1
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
1<?php
2
3/**
4 * Database service for tags.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2023.
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 Laminas\Db\Sql\Select;
33use VuFind\Db\Entity\TagsEntityInterface;
34use VuFind\Db\Entity\UserEntityInterface;
35use VuFind\Db\Entity\UserListEntityInterface;
36
37/**
38 * Database service for tags.
39 *
40 * @category VuFind
41 * @package  Database
42 * @author   Demian Katz <demian.katz@villanova.edu>
43 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
44 * @link     https://vufind.org/wiki/development:plugins:database_gateways Wiki
45 */
46class TagService extends AbstractDbService implements TagServiceInterface, \VuFind\Db\Table\DbTableAwareInterface
47{
48    use \VuFind\Db\Table\DbTableAwareTrait;
49
50    /**
51     * Get statistics on use of tags.
52     *
53     * @param bool $extended          Include extended (unique/anonymous) stats.
54     * @param bool $caseSensitiveTags Should we treat tags case-sensitively?
55     *
56     * @return array
57     */
58    public function getStatistics(bool $extended = false, bool $caseSensitiveTags = false): array
59    {
60        return $this->getDbTable('ResourceTags')->getStatistics($extended, $caseSensitiveTags);
61    }
62
63    /**
64     * Get the tags that match a string
65     *
66     * @param string $text          Tag to look up.
67     * @param string $sort          Sort type
68     * @param int    $limit         Maximum results to retrieve
69     * @param bool   $caseSensitive Should tags be treated as case-sensitive?
70     *
71     * @return array
72     */
73    public function getNonListTagsFuzzilyMatchingString(
74        string $text,
75        string $sort = 'alphabetical',
76        int $limit = 100,
77        bool $caseSensitive = false
78    ): array {
79        return $this->getDbTable('Tags')->matchText($text, $sort, $limit, $caseSensitive);
80    }
81
82    /**
83     * Get all matching tags by text. Normally, 0 or 1 results will be retrieved, but more
84     * may be retrieved under exceptional circumstances (e.g. if retrieving case-insensitively
85     * after storing data case-sensitively).
86     *
87     * @param string $text          Tag text to match
88     * @param bool   $caseSensitive Should tags be retrieved case-sensitively?
89     *
90     * @return TagsEntityInterface[]
91     */
92    public function getTagsByText(string $text, bool $caseSensitive = false): array
93    {
94        $callback = function ($select) use ($text, $caseSensitive) {
95            if ($caseSensitive) {
96                $select->where->equalTo('tag', $text);
97            } else {
98                $select->where->literal('lower(tag) = lower(?)', [$text]);
99            }
100        };
101        return iterator_to_array($this->getDbTable('Tags')->select($callback));
102    }
103
104    /**
105     * Get the first available matching tag by text; return null if no match is found.
106     *
107     * @param string $text          Tag text to match
108     * @param bool   $caseSensitive Should tags be retrieved case-sensitively?
109     *
110     * @return TagsEntityInterface[]
111     */
112    public function getTagByText(string $text, bool $caseSensitive = false): ?TagsEntityInterface
113    {
114        $tags = $this->getTagsByText($text, $caseSensitive);
115        return $tags[0] ?? null;
116    }
117
118    /**
119     * Get all resources associated with the provided tag query.
120     *
121     * @param string $q             Search query
122     * @param string $source        Record source (optional limiter)
123     * @param string $sort          Resource field to sort on (optional)
124     * @param int    $offset        Offset for results
125     * @param ?int   $limit         Limit for results (null for none)
126     * @param bool   $fuzzy         Are we doing an exact (false) or fuzzy (true) search?
127     * @param ?bool  $caseSensitive Should search be case sensitive? (Ignored when fuzzy = true)
128     *
129     * @return array
130     */
131    public function getResourcesMatchingTagQuery(
132        string $q,
133        string $source = null,
134        string $sort = null,
135        int $offset = 0,
136        ?int $limit = null,
137        bool $fuzzy = true,
138        bool $caseSensitive = false
139    ): array {
140        return iterator_to_array(
141            $this->getDbTable('Tags')->resourceSearch(
142                $q,
143                $source,
144                $sort,
145                $offset,
146                $limit,
147                $fuzzy,
148                $caseSensitive
149            )
150        );
151    }
152
153    /**
154     * Get a list of tags for the browse interface.
155     *
156     * @param string $sort          Sort/search parameter
157     * @param int    $limit         Maximum number of tags (default = 100, < 1 = no limit)
158     * @param bool   $caseSensitive Treat tags as case-sensitive?
159     *
160     * @return array
161     */
162    public function getTagBrowseList(string $sort, int $limit, bool $caseSensitive = false): array
163    {
164        $callback = function ($select) {
165            // Discard user list tags
166            $select->where->isNotNull('resource_tags.resource_id');
167        };
168        return $this->getDbTable('Tags')->getTagList($sort, $limit, $callback, $caseSensitive);
169    }
170
171    /**
172     * Get all tags associated with the specified record (and matching provided filters).
173     *
174     * @param string                           $id            Record ID to look up
175     * @param string                           $source        Source of record to look up
176     * @param int                              $limit         Max. number of tags to return (0 = no limit)
177     * @param UserListEntityInterface|int|null $listOrId      ID of list to load tags from (null for no restriction)
178     * @param UserEntityInterface|int|null     $userOrId      ID of user to load tags from (null for all users)
179     * @param string                           $sort          Sort type ('count' or 'tag')
180     * @param UserEntityInterface|int|null     $ownerOrId     ID of user to check for ownership
181     * @param bool                             $caseSensitive Treat tags as case-sensitive?
182     *
183     * @return array
184     */
185    public function getRecordTags(
186        string $id,
187        string $source = DEFAULT_SEARCH_BACKEND,
188        int $limit = 0,
189        UserListEntityInterface|int|null $listOrId = null,
190        UserEntityInterface|int|null $userOrId = null,
191        string $sort = 'count',
192        UserEntityInterface|int|null $ownerOrId = null,
193        bool $caseSensitive = false
194    ): array {
195        $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId;
196        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
197        $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId;
198        return $this->getDbTable('Tags')
199            ->getForResource($id, $source, $limit, $listId, $userId, $sort, $userToCheck, $caseSensitive)
200            ->toArray();
201    }
202
203    /**
204     * Get all tags from favorite lists associated with the specified record (and matching provided filters).
205     *
206     * @param string                           $id            Record ID to look up
207     * @param string                           $source        Source of record to look up
208     * @param int                              $limit         Max. number of tags to return (0 = no limit)
209     * @param UserListEntityInterface|int|null $listOrId      ID of list to load tags from (null for tags that
210     * are associated with ANY list, but excluding non-list tags)
211     * @param UserEntityInterface|int|null     $userOrId      ID of user to load tags from (null for all users)
212     * @param string                           $sort          Sort type ('count' or 'tag')
213     * @param UserEntityInterface|int|null     $ownerOrId     ID of user to check for ownership
214     * (this will not filter the result list, but rows owned by this user will have an is_me column set to 1)
215     * @param bool                             $caseSensitive Treat tags as case-sensitive?
216     *
217     * @return array
218     */
219    public function getRecordTagsFromFavorites(
220        string $id,
221        string $source = DEFAULT_SEARCH_BACKEND,
222        int $limit = 0,
223        UserListEntityInterface|int|null $listOrId = null,
224        UserEntityInterface|int|null $userOrId = null,
225        string $sort = 'count',
226        UserEntityInterface|int|null $ownerOrId = null,
227        bool $caseSensitive = false
228    ): array {
229        $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId;
230        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
231        $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId;
232        return $this->getDbTable('Tags')
233            ->getForResource($id, $source, $limit, $listId ?? true, $userId, $sort, $userToCheck, $caseSensitive)
234            ->toArray();
235    }
236
237    /**
238     * Get all tags outside of favorite lists associated with the specified record (and matching provided filters).
239     *
240     * @param string                       $id            Record ID to look up
241     * @param string                       $source        Source of record to look up
242     * @param int                          $limit         Max. number of tags to return (0 = no limit)
243     * @param UserEntityInterface|int|null $userOrId      User entity/ID to load tags from (null for all users)
244     * @param string                       $sort          Sort type ('count' or 'tag')
245     * @param UserEntityInterface|int|null $ownerOrId     Entity/ID representing user to check for ownership
246     * (this will not filter the result list, but rows owned by this user will have an is_me column set to 1)
247     * @param bool                         $caseSensitive Treat tags as case-sensitive?
248     *
249     * @return array
250     */
251    public function getRecordTagsNotInFavorites(
252        string $id,
253        string $source = DEFAULT_SEARCH_BACKEND,
254        int $limit = 0,
255        UserEntityInterface|int|null $userOrId = null,
256        string $sort = 'count',
257        UserEntityInterface|int|null $ownerOrId = null,
258        bool $caseSensitive = false
259    ): array {
260        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
261        $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId;
262        return $this->getDbTable('Tags')
263            ->getForResource($id, $source, $limit, false, $userId, $sort, $userToCheck, $caseSensitive)
264            ->toArray();
265    }
266
267    /**
268     * Get a list of duplicate tags (this should never happen, but past bugs and the introduction of case-insensitive
269     * tags have introduced problems).
270     *
271     * @param bool $caseSensitive Treat tags as case-sensitive?
272     *
273     * @return array
274     */
275    public function getDuplicateTags(bool $caseSensitive = false): array
276    {
277        return $this->getDbTable('Tags')->getDuplicates($caseSensitive)->toArray();
278    }
279
280    /**
281     * Get a list of all tags generated by the user in favorites lists. Note that the returned list WILL NOT include
282     * tags attached to records that are not saved in favorites lists. Returns an array of arrays with id and tag keys.
283     *
284     * @param UserEntityInterface|int          $userOrId      User ID to look up.
285     * @param UserListEntityInterface|int|null $listOrId      Filter for tags tied to a specific list (null for no
286     * filter).
287     * @param ?string                          $recordId      Filter for tags tied to a specific resource (null for no
288     * filter).
289     * @param ?string                          $source        Filter for tags tied to a specific record source (null
290     * for no filter).
291     * @param bool                             $caseSensitive Treat tags as case-sensitive?
292     *
293     * @return array
294     */
295    public function getUserTagsFromFavorites(
296        UserEntityInterface|int $userOrId,
297        UserListEntityInterface|int|null $listOrId = null,
298        ?string $recordId = null,
299        ?string $source = null,
300        bool $caseSensitive = false
301    ): array {
302        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
303        $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId;
304        return $this->getDbTable('Tags')->getListTagsForUser($userId, $recordId, $listId, $source, $caseSensitive)
305            ->toArray();
306    }
307
308    /**
309     * Get tags assigned to a user list. Returns an array of arrays with id and tag keys.
310     *
311     * @param UserListEntityInterface|int  $listOrId      List ID or entity
312     * @param UserEntityInterface|int|null $userOrId      User ID or entity to look up (null for no filter).
313     * @param bool                         $caseSensitive Treat tags as case-sensitive?
314     *
315     * @return array[]
316     */
317    public function getListTags(
318        UserListEntityInterface|int $listOrId,
319        UserEntityInterface|int|null $userOrId = null,
320        $caseSensitive = false
321    ): array {
322        $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId;
323        $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId;
324        return $this->getDbTable('Tags')->getForList($listId, $userId, $caseSensitive)->toArray();
325    }
326
327    /**
328     * Delete orphaned tags (those not present in resource_tags) from the tags table.
329     *
330     * @return void
331     */
332    public function deleteOrphanedTags(): void
333    {
334        $callback = function ($select) {
335            $subQuery = $this->getDbTable('ResourceTags')
336                ->getSql()
337                ->select()
338                ->quantifier(Select::QUANTIFIER_DISTINCT)
339                ->columns(['tag_id']);
340            $select->where->notIn('id', $subQuery);
341        };
342        $this->getDbTable('Tags')->delete($callback);
343    }
344
345    /**
346     * Retrieve a tag by ID.
347     *
348     * @param int $id Tag ID
349     *
350     * @return ?TagsEntityInterface
351     */
352    public function getTagById(int $id): ?TagsEntityInterface
353    {
354        return $this->getDbTable('Tags')->select(['id' => $id])->current();
355    }
356
357    /**
358     * Create a new Tag entity.
359     *
360     * @return TagsEntityInterface
361     */
362    public function createEntity(): TagsEntityInterface
363    {
364        return $this->getDbTable('Tags')->createRow();
365    }
366}