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

@@ -12,8 +12,6 @@ use MintyPHP\Repository\User\UserListQueryRepository;
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepository;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserSavedFilterRepository;
use MintyPHP\Repository\User\UserSavedFilterRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepository;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
@@ -22,7 +20,6 @@ class UserRepositoryFactory
private ?UserReadRepository $userReadRepository = null;
private ?UserWriteRepository $userWriteRepository = null;
private ?UserListQueryRepository $userListQueryRepository = null;
private ?UserSavedFilterRepository $userSavedFilterRepository = null;
private ?UserTenantRepository $userTenantRepository = null;
private ?UserRoleRepository $userRoleRepository = null;
private ?UserDepartmentRepository $userDepartmentRepository = null;
@@ -42,11 +39,6 @@ class UserRepositoryFactory
return $this->userListQueryRepository ??= new UserListQueryRepository();
}
public function createUserSavedFilterRepository(): UserSavedFilterRepositoryInterface
{
return $this->userSavedFilterRepository ??= new UserSavedFilterRepository();
}
public function createUserTenantRepository(): UserTenantRepositoryInterface
{
return $this->userTenantRepository ??= new UserTenantRepository();

View File

@@ -1,228 +0,0 @@
<?php
namespace MintyPHP\Service\User;
use MintyPHP\Repository\Support\RepoQuery;
use MintyPHP\Repository\User\UserSavedFilterRepositoryInterface;
class UserSavedFilterService
{
public const CONTEXT_ADDRESS_BOOK = 'address_book';
private const NAME_MAX_LENGTH = 120;
private const SEARCH_MAX_LENGTH = 200;
private const MAX_PER_USER_CONTEXT = 20;
public function __construct(private readonly UserSavedFilterRepositoryInterface $userSavedFilterRepository)
{
}
public function listForAddressBook(int $userId): array
{
return $this->listByContext($userId, self::CONTEXT_ADDRESS_BOOK);
}
public function saveAddressBookFilter(int $userId, string $name, array $rawQuery): array
{
return $this->saveByContext($userId, self::CONTEXT_ADDRESS_BOOK, $name, $rawQuery);
}
public function deleteAddressBookFilter(int $userId, string $uuid): bool
{
return $this->deleteByContext($userId, self::CONTEXT_ADDRESS_BOOK, $uuid);
}
public function listByContext(int $userId, string $context): array
{
if ($userId <= 0 || $context === '') {
return [];
}
$rows = $this->userSavedFilterRepository->listByUserAndContext($userId, $context);
$list = [];
foreach ($rows as $row) {
$name = trim((string) ($row['name'] ?? ''));
$uuid = trim((string) ($row['uuid'] ?? ''));
if ($name === '' || $uuid === '') {
continue;
}
$query = $this->normalizeQuery($this->decodeQuery((string) ($row['query_json'] ?? '')));
$list[] = [
'id' => (int) ($row['id'] ?? 0),
'uuid' => $uuid,
'name' => $name,
'query' => $query,
'created' => (string) ($row['created'] ?? ''),
];
}
return $list;
}
public function saveByContext(int $userId, string $context, string $name, array $rawQuery): array
{
$name = $this->truncate(trim($name), self::NAME_MAX_LENGTH);
if ($userId <= 0 || $context === '') {
return ['ok' => false, 'error' => 'invalid_user_or_context'];
}
if ($name === '') {
return ['ok' => false, 'error' => 'name_required'];
}
$count = $this->userSavedFilterRepository->countByUserAndContext($userId, $context);
if ($count >= self::MAX_PER_USER_CONTEXT) {
return ['ok' => false, 'error' => 'max_reached'];
}
$query = $this->normalizeQuery($rawQuery);
$queryJson = json_encode($query, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($queryJson)) {
$queryJson = '{}';
}
$id = $this->userSavedFilterRepository->create([
'user_id' => $userId,
'context' => $context,
'name' => $name,
'query_json' => $queryJson,
]);
if (!$id) {
return ['ok' => false, 'error' => 'create_failed'];
}
return ['ok' => true, 'id' => (int) $id, 'name' => $name, 'query' => $query];
}
public function deleteByContext(int $userId, string $context, string $uuid): bool
{
if ($userId <= 0 || $context === '' || trim($uuid) === '') {
return false;
}
return $this->userSavedFilterRepository->deleteByUuidForUserAndContext($uuid, $userId, $context);
}
private function decodeQuery(string $json): array
{
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : [];
}
private function normalizeQuery(array $rawQuery): array
{
$search = $this->truncate(trim((string) ($rawQuery['search'] ?? '')), self::SEARCH_MAX_LENGTH);
$tenants = $this->normalizeUuidList($rawQuery['tenants'] ?? []);
$departments = RepoQuery::normalizeIdList($rawQuery['departments'] ?? []);
$roles = RepoQuery::normalizeIdList($rawQuery['roles'] ?? []);
$custom = $this->normalizeCustomFieldQuery($rawQuery);
$query = [];
if ($search !== '') {
$query['search'] = $search;
}
if ($tenants) {
$query['tenants'] = $tenants;
}
if ($departments) {
$query['departments'] = $departments;
}
if ($roles) {
$query['roles'] = $roles;
}
foreach ($custom as $key => $value) {
$query[$key] = $value;
}
return $query;
}
private function normalizeCustomFieldQuery(array $rawQuery): array
{
$normalized = [];
foreach ($rawQuery as $rawKey => $rawValue) {
$key = strtolower(trim((string) $rawKey));
if ($key === '') {
continue;
}
if (preg_match('/^cf_[a-f0-9-]{36}$/', $key)) {
$value = trim((string) $rawValue);
if ($value !== '' && preg_match('/^[a-z0-9_-]{1,64}$/i', $value)) {
$normalized[$key] = $value;
}
continue;
}
if (preg_match('/^cfm_[a-f0-9-]{36}$/', $key)) {
$ids = RepoQuery::normalizeIdList($rawValue);
if ($ids) {
$normalized[$key] = $ids;
}
continue;
}
if (preg_match('/^cfd_[a-f0-9-]{36}_(from|to)$/', $key)) {
$value = trim((string) $rawValue);
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
if ($dt && $dt->format('Y-m-d') === $value) {
$normalized[$key] = $value;
}
}
}
ksort($normalized, SORT_STRING);
return $normalized;
}
private function normalizeUuidList($value): array
{
$items = $this->normalizeStringList($value);
$list = [];
foreach ($items as $item) {
$item = strtolower($item);
if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/', $item)) {
$list[] = $item;
}
}
$list = array_values(array_unique($list));
sort($list, SORT_STRING);
return $list;
}
private function normalizeStringList($value): array
{
$raw = is_array($value) ? $value : [$value];
$flat = [];
$collect = static function ($item) use (&$collect, &$flat): void {
if (is_array($item)) {
foreach ($item as $nested) {
$collect($nested);
}
return;
}
$text = trim((string) $item);
if ($text === '') {
return;
}
if (str_contains($text, ',')) {
foreach (explode(',', $text) as $part) {
$part = trim($part);
if ($part !== '') {
$flat[] = $part;
}
}
return;
}
$flat[] = $text;
};
foreach ($raw as $item) {
$collect($item);
}
return array_values(array_unique($flat));
}
private function truncate(string $value, int $maxLength): string
{
if ($maxLength <= 0 || $value === '') {
return $value;
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($value) > $maxLength) {
return mb_substr($value, 0, $maxLength);
}
return $value;
}
if (strlen($value) > $maxLength) {
return substr($value, 0, $maxLength);
}
return $value;
}
}

View File

@@ -8,7 +8,6 @@ use MintyPHP\Repository\Support\DatabaseSessionRepository;
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
use MintyPHP\Repository\User\UserListQueryRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserSavedFilterRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
use MintyPHP\Service\Access\AssignableRoleService;
use MintyPHP\Service\Audit\AuditServicesFactory;
@@ -19,7 +18,6 @@ class UserServicesFactory
private ?UserPasswordPolicyService $userPasswordPolicyService = null;
private ?UserPasswordService $userPasswordService = null;
private ?UserAvatarService $userAvatarService = null;
private ?UserSavedFilterService $userSavedFilterService = null;
private ?UserAssignmentService $userAssignmentService = null;
private ?UserTenantContextService $userTenantContextService = null;
private ?UserAccountService $userAccountService = null;
@@ -93,13 +91,6 @@ class UserServicesFactory
return $this->userAvatarService ??= new UserAvatarService();
}
public function createUserSavedFilterService(): UserSavedFilterService
{
return $this->userSavedFilterService ??= new UserSavedFilterService(
$this->createUserSavedFilterRepository()
);
}
public function createUserReadRepository(): UserReadRepositoryInterface
{
return $this->userRepositoryFactory->createUserReadRepository();
@@ -115,11 +106,6 @@ class UserServicesFactory
return $this->userRepositoryFactory->createUserListQueryRepository();
}
public function createUserSavedFilterRepository(): UserSavedFilterRepositoryInterface
{
return $this->userRepositoryFactory->createUserSavedFilterRepository();
}
public function createUserTenantRepository(): UserTenantRepositoryInterface
{
return $this->userRepositoryFactory->createUserTenantRepository();