From 43a32d189460778ad166551a789153d26395935b Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 09:30:34 +0000 Subject: [PATCH 01/12] Create UserDeletedRepository --- .../Repository/UserDeletedRepository.php | 27 ++++++++++++ .../Repository/UserDeletedRepositoryTest.php | 41 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/Database/Repository/UserDeletedRepository.php create mode 100644 tests/Unit/Database/Repository/UserDeletedRepositoryTest.php diff --git a/src/Database/Repository/UserDeletedRepository.php b/src/Database/Repository/UserDeletedRepository.php new file mode 100644 index 0000000000..5dad404d89 --- /dev/null +++ b/src/Database/Repository/UserDeletedRepository.php @@ -0,0 +1,27 @@ +database = $database; + } + + public function existsByUsername(string $username): bool + { + return $this->database->exists('userd', ['username' => $username]); + } +} diff --git a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php new file mode 100644 index 0000000000..e1b818d7d3 --- /dev/null +++ b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php @@ -0,0 +1,41 @@ +createStub(Database::class); + $database->method('exists')->willReturnMap([ + ['userd', ['username' => 'test'], true], + ]); + + $repo = new UserDeletedRepository($database); + + $this->assertTrue($repo->existsByUsername('test')); + } + + public function testExistsByUsernameReturnsFalse(): void + { + $database = $this->createStub(Database::class); + $database->method('exists')->willReturnMap([ + ['userd', ['username' => 'test'], false], + ]); + + $repo = new UserDeletedRepository($database); + + $this->assertFalse($repo->existsByUsername('test')); + } +} From 9cbf1e6982473c34b0466aa954517a72afd3a6d2 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 13:02:17 +0000 Subject: [PATCH 02/12] make use of UserDeletedRepository --- src/Module/Profile/Profile.php | 24 +++++++++++++++++++++--- src/Module/User/Import.php | 28 ++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index e84003e4dc..e522a5578b 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -24,6 +24,7 @@ use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; use Friendica\Database\DBA; +use Friendica\Database\Repository\UserDeletedRepository; use Friendica\Model\Contact; use Friendica\Model\Profile as ProfileModel; use Friendica\Model\Tag; @@ -46,6 +47,7 @@ class Profile extends BaseProfile { /** @var Database */ private $database; + private UserDeletedRepository $userDeletedRepository; /** @var AppHelper */ private $appHelper; /** @var IHandleUserSessions */ @@ -57,11 +59,27 @@ class Profile extends BaseProfile /** @var ProfileField */ private $profileField; - public function __construct(ProfileField $profileField, Page $page, IManageConfigValues $config, IHandleUserSessions $session, AppHelper $appHelper, Database $database, L10n $l10n, BaseURL $baseUrl, Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) - { + public function __construct( + ProfileField $profileField, + Page $page, + IManageConfigValues $config, + IHandleUserSessions $session, + AppHelper $appHelper, + Database $database, + UserDeletedRepository $userDeletedRepository, + L10n $l10n, + BaseURL $baseUrl, + Arguments $args, + LoggerInterface $logger, + Profiler $profiler, + Response $response, + array $server, + array $parameters = [] + ) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->database = $database; + $this->userDeletedRepository = $userDeletedRepository; $this->appHelper = $appHelper; $this->session = $session; $this->config = $config; @@ -84,7 +102,7 @@ class Profile extends BaseProfile } } - if ($this->database->exists('userd', ['username' => $this->parameters['nickname']])) { + if ($this->userDeletedRepository->existsByUsername($this->parameters['nickname'])) { // Known deleted user $data = ActivityPub\Transmitter::getDeletedUser($this->parameters['nickname']); diff --git a/src/Module/User/Import.php b/src/Module/User/Import.php index 74f2f9b6ad..55095a07a5 100644 --- a/src/Module/User/Import.php +++ b/src/Module/User/Import.php @@ -7,7 +7,8 @@ namespace Friendica\Module\User; -use Friendica\App; +use Friendica\App\Arguments; +use Friendica\App\BaseURL; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\PConfig\Capability\IManagePersonalConfigValues; @@ -19,6 +20,7 @@ use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\Database\Repository\UserDeletedRepository; use Friendica\Model\Photo; use Friendica\Model\Profile; use Friendica\Module\Response; @@ -47,20 +49,38 @@ class Import extends \Friendica\BaseModule /** @var Database */ private $database; + private UserDeletedRepository $userDeletedRepository; + /** @var PermissionSet */ private $permissionSet; /** @var UserSession */ private $session; - public function __construct(UserSession $session, PermissionSet $permissionSet, IManagePersonalConfigValues $pconfig, Database $database, SystemMessages $systemMessages, IManageConfigValues $config, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) - { + public function __construct( + UserSession $session, + PermissionSet $permissionSet, + IManagePersonalConfigValues $pconfig, + Database $database, + UserDeletedRepository $userDeletedRepository, + SystemMessages $systemMessages, + IManageConfigValues $config, + L10n $l10n, + BaseURL $baseUrl, + Arguments $args, + LoggerInterface $logger, + Profiler $profiler, + Response $response, + array $server, + array $parameters = [] + ) { parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->config = $config; $this->pconfig = $pconfig; $this->systemMessages = $systemMessages; $this->database = $database; + $this->userDeletedRepository = $userDeletedRepository; $this->permissionSet = $permissionSet; $this->session = $session; } @@ -213,7 +233,7 @@ class Import extends \Friendica\BaseModule // check for username // check if username matches deleted account if ($this->database->exists('user', ['nickname' => $account['user']['nickname']]) - || $this->database->exists('userd', ['username' => $account['user']['nickname']])) { + || $this->userDeletedRepository->existsByUsername($account['user']['nickname'])) { $this->systemMessages->addNotice($this->t("User '%s' already exists on this server!", $account['user']['nickname'])); return; } From 23d052c7fec27520307f767a38667483b92065c6 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 13:12:32 +0000 Subject: [PATCH 03/12] Create DatabaseService to access all repositories --- src/DI.php | 5 +++++ src/Database/DatabaseService.php | 28 ++++++++++++++++++++++++++++ src/Model/User.php | 8 ++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/Database/DatabaseService.php diff --git a/src/DI.php b/src/DI.php index bcbc17651b..fa9dfbda78 100644 --- a/src/DI.php +++ b/src/DI.php @@ -80,6 +80,11 @@ abstract class DI return self::$dice->create(AppHelper::class); } + public static function databaseService(): Database\DatabaseService + { + return self::$dice->create(Database\DatabaseService::class); + } + /** * @return Database\Database */ diff --git a/src/Database/DatabaseService.php b/src/Database/DatabaseService.php new file mode 100644 index 0000000000..3b46c95f1d --- /dev/null +++ b/src/Database/DatabaseService.php @@ -0,0 +1,28 @@ +database = $database; + } + + public function getUserDeletedRepository(): UserDeletedRepository + { + return new UserDeletedRepository($this->database); + } +} diff --git a/src/Model/User.php b/src/Model/User.php index 927a1a82bd..e9598b61e7 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -266,10 +266,12 @@ class User return $system_actor_name; } + $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + // List of possible actor names $possible_accounts = ['friendica', 'actor', 'system', 'internal']; foreach ($possible_accounts as $name) { - if (!DBA::exists('user', ['nickname' => $name]) && !DBA::exists('userd', ['username' => $name])) { + if (!DBA::exists('user', ['nickname' => $name]) && !$userDeletedRepository->existsByUsername($name)) { DI::config()->set('system', 'actor_name', $name); return $name; } @@ -1295,10 +1297,12 @@ class User throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.')); } + $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + // Check existing and deleted accounts for this nickname. if ( DBA::exists('user', ['nickname' => $nickname]) - || DBA::exists('userd', ['username' => $nickname]) + || $userDeletedRepository->existsByUsername($nickname) ) { throw new Exception(DI::l10n()->t('Nickname is already registered. Please choose another.')); } From 00b4bd19d899e653c3f44bd4bd640f572192b5bc Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 14:08:45 +0000 Subject: [PATCH 04/12] allow inserting username with UserDeletedRepository --- .../Repository/UserDeletedRepository.php | 24 ++++++++++++++++ .../Repository/UserDeletedRepositoryTest.php | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Database/Repository/UserDeletedRepository.php b/src/Database/Repository/UserDeletedRepository.php index 5dad404d89..72f3c1cfe8 100644 --- a/src/Database/Repository/UserDeletedRepository.php +++ b/src/Database/Repository/UserDeletedRepository.php @@ -9,8 +9,13 @@ declare(strict_types=1); namespace Friendica\Database\Repository; +use Exception; use Friendica\Database\Database; +use Friendica\Database\DatabaseException; +/** + * Repository for deleted users + */ final class UserDeletedRepository { private Database $database; @@ -20,6 +25,25 @@ final class UserDeletedRepository $this->database = $database; } + /** + * Insert a deleted user by username. + * + * @throws \Exception If the username could not be inserted + */ + public function insertByUsername(string $username): void + { + $result = $this->database->insert('userd', ['username' => $username]); + + if ($result === false) { + throw new Exception(sprintf('Error while inserting username `%s` as deleted user.', $username)); + } + } + + /** + * Check if a deleted username exists. + * + * @throws \Exception + */ public function existsByUsername(string $username): bool { return $this->database->exists('userd', ['username' => $username]); diff --git a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php index e1b818d7d3..d4d1c793e9 100644 --- a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php +++ b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php @@ -9,12 +9,40 @@ declare(strict_types=1); namespace Friendica\Test\Unit\Database\Repository; +use Exception; use Friendica\Database\Database; use Friendica\Database\Repository\UserDeletedRepository; use PHPUnit\Framework\TestCase; class UserDeletedRepositoryTest extends TestCase { + public function testInsertByUsernameReturnsFalse(): void + { + $database = $this->createMock(Database::class); + $database->expects($this->once())->method('insert')->willReturnMap([ + ['userd', ['username' => 'test'], 0, true], + ]); + + $repo = new UserDeletedRepository($database); + + $repo->insertByUsername('test'); + } + + public function testInsertByUsernameThrowsException(): void + { + $database = $this->createMock(Database::class); + $database->expects($this->once())->method('insert')->willReturnMap([ + ['userd', ['username' => 'test'], 0, false], + ]); + + $repo = new UserDeletedRepository($database); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Error while inserting username `test` as deleted user.'); + + $repo->insertByUsername('test'); + } + public function testExistsByUsernameReturnsTrue(): void { $database = $this->createStub(Database::class); From 2879db12dcfdd8e386332a0970fbc76e4d6f0ae4 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 14:23:04 +0000 Subject: [PATCH 05/12] Use UserDeletedRepository to insert deleted username --- src/Model/User.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Model/User.php b/src/Model/User.php index e9598b61e7..3f5ba9486e 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -1793,9 +1793,15 @@ class User Hook::callAll('remove_user', $user); + $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + // save username (actually the nickname as it is guaranteed // unique), so it cannot be re-registered in the future. - DBA::insert('userd', ['username' => $user['nickname']]); + try { + $userDeletedRepository->insertByUsername($user['nickname']); + } catch (\Throwable $th) { + DI::logger()->error('Error while inserting username of deleted user.', ['username' => $user['nickname'], 'exception' => $th]); + } // Remove all personal settings, especially connector settings DBA::delete('pconfig', ['uid' => $uid]); From e4a93fc9c7e84ed529ed8033fb65212c6336b325 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 15 Apr 2025 14:48:01 +0000 Subject: [PATCH 06/12] Work with DatabaseException in UserDeletedRepository --- src/Database/Database.php | 10 +++++----- src/Database/Repository/UserDeletedRepository.php | 10 ++++++---- tests/FixtureTestTrait.php | 2 +- .../Repository/UserDeletedRepositoryTest.php | 14 +++++++------- tests/Util/CreateDatabaseTrait.php | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 6ee4394901..cfa40663e6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -69,7 +69,7 @@ class Database private $affected_rows = 0; protected $in_transaction = false; protected $in_retrial = false; - protected $testmode = false; + private bool $throwExceptionsOnErrors = false; private $relation = []; /** @var DbaDefinition */ protected $dbaDefinition; @@ -205,9 +205,9 @@ class Database return $this->connected; } - public function setTestmode(bool $test) + public function throwExceptionsOnErrors(bool $throwExceptions): void { - $this->testmode = $test; + $this->throwExceptionsOnErrors = $throwExceptions; } /** @@ -672,7 +672,7 @@ class Database $error = $this->error; $errorno = $this->errorno; - if ($this->testmode) { + if ($this->throwExceptionsOnErrors) { throw new DatabaseException($error, $errorno, $this->replaceParameters($sql, $args)); } @@ -779,7 +779,7 @@ class Database $error = $this->error; $errorno = $this->errorno; - if ($this->testmode) { + if ($this->throwExceptionsOnErrors) { throw new DatabaseException($error, $errorno, $this->replaceParameters($sql, $params)); } diff --git a/src/Database/Repository/UserDeletedRepository.php b/src/Database/Repository/UserDeletedRepository.php index 72f3c1cfe8..baa1ab5f2a 100644 --- a/src/Database/Repository/UserDeletedRepository.php +++ b/src/Database/Repository/UserDeletedRepository.php @@ -28,14 +28,16 @@ final class UserDeletedRepository /** * Insert a deleted user by username. * - * @throws \Exception If the username could not be inserted + * @throws DatabaseException If the username could not be inserted */ public function insertByUsername(string $username): void { - $result = $this->database->insert('userd', ['username' => $username]); + $this->database->throwExceptionsOnErrors(true); - if ($result === false) { - throw new Exception(sprintf('Error while inserting username `%s` as deleted user.', $username)); + try { + $this->database->insert('userd', ['username' => $username]); + } finally { + $this->database->throwExceptionsOnErrors(false); } } diff --git a/tests/FixtureTestTrait.php b/tests/FixtureTestTrait.php index 8c4508eeb8..e8ffd69b03 100644 --- a/tests/FixtureTestTrait.php +++ b/tests/FixtureTestTrait.php @@ -60,7 +60,7 @@ trait FixtureTestTrait /** @var Database $dba */ $dba = $this->dice->create(Database::class); - $dba->setTestmode(true); + $dba->throwExceptionsOnErrors(true); DBStructure::checkInitialValues(); diff --git a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php index d4d1c793e9..80b706dd46 100644 --- a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php +++ b/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php @@ -9,14 +9,14 @@ declare(strict_types=1); namespace Friendica\Test\Unit\Database\Repository; -use Exception; use Friendica\Database\Database; +use Friendica\Database\DatabaseException; use Friendica\Database\Repository\UserDeletedRepository; use PHPUnit\Framework\TestCase; class UserDeletedRepositoryTest extends TestCase { - public function testInsertByUsernameReturnsFalse(): void + public function testInsertByUsernameCallsDatabase(): void { $database = $this->createMock(Database::class); $database->expects($this->once())->method('insert')->willReturnMap([ @@ -31,14 +31,14 @@ class UserDeletedRepositoryTest extends TestCase public function testInsertByUsernameThrowsException(): void { $database = $this->createMock(Database::class); - $database->expects($this->once())->method('insert')->willReturnMap([ - ['userd', ['username' => 'test'], 0, false], - ]); + $database->expects($this->exactly(2))->method('throwExceptionsOnErrors'); + $database->expects($this->once())->method('insert')->willThrowException( + new DatabaseException('An error occured.', 0, 'SQL query') + ); $repo = new UserDeletedRepository($database); - $this->expectException(Exception::class); - $this->expectExceptionMessage('Error while inserting username `test` as deleted user.'); + $this->expectException(DatabaseException::class); $repo->insertByUsername('test'); } diff --git a/tests/Util/CreateDatabaseTrait.php b/tests/Util/CreateDatabaseTrait.php index 05d3d6c0ca..c6ad09ef94 100644 --- a/tests/Util/CreateDatabaseTrait.php +++ b/tests/Util/CreateDatabaseTrait.php @@ -43,7 +43,7 @@ trait CreateDatabaseTrait ])); $database = new StaticDatabase($config, (new DbaDefinition($this->root->url()))->load(), (new ViewDefinition($this->root->url()))->load()); - $database->setTestmode(true); + $database->throwExceptionsOnErrors(true); return $database; } From 59ee2f34e71ae399049ce653172a4999ceaf6efc Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 16 Apr 2025 06:12:13 +0000 Subject: [PATCH 07/12] return previous value if database should throw exceptions --- src/Database/Database.php | 11 ++++++++++- src/Database/Repository/UserDeletedRepository.php | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index cfa40663e6..2495c03a77 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -205,9 +205,18 @@ class Database return $this->connected; } - public function throwExceptionsOnErrors(bool $throwExceptions): void + /** + * Should errors throwns as exceptions? + * + * @return bool returns the previous value + */ + public function throwExceptionsOnErrors(bool $throwExceptions): bool { + $prev = $this->throwExceptionsOnErrors; + $this->throwExceptionsOnErrors = $throwExceptions; + + return $prev; } /** diff --git a/src/Database/Repository/UserDeletedRepository.php b/src/Database/Repository/UserDeletedRepository.php index baa1ab5f2a..bd3e2301f7 100644 --- a/src/Database/Repository/UserDeletedRepository.php +++ b/src/Database/Repository/UserDeletedRepository.php @@ -32,12 +32,12 @@ final class UserDeletedRepository */ public function insertByUsername(string $username): void { - $this->database->throwExceptionsOnErrors(true); + $throw = $this->database->throwExceptionsOnErrors(true); try { $this->database->insert('userd', ['username' => $username]); } finally { - $this->database->throwExceptionsOnErrors(false); + $this->database->throwExceptionsOnErrors($throw); } } From a4ca74a3aaaddf9e022bce13eb642584f244e868 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 16 Apr 2025 10:40:07 +0000 Subject: [PATCH 08/12] Create CacheRepository for cache table --- src/Database/Repository/CacheRepository.php | 94 ++++++++++++++++++ .../Repository/CacheRepositoryTest.php | 97 +++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 src/Database/Repository/CacheRepository.php create mode 100644 tests/Unit/Database/Repository/CacheRepositoryTest.php diff --git a/src/Database/Repository/CacheRepository.php b/src/Database/Repository/CacheRepository.php new file mode 100644 index 0000000000..67d29840d8 --- /dev/null +++ b/src/Database/Repository/CacheRepository.php @@ -0,0 +1,94 @@ +database = $database; + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntil(string $expires): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, null); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException('Cannot fetch all keys without prefix', 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, $prefix); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot fetch all keys with prefix `%s`', $prefix), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + private function getAllKeys(string $expires, ?string $prefix = null): array + { + if ($prefix === null) { + $where = ['`expires` >= ?', $expires]; + } else { + $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', $expires, $prefix]; + } + + $stmt = $this->database->select('cache', ['k'], $where); + + $keys = []; + + try { + while ($key = $this->database->fetch($stmt)) { + array_push($keys, $key['k']); + } + } finally { + $this->database->close($stmt); + } + + return $keys; + } +} diff --git a/tests/Unit/Database/Repository/CacheRepositoryTest.php b/tests/Unit/Database/Repository/CacheRepositoryTest.php new file mode 100644 index 0000000000..b5cc1d453e --- /dev/null +++ b/tests/Unit/Database/Repository/CacheRepositoryTest.php @@ -0,0 +1,97 @@ +createStub(Database::class); + $database->method('select')->willReturnMap([ + ['cache', ['k'], ['`expires` >= ?', '2025-04-16 10:12:01'], [], $stmt], + ]); + $database->method('fetch')->willReturnOnConsecutiveCalls( + ['k' => 'value1'], + ['k' => 'value2'], + ['k' => 'value3'], + false + ); + + $repo = new CacheRepository($database); + + $this->assertSame( + [ + 'value1', + 'value2', + 'value3', + ], + $repo->getAllKeysValidUntil('2025-04-16 10:12:01') + ); + } + + public function testGetAllKeysValidUntilThrowsException(): void + { + $database = $this->createStub(Database::class); + $database->method('select')->willThrowException($this->createStub(Throwable::class)); + + $repo = new CacheRepository($database); + + $this->expectException(DatabaseException::class); + + $repo->getAllKeysValidUntil('2025-04-16 10:12:01'); + } + + public function testGetAllKeysValidUntilWithPrefixReturnsArray(): void + { + $stmt = new \stdClass; + + $database = $this->createStub(Database::class); + $database->method('select')->willReturnMap([ + ['cache', ['k'], ['`expires` >= ?', '2025-04-16 10:12:01'], [], $stmt], + ]); + $database->method('fetch')->willReturnOnConsecutiveCalls( + ['k' => 'value1'], + ['k' => 'value2'], + ['k' => 'value3'], + false + ); + + $repo = new CacheRepository($database); + + $this->assertSame( + [ + 'value1', + 'value2', + 'value3', + ], + $repo->getAllKeysValidUntilWithPrefix('2025-04-16 10:12:01', 'prefix') + ); + } + + public function testGetAllKeysValidUntilWithPrefixThrowsException(): void + { + $database = $this->createStub(Database::class); + $database->method('select')->willThrowException($this->createStub(Throwable::class)); + + $repo = new CacheRepository($database); + + $this->expectException(DatabaseException::class); + + $repo->getAllKeysValidUntilWithPrefix('2025-04-16 10:12:01', 'prefix'); + } +} From 03673419d48d5e2e5682c6cdd49e9c18e8ad315d Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 16 Apr 2025 12:42:36 +0000 Subject: [PATCH 09/12] Use CacheRepository in DatabaseCache --- src/Core/Cache/Type/DatabaseCache.php | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/Core/Cache/Type/DatabaseCache.php b/src/Core/Cache/Type/DatabaseCache.php index efb9a8d03e..14e22bfa72 100644 --- a/src/Core/Cache/Type/DatabaseCache.php +++ b/src/Core/Cache/Type/DatabaseCache.php @@ -11,6 +11,7 @@ use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Enum; use Friendica\Core\Cache\Exception\CachePersistenceException; use Friendica\Database\Database; +use Friendica\Database\Repository\CacheRepository; use Friendica\Util\DateTimeFormat; /** @@ -25,11 +26,15 @@ class DatabaseCache extends AbstractCache implements ICanCache */ private $dba; + private CacheRepository $cacheRepo; + public function __construct(string $hostname, Database $dba) { parent::__construct($hostname); $this->dba = $dba; + + $this->cacheRepo = new CacheRepository($dba); } /** @@ -41,27 +46,14 @@ class DatabaseCache extends AbstractCache implements ICanCache { try { if (empty($prefix)) { - $where = ['`expires` >= ?', DateTimeFormat::utcNow()]; + $keys = $this->cacheRepo->getAllKeysValidUntil(DateTimeFormat::utcNow()); } else { - $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix]; + $keys = $this->cacheRepo->getAllKeysValidUntilWithPrefix(DateTimeFormat::utcNow(), $prefix); } - - $stmt = $this->dba->select('cache', ['k'], $where); } catch (\Exception $exception) { throw new CachePersistenceException(sprintf('Cannot fetch all keys with prefix %s', $prefix), $exception); } - try { - $keys = []; - while ($key = $this->dba->fetch($stmt)) { - array_push($keys, $key['k']); - } - } catch (\Exception $exception) { - $this->dba->close($stmt); - throw new CachePersistenceException(sprintf('Cannot fetch all keys with prefix %s', $prefix), $exception); - } - - $this->dba->close($stmt); return $keys; } From f0ce49637dfa3803ed21582e196ba66e48ef1b49 Mon Sep 17 00:00:00 2001 From: Art4 Date: Wed, 16 Apr 2025 14:26:28 +0000 Subject: [PATCH 10/12] Create rich CacheEntity, allow CacheRepository find one entity by key --- src/Database/Entity/CacheEntity.php | 82 +++++++++++++++++++++ src/Database/Repository/CacheRepository.php | 39 ++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/Database/Entity/CacheEntity.php diff --git a/src/Database/Entity/CacheEntity.php b/src/Database/Entity/CacheEntity.php new file mode 100644 index 0000000000..295f4a2c26 --- /dev/null +++ b/src/Database/Entity/CacheEntity.php @@ -0,0 +1,82 @@ +k = (string) $data['k']; + $entity->v = $rawValue; + $entity->value = $value; + array_key_exists('expired', $data) ?? $entity->expired = (string) $data['expired']; + array_key_exists('updated', $data) ?? $entity->updated = (string) $data['updated']; + + return $entity; + } + + /** + * cache key + */ + private string $k = ''; + + /** + * cached serialized value + */ + private string $v = ''; + + /** + * + * @var mixed $value cached unserialized value + */ + private $value; + + /** + * datetime of cache expiration + */ + private string $expired = DBA::NULL_DATETIME; + + /** + * datetime of cache insertion + */ + private string $updated = DBA::NULL_DATETIME; + + private function __construct() {} + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Database/Repository/CacheRepository.php b/src/Database/Repository/CacheRepository.php index 67d29840d8..3d2e09bd15 100644 --- a/src/Database/Repository/CacheRepository.php +++ b/src/Database/Repository/CacheRepository.php @@ -11,6 +11,7 @@ namespace Friendica\Database\Repository; use Friendica\Database\Database; use Friendica\Database\DatabaseException; +use Friendica\Database\Entity\CacheEntity; use Throwable; /** @@ -69,6 +70,44 @@ final class CacheRepository } } + /** + * @throws DatabaseException + * + * @return CacheEntity|null + */ + public function findOneByKeyValidUntil(string $key, string $expires) + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + $cacheArray = $this->database->selectFirst( + 'cache', + ['v'], + ['`k` = ? AND (`expires` >= ? OR `expires` = -1)', $key, $expires] + ); + + if (!$this->database->isResult($cacheArray)) { + return null; + } + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot get cache entry with key `%s`', $key), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + + try { + $entity = CacheEntity::createFromArray($cacheArray); + } catch (Throwable $th) { + return null; + } + + return $entity; + } + private function getAllKeys(string $expires, ?string $prefix = null): array { if ($prefix === null) { From febcdd72c7b1011eea9016d60185e6fc5fd5c4ae Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 20 May 2025 07:32:04 +0000 Subject: [PATCH 11/12] move repositories and models into own namespace, create interfaces --- src/Core/Cache/Type/DatabaseCache.php | 2 +- src/Database/DatabaseService.php | 7 +- src/Database/Entity/CacheEntity.php | 65 +-------- src/Database/Repository/CacheRepository.php | 113 ++------------- .../Repository/DeletedUserRepository.php | 33 +++++ src/Model/CacheModel.php | 83 +++++++++++ src/Model/User.php | 6 +- src/Module/Profile/Profile.php | 10 +- src/Module/User/Import.php | 10 +- src/Repository/CacheRepository.php | 135 ++++++++++++++++++ .../UserdRepository.php} | 5 +- .../Repository/CacheRepositoryTest.php | 4 +- .../UserdRepositoryTest.php} | 22 ++- 13 files changed, 301 insertions(+), 194 deletions(-) create mode 100644 src/Database/Repository/DeletedUserRepository.php create mode 100644 src/Model/CacheModel.php create mode 100644 src/Repository/CacheRepository.php rename src/{Database/Repository/UserDeletedRepository.php => Repository/UserdRepository.php} (87%) rename tests/Unit/{Database => }/Repository/CacheRepositoryTest.php (95%) rename tests/Unit/{Database/Repository/UserDeletedRepositoryTest.php => Repository/UserdRepositoryTest.php} (73%) diff --git a/src/Core/Cache/Type/DatabaseCache.php b/src/Core/Cache/Type/DatabaseCache.php index 14e22bfa72..af79b111ec 100644 --- a/src/Core/Cache/Type/DatabaseCache.php +++ b/src/Core/Cache/Type/DatabaseCache.php @@ -11,7 +11,7 @@ use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Enum; use Friendica\Core\Cache\Exception\CachePersistenceException; use Friendica\Database\Database; -use Friendica\Database\Repository\CacheRepository; +use Friendica\Repository\CacheRepository; use Friendica\Util\DateTimeFormat; /** diff --git a/src/Database/DatabaseService.php b/src/Database/DatabaseService.php index 3b46c95f1d..8905c7e00c 100644 --- a/src/Database/DatabaseService.php +++ b/src/Database/DatabaseService.php @@ -10,7 +10,8 @@ declare(strict_types=1); namespace Friendica\Database; use Friendica\Database\Database; -use Friendica\Database\Repository\UserDeletedRepository; +use Friendica\Database\Repository\DeletedUserRepository; +use Friendica\Repository\UserdRepository; final class DatabaseService { @@ -21,8 +22,8 @@ final class DatabaseService $this->database = $database; } - public function getUserDeletedRepository(): UserDeletedRepository + public function getDeletedUserRepository(): DeletedUserRepository { - return new UserDeletedRepository($this->database); + return new UserdRepository($this->database); } } diff --git a/src/Database/Entity/CacheEntity.php b/src/Database/Entity/CacheEntity.php index 295f4a2c26..53d4e9048f 100644 --- a/src/Database/Entity/CacheEntity.php +++ b/src/Database/Entity/CacheEntity.php @@ -9,74 +9,13 @@ declare(strict_types=1); namespace Friendica\Database\Entity; -use Exception; -use Friendica\Database\DBA; - /** * Entity for a row in the cache table */ -final class CacheEntity +interface CacheEntity { - /** - * Validates the row array and creates the entity - * - * @throws Exception If $data['v'] contains invalid data that could not unserialize. - */ - public static function createFromArray(array $data): self - { - $rawValue = array_key_exists('v', $data) ? (string) $data['v'] : ''; - $value = @unserialize($rawValue); - - // Only return a value if the serialized value is valid. - // We also check if the db entry is a serialized - // boolean 'false' value (which we want to return). - if ($value === false && $rawValue !== 'b:0;' ) { - throw new Exception(sprintf('Invalid value data for cache object.')); - } - - $entity = new self(); - array_key_exists('k', $data) ?? $entity->k = (string) $data['k']; - $entity->v = $rawValue; - $entity->value = $value; - array_key_exists('expired', $data) ?? $entity->expired = (string) $data['expired']; - array_key_exists('updated', $data) ?? $entity->updated = (string) $data['updated']; - - return $entity; - } - - /** - * cache key - */ - private string $k = ''; - - /** - * cached serialized value - */ - private string $v = ''; - - /** - * - * @var mixed $value cached unserialized value - */ - private $value; - - /** - * datetime of cache expiration - */ - private string $expired = DBA::NULL_DATETIME; - - /** - * datetime of cache insertion - */ - private string $updated = DBA::NULL_DATETIME; - - private function __construct() {} - /** * @return mixed */ - public function getValue() - { - return $this->value; - } + public function getValue(); } diff --git a/src/Database/Repository/CacheRepository.php b/src/Database/Repository/CacheRepository.php index 3d2e09bd15..bdd46a870d 100644 --- a/src/Database/Repository/CacheRepository.php +++ b/src/Database/Repository/CacheRepository.php @@ -9,125 +9,32 @@ declare(strict_types=1); namespace Friendica\Database\Repository; -use Friendica\Database\Database; use Friendica\Database\DatabaseException; use Friendica\Database\Entity\CacheEntity; -use Throwable; /** - * Repository for cache table + * Interface for a cache repository */ -final class CacheRepository +interface CacheRepository { - private Database $database; - - public function __construct(Database $database) - { - $this->database = $database; - } + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntil(string $expires): array; /** * @throws DatabaseException * * @return array */ - public function getAllKeysValidUntil(string $expires): array - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - return $this->getAllKeys($expires, null); - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException('Cannot fetch all keys without prefix', 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - } - - /** - * @throws DatabaseException - * - * @return array - */ - public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - return $this->getAllKeys($expires, $prefix); - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException(sprintf('Cannot fetch all keys with prefix `%s`', $prefix), 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - } + public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array; /** * @throws DatabaseException * * @return CacheEntity|null */ - public function findOneByKeyValidUntil(string $key, string $expires) - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - $cacheArray = $this->database->selectFirst( - 'cache', - ['v'], - ['`k` = ? AND (`expires` >= ? OR `expires` = -1)', $key, $expires] - ); - - if (!$this->database->isResult($cacheArray)) { - return null; - } - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException(sprintf('Cannot get cache entry with key `%s`', $key), 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - - try { - $entity = CacheEntity::createFromArray($cacheArray); - } catch (Throwable $th) { - return null; - } - - return $entity; - } - - private function getAllKeys(string $expires, ?string $prefix = null): array - { - if ($prefix === null) { - $where = ['`expires` >= ?', $expires]; - } else { - $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', $expires, $prefix]; - } - - $stmt = $this->database->select('cache', ['k'], $where); - - $keys = []; - - try { - while ($key = $this->database->fetch($stmt)) { - array_push($keys, $key['k']); - } - } finally { - $this->database->close($stmt); - } - - return $keys; - } + public function findOneByKeyValidUntil(string $key, string $expires); } diff --git a/src/Database/Repository/DeletedUserRepository.php b/src/Database/Repository/DeletedUserRepository.php new file mode 100644 index 0000000000..7eeea4456f --- /dev/null +++ b/src/Database/Repository/DeletedUserRepository.php @@ -0,0 +1,33 @@ +k = (string) $data['k']; + $entity->v = $rawValue; + $entity->value = $value; + array_key_exists('expired', $data) ?? $entity->expired = (string) $data['expired']; + array_key_exists('updated', $data) ?? $entity->updated = (string) $data['updated']; + + return $entity; + } + + /** + * cache key + */ + private string $k = ''; + + /** + * cached serialized value + */ + private string $v = ''; + + /** + * + * @var mixed $value cached unserialized value + */ + private $value; + + /** + * datetime of cache expiration + */ + private string $expired = DBA::NULL_DATETIME; + + /** + * datetime of cache insertion + */ + private string $updated = DBA::NULL_DATETIME; + + private function __construct() {} + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } +} diff --git a/src/Model/User.php b/src/Model/User.php index 9767d942dd..dec9400604 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -266,7 +266,7 @@ class User return $system_actor_name; } - $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + $userDeletedRepository = DI::databaseService()->getDeletedUserRepository(); // List of possible actor names $possible_accounts = ['friendica', 'actor', 'system', 'internal']; @@ -1301,7 +1301,7 @@ class User throw new Exception(DI::l10n()->t('Your nickname can only contain a-z, 0-9 and _.')); } - $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + $userDeletedRepository = DI::databaseService()->getDeletedUserRepository(); // Check existing and deleted accounts for this nickname. if ( @@ -1816,7 +1816,7 @@ class User $user = $hook_data['user'] ?? $user; - $userDeletedRepository = DI::databaseService()->getUserDeletedRepository(); + $userDeletedRepository = DI::databaseService()->getDeletedUserRepository(); // save username (actually the nickname as it is guaranteed // unique), so it cannot be re-registered in the future. diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index 9355bd7190..12f593a870 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -23,7 +23,7 @@ use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; use Friendica\Database\DBA; -use Friendica\Database\Repository\UserDeletedRepository; +use Friendica\Database\Repository\DeletedUserRepository; use Friendica\Event\HtmlFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Profile as ProfileModel; @@ -48,7 +48,7 @@ class Profile extends BaseProfile { /** @var Database */ private $database; - private UserDeletedRepository $userDeletedRepository; + private DeletedUserRepository $deletedUserRepository; /** @var AppHelper */ private $appHelper; /** @var IHandleUserSessions */ @@ -68,7 +68,7 @@ class Profile extends BaseProfile IHandleUserSessions $session, AppHelper $appHelper, Database $database, - UserDeletedRepository $userDeletedRepository, + DeletedUserRepository $deletedUserRepository, EventDispatcherInterface $eventDispatcher, L10n $l10n, BaseURL $baseUrl, @@ -82,7 +82,7 @@ class Profile extends BaseProfile parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters); $this->database = $database; - $this->userDeletedRepository = $userDeletedRepository; + $this->deletedUserRepository = $deletedUserRepository; $this->appHelper = $appHelper; $this->session = $session; $this->config = $config; @@ -106,7 +106,7 @@ class Profile extends BaseProfile } } - if ($this->userDeletedRepository->existsByUsername($this->parameters['nickname'])) { + if ($this->deletedUserRepository->existsByUsername($this->parameters['nickname'])) { // Known deleted user $data = ActivityPub\Transmitter::getDeletedUser($this->parameters['nickname']); diff --git a/src/Module/User/Import.php b/src/Module/User/Import.php index 55095a07a5..1855755a06 100644 --- a/src/Module/User/Import.php +++ b/src/Module/User/Import.php @@ -20,7 +20,7 @@ use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; -use Friendica\Database\Repository\UserDeletedRepository; +use Friendica\Database\Repository\DeletedUserRepository; use Friendica\Model\Photo; use Friendica\Model\Profile; use Friendica\Module\Response; @@ -49,7 +49,7 @@ class Import extends \Friendica\BaseModule /** @var Database */ private $database; - private UserDeletedRepository $userDeletedRepository; + private DeletedUserRepository $deletedUserRepository; /** @var PermissionSet */ private $permissionSet; @@ -62,7 +62,7 @@ class Import extends \Friendica\BaseModule PermissionSet $permissionSet, IManagePersonalConfigValues $pconfig, Database $database, - UserDeletedRepository $userDeletedRepository, + DeletedUserRepository $deletedUserRepository, SystemMessages $systemMessages, IManageConfigValues $config, L10n $l10n, @@ -80,7 +80,7 @@ class Import extends \Friendica\BaseModule $this->pconfig = $pconfig; $this->systemMessages = $systemMessages; $this->database = $database; - $this->userDeletedRepository = $userDeletedRepository; + $this->deletedUserRepository = $deletedUserRepository; $this->permissionSet = $permissionSet; $this->session = $session; } @@ -233,7 +233,7 @@ class Import extends \Friendica\BaseModule // check for username // check if username matches deleted account if ($this->database->exists('user', ['nickname' => $account['user']['nickname']]) - || $this->userDeletedRepository->existsByUsername($account['user']['nickname'])) { + || $this->deletedUserRepository->existsByUsername($account['user']['nickname'])) { $this->systemMessages->addNotice($this->t("User '%s' already exists on this server!", $account['user']['nickname'])); return; } diff --git a/src/Repository/CacheRepository.php b/src/Repository/CacheRepository.php new file mode 100644 index 0000000000..6cdba5eafc --- /dev/null +++ b/src/Repository/CacheRepository.php @@ -0,0 +1,135 @@ +database = $database; + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntil(string $expires): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, null); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException('Cannot fetch all keys without prefix', 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, $prefix); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot fetch all keys with prefix `%s`', $prefix), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + /** + * @throws DatabaseException + * + * @return CacheEntity|null + */ + public function findOneByKeyValidUntil(string $key, string $expires) + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + $cacheArray = $this->database->selectFirst( + 'cache', + ['v'], + ['`k` = ? AND (`expires` >= ? OR `expires` = -1)', $key, $expires] + ); + + if (!$this->database->isResult($cacheArray)) { + return null; + } + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot get cache entry with key `%s`', $key), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + + try { + $entity = CacheModel::createFromArray($cacheArray); + } catch (Throwable $th) { + return null; + } + + return $entity; + } + + private function getAllKeys(string $expires, ?string $prefix = null): array + { + if ($prefix === null) { + $where = ['`expires` >= ?', $expires]; + } else { + $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', $expires, $prefix]; + } + + $stmt = $this->database->select('cache', ['k'], $where); + + $keys = []; + + try { + while ($key = $this->database->fetch($stmt)) { + array_push($keys, $key['k']); + } + } finally { + $this->database->close($stmt); + } + + return $keys; + } +} diff --git a/src/Database/Repository/UserDeletedRepository.php b/src/Repository/UserdRepository.php similarity index 87% rename from src/Database/Repository/UserDeletedRepository.php rename to src/Repository/UserdRepository.php index bd3e2301f7..b6f01772c8 100644 --- a/src/Database/Repository/UserDeletedRepository.php +++ b/src/Repository/UserdRepository.php @@ -7,16 +7,17 @@ declare(strict_types=1); -namespace Friendica\Database\Repository; +namespace Friendica\Repository; use Exception; use Friendica\Database\Database; use Friendica\Database\DatabaseException; +use Friendica\Database\Repository\DeletedUserRepository; /** * Repository for deleted users */ -final class UserDeletedRepository +final class UserdRepository implements DeletedUserRepository { private Database $database; diff --git a/tests/Unit/Database/Repository/CacheRepositoryTest.php b/tests/Unit/Repository/CacheRepositoryTest.php similarity index 95% rename from tests/Unit/Database/Repository/CacheRepositoryTest.php rename to tests/Unit/Repository/CacheRepositoryTest.php index b5cc1d453e..01a1f01348 100644 --- a/tests/Unit/Database/Repository/CacheRepositoryTest.php +++ b/tests/Unit/Repository/CacheRepositoryTest.php @@ -7,11 +7,11 @@ declare(strict_types=1); -namespace Friendica\Test\Unit\Database\Repository; +namespace Friendica\Test\Unit\Repository; use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Database\Repository\CacheRepository; +use Friendica\Repository\CacheRepository; use PHPUnit\Framework\TestCase; use Throwable; diff --git a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php b/tests/Unit/Repository/UserdRepositoryTest.php similarity index 73% rename from tests/Unit/Database/Repository/UserDeletedRepositoryTest.php rename to tests/Unit/Repository/UserdRepositoryTest.php index 80b706dd46..ebfeb5f3b2 100644 --- a/tests/Unit/Database/Repository/UserDeletedRepositoryTest.php +++ b/tests/Unit/Repository/UserdRepositoryTest.php @@ -7,15 +7,23 @@ declare(strict_types=1); -namespace Friendica\Test\Unit\Database\Repository; +namespace Friendica\Test\Unit\Repository; use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Database\Repository\UserDeletedRepository; +use Friendica\Database\Repository\DeletedUserRepository; +use Friendica\Repository\UserdRepository; use PHPUnit\Framework\TestCase; -class UserDeletedRepositoryTest extends TestCase +class UserdRepositoryTest extends TestCase { + public function testImplementationOfInterfaces(): void + { + $repo = new UserdRepository($this->createMock(Database::class)); + + $this->assertInstanceOf(DeletedUserRepository::class, $repo); + } + public function testInsertByUsernameCallsDatabase(): void { $database = $this->createMock(Database::class); @@ -23,7 +31,7 @@ class UserDeletedRepositoryTest extends TestCase ['userd', ['username' => 'test'], 0, true], ]); - $repo = new UserDeletedRepository($database); + $repo = new UserdRepository($database); $repo->insertByUsername('test'); } @@ -36,7 +44,7 @@ class UserDeletedRepositoryTest extends TestCase new DatabaseException('An error occured.', 0, 'SQL query') ); - $repo = new UserDeletedRepository($database); + $repo = new UserdRepository($database); $this->expectException(DatabaseException::class); @@ -50,7 +58,7 @@ class UserDeletedRepositoryTest extends TestCase ['userd', ['username' => 'test'], true], ]); - $repo = new UserDeletedRepository($database); + $repo = new UserdRepository($database); $this->assertTrue($repo->existsByUsername('test')); } @@ -62,7 +70,7 @@ class UserDeletedRepositoryTest extends TestCase ['userd', ['username' => 'test'], false], ]); - $repo = new UserDeletedRepository($database); + $repo = new UserdRepository($database); $this->assertFalse($repo->existsByUsername('test')); } From 02fb800d12905a6e79f705f1b35a5a53f046390b Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 20 May 2025 08:46:41 +0000 Subject: [PATCH 12/12] Change repository naming to avoid naming conflicts --- src/Core/Cache/Type/DatabaseCache.php | 4 +- src/Database/DatabaseService.php | 13 +- src/{ => Database}/Model/CacheModel.php | 4 +- src/Database/Repository/CacheRepository.php | 40 ------ .../Repository/CacheTableRepository.php | 135 ++++++++++++++++++ .../Repository/UserdTableRepository.php} | 8 +- src/{Database => }/Entity/CacheEntity.php | 4 +- src/Module/Profile/Profile.php | 2 +- src/Module/User/Import.php | 2 +- src/Repository/CacheRepository.php | 117 ++------------- .../Repository/DeletedUserRepository.php | 2 +- .../Repository/CacheTableRepositoryTest.php} | 14 +- .../Repository/UserdTableRepositoryTest.php} | 18 +-- 13 files changed, 186 insertions(+), 177 deletions(-) rename src/{ => Database}/Model/CacheModel.php (96%) delete mode 100644 src/Database/Repository/CacheRepository.php create mode 100644 src/Database/Repository/CacheTableRepository.php rename src/{Repository/UserdRepository.php => Database/Repository/UserdTableRepository.php} (84%) rename src/{Database => }/Entity/CacheEntity.php (78%) rename src/{Database => }/Repository/DeletedUserRepository.php (94%) rename tests/Unit/{Repository/CacheRepositoryTest.php => Database/Repository/CacheTableRepositoryTest.php} (86%) rename tests/Unit/{Repository/UserdRepositoryTest.php => Database/Repository/UserdTableRepositoryTest.php} (78%) diff --git a/src/Core/Cache/Type/DatabaseCache.php b/src/Core/Cache/Type/DatabaseCache.php index af79b111ec..8f5b554606 100644 --- a/src/Core/Cache/Type/DatabaseCache.php +++ b/src/Core/Cache/Type/DatabaseCache.php @@ -11,6 +11,7 @@ use Friendica\Core\Cache\Capability\ICanCache; use Friendica\Core\Cache\Enum; use Friendica\Core\Cache\Exception\CachePersistenceException; use Friendica\Database\Database; +use Friendica\DI; use Friendica\Repository\CacheRepository; use Friendica\Util\DateTimeFormat; @@ -34,7 +35,8 @@ class DatabaseCache extends AbstractCache implements ICanCache $this->dba = $dba; - $this->cacheRepo = new CacheRepository($dba); + // #TODO: Replace this with constuctor injection + $this->cacheRepo = DI::databaseService()->getCacheRepository(); } /** diff --git a/src/Database/DatabaseService.php b/src/Database/DatabaseService.php index 8905c7e00c..eafa7cbf4a 100644 --- a/src/Database/DatabaseService.php +++ b/src/Database/DatabaseService.php @@ -10,8 +10,10 @@ declare(strict_types=1); namespace Friendica\Database; use Friendica\Database\Database; -use Friendica\Database\Repository\DeletedUserRepository; -use Friendica\Repository\UserdRepository; +use Friendica\Database\Repository\CacheTableRepository; +use Friendica\Database\Repository\UserdTableRepository; +use Friendica\Repository\CacheRepository; +use Friendica\Repository\DeletedUserRepository; final class DatabaseService { @@ -24,6 +26,11 @@ final class DatabaseService public function getDeletedUserRepository(): DeletedUserRepository { - return new UserdRepository($this->database); + return new UserdTableRepository($this->database); + } + + public function getCacheRepository(): CacheRepository + { + return new CacheTableRepository($this->database); } } diff --git a/src/Model/CacheModel.php b/src/Database/Model/CacheModel.php similarity index 96% rename from src/Model/CacheModel.php rename to src/Database/Model/CacheModel.php index 83c287642c..9987645616 100644 --- a/src/Model/CacheModel.php +++ b/src/Database/Model/CacheModel.php @@ -7,11 +7,11 @@ declare(strict_types=1); -namespace Friendica\Model; +namespace Friendica\Database\Model; use Exception; use Friendica\Database\DBA; -use Friendica\Database\Entity\CacheEntity; +use Friendica\Entity\CacheEntity; /** * Model for a row in the cache table diff --git a/src/Database/Repository/CacheRepository.php b/src/Database/Repository/CacheRepository.php deleted file mode 100644 index bdd46a870d..0000000000 --- a/src/Database/Repository/CacheRepository.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - public function getAllKeysValidUntil(string $expires): array; - - /** - * @throws DatabaseException - * - * @return array - */ - public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array; - - /** - * @throws DatabaseException - * - * @return CacheEntity|null - */ - public function findOneByKeyValidUntil(string $key, string $expires); -} diff --git a/src/Database/Repository/CacheTableRepository.php b/src/Database/Repository/CacheTableRepository.php new file mode 100644 index 0000000000..1f0b278ec3 --- /dev/null +++ b/src/Database/Repository/CacheTableRepository.php @@ -0,0 +1,135 @@ +database = $database; + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntil(string $expires): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, null); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException('Cannot fetch all keys without prefix', 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + return $this->getAllKeys($expires, $prefix); + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot fetch all keys with prefix `%s`', $prefix), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + } + + /** + * @throws DatabaseException + * + * @return CacheEntity|null + */ + public function findOneByKeyValidUntil(string $key, string $expires) + { + $throw = $this->database->throwExceptionsOnErrors(true); + + try { + $cacheArray = $this->database->selectFirst( + 'cache', + ['v'], + ['`k` = ? AND (`expires` >= ? OR `expires` = -1)', $key, $expires] + ); + + if (!$this->database->isResult($cacheArray)) { + return null; + } + } catch (Throwable $th) { + if (! $th instanceof DatabaseException) { + $th = new DatabaseException(sprintf('Cannot get cache entry with key `%s`', $key), 0, '', $th); + } + + throw $th; + } finally { + $this->database->throwExceptionsOnErrors($throw); + } + + try { + $entity = CacheModel::createFromArray($cacheArray); + } catch (Throwable $th) { + return null; + } + + return $entity; + } + + private function getAllKeys(string $expires, ?string $prefix = null): array + { + if ($prefix === null) { + $where = ['`expires` >= ?', $expires]; + } else { + $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', $expires, $prefix]; + } + + $stmt = $this->database->select('cache', ['k'], $where); + + $keys = []; + + try { + while ($key = $this->database->fetch($stmt)) { + array_push($keys, $key['k']); + } + } finally { + $this->database->close($stmt); + } + + return $keys; + } +} diff --git a/src/Repository/UserdRepository.php b/src/Database/Repository/UserdTableRepository.php similarity index 84% rename from src/Repository/UserdRepository.php rename to src/Database/Repository/UserdTableRepository.php index b6f01772c8..9095bbfaaa 100644 --- a/src/Repository/UserdRepository.php +++ b/src/Database/Repository/UserdTableRepository.php @@ -7,17 +7,17 @@ declare(strict_types=1); -namespace Friendica\Repository; +namespace Friendica\Database\Repository; use Exception; use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Database\Repository\DeletedUserRepository; +use Friendica\Repository\DeletedUserRepository; /** - * Repository for deleted users + * Repository for userd table */ -final class UserdRepository implements DeletedUserRepository +final class UserdTableRepository implements DeletedUserRepository { private Database $database; diff --git a/src/Database/Entity/CacheEntity.php b/src/Entity/CacheEntity.php similarity index 78% rename from src/Database/Entity/CacheEntity.php rename to src/Entity/CacheEntity.php index 53d4e9048f..d32baee105 100644 --- a/src/Database/Entity/CacheEntity.php +++ b/src/Entity/CacheEntity.php @@ -7,10 +7,10 @@ declare(strict_types=1); -namespace Friendica\Database\Entity; +namespace Friendica\Entity; /** - * Entity for a row in the cache table + * Entity for a cache entry */ interface CacheEntity { diff --git a/src/Module/Profile/Profile.php b/src/Module/Profile/Profile.php index 12f593a870..d437f0579d 100644 --- a/src/Module/Profile/Profile.php +++ b/src/Module/Profile/Profile.php @@ -23,7 +23,6 @@ use Friendica\Core\Renderer; use Friendica\Core\Session\Capability\IHandleUserSessions; use Friendica\Database\Database; use Friendica\Database\DBA; -use Friendica\Database\Repository\DeletedUserRepository; use Friendica\Event\HtmlFilterEvent; use Friendica\Model\Contact; use Friendica\Model\Profile as ProfileModel; @@ -36,6 +35,7 @@ use Friendica\Network\HTTPException; use Friendica\Network\HTTPException\InternalServerErrorException; use Friendica\Profile\ProfileField\Repository\ProfileField; use Friendica\Protocol\ActivityPub; +use Friendica\Repository\DeletedUserRepository; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; use Friendica\Util\Profiler; diff --git a/src/Module/User/Import.php b/src/Module/User/Import.php index 1855755a06..a3ff223084 100644 --- a/src/Module/User/Import.php +++ b/src/Module/User/Import.php @@ -20,7 +20,6 @@ use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\Database\DBStructure; -use Friendica\Database\Repository\DeletedUserRepository; use Friendica\Model\Photo; use Friendica\Model\Profile; use Friendica\Module\Response; @@ -28,6 +27,7 @@ use Friendica\Navigation\SystemMessages; use Friendica\Network\HTTPException; use Friendica\Object\Image; use Friendica\Protocol\Delivery; +use Friendica\Repository\DeletedUserRepository; use Friendica\Security\PermissionSet\Repository\PermissionSet; use Friendica\Util\Profiler; use Friendica\Util\Strings; diff --git a/src/Repository/CacheRepository.php b/src/Repository/CacheRepository.php index 6cdba5eafc..6da86e7540 100644 --- a/src/Repository/CacheRepository.php +++ b/src/Repository/CacheRepository.php @@ -9,127 +9,32 @@ declare(strict_types=1); namespace Friendica\Repository; -use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Database\Entity\CacheEntity; -use Friendica\Database\Repository\CacheRepository as CacheRepositoryInterface; -use Friendica\Model\CacheModel; -use Throwable; +use Friendica\Entity\CacheEntity; /** - * Repository for cache table + * Interface for a cache repository */ -final class CacheRepository implements CacheRepositoryInterface +interface CacheRepository { - private Database $database; - - public function __construct(Database $database) - { - $this->database = $database; - } + /** + * @throws DatabaseException + * + * @return array + */ + public function getAllKeysValidUntil(string $expires): array; /** * @throws DatabaseException * * @return array */ - public function getAllKeysValidUntil(string $expires): array - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - return $this->getAllKeys($expires, null); - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException('Cannot fetch all keys without prefix', 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - } - - /** - * @throws DatabaseException - * - * @return array - */ - public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - return $this->getAllKeys($expires, $prefix); - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException(sprintf('Cannot fetch all keys with prefix `%s`', $prefix), 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - } + public function getAllKeysValidUntilWithPrefix(string $expires, string $prefix): array; /** * @throws DatabaseException * * @return CacheEntity|null */ - public function findOneByKeyValidUntil(string $key, string $expires) - { - $throw = $this->database->throwExceptionsOnErrors(true); - - try { - $cacheArray = $this->database->selectFirst( - 'cache', - ['v'], - ['`k` = ? AND (`expires` >= ? OR `expires` = -1)', $key, $expires] - ); - - if (!$this->database->isResult($cacheArray)) { - return null; - } - } catch (Throwable $th) { - if (! $th instanceof DatabaseException) { - $th = new DatabaseException(sprintf('Cannot get cache entry with key `%s`', $key), 0, '', $th); - } - - throw $th; - } finally { - $this->database->throwExceptionsOnErrors($throw); - } - - try { - $entity = CacheModel::createFromArray($cacheArray); - } catch (Throwable $th) { - return null; - } - - return $entity; - } - - private function getAllKeys(string $expires, ?string $prefix = null): array - { - if ($prefix === null) { - $where = ['`expires` >= ?', $expires]; - } else { - $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', $expires, $prefix]; - } - - $stmt = $this->database->select('cache', ['k'], $where); - - $keys = []; - - try { - while ($key = $this->database->fetch($stmt)) { - array_push($keys, $key['k']); - } - } finally { - $this->database->close($stmt); - } - - return $keys; - } + public function findOneByKeyValidUntil(string $key, string $expires); } diff --git a/src/Database/Repository/DeletedUserRepository.php b/src/Repository/DeletedUserRepository.php similarity index 94% rename from src/Database/Repository/DeletedUserRepository.php rename to src/Repository/DeletedUserRepository.php index 7eeea4456f..54b8a36fbc 100644 --- a/src/Database/Repository/DeletedUserRepository.php +++ b/src/Repository/DeletedUserRepository.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace Friendica\Database\Repository; +namespace Friendica\Repository; use Exception; use Friendica\Database\DatabaseException; diff --git a/tests/Unit/Repository/CacheRepositoryTest.php b/tests/Unit/Database/Repository/CacheTableRepositoryTest.php similarity index 86% rename from tests/Unit/Repository/CacheRepositoryTest.php rename to tests/Unit/Database/Repository/CacheTableRepositoryTest.php index 01a1f01348..cb710b2a59 100644 --- a/tests/Unit/Repository/CacheRepositoryTest.php +++ b/tests/Unit/Database/Repository/CacheTableRepositoryTest.php @@ -7,15 +7,15 @@ declare(strict_types=1); -namespace Friendica\Test\Unit\Repository; +namespace Friendica\Test\Unit\Database\Repository; use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Repository\CacheRepository; +use Friendica\Database\Repository\CacheTableRepository; use PHPUnit\Framework\TestCase; use Throwable; -class CacheRepositoryTest extends TestCase +class CacheTableRepositoryTest extends TestCase { public function testGetAllKeysValidUntilReturnsArray(): void { @@ -32,7 +32,7 @@ class CacheRepositoryTest extends TestCase false ); - $repo = new CacheRepository($database); + $repo = new CacheTableRepository($database); $this->assertSame( [ @@ -49,7 +49,7 @@ class CacheRepositoryTest extends TestCase $database = $this->createStub(Database::class); $database->method('select')->willThrowException($this->createStub(Throwable::class)); - $repo = new CacheRepository($database); + $repo = new CacheTableRepository($database); $this->expectException(DatabaseException::class); @@ -71,7 +71,7 @@ class CacheRepositoryTest extends TestCase false ); - $repo = new CacheRepository($database); + $repo = new CacheTableRepository($database); $this->assertSame( [ @@ -88,7 +88,7 @@ class CacheRepositoryTest extends TestCase $database = $this->createStub(Database::class); $database->method('select')->willThrowException($this->createStub(Throwable::class)); - $repo = new CacheRepository($database); + $repo = new CacheTableRepository($database); $this->expectException(DatabaseException::class); diff --git a/tests/Unit/Repository/UserdRepositoryTest.php b/tests/Unit/Database/Repository/UserdTableRepositoryTest.php similarity index 78% rename from tests/Unit/Repository/UserdRepositoryTest.php rename to tests/Unit/Database/Repository/UserdTableRepositoryTest.php index ebfeb5f3b2..ff08ab5b5e 100644 --- a/tests/Unit/Repository/UserdRepositoryTest.php +++ b/tests/Unit/Database/Repository/UserdTableRepositoryTest.php @@ -7,19 +7,19 @@ declare(strict_types=1); -namespace Friendica\Test\Unit\Repository; +namespace Friendica\Test\Unit\Database\Repository; use Friendica\Database\Database; use Friendica\Database\DatabaseException; -use Friendica\Database\Repository\DeletedUserRepository; -use Friendica\Repository\UserdRepository; +use Friendica\Database\Repository\UserdTableRepository; +use Friendica\Repository\DeletedUserRepository; use PHPUnit\Framework\TestCase; -class UserdRepositoryTest extends TestCase +class UserdTableRepositoryTest extends TestCase { public function testImplementationOfInterfaces(): void { - $repo = new UserdRepository($this->createMock(Database::class)); + $repo = new UserdTableRepository($this->createMock(Database::class)); $this->assertInstanceOf(DeletedUserRepository::class, $repo); } @@ -31,7 +31,7 @@ class UserdRepositoryTest extends TestCase ['userd', ['username' => 'test'], 0, true], ]); - $repo = new UserdRepository($database); + $repo = new UserdTableRepository($database); $repo->insertByUsername('test'); } @@ -44,7 +44,7 @@ class UserdRepositoryTest extends TestCase new DatabaseException('An error occured.', 0, 'SQL query') ); - $repo = new UserdRepository($database); + $repo = new UserdTableRepository($database); $this->expectException(DatabaseException::class); @@ -58,7 +58,7 @@ class UserdRepositoryTest extends TestCase ['userd', ['username' => 'test'], true], ]); - $repo = new UserdRepository($database); + $repo = new UserdTableRepository($database); $this->assertTrue($repo->existsByUsername('test')); } @@ -70,7 +70,7 @@ class UserdRepositoryTest extends TestCase ['userd', ['username' => 'test'], false], ]); - $repo = new UserdRepository($database); + $repo = new UserdTableRepository($database); $this->assertFalse($repo->existsByUsername('test')); }