3 namespace wcf\system\tagging
;
5 use wcf\data\
object\type\ObjectTypeCache
;
7 use wcf\data\tag\TagAction
;
8 use wcf\data\tag\TagList
;
9 use wcf\system\database\util\PreparedStatementConditionBuilder
;
10 use wcf\system\exception\InvalidObjectTypeException
;
11 use wcf\system\language\LanguageFactory
;
12 use wcf\system\SingletonFactory
;
14 use wcf\util\ArrayUtil
;
17 * Manages the tagging of objects.
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
24 class TagEngine
extends SingletonFactory
27 * Adds tags to a tagged object.
29 * @param string $objectType
30 * @param int $objectID
32 * @param int $languageID
33 * @param bool $replace
35 public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true)
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', []));
42 // remove tags prior to apply the new ones (prevents duplicate entries)
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 = ?";
51 $statement = WCF
::getDB()->prepareStatement($sql);
61 foreach ($tags as $tag) {
67 if (\
mb_strlen($tag) > TAGGING_MAX_TAG_LENGTH
) {
68 $tag = \
mb_substr($tag, 0, TAGGING_MAX_TAG_LENGTH
);
72 $tagObj = Tag
::getTag($tag, $languageID);
73 if ($tagObj === null) {
75 $tagAction = new TagAction([], 'create', [
78 'languageID' => $languageID,
82 $tagAction->executeAction();
83 $returnValues = $tagAction->getReturnValues();
84 $tagObj = $returnValues['returnValues'];
87 if ($tagObj->synonymFor
!== null) {
88 $tagIDs[$tagObj->synonymFor
] = $tagObj->synonymFor
;
90 $tagIDs[$tagObj->tagID
] = $tagObj->tagID
;
95 $sql = "INSERT INTO wcf" . WCF_N
. "_tag_to_object
96 (objectID, tagID, objectTypeID, languageID)
98 WCF
::getDB()->beginTransaction();
99 $statement = WCF
::getDB()->prepareStatement($sql);
100 foreach ($tagIDs as $tagID) {
101 $statement->execute([$objectID, $tagID, $objectTypeID, $languageID]);
103 WCF
::getDB()->commitTransaction();
107 * Deletes all tags assigned to given tagged object.
109 * @param string $objectType
110 * @param int $objectID
111 * @param int $languageID
113 public function deleteObjectTags($objectType, $objectID, $languageID = null)
115 $objectTypeID = $this->getObjectTypeID($objectType);
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 = ?" : "");
124 $statement = WCF
::getDB()->prepareStatement($sql);
129 if ($languageID !== null) {
130 $parameters[] = $languageID;
132 $statement->execute($parameters);
136 * Deletes all tags assigned to given tagged objects.
138 * @param string $objectType
139 * @param int[] $objectIDs
141 public function deleteObjects($objectType, array $objectIDs)
143 $objectTypeID = $this->getObjectTypeID($objectType);
145 $conditionsBuilder = new PreparedStatementConditionBuilder();
146 $conditionsBuilder->add('objectTypeID = ?', [$objectTypeID]);
147 $conditionsBuilder->add('objectID IN (?)', [$objectIDs]);
149 $sql = "DELETE FROM wcf" . WCF_N
. "_tag_to_object
150 " . $conditionsBuilder;
151 $statement = WCF
::getDB()->prepareStatement($sql);
152 $statement->execute($conditionsBuilder->getParameters());
156 * Returns all tags set for given object.
158 * @param string $objectType
159 * @param int $objectID
160 * @param int[] $languageIDs
163 public function getObjectTags($objectType, $objectID, array $languageIDs = [])
165 $tags = $this->getObjectsTags($objectType, [$objectID], $languageIDs);
167 return $tags[$objectID] ??
[];
171 * Returns all tags set for given objects.
173 * @param string $objectType
174 * @param int[] $objectIDs
175 * @param int[] $languageIDs
178 public function getObjectsTags($objectType, array $objectIDs, array $languageIDs = [])
180 $objectTypeID = $this->getObjectTypeID($objectType);
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) {
188 unset($languageIDs[$index]);
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
;
202 $conditions->add("tag.languageID IN (?)", [$languageIDs]);
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
208 ON tag.tagID = tag_to_object.tagID
210 $statement = WCF
::getDB()->prepareStatement($sql);
211 $statement->execute($conditions->getParameters());
214 while ($tag = $statement->fetchObject(Tag
::class)) {
215 /** @noinspection PhpUndefinedFieldInspection */
216 $objectID = $tag->objectID
;
217 if (!isset($tags[$objectID])) {
218 $tags[$objectID] = [];
220 $tags[$objectID][$tag->tagID
] = $tag;
227 * Returns id of the object type with the given name.
229 * @param string $objectType
231 * @throws InvalidObjectTypeException
233 public function getObjectTypeID($objectType)
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');
242 return $objectTypeObj->objectTypeID
;
246 * Returns the implicit language id based on the language id of existing tags.
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.
252 * @param string $objectType
253 * @param int $objectID
256 public function getImplicitLanguageID($objectType, $objectID)
258 $existingTags = $this->getObjectTags($objectType, $objectID);
259 if (empty($existingTags)) {
264 foreach ($existingTags as $tag) {
265 if (!isset($languageIDs[$tag->languageID
])) {
266 $languageIDs[$tag->languageID
] = 0;
268 $languageIDs[$tag->languageID
]++
;
271 \arsort
($languageIDs, \SORT_NUMERIC
);
273 return \
key($languageIDs);
280 public function getTagIDs($tags)
282 return \array_map
(static function ($tag) {
288 * Generates the inner SQL statement to fetch object ids that have all listed
289 * tags assigned to them.
291 * @param string $objectType
296 public function getSubselectForObjectsByTags($objectType, array $tags)
298 $parameters = [$this->getObjectTypeID($objectType)];
299 $tagIDs = \
implode(',', \array_map
(static function (Tag
$tag) use (&$parameters) {
300 $parameters[] = $tag->tagID
;
304 $parameters[] = \
count($tags);
306 $sql = "SELECT objectID
307 FROM wcf" . WCF_N
. "_tag_to_object
308 WHERE objectTypeID = ?
309 AND tagID IN (" . $tagIDs . ")
311 HAVING COUNT(objectID) = ?";
315 'parameters' => $parameters,
320 * Returns the matching tags by name.
322 * @param string[] $names
323 * @param int $languageID
327 public function getTagsByName(array $names, $languageID)
329 $tagList = new TagList();
330 $tagList->getConditionBuilder()->add('name IN (?)', [$names]);
331 $tagList->getConditionBuilder()->add('languageID = ?', [$languageID ?
: WCF
::getLanguage()->languageID
]);
332 $tagList->readObjects();
334 return $tagList->getObjects();