Global Boomarks

This commit is contained in:
2026-03-14 21:45:58 +01:00
parent 921977bdd3
commit 9688848401
53 changed files with 4232 additions and 966 deletions

View File

@@ -0,0 +1,209 @@
<?php
namespace MintyPHP\Repository\User;
use MintyPHP\DB;
class BookmarkGroupRepository implements BookmarkGroupRepositoryInterface
{
private function unwrapList(mixed $rows): array
{
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['user_bookmark_groups'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public function listByUser(int $userId): array
{
if ($userId <= 0) {
return [];
}
$rows = DB::select(
'select id, user_id, name, icon, sort_order, created, modified from user_bookmark_groups where user_id = ? order by sort_order, id',
(string) $userId
);
return $this->unwrapList($rows);
}
public function nextGroupSortOrder(int $userId): int
{
if ($userId <= 0) {
return 1;
}
$max = DB::selectValue(
'select COALESCE(max(sort_order), 0) from user_bookmark_groups where user_id = ?',
(string) $userId
);
return max(1, ((int) $max) + 1);
}
public function countByUser(int $userId): int
{
if ($userId <= 0) {
return 0;
}
$count = DB::selectValue(
'select count(*) from user_bookmark_groups where user_id = ?',
(string) $userId
);
return $count ? (int) $count : 0;
}
public function findById(int $id, int $userId): array|false
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$rows = DB::select(
'select id, user_id, name, icon, sort_order, created, modified from user_bookmark_groups where id = ? and user_id = ?',
(string) $id,
(string) $userId
);
$list = $this->unwrapList($rows);
return $list[0] ?? false;
}
public function create(array $data): int|false
{
$userId = (int) ($data['user_id'] ?? 0);
$name = trim((string) ($data['name'] ?? ''));
$icon = trim((string) ($data['icon'] ?? 'bi-folder'));
$sortOrder = (int) ($data['sort_order'] ?? 0);
if ($userId <= 0 || $name === '' || $icon === '') {
return false;
}
return DB::insert(
'insert into user_bookmark_groups (user_id, name, icon, sort_order, created) values (?,?,?,?,NOW())',
(string) $userId,
$name,
$icon,
(string) $sortOrder
);
}
public function update(int $id, int $userId, array $data): bool
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$sets = [];
$params = [];
if (array_key_exists('name', $data)) {
$name = trim((string) $data['name']);
if ($name === '') {
return false;
}
$sets[] = 'name = ?';
$params[] = $name;
}
if (array_key_exists('icon', $data)) {
$icon = trim((string) $data['icon']);
if ($icon === '') {
return false;
}
$sets[] = 'icon = ?';
$params[] = $icon;
}
if ($sets === []) {
return false;
}
$params[] = (string) $id;
$params[] = (string) $userId;
$affected = DB::update(
'update user_bookmark_groups set ' . implode(', ', $sets) . ' where id = ? and user_id = ?',
...$params
);
return $affected !== false;
}
public function delete(int $id, int $userId): bool
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$deleted = DB::delete(
'delete from user_bookmark_groups where id = ? and user_id = ?',
(string) $id,
(string) $userId
);
return $deleted !== false && (int) $deleted > 0;
}
public function updateSortOrders(int $userId, array $idOrderPairs): bool
{
if ($userId <= 0 || $idOrderPairs === []) {
return false;
}
$idList = [];
foreach ($idOrderPairs as $pair) {
$id = (int) $pair['id'];
if ($id <= 0) {
return false;
}
$idList[] = $id;
}
$idList = array_values(array_unique($idList));
$existingCount = DB::selectValue(
'select count(*) from user_bookmark_groups where user_id = ? and id in (???)',
(string) $userId,
array_map('strval', $idList)
);
if ((int) $existingCount !== count($idList)) {
return false;
}
$db = DB::handle();
try {
$db->begin_transaction();
foreach ($idOrderPairs as $pair) {
$id = (int) $pair['id'];
$sortOrder = (int) $pair['sort_order'];
if ($id <= 0 || $sortOrder <= 0) {
$db->rollback();
return false;
}
$updated = DB::update(
'update user_bookmark_groups set sort_order = ? where id = ? and user_id = ?',
(string) $sortOrder,
(string) $id,
(string) $userId
);
if ($updated === false) {
$db->rollback();
return false;
}
}
$db->commit();
return true;
} catch (\Throwable) {
try {
$db->rollback();
} catch (\Throwable) {
// no-op
}
return false;
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace MintyPHP\Repository\User;
interface BookmarkGroupRepositoryInterface
{
/** @return list<array<string, mixed>> */
public function listByUser(int $userId): array;
public function nextGroupSortOrder(int $userId): int;
public function countByUser(int $userId): int;
/** @return array<string, mixed>|false */
public function findById(int $id, int $userId): array|false;
public function create(array $data): int|false;
public function update(int $id, int $userId, array $data): bool;
public function delete(int $id, int $userId): bool;
/** @param list<array{id: int, sort_order: int}> $idOrderPairs */
public function updateSortOrders(int $userId, array $idOrderPairs): bool;
}

View File

@@ -0,0 +1,125 @@
<?php
namespace MintyPHP\Repository\User;
use MintyPHP\DB;
class BookmarkNavigationRepository implements BookmarkNavigationRepositoryInterface
{
public function nextRootSortOrder(int $userId): int
{
if ($userId <= 0) {
return 1;
}
$max = DB::selectValue(
'select GREATEST(
COALESCE((select max(sort_order) from user_bookmark_groups where user_id = ?), 0),
COALESCE((select max(sort_order) from user_bookmarks where user_id = ? and group_id is NULL), 0)
)',
(string) $userId,
(string) $userId
);
return max(1, ((int) $max) + 1);
}
public function updateRootSortOrders(int $userId, array $rootOrderPairs): bool
{
if ($userId <= 0 || $rootOrderPairs === []) {
return false;
}
$groupPairs = [];
$bookmarkPairs = [];
$groupIds = [];
$bookmarkIds = [];
foreach ($rootOrderPairs as $pair) {
$kind = trim((string) ($pair['kind'] ?? ''));
$id = (int) ($pair['id'] ?? 0);
$sortOrder = (int) ($pair['sort_order'] ?? 0);
if ($id <= 0 || $sortOrder <= 0 || ($kind !== 'group' && $kind !== 'bookmark')) {
return false;
}
if ($kind === 'group') {
$groupPairs[] = ['id' => $id, 'sort_order' => $sortOrder];
$groupIds[] = $id;
continue;
}
$bookmarkPairs[] = ['id' => $id, 'sort_order' => $sortOrder];
$bookmarkIds[] = $id;
}
if ($groupPairs === [] && $bookmarkPairs === []) {
return false;
}
if ($groupIds !== []) {
$uniqueGroupIds = array_values(array_unique($groupIds));
$groupCount = DB::selectValue(
'select count(*) from user_bookmark_groups where user_id = ? and id in (???)',
(string) $userId,
array_map('strval', $uniqueGroupIds)
);
if ((int) $groupCount !== count($uniqueGroupIds)) {
return false;
}
}
if ($bookmarkIds !== []) {
$uniqueBookmarkIds = array_values(array_unique($bookmarkIds));
$bookmarkCount = DB::selectValue(
'select count(*) from user_bookmarks where user_id = ? and group_id is NULL and id in (???)',
(string) $userId,
array_map('strval', $uniqueBookmarkIds)
);
if ((int) $bookmarkCount !== count($uniqueBookmarkIds)) {
return false;
}
}
$db = DB::handle();
try {
$db->begin_transaction();
foreach ($groupPairs as $pair) {
$updated = DB::update(
'update user_bookmark_groups set sort_order = ? where id = ? and user_id = ?',
(string) $pair['sort_order'],
(string) $pair['id'],
(string) $userId
);
if ($updated === false) {
$db->rollback();
return false;
}
}
foreach ($bookmarkPairs as $pair) {
$updated = DB::update(
'update user_bookmarks set sort_order = ? where id = ? and user_id = ? and group_id is NULL',
(string) $pair['sort_order'],
(string) $pair['id'],
(string) $userId
);
if ($updated === false) {
$db->rollback();
return false;
}
}
$db->commit();
return true;
} catch (\Throwable) {
try {
$db->rollback();
} catch (\Throwable) {
// no-op
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace MintyPHP\Repository\User;
interface BookmarkNavigationRepositoryInterface
{
public function nextRootSortOrder(int $userId): int;
/** @param list<array{kind: 'group'|'bookmark', id: int, sort_order: int}> $rootOrderPairs */
public function updateRootSortOrders(int $userId, array $rootOrderPairs): bool;
}

View File

@@ -0,0 +1,308 @@
<?php
namespace MintyPHP\Repository\User;
use MintyPHP\DB;
class BookmarkRepository implements BookmarkRepositoryInterface
{
private function unwrapList(mixed $rows): array
{
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['user_bookmarks'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public function listByUser(int $userId): array
{
if ($userId <= 0) {
return [];
}
$rows = DB::select(
'select id, user_id, group_id, name, url, sort_order, created, modified from user_bookmarks where user_id = ? order by sort_order, id',
(string) $userId
);
return $this->unwrapList($rows);
}
public function nextBookmarkSortOrder(int $userId, ?int $groupId): int
{
if ($userId <= 0) {
return 1;
}
if ($groupId !== null && $groupId > 0) {
$max = DB::selectValue(
'select COALESCE(max(sort_order), 0) from user_bookmarks where user_id = ? and group_id = ?',
(string) $userId,
(string) $groupId
);
return max(1, ((int) $max) + 1);
}
$max = DB::selectValue(
'select COALESCE(max(sort_order), 0) from user_bookmarks where user_id = ? and group_id is NULL',
(string) $userId
);
return max(1, ((int) $max) + 1);
}
public function countByUser(int $userId): int
{
if ($userId <= 0) {
return 0;
}
$count = DB::selectValue(
'select count(*) from user_bookmarks where user_id = ?',
(string) $userId
);
return $count ? (int) $count : 0;
}
public function findById(int $id, int $userId): array|false
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$rows = DB::select(
'select id, user_id, group_id, name, url, sort_order, created, modified from user_bookmarks where id = ? and user_id = ?',
(string) $id,
(string) $userId
);
$list = $this->unwrapList($rows);
return $list[0] ?? false;
}
public function findByUserAndUrl(int $userId, string $url): array|false
{
$url = trim($url);
if ($userId <= 0 || $url === '') {
return false;
}
$rows = DB::select(
'select id, user_id, group_id, name, url, sort_order, created, modified from user_bookmarks where user_id = ? and url = ? order by id desc limit 1',
(string) $userId,
$url
);
$list = $this->unwrapList($rows);
return $list[0] ?? false;
}
public function create(array $data): int|false
{
$userId = (int) ($data['user_id'] ?? 0);
$name = trim((string) ($data['name'] ?? ''));
$url = trim((string) ($data['url'] ?? ''));
$groupId = isset($data['group_id']) ? (int) $data['group_id'] : null;
$sortOrder = (int) ($data['sort_order'] ?? 0);
if ($userId <= 0 || $name === '' || $url === '') {
return false;
}
$groupIdParam = ($groupId !== null && $groupId > 0) ? (string) $groupId : null;
return DB::insert(
'insert into user_bookmarks (user_id, group_id, name, url, sort_order, created) values (?,?,?,?,?,NOW())',
(string) $userId,
$groupIdParam,
$name,
$url,
(string) $sortOrder
);
}
public function update(int $id, int $userId, array $data): bool
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$sets = [];
$params = [];
if (array_key_exists('name', $data)) {
$name = trim((string) $data['name']);
if ($name === '') {
return false;
}
$sets[] = 'name = ?';
$params[] = $name;
}
if (array_key_exists('group_id', $data)) {
$sets[] = 'group_id = ?';
$groupId = $data['group_id'];
$params[] = ($groupId !== null && (int) $groupId > 0) ? (string) (int) $groupId : null;
}
if (array_key_exists('url', $data)) {
$url = trim((string) $data['url']);
if ($url === '') {
return false;
}
$sets[] = 'url = ?';
$params[] = $url;
}
if (array_key_exists('sort_order', $data)) {
$sortOrder = (int) $data['sort_order'];
if ($sortOrder <= 0) {
return false;
}
$sets[] = 'sort_order = ?';
$params[] = (string) $sortOrder;
}
if ($sets === []) {
return false;
}
$params[] = (string) $id;
$params[] = (string) $userId;
$affected = DB::update(
'update user_bookmarks set ' . implode(', ', $sets) . ' where id = ? and user_id = ?',
...$params
);
return $affected !== false;
}
public function delete(int $id, int $userId): bool
{
if ($id <= 0 || $userId <= 0) {
return false;
}
$deleted = DB::delete(
'delete from user_bookmarks where id = ? and user_id = ?',
(string) $id,
(string) $userId
);
return $deleted !== false && (int) $deleted > 0;
}
public function updateSortOrders(int $userId, array $idOrderPairs): bool
{
if ($userId <= 0 || $idOrderPairs === []) {
return false;
}
$idList = [];
foreach ($idOrderPairs as $pair) {
$id = (int) $pair['id'];
if ($id <= 0) {
return false;
}
$idList[] = $id;
}
$idList = array_values(array_unique($idList));
$existingCount = DB::selectValue(
'select count(*) from user_bookmarks where user_id = ? and id in (???)',
(string) $userId,
array_map('strval', $idList)
);
if ((int) $existingCount !== count($idList)) {
return false;
}
$db = DB::handle();
try {
$db->begin_transaction();
foreach ($idOrderPairs as $pair) {
$id = (int) $pair['id'];
$sortOrder = (int) $pair['sort_order'];
if ($id <= 0 || $sortOrder <= 0) {
$db->rollback();
return false;
}
$updated = DB::update(
'update user_bookmarks set sort_order = ? where id = ? and user_id = ?',
(string) $sortOrder,
(string) $id,
(string) $userId
);
if ($updated === false) {
$db->rollback();
return false;
}
}
$db->commit();
return true;
} catch (\Throwable) {
try {
$db->rollback();
} catch (\Throwable) {
// no-op
}
return false;
}
}
public function clearGroupId(int $groupId, int $userId): bool
{
if ($groupId <= 0 || $userId <= 0) {
return false;
}
$db = DB::handle();
try {
$db->begin_transaction();
$baseSort = DB::selectValue(
'select GREATEST(
COALESCE((select max(sort_order) from user_bookmarks where user_id = ? and group_id is NULL), 0),
COALESCE((select max(sort_order) from user_bookmark_groups where user_id = ?), 0)
)',
(string) $userId,
(string) $userId
);
$nextSort = (int) $baseSort;
$rows = DB::select(
'select id from user_bookmarks where user_id = ? and group_id = ? order by sort_order, id',
(string) $userId,
(string) $groupId
);
$bookmarks = $this->unwrapList($rows);
foreach ($bookmarks as $bookmark) {
$bookmarkId = (int) ($bookmark['id'] ?? 0);
if ($bookmarkId <= 0) {
continue;
}
$nextSort++;
$updated = DB::update(
'update user_bookmarks set group_id = NULL, sort_order = ? where id = ? and user_id = ?',
(string) $nextSort,
(string) $bookmarkId,
(string) $userId
);
if ($updated === false) {
$db->rollback();
return false;
}
}
$db->commit();
return true;
} catch (\Throwable) {
try {
$db->rollback();
} catch (\Throwable) {
// no-op
}
return false;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace MintyPHP\Repository\User;
interface BookmarkRepositoryInterface
{
/** @return list<array<string, mixed>> */
public function listByUser(int $userId): array;
public function nextBookmarkSortOrder(int $userId, ?int $groupId): int;
public function countByUser(int $userId): int;
/** @return array<string, mixed>|false */
public function findById(int $id, int $userId): array|false;
/** @return array<string, mixed>|false */
public function findByUserAndUrl(int $userId, string $url): array|false;
public function create(array $data): int|false;
public function update(int $id, int $userId, array $data): bool;
public function delete(int $id, int $userId): bool;
/** @param list<array{id: int, sort_order: int}> $idOrderPairs */
public function updateSortOrders(int $userId, array $idOrderPairs): bool;
public function clearGroupId(int $groupId, int $userId): bool;
}

View File

@@ -1,86 +0,0 @@
<?php
namespace MintyPHP\Repository\User;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
/** Persists and retrieves user-defined search filter presets per list context. */
class UserSavedFilterRepository implements UserSavedFilterRepositoryInterface
{
private function unwrapList($rows): array
{
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['user_saved_filters'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public function listByUserAndContext(int $userId, string $context): array
{
if ($userId <= 0 || $context === '') {
return [];
}
$rows = DB::select(
'select id, uuid, user_id, context, name, query_json, created, modified from user_saved_filters where user_id = ? and context = ? order by created desc, id desc',
(string) $userId,
$context
);
return $this->unwrapList($rows);
}
public function countByUserAndContext(int $userId, string $context): int
{
if ($userId <= 0 || $context === '') {
return 0;
}
$count = DB::selectValue(
'select count(*) from user_saved_filters where user_id = ? and context = ?',
(string) $userId,
$context
);
return $count ? (int) $count : 0;
}
public function create(array $data): int|false
{
$userId = (int) ($data['user_id'] ?? 0);
$context = trim((string) ($data['context'] ?? ''));
$name = trim((string) ($data['name'] ?? ''));
$queryJson = (string) ($data['query_json'] ?? '');
if ($userId <= 0 || $context === '' || $name === '' || $queryJson === '') {
return false;
}
return DB::insert(
'insert into user_saved_filters (uuid, user_id, context, name, query_json, created) values (?,?,?,?,?,NOW())',
$data['uuid'] ?? RepoQuery::uuidV4(),
(string) $userId,
$context,
$name,
$queryJson
);
}
public function deleteByUuidForUserAndContext(string $uuid, int $userId, string $context): bool
{
$uuid = trim($uuid);
$context = trim($context);
if ($uuid === '' || $userId <= 0 || $context === '') {
return false;
}
$deleted = DB::delete(
'delete from user_saved_filters where uuid = ? and user_id = ? and context = ?',
$uuid,
(string) $userId,
$context
);
return $deleted !== false && (int) $deleted > 0;
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace MintyPHP\Repository\User;
/** Contract for per-user, per-context persistence of saved search filters. */
interface UserSavedFilterRepositoryInterface
{
public function listByUserAndContext(int $userId, string $context): array;
public function countByUserAndContext(int $userId, string $context): int;
public function create(array $data): int|false;
public function deleteByUuidForUserAndContext(string $uuid, int $userId, string $context): bool;
}