2 declare(strict_types
=1);
3 namespace wcf\system\like
;
4 use wcf\data\like\
object\ILikeObject
;
5 use wcf\data\like\
object\LikeObject
;
6 use wcf\data\like\
object\LikeObjectEditor
;
7 use wcf\data\like\
object\LikeObjectList
;
8 use wcf\data\like\Like
;
9 use wcf\data\like\LikeEditor
;
10 use wcf\data\like\LikeList
;
11 use wcf\data\
object\type\ObjectType
;
12 use wcf\data\
object\type\ObjectTypeCache
;
13 use wcf\data\user\User
;
14 use wcf\data\user\UserEditor
;
15 use wcf\system\database\util\PreparedStatementConditionBuilder
;
16 use wcf\system\database\DatabaseException
;
17 use wcf\system\user\activity\event\UserActivityEventHandler
;
18 use wcf\system\user\activity\point\UserActivityPointHandler
;
19 use wcf\system\user\notification\UserNotificationHandler
;
20 use wcf\system\SingletonFactory
;
24 * Handles the likes of liked objects.
26 * Usage (retrieve all likes for a list of objects):
28 * $objectType = LikeHandler::getInstance()->getObjectType('com.woltlab.wcf.foo.bar');
30 * LikeHandler::getInstance()->loadLikeObjects($objectType, $objectIDs);
32 * $likeObjects = LikeHandler::getInstance()->getLikeObjects($objectType);
35 * @copyright 2001-2018 WoltLab GmbH
36 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
37 * @package WoltLabSuite\Core\System\Like
39 class LikeHandler
extends SingletonFactory
{
44 protected $likeObjectCache = [];
50 protected $cache = null;
53 * Creates a new LikeHandler instance.
55 protected function init() {
57 $this->cache
= ObjectTypeCache
::getInstance()->getObjectTypes('com.woltlab.wcf.like.likeableObject');
61 * Returns an object type from cache.
63 * @param string $objectName
66 public function getObjectType($objectName) {
67 if (isset($this->cache
[$objectName])) {
68 return $this->cache
[$objectName];
75 * Returns a like object.
77 * @param ObjectType $objectType
78 * @param integer $objectID
79 * @return LikeObject|null
81 public function getLikeObject(ObjectType
$objectType, $objectID) {
82 if (isset($this->likeObjectCache
[$objectType->objectTypeID
][$objectID])) {
83 return $this->likeObjectCache
[$objectType->objectTypeID
][$objectID];
90 * Returns the like objects of a specific object type.
92 * @param ObjectType $objectType
93 * @return LikeObject[]
95 public function getLikeObjects(ObjectType
$objectType) {
96 if (isset($this->likeObjectCache
[$objectType->objectTypeID
])) {
97 return $this->likeObjectCache
[$objectType->objectTypeID
];
104 * Loads the like data for a set of objects and returns the number of loaded
107 * @param ObjectType $objectType
108 * @param array $objectIDs
111 public function loadLikeObjects(ObjectType
$objectType, array $objectIDs) {
112 if (empty($objectIDs)) {
118 $conditions = new PreparedStatementConditionBuilder();
119 $conditions->add("like_object.objectTypeID = ?", [$objectType->objectTypeID
]);
120 $conditions->add("like_object.objectID IN (?)", [$objectIDs]);
121 $parameters = $conditions->getParameters();
123 if (WCF
::getUser()->userID
) {
124 $sql = "SELECT like_object.*,
125 CASE WHEN like_table.userID IS NOT NULL THEN like_table.likeValue ELSE 0 END AS liked
126 FROM wcf".WCF_N
."_like_object like_object
127 LEFT JOIN wcf".WCF_N
."_like like_table
128 ON (like_table.objectTypeID = like_object.objectTypeID
129 AND like_table.objectID = like_object.objectID
130 AND like_table.userID = ?)
133 array_unshift($parameters, WCF
::getUser()->userID
);
136 $sql = "SELECT like_object.*, 0 AS liked
137 FROM wcf".WCF_N
."_like_object like_object
141 $statement = WCF
::getDB()->prepareStatement($sql);
142 $statement->execute($parameters);
143 while ($row = $statement->fetchArray()) {
144 $this->likeObjectCache
[$objectType->objectTypeID
][$row['objectID']] = new LikeObject(null, $row);
152 * Saves the like of an object.
154 * @param ILikeObject $likeable
156 * @param integer $likeValue
157 * @param integer $time
160 public function like(ILikeObject
$likeable, User
$user, $likeValue, $time = TIME_NOW
) {
161 // verify if object is already liked by user
162 $like = Like
::getLike($likeable->getObjectType()->objectTypeID
, $likeable->getObjectID(), $user->userID
);
165 $likeObject = LikeObject
::getLikeObject($likeable->getObjectType()->objectTypeID
, $likeable->getObjectID());
167 // if vote is identically just revert the vote
168 if ($like->likeID
&& ($like->likeValue
== $likeValue)) {
169 return $this->revertLike($like, $likeable, $likeObject, $user);
173 /** @noinspection PhpUnusedLocalVariableInspection */
174 $cumulativeLikes = 0;
175 /** @noinspection PhpUnusedLocalVariableInspection */
176 $newValue = $oldValue = null;
180 WCF
::getDB()->beginTransaction();
182 // update existing object
183 if ($likeObject->likeObjectID
) {
184 $likes = $likeObject->likes
;
185 $dislikes = $likeObject->dislikes
;
186 $cumulativeLikes = $likeObject->cumulativeLikes
;
188 // previous (dis-)like already exists
190 $oldValue = $like->likeValue
;
192 // revert like and replace it with a dislike
193 if ($like->likeValue
== Like
::LIKE
) {
196 $cumulativeLikes -= 2;
197 $newValue = Like
::DISLIKE
;
200 // revert dislike and replace it with a like
203 $cumulativeLikes +
= 2;
204 $newValue = Like
::LIKE
;
208 if ($likeValue == Like
::LIKE
) {
211 $newValue = Like
::LIKE
;
216 $newValue = Like
::DISLIKE
;
223 'dislikes' => $dislikes,
224 'cumulativeLikes' => $cumulativeLikes
227 if ($likeValue == 1) {
228 $users = unserialize($likeObject->cachedUsers
);
229 if (count($users) < 3) {
230 $users[$user->userID
] = ['userID' => $user->userID
, 'username' => $user->username
];
231 $updateData['cachedUsers'] = serialize($users);
234 else if ($likeValue == -1) {
235 $users = unserialize($likeObject->cachedUsers
);
236 if (isset($users[$user->userID
])) {
237 unset($users[$user->userID
]);
238 $updateData['cachedUsers'] = serialize($users);
243 $likeObjectEditor = new LikeObjectEditor($likeObject);
244 $likeObjectEditor->update($updateData);
247 $cumulativeLikes = $likeValue;
248 $newValue = $likeValue;
250 if ($likeValue == 1) $users = [$user->userID
=> ['userID' => $user->userID
, 'username' => $user->username
]];
253 $likeObject = LikeObjectEditor
::create([
254 'objectTypeID' => $likeable->getObjectType()->objectTypeID
,
255 'objectID' => $likeable->getObjectID(),
256 'objectUserID' => $likeable->getUserID() ?
: null,
257 'likes' => ($likeValue == Like
::LIKE
) ?
1 : 0,
258 'dislikes' => ($likeValue == Like
::DISLIKE
) ?
1 : 0,
259 'cumulativeLikes' => $cumulativeLikes,
260 'cachedUsers' => serialize($users)
264 // update owner's like counter
265 if ($likeable->getUserID()) {
267 $userEditor = new UserEditor(new User($likeable->getUserID()));
268 $userEditor->updateCounters([
269 'likesReceived' => $like->likeValue
== Like
::LIKE ?
-1 : 1
272 else if ($likeValue == Like
::LIKE
) {
273 $userEditor = new UserEditor(new User($likeable->getUserID()));
274 $userEditor->updateCounters([
280 if (!$like->likeID
) {
282 $like = LikeEditor
::create([
283 'objectID' => $likeable->getObjectID(),
284 'objectTypeID' => $likeable->getObjectType()->objectTypeID
,
285 'objectUserID' => $likeable->getUserID() ?
: null,
286 'userID' => $user->userID
,
288 'likeValue' => $likeValue
291 if ($likeValue == Like
::LIKE
&& $likeable->getUserID()) {
292 UserActivityPointHandler
::getInstance()->fireEvent('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $like->likeID
, $likeable->getUserID());
293 $likeable->sendNotification($like);
297 $likeEditor = new LikeEditor($like);
298 $likeEditor->update([
300 'likeValue' => $likeValue
303 if ($likeable->getUserID()) {
304 if ($likeValue == Like
::DISLIKE
) {
305 UserActivityPointHandler
::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', [$likeable->getUserID() => 1]);
308 UserActivityPointHandler
::getInstance()->fireEvent('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $like->likeID
, $likeable->getUserID());
309 $likeable->sendNotification($like);
314 // update object's like counter
315 $likeable->updateLikeCounter($cumulativeLikes);
317 WCF
::getDB()->commitTransaction();
319 catch (DatabaseException
$e) {
320 WCF
::getDB()->rollBackTransaction();
324 'data' => $this->loadLikeStatus($likeObject, $user),
326 'newValue' => $newValue,
327 'oldValue' => $oldValue,
333 * Reverts the like of an object.
336 * @param ILikeObject $likeable
337 * @param LikeObject $likeObject
341 public function revertLike(Like
$like, ILikeObject
$likeable, LikeObject
$likeObject, User
$user) {
345 WCF
::getDB()->beginTransaction();
348 $editor = new LikeEditor($like);
351 // update like object cache
352 $likes = $likeObject->likes
;
353 $dislikes = $likeObject->dislikes
;
354 $cumulativeLikes = $likeObject->cumulativeLikes
;
356 if ($like->likeValue
== Like
::LIKE
) {
368 'dislikes' => $dislikes,
369 'cumulativeLikes' => $cumulativeLikes
372 $users = $likeObject->getUsers();
373 foreach ($users as $user2) {
374 $usersArray[$user2->userID
] = ['userID' => $user2->userID
, 'username' => $user2->username
];
377 if (isset($usersArray[$user->userID
])) {
378 unset($usersArray[$user->userID
]);
379 $updateData['cachedUsers'] = serialize($usersArray);
382 $likeObjectEditor = new LikeObjectEditor($likeObject);
383 if (!$updateData['likes'] && !$updateData['dislikes']) {
384 // remove object instead
385 $likeObjectEditor->delete();
389 $likeObjectEditor->update($updateData);
392 // update owner's like counter and activity points
393 if ($likeable->getUserID()) {
394 if ($like->likeValue
== Like
::LIKE
) {
395 $userEditor = new UserEditor(new User($likeable->getUserID()));
396 $userEditor->updateCounters([
397 'likesReceived' => -1
400 UserActivityPointHandler
::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', [$likeable->getUserID() => 1]);
404 // update object's like counter
405 $likeable->updateLikeCounter($cumulativeLikes);
407 WCF
::getDB()->commitTransaction();
409 catch (DatabaseException
$e) {
410 WCF
::getDB()->rollBackTransaction();
414 'data' => $this->loadLikeStatus($likeObject, $user),
416 'oldValue' => $like->likeValue
,
417 'users' => $usersArray
422 * Removes all likes for given objects.
424 * @param string $objectType
425 * @param integer[] $objectIDs
426 * @param string[] $notificationObjectTypes
428 public function removeLikes($objectType, array $objectIDs, array $notificationObjectTypes = []) {
429 $objectTypeObj = $this->getObjectType($objectType);
432 $likeObjectList = new LikeObjectList();
433 $likeObjectList->getConditionBuilder()->add('like_object.objectTypeID = ?', [$objectTypeObj->objectTypeID
]);
434 $likeObjectList->getConditionBuilder()->add('like_object.objectID IN (?)', [$objectIDs]);
435 $likeObjectList->readObjects();
436 $likeObjects = $likeObjectList->getObjects();
437 $likeObjectIDs = $likeObjectList->getObjectIDs();
439 // reduce count of received users
441 foreach ($likeObjects as $likeObject) {
442 if ($likeObject->likes
) {
443 if (!isset($users[$likeObject->objectUserID
])) $users[$likeObject->objectUserID
] = 0;
444 $users[$likeObject->objectUserID
] +
= $likeObject->likes
;
447 foreach ($users as $userID => $receivedLikes) {
448 $userEditor = new UserEditor(new User(null, ['userID' => $userID]));
449 $userEditor->updateCounters([
450 'likesReceived' => $receivedLikes * -1
455 $likeList = new LikeList();
456 $likeList->getConditionBuilder()->add('like_table.objectTypeID = ?', [$objectTypeObj->objectTypeID
]);
457 $likeList->getConditionBuilder()->add('like_table.objectID IN (?)', [$objectIDs]);
458 $likeList->readObjects();
460 if (count($likeList)) {
462 foreach ($likeList as $like) {
463 $likeData[$like->likeID
] = $like->userID
;
466 // delete like notifications
467 if (!empty($notificationObjectTypes)) {
468 foreach ($notificationObjectTypes as $notificationObjectType) {
469 UserNotificationHandler
::getInstance()->removeNotifications($notificationObjectType, $likeList->getObjectIDs());
472 else if (UserNotificationHandler
::getInstance()->getObjectTypeID($objectType.'.notification')) {
473 UserNotificationHandler
::getInstance()->removeNotifications($objectType.'.notification', $likeList->getObjectIDs());
476 // revoke activity points
477 UserActivityPointHandler
::getInstance()->removeEvents('com.woltlab.wcf.like.activityPointEvent.receivedLikes', $likeData);
480 LikeEditor
::deleteAll(array_keys($likeData));
483 // delete like objects
484 if (!empty($likeObjectIDs)) {
485 LikeObjectEditor
::deleteAll($likeObjectIDs);
488 // delete activity events
489 if (UserActivityEventHandler
::getInstance()->getObjectTypeID($objectTypeObj->objectType
.'.recentActivityEvent')) {
490 UserActivityEventHandler
::getInstance()->removeEvents($objectTypeObj->objectType
.'.recentActivityEvent', $objectIDs);
495 * Returns current like object status.
497 * @param LikeObject $likeObject
501 protected function loadLikeStatus(LikeObject
$likeObject, User
$user) {
502 $sql = "SELECT like_object.likes, like_object.dislikes, like_object.cumulativeLikes,
503 CASE WHEN like_table.likeValue IS NOT NULL THEN like_table.likeValue ELSE 0 END AS liked
504 FROM wcf".WCF_N
."_like_object like_object
505 LEFT JOIN wcf".WCF_N
."_like like_table
506 ON (like_table.objectTypeID = ?
507 AND like_table.objectID = like_object.objectID
508 AND like_table.userID = ?)
509 WHERE like_object.likeObjectID = ?";
510 $statement = WCF
::getDB()->prepareStatement($sql);
511 $statement->execute([
512 $likeObject->objectTypeID
,
514 $likeObject->likeObjectID
517 return $statement->fetchArray();