282 lines
9.9 KiB
PHP
282 lines
9.9 KiB
PHP
|
<?php
|
||
|
namespace Misuzu\Changelog;
|
||
|
|
||
|
use JsonSerializable;
|
||
|
use UnexpectedValueException;
|
||
|
use Misuzu\DB;
|
||
|
use Misuzu\Memoizer;
|
||
|
use Misuzu\Pagination;
|
||
|
use Misuzu\Comments\CommentsCategory;
|
||
|
use Misuzu\Comments\CommentsCategoryNotFoundException;
|
||
|
use Misuzu\Parsers\Parser;
|
||
|
use Misuzu\Users\User;
|
||
|
use Misuzu\Users\UserNotFoundException;
|
||
|
|
||
|
class ChangelogChangeException extends ChangelogException {}
|
||
|
class ChangelogChangeNotFoundException extends ChangelogChangeException {}
|
||
|
|
||
|
class ChangelogChange implements JsonSerializable {
|
||
|
public const ACTION_UNKNOWN = 0;
|
||
|
public const ACTION_ADD = 1;
|
||
|
public const ACTION_REMOVE = 2;
|
||
|
public const ACTION_UPDATE = 3;
|
||
|
public const ACTION_FIX = 4;
|
||
|
public const ACTION_IMPORT = 5;
|
||
|
public const ACTION_REVERT = 6;
|
||
|
|
||
|
private const ACTION_STRINGS = [
|
||
|
self::ACTION_UNKNOWN => ['unknown', 'Changed'],
|
||
|
self::ACTION_ADD => ['add', 'Added'],
|
||
|
self::ACTION_REMOVE => ['remove', 'Removed'],
|
||
|
self::ACTION_UPDATE => ['update', 'Updated'],
|
||
|
self::ACTION_FIX => ['fix', 'Fixed'],
|
||
|
self::ACTION_IMPORT => ['import', 'Imported'],
|
||
|
self::ACTION_REVERT => ['revert', 'Reverted'],
|
||
|
];
|
||
|
|
||
|
public const DEFAULT_DATE = '0000-00-00';
|
||
|
|
||
|
// Database fields
|
||
|
private $change_id = -1;
|
||
|
private $user_id = null;
|
||
|
private $change_action = null; // defaults null apparently, probably a previous oversight
|
||
|
private $change_created = null;
|
||
|
private $change_log = '';
|
||
|
private $change_text = '';
|
||
|
|
||
|
private $user = null;
|
||
|
private $userLookedUp = false;
|
||
|
private $comments = null;
|
||
|
private $tags = null;
|
||
|
private $tagRelations = null;
|
||
|
|
||
|
public const TABLE = 'changelog_changes';
|
||
|
private const QUERY_SELECT = 'SELECT %1$s FROM `' . DB::PREFIX . self::TABLE . '` AS '. self::TABLE;
|
||
|
private const SELECT = '%1$s.`change_id`, %1$s.`user_id`, %1$s.`change_action`, %1$s.`change_log`, %1$s.`change_text`'
|
||
|
. ', UNIX_TIMESTAMP(%1$s.`change_created`) AS `change_created`';
|
||
|
|
||
|
public function getId(): int {
|
||
|
return $this->change_id < 1 ? -1 : $this->change_id;
|
||
|
}
|
||
|
|
||
|
public function getUserId(): int {
|
||
|
return $this->user_id < 1 ? -1 : $this->user_id;
|
||
|
}
|
||
|
public function setUserId(?int $userId): self {
|
||
|
$this->user_id = $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 getAction(): int {
|
||
|
return $this->change_action ?? self::ACTION_UNKNOWN;
|
||
|
}
|
||
|
public function setAction(int $actionId): self {
|
||
|
$this->change_action = $actionId;
|
||
|
return $this;
|
||
|
}
|
||
|
private function getActionInfo(): array {
|
||
|
return self::ACTION_STRINGS[$this->getAction()] ?? self::ACTION_STRINGS[self::ACTION_UNKNOWN];
|
||
|
}
|
||
|
public function getActionClass(): string {
|
||
|
return $this->getActionInfo()[0];
|
||
|
}
|
||
|
public function getActionString(): string {
|
||
|
return $this->getActionInfo()[1];
|
||
|
}
|
||
|
|
||
|
public function getCreatedTime(): int {
|
||
|
return $this->change_created ?? -1;
|
||
|
}
|
||
|
public function getDate(): string {
|
||
|
return ($time = $this->getCreatedTime()) < 0 ? self::DEFAULT_DATE : gmdate('Y-m-d', $time);
|
||
|
}
|
||
|
|
||
|
public function getHeader(): string {
|
||
|
return $this->change_log;
|
||
|
}
|
||
|
public function setHeader(string $header): self {
|
||
|
$this->change_log = $header;
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function getBody(): string {
|
||
|
return $this->change_text ?? '';
|
||
|
}
|
||
|
public function setBody(string $body): self {
|
||
|
$this->change_text = $body;
|
||
|
return $this;
|
||
|
}
|
||
|
public function hasBody(): bool {
|
||
|
return !empty($this->change_text);
|
||
|
}
|
||
|
public function getParsedBody(): string {
|
||
|
return Parser::instance(Parser::MARKDOWN)->parseText($this->getBody());
|
||
|
}
|
||
|
|
||
|
public function getCommentsCategoryName(): ?string {
|
||
|
return ($date = $this->getDate()) === self::DEFAULT_DATE ? null : sprintf('changelog-date-%s', $this->getDate());
|
||
|
}
|
||
|
public function hasCommentsCategory(): bool {
|
||
|
return $this->getCreatedTime() >= 0;
|
||
|
}
|
||
|
public function getCommentsCategory(): CommentsCategory {
|
||
|
if($this->comments === null) {
|
||
|
$categoryName = $this->getCommentsCategoryName();
|
||
|
|
||
|
if(empty($categoryName))
|
||
|
throw new UnexpectedValueException('Change comments category name is empty.');
|
||
|
|
||
|
try {
|
||
|
$this->comments = CommentsCategory::byName($categoryName);
|
||
|
} catch(CommentsCategoryNotFoundException $ex) {
|
||
|
$this->comments = new CommentsCategory($categoryName);
|
||
|
$this->comments->save();
|
||
|
}
|
||
|
}
|
||
|
return $this->comments;
|
||
|
}
|
||
|
|
||
|
public function getTags(): array {
|
||
|
if($this->tags === null)
|
||
|
$this->tags = ChangelogTag::byChange($this);
|
||
|
return $this->tags;
|
||
|
}
|
||
|
public function getTagRelations(): array {
|
||
|
if($this->tagRelations === null)
|
||
|
$this->tagRelations = ChangelogChangeTag::byChange($this);
|
||
|
return $this->tagRelations;
|
||
|
}
|
||
|
public function setTags(array $tags): self {
|
||
|
ChangelogChangeTag::purgeChange($this);
|
||
|
foreach($tags as $tag)
|
||
|
if($tag instanceof ChangelogTag)
|
||
|
ChangelogChangeTag::create($this, $tag);
|
||
|
$this->tags = $tags;
|
||
|
$this->tagRelations = null;
|
||
|
return $this;
|
||
|
}
|
||
|
public function hasTag(ChangelogTag $other): bool {
|
||
|
foreach($this->getTags() as $tag)
|
||
|
if($tag->compare($other))
|
||
|
return true;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
public function jsonSerialize(): mixed {
|
||
|
return [
|
||
|
'id' => $this->getId(),
|
||
|
'user' => $this->getUserId(),
|
||
|
'action' => $this->getAction(),
|
||
|
'header' => $this->getHeader(),
|
||
|
'body' => $this->getBody(),
|
||
|
'comments' => $this->getCommentsCategoryName(),
|
||
|
'created' => ($time = $this->getCreatedTime()) < 0 ? null : date('c', $time),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
public function save(): void {
|
||
|
$isInsert = $this->getId() < 1;
|
||
|
if($isInsert) {
|
||
|
$query = 'INSERT INTO `%1$s%2$s` (`user_id`, `change_action`, `change_log`, `change_text`)'
|
||
|
. ' VALUES (:user, :action, :header, :body)';
|
||
|
} else {
|
||
|
$query = 'UPDATE `%1$s%2$s` SET `user_id` = :user, `change_action` = :action, `change_log` = :header, `change_text` = :body'
|
||
|
. ' WHERE `change_id` = :change';
|
||
|
}
|
||
|
|
||
|
$saveChange = DB::prepare(sprintf($query, DB::PREFIX, self::TABLE))
|
||
|
->bind('user', $this->user_id)
|
||
|
->bind('action', $this->change_action)
|
||
|
->bind('header', $this->change_log)
|
||
|
->bind('body', $this->change_text);
|
||
|
|
||
|
if($isInsert) {
|
||
|
$this->change_id = $saveChange->executeGetId();
|
||
|
$this->change_created = time();
|
||
|
} else {
|
||
|
$saveChange->bind('change', $this->getId())
|
||
|
->execute();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static function countQueryBase(): string {
|
||
|
return sprintf(self::QUERY_SELECT, sprintf('COUNT(%s.`change_id`)', self::TABLE));
|
||
|
}
|
||
|
public static function countAll(?int $date = null, ?User $user = null): int {
|
||
|
$countChanges = DB::prepare(
|
||
|
self::countQueryBase()
|
||
|
. ' WHERE 1' // this is still disgusting
|
||
|
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
|
||
|
. ($user === null ? '' : ' AND `user_id` = :user')
|
||
|
);
|
||
|
if($date !== null)
|
||
|
$countChanges->bind('date', gmdate('Y-m-d', $date));
|
||
|
if($user !== null)
|
||
|
$countChanges->bind('user', $user->getId());
|
||
|
return (int)$countChanges->fetchColumn();
|
||
|
}
|
||
|
|
||
|
private static function memoizer(): Memoizer {
|
||
|
static $memoizer = null;
|
||
|
if($memoizer === null)
|
||
|
$memoizer = new Memoizer;
|
||
|
return $memoizer;
|
||
|
}
|
||
|
|
||
|
private static function byQueryBase(): string {
|
||
|
return sprintf(self::QUERY_SELECT, sprintf(self::SELECT, self::TABLE));
|
||
|
}
|
||
|
public static function byId(int $changeId): self {
|
||
|
return self::memoizer()->find($changeId, function() use ($changeId) {
|
||
|
$change = DB::prepare(self::byQueryBase() . ' WHERE `change_id` = :change')
|
||
|
->bind('change', $changeId)
|
||
|
->fetchObject(self::class);
|
||
|
if(!$change)
|
||
|
throw new ChangelogChangeNotFoundException;
|
||
|
return $change;
|
||
|
});
|
||
|
}
|
||
|
public static function all(?Pagination $pagination = null, ?int $date = null, ?User $user = null): array {
|
||
|
$changeQuery = self::byQueryBase()
|
||
|
. ' WHERE 1' // this is still disgusting
|
||
|
. ($date === null ? '' : ' AND DATE(`change_created`) = :date')
|
||
|
. ($user === null ? '' : ' AND `user_id` = :user')
|
||
|
. ' GROUP BY `change_created`, `change_id`'
|
||
|
. ' ORDER BY `change_created` DESC, `change_id` DESC';
|
||
|
|
||
|
if($pagination !== null)
|
||
|
$changeQuery .= ' LIMIT :range OFFSET :offset';
|
||
|
|
||
|
$getChanges = DB::prepare($changeQuery);
|
||
|
|
||
|
if($date !== null)
|
||
|
$getChanges->bind('date', gmdate('Y-m-d', $date));
|
||
|
|
||
|
if($user !== null)
|
||
|
$getChanges->bind('user', $user->getId());
|
||
|
|
||
|
if($pagination !== null)
|
||
|
$getChanges->bind('range', $pagination->getRange())
|
||
|
->bind('offset', $pagination->getOffset());
|
||
|
|
||
|
return $getChanges->fetchObjects(self::class);
|
||
|
}
|
||
|
}
|