2 namespace wcf\system\tagging
;
3 use wcf\data\
object\type\ObjectTypeCache
;
5 use wcf\data\tag\TagAction
;
6 use wcf\data\tag\TagList
;
7 use wcf\system\database\util\PreparedStatementConditionBuilder
;
8 use wcf\system\exception\InvalidObjectTypeException
;
9 use wcf\system\language\LanguageFactory
;
10 use wcf\system\SingletonFactory
;
12 use wcf\util\ArrayUtil
;
15 * Manages the tagging of objects.
18 * @copyright 2001-2019 WoltLab GmbH
19 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
20 * @package WoltLabSuite\Core\System\Tagging
22 class TagEngine
extends SingletonFactory
{
24 * Adds tags to a tagged object.
26 * @param string $objectType
27 * @param integer $objectID
29 * @param integer $languageID
30 * @param boolean $replace
32 public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true) {
33 $objectTypeID = $this->getObjectTypeID($objectType);
34 $tags = array_unique(array_reduce(ArrayUtil
::trim(array_map(function($tag) {
35 return explode(',', $tag);
36 }, $tags)), 'array_merge', []));
38 // remove tags prior to apply the new ones (prevents duplicate entries)
40 $sql = "DELETE FROM wcf".WCF_N
."_tag_to_object
41 WHERE objectTypeID = ?
44 $statement = WCF
::getDB()->prepareStatement($sql);
54 foreach ($tags as $tag) {
55 if (empty($tag)) continue;
58 if (mb_strlen($tag) > TAGGING_MAX_TAG_LENGTH
) {
59 $tag = mb_substr($tag, 0, TAGGING_MAX_TAG_LENGTH
);
63 $tagObj = Tag
::getTag($tag, $languageID);
64 if ($tagObj === null) {
66 $tagAction = new TagAction([], 'create', ['data' => [
68 'languageID' => $languageID
71 $tagAction->executeAction();
72 $returnValues = $tagAction->getReturnValues();
73 $tagObj = $returnValues['returnValues'];
76 if ($tagObj->synonymFor
!== null) $tagIDs[$tagObj->synonymFor
] = $tagObj->synonymFor
;
77 else $tagIDs[$tagObj->tagID
] = $tagObj->tagID
;
81 $sql = "INSERT INTO wcf".WCF_N
."_tag_to_object
82 (objectID, tagID, objectTypeID, languageID)
84 WCF
::getDB()->beginTransaction();
85 $statement = WCF
::getDB()->prepareStatement($sql);
86 foreach ($tagIDs as $tagID) {
87 $statement->execute([$objectID, $tagID, $objectTypeID, $languageID]);
89 WCF
::getDB()->commitTransaction();
93 * Deletes all tags assigned to given tagged object.
95 * @param string $objectType
96 * @param integer $objectID
97 * @param integer $languageID
99 public function deleteObjectTags($objectType, $objectID, $languageID = null) {
100 $objectTypeID = $this->getObjectTypeID($objectType);
102 $sql = "DELETE FROM wcf".WCF_N
."_tag_to_object
103 WHERE objectTypeID = ?
105 ".($languageID !== null ?
"AND languageID = ?" : "");
106 $statement = WCF
::getDB()->prepareStatement($sql);
111 if ($languageID !== null) $parameters[] = $languageID;
112 $statement->execute($parameters);
116 * Deletes all tags assigned to given tagged objects.
118 * @param string $objectType
119 * @param integer[] $objectIDs
121 public function deleteObjects($objectType, array $objectIDs) {
122 $objectTypeID = $this->getObjectTypeID($objectType);
124 $conditionsBuilder = new PreparedStatementConditionBuilder();
125 $conditionsBuilder->add('objectTypeID = ?', [$objectTypeID]);
126 $conditionsBuilder->add('objectID IN (?)', [$objectIDs]);
128 $sql = "DELETE FROM wcf".WCF_N
."_tag_to_object
129 ".$conditionsBuilder;
130 $statement = WCF
::getDB()->prepareStatement($sql);
131 $statement->execute($conditionsBuilder->getParameters());
135 * Returns all tags set for given object.
137 * @param string $objectType
138 * @param integer $objectID
139 * @param integer[] $languageIDs
142 public function getObjectTags($objectType, $objectID, array $languageIDs = []) {
143 $tags = $this->getObjectsTags($objectType, [$objectID], $languageIDs);
145 return isset($tags[$objectID]) ?
$tags[$objectID] : [];
149 * Returns all tags set for given objects.
151 * @param string $objectType
152 * @param integer[] $objectIDs
153 * @param integer[] $languageIDs
156 public function getObjectsTags($objectType, array $objectIDs, array $languageIDs = []) {
157 $objectTypeID = $this->getObjectTypeID($objectType);
159 $conditions = new PreparedStatementConditionBuilder();
160 $conditions->add("tag_to_object.objectTypeID = ?", [$objectTypeID]);
161 $conditions->add("tag_to_object.objectID IN (?)", [$objectIDs]);
162 if (!empty($languageIDs)) {
163 foreach ($languageIDs as $index => $languageID) {
164 if (!$languageID) unset($languageIDs[$index]);
167 // The `languageID` is part of the index, skipping it will cause MySQL to skip the
168 // `objectID` column, causing a partial table scan.
169 if (empty($languageIDs)) {
170 // The `languageID` column is never null, tags are always assigned to a language
171 // thus we cannot use the content language ids here.
172 foreach (LanguageFactory
::getInstance()->getLanguages() as $language) {
173 $languageIDs[] = $language->languageID
;
177 $conditions->add("tag_to_object.languageID IN (?)", [$languageIDs]);
180 $sql = "SELECT tag.*, tag_to_object.objectID
181 FROM wcf".WCF_N
."_tag_to_object tag_to_object
182 LEFT JOIN wcf".WCF_N
."_tag tag
183 ON (tag.tagID = tag_to_object.tagID)
185 $statement = WCF
::getDB()->prepareStatement($sql);
186 $statement->execute($conditions->getParameters());
189 while ($tag = $statement->fetchObject(Tag
::class)) {
190 /** @noinspection PhpUndefinedFieldInspection */
191 $objectID = $tag->objectID
;
192 if (!isset($tags[$objectID])) {
193 $tags[$objectID] = [];
195 $tags[$objectID][$tag->tagID
] = $tag;
202 * Returns id of the object type with the given name.
204 * @param string $objectType
206 * @throws InvalidObjectTypeException
208 public function getObjectTypeID($objectType) {
210 $objectTypeObj = ObjectTypeCache
::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType);
211 if ($objectTypeObj === null) {
212 throw new InvalidObjectTypeException($objectType, 'com.woltlab.wcf.tagging.taggableObject');
215 return $objectTypeObj->objectTypeID
;
219 * Returns the implicit language id based on the language id of existing tags.
221 * NULL indicates that there are no tags, otherwise the language id with the most
222 * associated tags for that object is returned, but can still be arbitrary if
223 * there are two or more top language ids with the same amount of tags.
225 * @param string $objectType
226 * @param integer $objectID
227 * @return integer|null
229 public function getImplicitLanguageID($objectType, $objectID) {
230 $existingTags = $this->getObjectTags($objectType, $objectID);
231 if (empty($existingTags)) {
236 foreach ($existingTags as $tag) {
237 if (!isset($languageIDs[$tag->languageID
])) $languageIDs[$tag->languageID
] = 0;
238 $languageIDs[$tag->languageID
]++
;
241 arsort($languageIDs, SORT_NUMERIC
);
243 return key($languageIDs);
250 public function getTagIDs($tags) {
251 return array_map(function($tag) {
257 * Generates the inner SQL statement to fetch object ids that have all listed
258 * tags assigned to them.
260 * @param string $objectType
265 public function getSubselectForObjectsByTags($objectType, array $tags) {
266 $parameters = [$this->getObjectTypeID($objectType)];
267 $tagIDs = implode(',', array_map(function(Tag
$tag) use (&$parameters) {
268 $parameters[] = $tag->tagID
;
272 $parameters[] = count($tags);
274 $sql = "SELECT objectID
275 FROM wcf".WCF_N
."_tag_to_object
276 WHERE objectTypeID = ?
277 AND tagID IN (".$tagIDs.")
279 HAVING COUNT(objectID) = ?";
283 'parameters' => $parameters,
288 * Returns the matching tags by name.
290 * @param string[] $names
291 * @param int $languageID
295 public function getTagsByName(array $names, $languageID) {
296 $tagList = new TagList();
297 $tagList->getConditionBuilder()->add('name IN (?)', [$names]);
298 $tagList->getConditionBuilder()->add('languageID = ?', [$languageID ?
: WCF
::getLanguage()->languageID
]);
299 $tagList->readObjects();
301 return $tagList->getObjects();