findByClinicWithFilters left-joined specialties only for filtering, so toListArray() lazy-loaded them per doctor (N+1). addSelect them and switch the result fetch to Paginator(fetchJoinCollection: true) so LIMIT still paginates by doctor. Test infra: ApiTestCase::countQueries() (via doctrine.debug_data_holder). Regression: tests/Doctor/ClinicDoctorListNPlusOneTest asserts the query count does not grow with doctor count (4→10 without the fix). Also relaxed AppointmentExpiryServiceTest's exact-count assertion (it counts all stale pendings in the shared db_test, which accumulates) — logged test-isolation debt as E6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
2.4 KiB
PHP
65 lines
2.4 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Doctor;
|
|
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Specialty\Entity\Specialty;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* The clinic doctor-list endpoint must hydrate each doctor's specialties in the
|
|
* list query, not lazily per doctor. The query count must therefore be constant
|
|
* regardless of how many doctors the clinic has.
|
|
*/
|
|
class ClinicDoctorListNPlusOneTest extends ApiTestCase
|
|
{
|
|
private function makeClinicWithDoctors(int $doctorCount): Clinic
|
|
{
|
|
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
|
|
$this->em->persist($clinic);
|
|
|
|
for ($i = 0; $i < $doctorCount; $i++) {
|
|
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), "دکتر $i");
|
|
$slug = 'sp-' . bin2hex(random_bytes(6));
|
|
$specialty = new Specialty('تخصص', $slug);
|
|
$this->em->persist($specialty);
|
|
$doctor->getSpecialties()->add($specialty);
|
|
$this->em->persist($doctor);
|
|
$clinic->getDoctors()->add($doctor);
|
|
}
|
|
$this->em->flush();
|
|
|
|
return $clinic;
|
|
}
|
|
|
|
public function testQueryCountDoesNotGrowWithDoctorCount(): void
|
|
{
|
|
// Keep one kernel/container across both requests so the shared query
|
|
// logger (doctrine.debug_data_holder) stays consistent.
|
|
$this->client->disableReboot();
|
|
|
|
$small = $this->makeClinicWithDoctors(2);
|
|
$large = $this->makeClinicWithDoctors(6);
|
|
|
|
$qSmall = $this->countQueries(fn () => $this->client->request(
|
|
'GET', '/api/v1/clinic/doctor-list/' . $small->getUuid()
|
|
));
|
|
$this->assertSame(200, $this->responseCode());
|
|
|
|
$qLarge = $this->countQueries(fn () => $this->client->request(
|
|
'GET', '/api/v1/clinic/doctor-list/' . $large->getUuid()
|
|
));
|
|
$this->assertSame(200, $this->responseCode());
|
|
|
|
// No N+1: 4 extra doctors must not add ~4 extra queries. A tiny slack
|
|
// absorbs one-off warmup variance; the N+1 signal (≈ +doctorCount) is
|
|
// far larger than the slack.
|
|
$this->assertLessThanOrEqual($qSmall + 1, $qLarge, "N+1: query count grew from $qSmall to $qLarge with more doctors");
|
|
|
|
// Correctness preserved: specialties are present in the payload.
|
|
$body = json_decode($this->client->getResponse()->getContent(), true);
|
|
$this->assertNotEmpty($body['data']['data'][0]['specialties']);
|
|
}
|
|
}
|