diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 30bf4b0a..5834ce69 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -71,6 +71,21 @@ public function getAllUsers(): array return $this->toEntities(get_users()); } + /** @return array */ + public function getUserIdsBatch(int $number, int $offset): array + { + return array_map( + 'intval', + get_users([ + 'fields' => 'ID', + 'number' => $number, + 'offset' => $offset, + 'orderby' => 'ID', + 'order' => 'ASC', + ]), + ); + } + /** @return array */ public function getAllMemberUsers(): array { diff --git a/src/Service/MembershipService.php b/src/Service/MembershipService.php index 13e61f17..a677146a 100644 --- a/src/Service/MembershipService.php +++ b/src/Service/MembershipService.php @@ -368,11 +368,18 @@ private function extendSectionsDates(int $userId): void public function timeUnlockLevelsForAllUsers(): void { - $users = $this->userRepository->getAllUsers(); + $batchSize = 100; + $offset = 0; - foreach ($users as $user) { - $this->timeUnlockLevelsForUser($user->getId()); - } + do { + $userIds = $this->userRepository->getUserIdsBatch($batchSize, $offset); + + foreach ($userIds as $userId) { + $this->timeUnlockLevelsForUser($userId); + } + + $offset += count($userIds); + } while (count($userIds) === $batchSize); } public function timeUnlockLevelsForUser(int $userId): void diff --git a/tests/access_logic_tests.php b/tests/access_logic_tests.php index 1b4bf2a9..cd3e10ad 100644 --- a/tests/access_logic_tests.php +++ b/tests/access_logic_tests.php @@ -233,6 +233,47 @@ public function getActiveWithAccessByUserId(int $userId): array } } +final class BatchedUserRepository extends UserRepository +{ + /** @var array */ + public array $calls = []; + + public function __construct(private int $userCount) + { + } + + public function getUserIdsBatch(int $number, int $offset): array + { + $this->calls[] = [$number, $offset]; + + if ($offset >= $this->userCount) { + return []; + } + + return range( + $offset + 1, + min($offset + $number, $this->userCount), + ); + } +} + +final class BatchedMembershipService extends MembershipService +{ + /** @var array */ + public array $processedUserIds = []; + + public function __construct(UserRepository $userRepository) + { + $property = new ReflectionProperty(MembershipService::class, 'userRepository'); + $property->setValue($this, $userRepository); + } + + public function timeUnlockLevelsForUser(int $userId): void + { + $this->processedUserIds[] = $userId; + } +} + final class LoginLevelRepository extends LevelRepository { public function __construct(private MemberLevel $level) @@ -431,4 +472,19 @@ function createUnlockController( assertSameValue(null, $capturingService->savedMembership, 'Denied button unlock must not save a membership.'); } +$batchedUserRepository = new BatchedUserRepository(205); +$batchedMembershipService = new BatchedMembershipService($batchedUserRepository); +$batchedMembershipService->timeUnlockLevelsForAllUsers(); + +assertSameValue( + [[100, 0], [100, 100], [100, 200]], + $batchedUserRepository->calls, + 'Users must be loaded in batches of 100.', +); +assertSameValue( + range(1, 205), + $batchedMembershipService->processedUserIds, + 'Every user must be processed exactly once.', +); + echo "Access logic tests passed.\n";