Fix countArchived() loading all members into memory (Closes #201)
Database Portability Tests / Unit Tests (PlatformHelper) (pull_request) Failing after 38s
Database Portability Tests / Integration (mysql) (pull_request) Has been skipped
Database Portability Tests / Integration (postgres) (pull_request) Has been skipped
Database Portability Tests / Integration (sqlite) (pull_request) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (pull_request) Successful in 5s

Replace in-memory filtering with a SQL COUNT(*) query in the mapper,
matching the pattern of the existing countAll() method.
This commit is contained in:
shahondin1624
2026-04-29 19:13:52 +02:00
parent 967bacf231
commit d4e25fe739
5 changed files with 50 additions and 9 deletions
+23
View File
@@ -372,6 +372,29 @@ class MemberMapper extends QBMapper {
return $count;
}
/**
* Count all soft-deleted (archived) members.
*
* Uses a single SQL COUNT query instead of loading all members
* into memory.
*
* Part of Issue #201.
*
* @throws Exception
*/
public function countArchived(): int {
$qb = $this->db->getQueryBuilder();
$qb->select($qb->createFunction('COUNT(*)'))
->from($this->getTableName())
->where($qb->expr()->isNotNull('deleted_at'));
$result = $qb->executeQuery();
$count = (int)$result->fetchOne();
$result->closeCursor();
return $count;
}
// ── Joined fetch methods (N+1 avoidance) ────────────────────────
/**
+4 -2
View File
@@ -761,11 +761,13 @@ class MemberService {
/**
* Count all soft-deleted (archived) members.
*
* Part of Issue #201: uses a single SQL COUNT query via the mapper
* instead of loading all members into memory.
*
* @throws Exception
*/
public function countArchived(): int {
$allMembers = $this->memberMapper->findAll(null, null, true);
return count(array_filter($allMembers, fn(Member $m) => $m->getDeletedAt() !== null));
return $this->memberMapper->countArchived();
}
/**
+14
View File
@@ -543,6 +543,20 @@ class MapperTest extends TestCase {
$this->assertSame(0, $count);
}
public function testMemberMapperCountArchived(): void {
$this->configureResultCount(7);
$mapper = new MemberMapper($this->db);
$count = $mapper->countArchived();
$this->assertSame(7, $count);
}
public function testMemberMapperCountArchivedZero(): void {
$this->configureResultCount(0);
$mapper = new MemberMapper($this->db);
$count = $mapper->countArchived();
$this->assertSame(0, $count);
}
// ══════════════════════════════════════════════════════════════════
// ── FamilyMapper ─────────────────────────────────────────────────
// ══════════════════════════════════════════════════════════════════
+9 -7
View File
@@ -1299,17 +1299,19 @@ class MemberServiceTest extends TestCase {
// ── countArchived() ──────────────────────────────────────────────
public function testCountArchivedReturnsCorrectCount(): void {
$activeMember = $this->createMember(1);
$deletedMember = $this->createMember(2);
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
$this->memberMapper->method('findAll')
->with(null, null, true)
->willReturn([$activeMember, $deletedMember]);
// Issue #201: countArchived() now delegates to MemberMapper::countArchived()
// which uses a SQL COUNT query instead of loading all members into memory.
$this->memberMapper->method('countArchived')->willReturn(1);
$this->assertSame(1, $this->service->countArchived());
}
public function testCountArchivedReturnsZeroWhenNoArchived(): void {
$this->memberMapper->method('countArchived')->willReturn(0);
$this->assertSame(0, $this->service->countArchived());
}
// ── Joined methods: findAllWithRelations ──────────────────────
public function testFindAllWithRelationsReturnsMemberWithNestedSubEntities(): void {