Add explicit `return null;` statements
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / tagging / TagEngine.class.php
CommitLineData
04c06e85 1<?php
a9229942 2
04c06e85 3namespace wcf\system\tagging;
a9229942 4
04c06e85
MW
5use wcf\data\object\type\ObjectTypeCache;
6use wcf\data\tag\Tag;
7use wcf\data\tag\TagAction;
1bcacda8 8use wcf\data\tag\TagList;
04c06e85 9use wcf\system\database\util\PreparedStatementConditionBuilder;
79bbb75a 10use wcf\system\exception\InvalidObjectTypeException;
95939e32 11use wcf\system\language\LanguageFactory;
04c06e85
MW
12use wcf\system\SingletonFactory;
13use wcf\system\WCF;
32775cf9 14use wcf\util\ArrayUtil;
04c06e85
MW
15
16/**
17 * Manages the tagging of objects.
a9229942
TD
18 *
19 * @author Marcel Werk
20 * @copyright 2001-2019 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @package WoltLabSuite\Core\System\Tagging
04c06e85 23 */
a9229942
TD
24class TagEngine extends SingletonFactory
25{
26 /**
27 * Adds tags to a tagged object.
28 *
29 * @param string $objectType
30 * @param int $objectID
31 * @param array $tags
32 * @param int $languageID
33 * @param bool $replace
34 */
35 public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true)
36 {
37 $objectTypeID = $this->getObjectTypeID($objectType);
38 $tags = \array_unique(\array_reduce(ArrayUtil::trim(\array_map(static function ($tag) {
39 return \explode(',', $tag);
40 }, $tags)), 'array_merge', []));
41
42 // remove tags prior to apply the new ones (prevents duplicate entries)
43 if ($replace) {
683280d5
TD
44 $sql = "DELETE tag_to_object
45 FROM wcf" . WCF_N . "_tag_to_object tag_to_object
46 INNER JOIN wcf" . WCF_N . "_tag tag
47 ON tag.tagID = tag_to_object.tagID
48 WHERE tag_to_object.objectTypeID = ?
49 AND tag_to_object.objectID = ?
50 AND tag.languageID = ?";
a9229942
TD
51 $statement = WCF::getDB()->prepareStatement($sql);
52 $statement->execute([
53 $objectTypeID,
54 $objectID,
55 $languageID,
56 ]);
57 }
58
59 // get tag ids
60 $tagIDs = [];
61 foreach ($tags as $tag) {
62 if (empty($tag)) {
63 continue;
64 }
65
66 // enforce max length
67 if (\mb_strlen($tag) > TAGGING_MAX_TAG_LENGTH) {
68 $tag = \mb_substr($tag, 0, TAGGING_MAX_TAG_LENGTH);
69 }
70
71 // find existing tag
72 $tagObj = Tag::getTag($tag, $languageID);
73 if ($tagObj === null) {
74 // create new tag
75 $tagAction = new TagAction([], 'create', [
76 'data' => [
77 'name' => $tag,
78 'languageID' => $languageID,
79 ],
80 ]);
81
82 $tagAction->executeAction();
83 $returnValues = $tagAction->getReturnValues();
84 $tagObj = $returnValues['returnValues'];
85 }
86
87 if ($tagObj->synonymFor !== null) {
88 $tagIDs[$tagObj->synonymFor] = $tagObj->synonymFor;
89 } else {
90 $tagIDs[$tagObj->tagID] = $tagObj->tagID;
91 }
92 }
93
94 // save tags
95 $sql = "INSERT INTO wcf" . WCF_N . "_tag_to_object
96 (objectID, tagID, objectTypeID, languageID)
97 VALUES (?, ?, ?, ?)";
98 WCF::getDB()->beginTransaction();
99 $statement = WCF::getDB()->prepareStatement($sql);
100 foreach ($tagIDs as $tagID) {
101 $statement->execute([$objectID, $tagID, $objectTypeID, $languageID]);
102 }
103 WCF::getDB()->commitTransaction();
104 }
105
106 /**
107 * Deletes all tags assigned to given tagged object.
108 *
109 * @param string $objectType
110 * @param int $objectID
111 * @param int $languageID
112 */
113 public function deleteObjectTags($objectType, $objectID, $languageID = null)
114 {
115 $objectTypeID = $this->getObjectTypeID($objectType);
116
683280d5
TD
117 $sql = "DELETE tag_to_object
118 FROM wcf" . WCF_N . "_tag_to_object tag_to_object
119 INNER JOIN wcf" . WCF_N . "_tag tag
120 ON tag.tagID = tag_to_object.tagID
121 WHERE tag_to_object.objectTypeID = ?
122 AND tag_to_object.objectID = ?
123 " . ($languageID !== null ? "AND tag.languageID = ?" : "");
a9229942
TD
124 $statement = WCF::getDB()->prepareStatement($sql);
125 $parameters = [
126 $objectTypeID,
127 $objectID,
128 ];
129 if ($languageID !== null) {
130 $parameters[] = $languageID;
131 }
132 $statement->execute($parameters);
133 }
134
135 /**
136 * Deletes all tags assigned to given tagged objects.
137 *
138 * @param string $objectType
139 * @param int[] $objectIDs
140 */
141 public function deleteObjects($objectType, array $objectIDs)
142 {
143 $objectTypeID = $this->getObjectTypeID($objectType);
144
145 $conditionsBuilder = new PreparedStatementConditionBuilder();
146 $conditionsBuilder->add('objectTypeID = ?', [$objectTypeID]);
147 $conditionsBuilder->add('objectID IN (?)', [$objectIDs]);
148
149 $sql = "DELETE FROM wcf" . WCF_N . "_tag_to_object
150 " . $conditionsBuilder;
151 $statement = WCF::getDB()->prepareStatement($sql);
152 $statement->execute($conditionsBuilder->getParameters());
153 }
154
155 /**
156 * Returns all tags set for given object.
157 *
158 * @param string $objectType
159 * @param int $objectID
160 * @param int[] $languageIDs
161 * @return Tag[]
162 */
163 public function getObjectTags($objectType, $objectID, array $languageIDs = [])
164 {
165 $tags = $this->getObjectsTags($objectType, [$objectID], $languageIDs);
166
167 return $tags[$objectID] ?? [];
168 }
169
170 /**
171 * Returns all tags set for given objects.
172 *
173 * @param string $objectType
174 * @param int[] $objectIDs
175 * @param int[] $languageIDs
176 * @return array
177 */
178 public function getObjectsTags($objectType, array $objectIDs, array $languageIDs = [])
179 {
180 $objectTypeID = $this->getObjectTypeID($objectType);
181
182 $conditions = new PreparedStatementConditionBuilder();
183 $conditions->add("tag_to_object.objectTypeID = ?", [$objectTypeID]);
184 $conditions->add("tag_to_object.objectID IN (?)", [$objectIDs]);
185 if (!empty($languageIDs)) {
186 foreach ($languageIDs as $index => $languageID) {
187 if (!$languageID) {
188 unset($languageIDs[$index]);
189 }
190 }
191
192 // The `languageID` is part of the index, skipping it will cause MySQL to skip the
193 // `objectID` column, causing a partial table scan.
194 if (empty($languageIDs)) {
195 // The `languageID` column is never null, tags are always assigned to a language
196 // thus we cannot use the content language ids here.
197 foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
198 $languageIDs[] = $language->languageID;
199 }
200 }
201
e7a6cb1a 202 $conditions->add("tag.languageID IN (?)", [$languageIDs]);
a9229942
TD
203 }
204
205 $sql = "SELECT tag.*, tag_to_object.objectID
206 FROM wcf" . WCF_N . "_tag_to_object tag_to_object
207 LEFT JOIN wcf" . WCF_N . "_tag tag
c240c98a 208 ON tag.tagID = tag_to_object.tagID
a9229942
TD
209 " . $conditions;
210 $statement = WCF::getDB()->prepareStatement($sql);
211 $statement->execute($conditions->getParameters());
212
213 $tags = [];
214 while ($tag = $statement->fetchObject(Tag::class)) {
215 /** @noinspection PhpUndefinedFieldInspection */
216 $objectID = $tag->objectID;
217 if (!isset($tags[$objectID])) {
218 $tags[$objectID] = [];
219 }
220 $tags[$objectID][$tag->tagID] = $tag;
221 }
222
223 return $tags;
224 }
225
226 /**
227 * Returns id of the object type with the given name.
228 *
229 * @param string $objectType
230 * @return int
231 * @throws InvalidObjectTypeException
232 */
233 public function getObjectTypeID($objectType)
234 {
235 // get object type
236 $objectTypeObj = ObjectTypeCache::getInstance()
237 ->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType);
238 if ($objectTypeObj === null) {
239 throw new InvalidObjectTypeException($objectType, 'com.woltlab.wcf.tagging.taggableObject');
240 }
241
242 return $objectTypeObj->objectTypeID;
243 }
244
245 /**
246 * Returns the implicit language id based on the language id of existing tags.
247 *
248 * NULL indicates that there are no tags, otherwise the language id with the most
249 * associated tags for that object is returned, but can still be arbitrary if
250 * there are two or more top language ids with the same amount of tags.
251 *
252 * @param string $objectType
253 * @param int $objectID
254 * @return int|null
255 */
256 public function getImplicitLanguageID($objectType, $objectID)
257 {
258 $existingTags = $this->getObjectTags($objectType, $objectID);
259 if (empty($existingTags)) {
c0b28aa2 260 return null;
a9229942
TD
261 }
262
263 $languageIDs = [];
264 foreach ($existingTags as $tag) {
265 if (!isset($languageIDs[$tag->languageID])) {
266 $languageIDs[$tag->languageID] = 0;
267 }
268 $languageIDs[$tag->languageID]++;
269 }
270
271 \arsort($languageIDs, \SORT_NUMERIC);
272
273 return \key($languageIDs);
274 }
275
276 /**
277 * @param Tag[] $tags
278 * @return int[]
279 */
280 public function getTagIDs($tags)
281 {
282 return \array_map(static function ($tag) {
283 return $tag->tagID;
284 }, $tags);
285 }
286
287 /**
288 * Generates the inner SQL statement to fetch object ids that have all listed
289 * tags assigned to them.
290 *
291 * @param string $objectType
292 * @param Tag[] $tags
293 * @return array
294 * @since 5.2
295 */
296 public function getSubselectForObjectsByTags($objectType, array $tags)
297 {
298 $parameters = [$this->getObjectTypeID($objectType)];
299 $tagIDs = \implode(',', \array_map(static function (Tag $tag) use (&$parameters) {
300 $parameters[] = $tag->tagID;
301
302 return '?';
303 }, $tags));
304 $parameters[] = \count($tags);
305
306 $sql = "SELECT objectID
307 FROM wcf" . WCF_N . "_tag_to_object
308 WHERE objectTypeID = ?
309 AND tagID IN (" . $tagIDs . ")
310 GROUP BY objectID
311 HAVING COUNT(objectID) = ?";
312
313 return [
314 'sql' => $sql,
315 'parameters' => $parameters,
316 ];
317 }
318
319 /**
320 * Returns the matching tags by name.
321 *
322 * @param string[] $names
323 * @param int $languageID
324 * @return Tag[]
325 * @since 5.2
326 */
327 public function getTagsByName(array $names, $languageID)
328 {
329 $tagList = new TagList();
330 $tagList->getConditionBuilder()->add('name IN (?)', [$names]);
331 $tagList->getConditionBuilder()->add('languageID = ?', [$languageID ?: WCF::getLanguage()->languageID]);
332 $tagList->readObjects();
333
334 return $tagList->getObjects();
335 }
04c06e85 336}