Merge pull request 'Fix findArchived() filtering all members in memory (Closes #202)' (#202) from fix/find-archived-filtering-in-memory into main
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 37s
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 5s
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 37s
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 5s
This commit was merged in pull request #202.
This commit is contained in:
@@ -395,6 +395,34 @@ class MemberMapper extends QBMapper {
|
|||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all soft-deleted (archived) members with optional pagination.
|
||||||
|
*
|
||||||
|
* Uses a single SQL query with WHERE deleted_at IS NOT NULL instead
|
||||||
|
* of loading all members into memory and filtering in PHP.
|
||||||
|
*
|
||||||
|
* Part of Issue #202.
|
||||||
|
*
|
||||||
|
* @return Member[]
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function findArchived(?int $limit = null, ?int $offset = null): array {
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->getTableName())
|
||||||
|
->where($qb->expr()->isNotNull('deleted_at'))
|
||||||
|
->orderBy('deleted_at', 'DESC');
|
||||||
|
|
||||||
|
if ($limit !== null) {
|
||||||
|
$qb->setMaxResults($limit);
|
||||||
|
}
|
||||||
|
if ($offset !== null) {
|
||||||
|
$qb->setFirstResult($offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->findEntities($qb);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Joined fetch methods (N+1 avoidance) ────────────────────────
|
// ── Joined fetch methods (N+1 avoidance) ────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -738,10 +738,7 @@ class MemberService {
|
|||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function findArchived(?int $limit = null, ?int $offset = null): array {
|
public function findArchived(?int $limit = null, ?int $offset = null): array {
|
||||||
$members = $this->memberMapper->findAll($limit, $offset, true);
|
$members = $this->memberMapper->findArchived($limit, $offset);
|
||||||
|
|
||||||
// Filter to only soft-deleted members
|
|
||||||
$archived = array_filter($members, fn(Member $m) => $m->getDeletedAt() !== null);
|
|
||||||
|
|
||||||
return array_values(array_map(function (Member $member) {
|
return array_values(array_map(function (Member $member) {
|
||||||
// Only return retained fields
|
// Only return retained fields
|
||||||
@@ -755,7 +752,7 @@ class MemberService {
|
|||||||
'deletedAt' => $member->getDeletedAt(),
|
'deletedAt' => $member->getDeletedAt(),
|
||||||
'mitgliedsdauer' => $this->calculateMitgliedsdauer($member),
|
'mitgliedsdauer' => $this->calculateMitgliedsdauer($member),
|
||||||
];
|
];
|
||||||
}, $archived));
|
}, $members));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -557,6 +557,31 @@ class MapperTest extends TestCase {
|
|||||||
$this->assertSame(0, $count);
|
$this->assertSame(0, $count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testMemberMapperFindArchivedNoParams(): void {
|
||||||
|
$row = $this->memberRow(1);
|
||||||
|
$row['deleted_at'] = '2026-01-01 00:00:00';
|
||||||
|
$this->configureResultRows([$row]);
|
||||||
|
$mapper = new MemberMapper($this->db);
|
||||||
|
$members = $mapper->findArchived();
|
||||||
|
$this->assertCount(1, $members);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMemberMapperFindArchivedWithPagination(): void {
|
||||||
|
$row = $this->memberRow(1);
|
||||||
|
$row['deleted_at'] = '2026-01-01 00:00:00';
|
||||||
|
$this->configureResultRows([$row]);
|
||||||
|
$mapper = new MemberMapper($this->db);
|
||||||
|
$members = $mapper->findArchived(10, 0);
|
||||||
|
$this->assertCount(1, $members);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMemberMapperFindArchivedEmpty(): void {
|
||||||
|
$this->configureResultRows([]);
|
||||||
|
$mapper = new MemberMapper($this->db);
|
||||||
|
$members = $mapper->findArchived();
|
||||||
|
$this->assertCount(0, $members);
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
// ── FamilyMapper ─────────────────────────────────────────────────
|
// ── FamilyMapper ─────────────────────────────────────────────────
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1276,17 +1276,20 @@ class MemberServiceTest extends TestCase {
|
|||||||
|
|
||||||
// ── findArchived() ──────────────────────────────────────────────
|
// ── findArchived() ──────────────────────────────────────────────
|
||||||
|
|
||||||
public function testFindArchivedReturnsOnlyDeletedMembers(): void {
|
public function testFindArchivedDelegatesToMapper(): void {
|
||||||
$activeMember = $this->createMember(1);
|
// Issue #202: findArchived() now delegates to MemberMapper::findArchived()
|
||||||
$deletedMember = $this->createMember(2, 'Anna', 'Schmidt');
|
// which uses a SQL WHERE deleted_at IS NOT NULL query instead of
|
||||||
|
// loading all members into memory and filtering in PHP.
|
||||||
|
$deletedMember = $this->createMember(1, 'Anna', 'Schmidt');
|
||||||
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
|
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
|
||||||
$deletedMember->setStatus('geloescht');
|
$deletedMember->setStatus('geloescht');
|
||||||
$deletedMember->setEintritt('2018-01-01');
|
$deletedMember->setEintritt('2018-01-01');
|
||||||
$deletedMember->setAustritt('2025-12-31');
|
$deletedMember->setAustritt('2025-12-31');
|
||||||
|
|
||||||
$this->memberMapper->method('findAll')
|
$this->memberMapper->expects($this->once())
|
||||||
->with(null, null, true)
|
->method('findArchived')
|
||||||
->willReturn([$activeMember, $deletedMember]);
|
->with(null, null)
|
||||||
|
->willReturn([$deletedMember]);
|
||||||
|
|
||||||
$result = $this->service->findArchived();
|
$result = $this->service->findArchived();
|
||||||
|
|
||||||
@@ -1296,6 +1299,49 @@ class MemberServiceTest extends TestCase {
|
|||||||
$this->assertArrayHasKey('deletedAt', $result[0]);
|
$this->assertArrayHasKey('deletedAt', $result[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindArchivedPassesPaginationToMapper(): void {
|
||||||
|
$this->memberMapper->expects($this->once())
|
||||||
|
->method('findArchived')
|
||||||
|
->with(10, 20)
|
||||||
|
->willReturn([]);
|
||||||
|
|
||||||
|
$this->service->findArchived(10, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindArchivedReturnsEmptyArrayWhenNoArchivedMembers(): void {
|
||||||
|
$this->memberMapper->method('findArchived')->willReturn([]);
|
||||||
|
|
||||||
|
$result = $this->service->findArchived();
|
||||||
|
$this->assertSame([], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindArchivedReturnsOnlyRetainedFields(): void {
|
||||||
|
$deletedMember = $this->createMember(1, 'Anna', 'Schmidt');
|
||||||
|
$deletedMember->setDeletedAt('2026-01-01 00:00:00');
|
||||||
|
$deletedMember->setEintritt('2018-01-01');
|
||||||
|
$deletedMember->setAustritt('2025-12-31');
|
||||||
|
|
||||||
|
$this->memberMapper->method('findArchived')->willReturn([$deletedMember]);
|
||||||
|
|
||||||
|
$result = $this->service->findArchived();
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$entry = $result[0];
|
||||||
|
// Retained fields should be present
|
||||||
|
$this->assertArrayHasKey('id', $entry);
|
||||||
|
$this->assertArrayHasKey('vorname', $entry);
|
||||||
|
$this->assertArrayHasKey('nachname', $entry);
|
||||||
|
$this->assertArrayHasKey('geburtsdatum', $entry);
|
||||||
|
$this->assertArrayHasKey('eintritt', $entry);
|
||||||
|
$this->assertArrayHasKey('austritt', $entry);
|
||||||
|
$this->assertArrayHasKey('deletedAt', $entry);
|
||||||
|
$this->assertArrayHasKey('mitgliedsdauer', $entry);
|
||||||
|
// Sensitive fields should NOT be present
|
||||||
|
$this->assertArrayNotHasKey('notizen', $entry);
|
||||||
|
$this->assertArrayNotHasKey('allergienEncrypted', $entry);
|
||||||
|
$this->assertArrayNotHasKey('kvTyp', $entry);
|
||||||
|
}
|
||||||
|
|
||||||
// ── countArchived() ──────────────────────────────────────────────
|
// ── countArchived() ──────────────────────────────────────────────
|
||||||
|
|
||||||
public function testCountArchivedReturnsCorrectCount(): void {
|
public function testCountArchivedReturnsCorrectCount(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user