dbConn = $dbConn; $this->cache = new DbStatementCache($dbConn); } public static function convertFromActionId(int $actionId): string { return match($actionId) { 1 => 'add', 2 => 'remove', 3 => 'update', 4 => 'fix', 5 => 'import', 6 => 'revert', default => 'unknown', }; } public static function convertToActionId(string $action): int { return match($action) { 'add' => 1, 'remove' => 2, 'update' => 3, 'fix' => 4, 'import' => 5, 'revert' => 6, default => 0, }; } public static function actionText(string|int $action): string { if(is_int($action)) $action = self::convertFromActionId($action); return match($action) { 'add' => 'Added', 'remove' => 'Removed', 'update' => 'Updated', 'fix' => 'Fixed', 'import' => 'Imported', 'revert' => 'Reverted', default => 'Changed', }; } public function countChanges( UserInfo|string|null $userInfo = null, DateTime|int|null $dateTime = null, ?array $tags = null ): int { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($dateTime instanceof DateTime) $dateTime = $dateTime->getUnixTimeSeconds(); $args = 0; $hasUserInfo = $userInfo !== null; $hasDateTime = $dateTime !== null; $hasTags = !empty($tags); $query = 'SELECT COUNT(*) FROM msz_changelog_changes'; if($hasUserInfo) { ++$args; $query .= ' WHERE user_id = ?'; } if($hasDateTime) { $query .= (++$args > 1 ? ' AND' : ' WHERE'); $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))'; } if($hasTags) { $query .= (++$args > 1 ? ' AND' : ' WHERE'); $query .= sprintf( ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))', DbTools::prepareListString($tags) ); } $stmt = $this->cache->get($query); $args = 0; if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasDateTime) $stmt->addParameter(++$args, $dateTime); if($hasTags) foreach($tags as $tag) $stmt->addParameter(++$args, (string)$tag); $stmt->execute(); $result = $stmt->getResult(); $count = 0; if($result->next()) $count = $result->getInteger(0); return $count; } public function getChanges( UserInfo|string|null $userInfo = null, DateTime|int|null $dateTime = null, ?array $tags = null, ?Pagination $pagination = null ): iterable { if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($dateTime instanceof DateTime) $dateTime = $dateTime->getUnixTimeSeconds(); $args = 0; $hasUserInfo = $userInfo !== null; $hasDateTime = $dateTime !== null; $hasTags = !empty($tags); $hasPagination = $pagination !== null; $query = 'SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes'; if($hasUserInfo) { ++$args; $query .= ' WHERE user_id = ?'; } if($hasDateTime) { $query .= (++$args > 1 ? ' AND' : ' WHERE'); $query .= ' DATE(change_created) = DATE(FROM_UNIXTIME(?))'; } if($hasTags) { $query .= (++$args > 1 ? ' AND' : ' WHERE'); $query .= sprintf( ' change_id IN (SELECT change_id FROM msz_changelog_change_tags WHERE tag_id IN (%s))', DbTools::prepareListString($tags) ); } $query .= ' GROUP BY change_created, change_id ORDER BY change_created DESC, change_id DESC'; if($hasPagination) $query .= ' LIMIT ? OFFSET ?'; $stmt = $this->cache->get($query); $args = 0; if($hasUserInfo) $stmt->addParameter(++$args, $userInfo); if($hasDateTime) $stmt->addParameter(++$args, $dateTime); if($hasTags) foreach($tags as $tag) $stmt->addParameter(++$args, (string)$tag); if($hasPagination) { $stmt->addParameter(++$args, $pagination->getRange()); $stmt->addParameter(++$args, $pagination->getOffset()); } $stmt->execute(); return $stmt->getResult()->getIterator(ChangeInfo::fromResult(...)); } public function getChange(string $changeId): ChangeInfo { $stmt = $this->cache->get('SELECT change_id, user_id, change_action, UNIX_TIMESTAMP(change_created), change_log, change_text FROM msz_changelog_changes WHERE change_id = ?'); $stmt->addParameter(1, $changeId); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('No tag with that ID exists.'); return ChangeInfo::fromResult($result); } public function createChange( string|int $action, string $summary, string $body = '', UserInfo|string|null $userInfo = null, DateTime|int|null $createdAt = null ): ChangeInfo { if(is_string($action)) $action = self::convertToActionId($action); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($createdAt instanceof DateTime) $createdAt = $createdAt->getUnixTimeSeconds(); $summary = trim($summary); if(empty($summary)) throw new InvalidArgumentException('$summary may not be empty'); $body = trim($body); if(empty($body)) $body = null; $stmt = $this->cache->get('INSERT INTO msz_changelog_changes (user_id, change_action, change_created, change_log, change_text) VALUES (?, ?, FROM_UNIXTIME(?), ?, ?)'); $stmt->addParameter(1, $userInfo); $stmt->addParameter(2, $action); $stmt->addParameter(3, $createdAt); $stmt->addParameter(4, $summary); $stmt->addParameter(5, $body); $stmt->execute(); return $this->getChange((string)$this->dbConn->getLastInsertId()); } public function deleteChange(ChangeInfo|string $infoOrId): void { if($infoOrId instanceof ChangeInfo) $infoOrId = $infoOrId->getId(); $stmt = $this->cache->get('DELETE FROM msz_changelog_changes WHERE change_id = ?'); $stmt->addParameter(1, $infoOrId); $stmt->execute(); } public function updateChange( ChangeInfo|string $infoOrId, string|int|null $action = null, ?string $summary = null, ?string $body = null, bool $updateUserInfo = false, UserInfo|string|null $userInfo = null, DateTime|int|null $createdAt = null ): void { if($infoOrId instanceof ChangeInfo) $infoOrId = $infoOrId->getId(); if(is_string($action)) $action = self::convertToActionId($action); if($userInfo instanceof UserInfo) $userInfo = $userInfo->getId(); if($createdAt instanceof DateTime) $createdAt = $createdAt->getUnixTimeSeconds(); if($summary !== null) { $summary = trim($summary); if(empty($summary)) throw new InvalidArgumentException('$summary may not be empty'); } $hasBody = $body !== null; if($hasBody) { $body = trim($body); if(empty($body)) $body = null; } $stmt = $this->cache->get('UPDATE msz_changelog_changes SET change_action = COALESCE(?, change_action), change_log = COALESCE(?, change_log), change_text = IF(?, ?, change_text), user_id = IF(?, ?, user_id), change_created = COALESCE(FROM_UNIXTIME(?), change_created) WHERE change_id = ?'); $stmt->addParameter(1, $action); $stmt->addParameter(2, $summary); $stmt->addParameter(3, $hasBody ? 1 : 0); $stmt->addParameter(4, $body); $stmt->addParameter(5, $updateUserInfo ? 1 : 0); $stmt->addParameter(6, $userInfo); $stmt->addParameter(7, $createdAt); $stmt->addParameter(8, $infoOrId); $stmt->execute(); } public function getTags( ChangeInfo|string|null $changeInfo = null ): array { if($changeInfo instanceof ChangeInfo) $changeInfo = $changeInfo->getId(); $hasChangeInfo = $changeInfo !== null; $query = 'SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), (SELECT COUNT(*) FROM msz_changelog_change_tags AS ct WHERE ct.tag_id = t.tag_id) AS tag_changes FROM msz_changelog_tags AS t'; if($hasChangeInfo) $query .= ' WHERE tag_id IN (SELECT tag_id FROM msz_changelog_change_tags WHERE change_id = ?)'; $stmt = $this->cache->get($query); if($hasChangeInfo) $stmt->addParameter(1, $changeInfo); $stmt->execute(); $result = $stmt->getResult(); $tags = []; while($result->next()) { $tagId = (string)$result->getInteger(0); if(array_key_exists($tagId, $this->tags)) $tags[] = $this->tags[$tagId]; else $tags[] = $this->tags[$tagId] = new ChangeTagInfo($result); } return $tags; } public function getTag(string $tagId): ChangeTagInfo { $stmt = $this->cache->get('SELECT tag_id, tag_name, tag_description, UNIX_TIMESTAMP(tag_created), UNIX_TIMESTAMP(tag_archived), 0 AS `tag_changes` FROM msz_changelog_tags WHERE tag_id = ?'); $stmt->addParameter(1, $tagId); $stmt->execute(); $result = $stmt->getResult(); if(!$result->next()) throw new RuntimeException('No tag with that ID exists.'); return new ChangeTagInfo($result); } public function createTag( string $name, string $description, bool $archived ): ChangeTagInfo { $name = trim($name); if(empty($name)) throw new InvalidArgumentException('$name may not be empty'); $description = trim($description); if(empty($description)) $description = null; $stmt = $this->cache->get('INSERT INTO msz_changelog_tags (tag_name, tag_description, tag_archived) VALUES (?, ?, IF(?, NOW(), NULL))'); $stmt->addParameter(1, $name); $stmt->addParameter(2, $description); $stmt->addParameter(3, $archived ? 1 : 0); $stmt->execute(); return $this->getTag((string)$this->dbConn->getLastInsertId()); } public function deleteTag(ChangeTagInfo|string $infoOrId): void { if($infoOrId instanceof ChangeTagInfo) $infoOrId = $infoOrId->getId(); $stmt = $this->cache->get('DELETE FROM msz_changelog_tags WHERE tag_id = ?'); $stmt->addParameter(1, $infoOrId); $stmt->execute(); } public function updateTag( ChangeTagInfo|string $infoOrId, ?string $name = null, ?string $description = null, ?bool $archived = null ): void { if($infoOrId instanceof ChangeTagInfo) $infoOrId = $infoOrId->getId(); if($name !== null) { $name = trim($name); if(empty($name)) throw new InvalidArgumentException('$name may not be empty'); } $hasDescription = $description !== null; if($hasDescription) { $description = trim($description); if(empty($description)) $description = null; } $hasArchived = $archived !== null; $stmt = $this->cache->get('UPDATE msz_changelog_tags SET tag_name = COALESCE(?, tag_name), tag_description = IF(?, ?, tag_description), tag_archived = IF(?, IF(?, NOW(), NULL), tag_archived) WHERE tag_id = ?'); $stmt->addParameter(1, $name); $stmt->addParameter(2, $hasDescription ? 1 : 0); $stmt->addParameter(3, $description); $stmt->addParameter(4, $hasArchived ? 1 : 0); $stmt->addParameter(5, $archived ? 1 : 0); $stmt->addParameter(6, $infoOrId); $stmt->execute(); } public function addTagToChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void { if($change instanceof ChangeInfo) $change = $change->getId(); if($tag instanceof ChangeTagInfo) $tag = $tag->getId(); $stmt = $this->cache->get('INSERT INTO msz_changelog_change_tags (change_id, tag_id) VALUES (?, ?)'); $stmt->addParameter(1, $change); $stmt->addParameter(2, $tag); $stmt->execute(); } public function removeTagFromChange(ChangeInfo|string $change, ChangeTagInfo|string $tag): void { if($change instanceof ChangeInfo) $change = $change->getId(); if($tag instanceof ChangeTagInfo) $tag = $tag->getId(); $stmt = $this->cache->get('DELETE FROM msz_changelog_change_tags WHERE change_id = ? AND tag_id = ?'); $stmt->addParameter(1, $change); $stmt->addParameter(2, $tag); $stmt->execute(); } }