1
0

feat: extract bookmarks as standalone module with MintyPHP\Module\Bookmarks namespace

Moves bookmarks from core hardcoding into modules/bookmarks/ as a
fully self-contained module, following the same pattern as addressbook.

Module contributions via platform slots:
- aside.tab_panel: bookmark sidebar panel
- topbar.right_item: bookmark toggle button
- layout.body_end_template: bookmark/group dialogs
- layout.head_style: bookmark CSS (form + sidebar)
- runtime.component: bookmark-save and bookmark-panel (phase: late)
- search.resource_item: bookmarks in global search (user-scoped via {{userId}})

Backend fully in module namespace (MintyPHP\Module\Bookmarks\*):
- Service: BookmarkService, BookmarkServicesFactory, BookmarkRepositoryFactory
- Repository: BookmarkRepository, BookmarkGroupRepository, BookmarkNavigationRepository
- Support: BookmarkUrlNormalizer
- Providers: BookmarksSessionProvider, BookmarksLayoutProvider, BookmarksSearchProvider

Core cleanup:
- Removed all bookmark-specific markup from core templates
- Removed core DI registrations for bookmark services
- Removed core bookmark pages, JS, CSS
- AuthSessionTenantContextService delegates to module SessionProvider

Architecture guards:
- NoBookmarksHardcodingTest: verifies zero bookmark references in core
- testModuleClassesUseModuleNamespace: prevents core namespace leakage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 22:20:20 +01:00
parent c7b8fd516a
commit 4871c6032e
37 changed files with 1514 additions and 294 deletions

View File

@@ -0,0 +1,125 @@
<?php
namespace MintyPHP\Module\Bookmarks\Repository;
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'];
$sortOrder = (int) $pair['sort_order'];
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;
}
}
}