misuzu/src/Changelog/Changelog.php

405 lines
14 KiB
PHP

<?php
namespace Misuzu\Changelog;
use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Data\IDbResult;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class Changelog {
// not a strict list but useful to have
public const ACTIONS = ['add', 'remove', 'update', 'fix', 'import', 'revert'];
private IDbConnection $dbConn;
private DbStatementCache $cache;
private array $tags = [];
public function __construct(IDbConnection $dbConn) {
$this->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();
}
}