diff --git a/composer.json b/composer.json index cf3127706..65d1bf50b 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "phpunit/phpunit": "^9.5", "rector/rector": "^0.14", "symplify/easy-coding-standard": "^11.1", - "vimeo/psalm": "^4.26" + "vimeo/psalm": "^4.27" }, "suggest": { "bavix/laravel-wallet-swap": "Addition to the laravel-wallet library for quick setting of exchange rates", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b698303bb..331194d0f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -35,6 +35,16 @@ parameters: count: 1 path: src/Internal/Repository/WalletRepository.php + - + message: "#^Method Bavix\\\\Wallet\\\\Internal\\\\Service\\\\StateService\\:\\:get\\(\\) should return string\\|null but returns mixed\\.$#" + count: 2 + path: src/Internal/Service/StateService.php + + - + message: "#^Parameter \\#2 \\$callback of method Illuminate\\\\Contracts\\\\Cache\\\\Repository\\:\\:rememberForever\\(\\) expects Closure, mixed given\\.$#" + count: 1 + path: src/Internal/Service/StateService.php + - message: "#^Parameter \\#1 \\$number of method Bavix\\\\Wallet\\\\Internal\\\\Service\\\\MathServiceInterface\\:\\:round\\(\\) expects float\\|int\\|string, mixed given\\.$#" count: 1 @@ -74,3 +84,13 @@ parameters: message: "#^Parameter \\#2 \\$holderId of method Bavix\\\\Wallet\\\\Internal\\\\Repository\\\\WalletRepositoryInterface\\:\\:getBySlug\\(\\) expects int\\|string, mixed given\\.$#" count: 1 path: src/Services/WalletService.php + + - + message: "#^Cannot call method needs\\(\\) on mixed\\.$#" + count: 3 + path: src/WalletServiceProvider.php + + - + message: "#^Parameter \\#1 \\$abstract of method Illuminate\\\\Contracts\\\\Container\\\\Container\\:\\:make\\(\\) expects class\\-string\\, string given\\.$#" + count: 2 + path: src/WalletServiceProvider.php diff --git a/src/Internal/Service/DatabaseService.php b/src/Internal/Service/DatabaseService.php index 190eb7978..5927fc739 100644 --- a/src/Internal/Service/DatabaseService.php +++ b/src/Internal/Service/DatabaseService.php @@ -41,41 +41,42 @@ public function transaction(callable $callback): mixed ); } + if ($level > 0) { + return $callback(); + } + $this->init = true; try { - if ($level > 0) { - return $callback(); - } - $this->regulatorService->purge(); return $this->connection->transaction(function () use ($callback) { $result = $callback(); $this->init = false; - if ($result === false || (is_countable($result) && count($result) === 0)) { - $this->regulatorService->purge(); - } else { - $this->regulatorService->approve(); + if ($result === false) { + return false; } + if (is_countable($result) && count($result) === 0) { + return $result; + } + + $this->regulatorService->approve(); + return $result; }); } catch (RecordsNotFoundException|ExceptionInterface $exception) { - $this->regulatorService->purge(); - $this->init = false; - throw $exception; } catch (Throwable $throwable) { - $this->regulatorService->purge(); - $this->init = false; - throw new TransactionFailedException( 'Transaction failed. Message: ' . $throwable->getMessage(), ExceptionInterface::TRANSACTION_FAILED, $throwable ); + } finally { + $this->regulatorService->purge(); + $this->init = false; } } } diff --git a/src/Internal/Service/LockService.php b/src/Internal/Service/LockService.php index 1e0938eb2..029015fee 100644 --- a/src/Internal/Service/LockService.php +++ b/src/Internal/Service/LockService.php @@ -12,12 +12,13 @@ final class LockService implements LockServiceInterface { - private const PREFIX = 'wallet_lock::'; + private const LOCK_KEY = 'wallet_lock::'; - /** - * @var array - */ - private array $lockedKeys = []; + private const INNER_KEYS = 'inner_keys::'; + + private ?LockProvider $lockProvider = null; + + private CacheRepository $lockedKeys; private CacheRepository $cache; @@ -25,8 +26,9 @@ final class LockService implements LockServiceInterface public function __construct(CacheFactory $cacheFactory) { - $this->seconds = (int) config('wallet.lock.seconds', 1); $this->cache = $cacheFactory->store(config('wallet.lock.driver', 'array')); + $this->seconds = (int) config('wallet.lock.seconds', 1); + $this->lockedKeys = $cacheFactory->store('array'); } /** @@ -38,21 +40,20 @@ public function block(string $key, callable $callback): mixed return $callback(); } - $this->lockedKeys[$key] = true; + $lock = $this->getLockProvider() + ->lock(self::LOCK_KEY . $key, $this->seconds); + $this->lockedKeys->put(self::INNER_KEYS . $key, true, $this->seconds); try { - return $this->getLockProvider() - ->lock(self::PREFIX . $key) - ->block($this->seconds, $callback) - ; + return $lock->block($this->seconds, $callback); } finally { - unset($this->lockedKeys[$key]); + $this->lockedKeys->delete(self::INNER_KEYS . $key); } } public function isBlocked(string $key): bool { - return $this->lockedKeys[$key] ?? false; + return $this->lockedKeys->get(self::INNER_KEYS . $key) === true; } /** @@ -61,14 +62,18 @@ public function isBlocked(string $key): bool */ private function getLockProvider(): LockProvider { - $store = $this->cache->getStore(); - if ($store instanceof LockProvider) { - return $store; + if ($this->lockProvider === null) { + $store = $this->cache->getStore(); + if (! ($store instanceof LockProvider)) { + throw new LockProviderNotFoundException( + 'Lockable cache not found', + ExceptionInterface::LOCK_PROVIDER_NOT_FOUND + ); + } + + $this->lockProvider = $store; } - throw new LockProviderNotFoundException( - 'Lockable cache not found', - ExceptionInterface::LOCK_PROVIDER_NOT_FOUND - ); + return $this->lockProvider; } } diff --git a/src/Internal/Service/StateService.php b/src/Internal/Service/StateService.php index 2c99bffce..1e7ecce10 100644 --- a/src/Internal/Service/StateService.php +++ b/src/Internal/Service/StateService.php @@ -4,37 +4,45 @@ namespace Bavix\Wallet\Internal\Service; +use Illuminate\Contracts\Cache\Factory as CacheFactory; +use Illuminate\Contracts\Cache\Repository as CacheRepository; + final class StateService implements StateServiceInterface { - /** - * @var array - */ - private array $forks = []; + private const PREFIX_FORKS = 'wallet_forks::'; + + private const PREFIX_FORK_CALL = 'wallet_fork_call::'; + + private CacheRepository $forks; + + private CacheRepository $forkCallables; - /** - * @var array - */ - private array $forkCallables = []; + public function __construct(CacheFactory $cacheFactory) + { + $this->forks = $cacheFactory->store('array'); + $this->forkCallables = $cacheFactory->store('array'); + } public function fork(string $uuid, callable $value): void { - $this->forkCallables[$uuid] ??= $value; + if (! $this->forks->has(self::PREFIX_FORKS . $uuid)) { + $this->forkCallables->put(self::PREFIX_FORK_CALL . $uuid, $value); + } } public function get(string $uuid): ?string { - if ($this->forkCallables[$uuid] ?? null) { - $callable = $this->forkCallables[$uuid]; - unset($this->forkCallables[$uuid]); - - $this->forks[$uuid] = $callable(); + $callable = $this->forkCallables->pull(self::PREFIX_FORK_CALL . $uuid); + if ($callable !== null) { + return $this->forks->rememberForever(self::PREFIX_FORKS . $uuid, $callable); } - return $this->forks[$uuid] ?? null; + return $this->forks->get(self::PREFIX_FORKS . $uuid); } public function drop(string $uuid): void { - unset($this->forks[$uuid], $this->forkCallables[$uuid]); + $this->forkCallables->forget(self::PREFIX_FORK_CALL . $uuid); + $this->forks->forget(self::PREFIX_FORKS . $uuid); } } diff --git a/src/Internal/Service/StorageService.php b/src/Internal/Service/StorageService.php index e6db09943..01f480288 100644 --- a/src/Internal/Service/StorageService.php +++ b/src/Internal/Service/StorageService.php @@ -10,6 +10,8 @@ final class StorageService implements StorageServiceInterface { + private const PREFIX = 'wallet_sg::'; + public function __construct( private MathServiceInterface $mathService, private CacheRepository $cacheRepository @@ -23,7 +25,7 @@ public function flush(): bool public function missing(string $uuid): bool { - return $this->cacheRepository->forget($uuid); + return $this->cacheRepository->forget(self::PREFIX . $uuid); } /** @@ -31,7 +33,7 @@ public function missing(string $uuid): bool */ public function get(string $uuid): string { - $value = $this->cacheRepository->get($uuid); + $value = $this->cacheRepository->get(self::PREFIX . $uuid); if ($value === null) { throw new RecordNotFoundException( 'The repository did not find the object', @@ -44,7 +46,7 @@ public function get(string $uuid): string public function sync(string $uuid, float|int|string $value): bool { - return $this->cacheRepository->set($uuid, $value); + return $this->cacheRepository->forever(self::PREFIX . $uuid, $this->mathService->round($value)); } /** @@ -53,7 +55,7 @@ public function sync(string $uuid, float|int|string $value): bool public function increase(string $uuid, float|int|string $value): string { $result = $this->mathService->add($this->get($uuid), $value); - $this->sync($uuid, $result); + $this->sync($uuid, $this->mathService->round($result)); return $this->mathService->round($result); } diff --git a/src/Services/RegulatorService.php b/src/Services/RegulatorService.php index c04cbd660..63d8e09eb 100644 --- a/src/Services/RegulatorService.php +++ b/src/Services/RegulatorService.php @@ -86,29 +86,31 @@ public function decrease(Wallet $wallet, float|int|string $value): string public function approve(): void { - foreach ($this->wallets as $wallet) { - $diffValue = $this->diff($wallet); - if ($this->mathService->compare($diffValue, 0) === 0) { - continue; - } - - $balance = $this->bookkeeperService->increase($wallet, $diffValue); - $wallet->newQuery() - ->whereKey($wallet->getKey()) - ->update([ + try { + foreach ($this->wallets as $wallet) { + $diffValue = $this->diff($wallet); + if ($this->mathService->compare($diffValue, 0) === 0) { + continue; + } + + $balance = $this->bookkeeperService->increase($wallet, $diffValue); + $wallet->newQuery() + ->whereKey($wallet->getKey()) + ->update([ + 'balance' => $balance, + ]) // ?qN + ; + $wallet->fill([ 'balance' => $balance, - ]) // ?qN - ; - $wallet->fill([ - 'balance' => $balance, - ])->syncOriginalAttribute('balance'); - - $event = $this->balanceUpdatedEventAssembler->create($wallet); - $this->dispatcherService->dispatch($event); - } + ])->syncOriginalAttribute('balance'); - $this->dispatcherService->flush(); - $this->purge(); + $event = $this->balanceUpdatedEventAssembler->create($wallet); + $this->dispatcherService->dispatch($event); + } + $this->dispatcherService->flush(); + } finally { + $this->purge(); + } } public function purge(): void diff --git a/src/WalletServiceProvider.php b/src/WalletServiceProvider.php index 090fe4151..5cf2dbf46 100644 --- a/src/WalletServiceProvider.php +++ b/src/WalletServiceProvider.php @@ -203,7 +203,7 @@ private function shouldMigrate(): bool */ private function internal(array $configure): void { - $this->app->bind(StorageServiceInterface::class, $configure['storage'] ?? StorageService::class); + $this->app->alias($configure['storage'] ?? StorageService::class, 'wallet.internal.storage'); $this->app->singleton(ClockServiceInterface::class, $configure['clock'] ?? ClockService::class); $this->app->singleton(DatabaseServiceInterface::class, $configure['database'] ?? DatabaseService::class); @@ -247,31 +247,39 @@ private function services(array $configure, array $cache): void $this->app->singleton(TransferServiceInterface::class, $configure['transfer'] ?? TransferService::class); $this->app->singleton(WalletServiceInterface::class, $configure['wallet'] ?? WalletService::class); - $this->app->singleton(BookkeeperServiceInterface::class, fn () => $this->app->make( - $configure['bookkeeper'] ?? BookkeeperService::class, - [ - 'storageService' => $this->app->make( - StorageServiceLockDecorator::class, + // bookkeepper service + $this->app->when(StorageServiceLockDecorator::class) + ->needs(StorageServiceInterface::class) + ->give(function () use ($cache) { + return $this->app->make( + 'wallet.internal.storage', [ - 'cacheRepository' => $this->app->make(CacheFactory::class) + 'cacheRepository' => $this->app->get(CacheFactory::class) ->store($cache['driver'] ?? 'array'), ], - ), - ] - )); - - $this->app->singleton(RegulatorServiceInterface::class, fn () => $this->app->make( - $configure['regulator'] ?? RegulatorService::class, - [ - 'storageService' => $this->app->make( - StorageServiceInterface::class, + ); + }); + + $this->app->when($configure['bookkeeper'] ?? BookkeeperService::class) + ->needs(StorageServiceInterface::class) + ->give(StorageServiceLockDecorator::class); + + $this->app->singleton(BookkeeperServiceInterface::class, $configure['bookkeeper'] ?? BookkeeperService::class); + + // regulator service + $this->app->when($configure['regulator'] ?? RegulatorService::class) + ->needs(StorageServiceInterface::class) + ->give(function () { + return $this->app->make( + 'wallet.internal.storage', [ 'cacheRepository' => clone $this->app->make(CacheFactory::class) ->store('array'), ], - ), - ] - )); + ); + }); + + $this->app->singleton(RegulatorServiceInterface::class, $configure['regulator'] ?? RegulatorService::class); } /** diff --git a/tests/Units/Service/StorageTest.php b/tests/Units/Service/StorageTest.php index a41c85a0b..274836d03 100644 --- a/tests/Units/Service/StorageTest.php +++ b/tests/Units/Service/StorageTest.php @@ -7,7 +7,7 @@ use Bavix\Wallet\Internal\Decorator\StorageServiceLockDecorator; use Bavix\Wallet\Internal\Exceptions\ExceptionInterface; use Bavix\Wallet\Internal\Exceptions\RecordNotFoundException; -use Bavix\Wallet\Internal\Service\StorageServiceInterface; +use Bavix\Wallet\Internal\Service\StorageService; use Bavix\Wallet\Test\Infra\TestCase; /** @@ -19,7 +19,7 @@ public function testFlush(): void { $this->expectException(RecordNotFoundException::class); $this->expectExceptionCode(ExceptionInterface::RECORD_NOT_FOUND); - $storage = app(StorageServiceInterface::class); + $storage = app(StorageService::class); self::assertTrue($storage->sync('hello', 34)); self::assertTrue($storage->sync('world', 42));