misuzu/src/Comments/CommentsPost.php

351 lines
14 KiB
PHP

<?php
namespace Misuzu\Comments;
use JsonSerializable;
use Misuzu\DB;
use Misuzu\Pagination;
use Misuzu\Users\User;
use Misuzu\Users\UserNotFoundException;
class CommentsPostException extends CommentsException {}
class CommentsPostNotFoundException extends CommentsPostException {}
class CommentsPostHasNoParentException extends CommentsPostException {}
class CommentsPostSaveFailedException extends CommentsPostException {}
class CommentsPost implements JsonSerializable {
// Database fields
private $comment_id = -1;
private $category_id = -1;
private $user_id = null;
private $comment_reply_to = null;
private $comment_text = '';
private $comment_created = null;
private $comment_pinned = null;
private $comment_edited = null;
private $comment_deleted = null;
// Virtual fields
private $comment_likes = -1;
private $comment_dislikes = -1;
private $user_vote = null;
private $category = null;
private $user = null;
private $userLookedUp = false;
private $parentPost = null;
public const TABLE = 'comments_posts';
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
private const SELECT = '%1$s.`comment_id`, %1$s.`category_id`, %1$s.`user_id`, %1$s.`comment_reply_to`, %1$s.`comment_text`'
. ', UNIX_TIMESTAMP(%1$s.`comment_created`) AS `comment_created`'
. ', UNIX_TIMESTAMP(%1$s.`comment_pinned`) AS `comment_pinned`'
. ', UNIX_TIMESTAMP(%1$s.`comment_edited`) AS `comment_edited`'
. ', UNIX_TIMESTAMP(%1$s.`comment_deleted`) AS `comment_deleted`';
private const LIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::LIKE . ') AS `comment_likes`';
private const DISLIKE_VOTE_SELECT = '(SELECT COUNT(`comment_id`) FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `comment_vote` = ' . CommentsVote::DISLIKE . ') AS `comment_dislikes`';
private const USER_VOTE_SELECT = '(SELECT `comment_vote` FROM `' . DB::PREFIX . CommentsVote::TABLE . '` WHERE `comment_id` = %1$s.`comment_id` AND `user_id` = :user) AS `user_vote`';
public function getId(): int {
return $this->comment_id < 1 ? -1 : $this->comment_id;
}
public function getCategoryId(): int {
return $this->category_id < 1 ? -1 : $this->category_id;
}
public function setCategoryId(int $categoryId): self {
$this->category_id = $categoryId;
$this->category = null;
return $this;
}
public function getCategory(): CommentsCategory {
if($this->category === null)
$this->category = CommentsCategory::byId($this->getCategoryId());
return $this->category;
}
public function setCategory(CommentsCategory $category): self {
$this->category_id = $category->getId();
$this->category = null;
return $this;
}
public function getUserId(): int {
return $this->user_id < 1 ? -1 : $this->user_id;
}
public function setUserId(int $userId): self {
$this->user_id = $userId < 1 ? null : $userId;
$this->userLookedUp = false;
$this->user = null;
return $this;
}
public function getUser(): ?User {
if(!$this->userLookedUp && ($userId = $this->getUserId()) > 0) {
$this->userLookedUp = true;
try {
$this->user = User::byId($userId);
} catch(UserNotFoundException $ex) {}
}
return $this->user;
}
public function setUser(?User $user): self {
$this->user_id = $user === null ? null : $user->getId();
$this->userLookedUp = true;
$this->user = $user;
return $this;
}
public function getParentId(): int {
return $this->comment_reply_to < 1 ? -1 : $this->comment_reply_to;
}
public function setParentId(int $parentId): self {
$this->comment_reply_to = $parentId < 1 ? null : $parentId;
$this->parentPost = null;
return $this;
}
public function hasParent(): bool {
return $this->getParentId() > 0;
}
public function getParent(): CommentsPost {
if(!$this->hasParent())
throw new CommentsPostHasNoParentException;
if($this->parentPost === null)
$this->parentPost = CommentsPost::byId($this->getParentId());
return $this->parentPost;
}
public function setParent(?CommentsPost $parent): self {
$this->comment_reply_to = $parent === null ? null : $parent->getId();
$this->parentPost = $parent;
return $this;
}
public function getText(): string {
return $this->comment_text;
}
public function setText(string $text): self {
$this->comment_text = $text;
return $this;
}
public function getParsedText(): string {
return CommentsParser::parseForDisplay($this->getText());
}
public function setParsedText(string $text): self {
return $this->setText(CommentsParser::parseForStorage($text));
}
public function getCreatedTime(): int {
return $this->comment_created === null ? -1 : $this->comment_created;
}
public function getPinnedTime(): int {
return $this->comment_pinned === null ? -1 : $this->comment_pinned;
}
public function isPinned(): bool {
return $this->getPinnedTime() >= 0;
}
public function setPinned(bool $pinned): self {
if($this->isPinned() !== $pinned)
$this->comment_pinned = $pinned ? time() : null;
return $this;
}
public function getEditedTime(): int {
return $this->comment_edited === null ? -1 : $this->comment_edited;
}
public function isEdited(): bool {
return $this->getEditedTime() >= 0;
}
public function getDeletedTime(): int {
return $this->comment_deleted === null ? -1 : $this->comment_deleted;
}
public function isDeleted(): bool {
return $this->getDeletedTime() >= 0;
}
public function setDeleted(bool $deleted): self {
if($this->isDeleted() !== $deleted)
$this->comment_deleted = $deleted ? time() : null;
return $this;
}
public function getLikes(): int {
return $this->comment_likes;
}
public function getDislikes(): int {
return $this->comment_dislikes;
}
public function hasUserVote(): bool {
return $this->user_vote !== null;
}
public function getUserVote(): int {
return $this->user_vote ?? 0;
}
public function jsonSerialize(): mixed {
$json = [
'id' => $this->getId(),
'category' => $this->getCategoryId(),
'user' => $this->getUserId(),
'parent' => ($parent = $this->getParentId()) < 1 ? null : $parent,
'text' => $this->getText(),
'created' => ($created = $this->getCreatedTime()) < 0 ? null : date('c', $created),
'pinned' => ($pinned = $this->getPinnedTime()) < 0 ? null : date('c', $pinned),
'edited' => ($edited = $this->getEditedTime()) < 0 ? null : date('c', $edited),
'deleted' => ($deleted = $this->getDeletedTime()) < 0 ? null : date('c', $deleted),
];
if(($likes = $this->getLikes()) >= 0)
$json['likes'] = $likes;
if(($dislikes = $this->getDislikes()) >= 0)
$json['dislikes'] = $dislikes;
if($this->hasUserVote())
$json['user_vote'] = $this->getUserVote();
return $json;
}
public function save(): void {
$isInsert = $this->getId() < 1;
if($isInsert) {
$query = 'INSERT INTO `%1$s%2$s` (`category_id`, `user_id`, `comment_reply_to`, `comment_text`'
. ', `comment_pinned`, `comment_deleted`) VALUES'
. ' (:category, :user, :parent, :text, FROM_UNIXTIME(:pinned), FROM_UNIXTIME(:deleted))';
} else {
$query = 'UPDATE `%1$s%2$s` SET `category_id` = :category, `user_id` = :user, `comment_reply_to` = :parent'
. ', `comment_text` = :text, `comment_pinned` = FROM_UNIXTIME(:pinned), `comment_deleted` = FROM_UNIXTIME(:deleted)'
. ' WHERE `comment_id` = :post';
}
$savePost = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
->bind('category', $this->category_id)
->bind('user', $this->user_id)
->bind('parent', $this->comment_reply_to)
->bind('text', $this->comment_text)
->bind('pinned', $this->comment_pinned)
->bind('deleted', $this->comment_deleted);
if($isInsert) {
$this->comment_id = $savePost->executeGetId();
if($this->comment_id < 1)
throw new CommentsPostSaveFailedException;
$this->comment_created = time();
} else {
$this->comment_edited = time();
$savePost->bind('post', $this->getId());
if(!$savePost->execute())
throw new CommentsPostSaveFailedException;
}
}
public function nuke(): void {
$replies = $this->replies(null, true);
foreach($replies as $reply)
$reply->nuke();
DB::prepare('DELETE FROM `' . DB::PREFIX . self::TABLE . '` WHERE `comment_id` = :comment')
->bind('comment_id', $this->getId())
->execute();
}
public function replies(?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
return CommentsPost::byParent($this, $voteUser, $includeVotes, $pagination, $includeDeleted);
}
public function votes(): CommentsVoteCount {
return CommentsVote::countByPost($this);
}
public function childVotes(?User $user = null, ?Pagination $pagination = null): array {
return CommentsVote::byParent($this, $user, $pagination);
}
public function addPositiveVote(User $user): void {
CommentsVote::create($this, $user, CommentsVote::LIKE);
}
public function addNegativeVote(User $user): void {
CommentsVote::create($this, $user, CommentsVote::DISLIKE);
}
public function removeVote(User $user): void {
CommentsVote::delete($this, $user);
}
public function getVoteFromUser(User $user): CommentsVote {
return CommentsVote::byExact($this, $user);
}
private static function byQueryBase(bool $includeVotes = true, bool $includeUserVote = false): string {
$select = self::SELECT;
if($includeVotes)
$select .= ', ' . self::LIKE_VOTE_SELECT
. ', ' . self::DISLIKE_VOTE_SELECT;
if($includeUserVote)
$select .= ', ' . self::USER_VOTE_SELECT;
return sprintf(self::QUERY_SELECT, sprintf($select, self::TABLE));
}
public static function byId(int $postId): self {
$getPost = DB::prepare(self::byQueryBase() . ' WHERE `comment_id` = :post_id');
$getPost->bind('post_id', $postId);
$post = $getPost->fetchObject(self::class);
if(!$post)
throw new CommentsPostNotFoundException;
return $post;
}
public static function byCategory(CommentsCategory $category, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = true): array {
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
. ' WHERE `category_id` = :category'
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('category', $category->getId());
if($voteUser !== null)
$getPosts->bind('user', $voteUser->getId());
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
public static function byParent(CommentsPost $parent, ?User $voteUser = null, bool $includeVotes = true, ?Pagination $pagination = null, bool $includeDeleted = true): array {
$postsQuery = self::byQueryBase($includeVotes, $voteUser !== null)
. ' WHERE `comment_reply_to` = :parent'
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_deleted` ASC, `comment_pinned` DESC, `comment_id` ASC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery)
->bind('parent', $parent->getId());
if($voteUser !== null)
$getPosts->bind('user', $voteUser->getId());
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
public static function all(?Pagination $pagination = null, bool $rootOnly = true, bool $includeDeleted = false): array {
$postsQuery = self::byQueryBase()
. ' WHERE 1' // this is disgusting
. (!$rootOnly ? '' : ' AND `comment_reply_to` IS NULL')
. ($includeDeleted ? '' : ' AND `comment_deleted` IS NULL')
. ' ORDER BY `comment_id` DESC';
if($pagination !== null)
$postsQuery .= ' LIMIT :range OFFSET :offset';
$getPosts = DB::prepare($postsQuery);
if($pagination !== null)
$getPosts->bind('range', $pagination->getRange())
->bind('offset', $pagination->getOffset());
return $getPosts->fetchObjects(self::class);
}
}