['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); } }