# تعداد پزشکان هر تخصص (به تفکیک شهر) ## پروژه `clinicpro` (Backend — منبع حقیقت). > **Cross-repo:** صفحه‌ی عمومی `/specialties` در `nobat724_front` این داده را مصرف می‌کند تا زیر هر تخصص «N پزشک» نشان دهد (پرامپت همتا: `nobat724_front/.claude/prompt/specialties-doctor-count-wire.md`). این پرامپت **اول** اجرا شود. ## زمینه صفحه‌ی `/specialties` در سایت عمومی، زیر هر کارت تخصص می‌خواهد تعداد پزشکان آن تخصص را نشان دهد (`{number_of_doctors} پزشک`). اما هیچ endpointی این تعداد را نمی‌دهد؛ `GET /api/v1/specialties` فقط `{id, uuid, name, slug, status, weight, parent_id}` برمی‌گرداند. سایت چند-شهری است و شمارش باید **فقط پزشکانِ شهرِ دامنه‌ی جاری** باشد. داده‌ی موجود: join table `doctor_specialties` (پزشک↔تخصص) و `doctor_cities` (پزشک↔شهر). `Doctor::getSpecialties()` و `getCities()` روابط ManyToMany‌اند. endpoint `GET /api/v1/doctors` از قبل پارامترهای `specialty_id` و `city_id` را می‌پذیرد (الگوی فیلتر شهر). ## مشکل / هدف یک endpoint عمومی که برای هر تخصصِ فعال، تعداد پزشکانِ آن تخصص را در یک شهر مشخص (`city_id`) برگرداند — در یک فراخوانی (نه N درخواست). ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/Specialty/Controller/SpecialtyController.php` | افزودن endpoint `doctorCounts` | | `src/Specialty/Repository/SpecialtyRepository.php` | متد شمارش پزشک per specialty با فیلتر شهر | | `src/Doctor/Entity/Doctor.php` | روابط `specialties` (doctor_specialties) و `cities` (doctor_cities) — مرجع | | `config/packages/security.yaml` | endpoint جدید زیر `public_endpoints` (مثل `specialties`) | | `docs/api/specialty.md` | مستندسازی | ## وضعیت فعلی (کد واقعی) `SpecialtyController::list` (مرجع سبک): ```php #[Route('/api/v1/specialties', methods: ['GET'])] public function list(Request $request): JsonResponse { $parentId = $request->query->get('parent_id'); $items = array_map(fn(Specialty $s) => $s->toArray(), $this->repo->findActive($parentId !== null ? (int)$parentId : null)); return $this->success(['data' => $items]); } ``` `Specialty::toArray`: `{id, uuid, name, slug, status, weight, parent_id}` — بدون تعداد پزشک. `Doctor`: ```php #[ORM\ManyToMany(targetEntity: Specialty::class)] #[ORM\JoinTable(name: 'doctor_specialties', ...)] private Collection $specialties; #[ORM\ManyToMany(targetEntity: City::class)] // doctor_cities private Collection $cities; ``` ## وظایف ### ۱. متد شمارش در `SpecialtyRepository` متدی که با یک کوئری، نگاشت `specialty_id => count(distinct doctor)` را برای پزشکانِ یک شهر برگرداند: ```php /** @return array specialtyId => doctorCount (within a city if given) */ public function doctorCountsByCity(?int $cityId): array { $qb = $this->getEntityManager()->createQueryBuilder() ->select('s.id AS specialty_id', 'COUNT(DISTINCT d.id) AS cnt') ->from(\App\Doctor\Entity\Doctor::class, 'd') ->join('d.specialties', 's') ->groupBy('s.id'); if ($cityId !== null) { $qb->join('d.cities', 'c')->andWhere('c.id = :city')->setParameter('city', $cityId); } // در صورت وجود فیلد وضعیت پزشک (فعال/تأییدشده) آن را هم اعمال کن تا با لیست عمومی doctors هم‌خوان باشد $rows = $qb->getQuery()->getArrayResult(); $map = []; foreach ($rows as $r) { $map[(int)$r['specialty_id']] = (int)$r['cnt']; } return $map; } ``` > اگر `Doctor` فیلد وضعیت/visibility دارد (مثل آنچه `GET /doctors` برای لیست عمومی فیلتر می‌کند)، **همان شرط** را اینجا هم بگذار تا تعداد با نتیجه‌ی واقعیِ `/doctors?specialty_id=&city_id=` یکی باشد. الگوی فیلتر را از `DoctorController::list` بردار. ### ۲. endpoint `doctorCounts` در `SpecialtyController` ```php #[Route('/api/v1/specialties/doctor-counts', methods: ['GET'])] public function doctorCounts(Request $request): JsonResponse { $cityId = $request->query->get('city_id'); $counts = $this->repo->doctorCountsByCity($cityId !== null ? (int) $cityId : null); $items = array_map(function (Specialty $s) use ($counts) { $arr = $s->toArray(); $arr['number_of_doctors'] = $counts[$s->getId()] ?? 0; return $arr; }, $this->repo->findActive(null)); return $this->success(['data' => $items]); } ``` - خروجی: همان شکل `specialties` + کلید `number_of_doctors`. - تخصص‌های بدون پزشک → `0`. ### ۳. عمومی‌کردن مسیر در `security.yaml` `specialties` از قبل عمومی است؛ مطمئن شو `/api/v1/specialties/doctor-counts` هم زیر `public_endpoints` می‌افتد (الگوی `api/v1/specialties` معمولاً با prefix پوشش می‌دهد — تأیید کن مسیر جدید عمومی است، وگرنه اضافه کن). ### ۴. مستندسازی `docs/api/specialty.md` endpoint جدید: method/path، query `city_id` (اختیاری)، و response با `number_of_doctors`. ## نکات مهم - شمارش `DISTINCT d.id` تا اگر پزشک چند شهر/رابطه دارد دوبار شمرده نشود. - `city_id` همان id شهر در `categories` (bundle=city) است که فرانت از `getStateInfo().matchedCity.id` می‌فرستد. - اگر `city_id` نیامد، شمارش سراسری برگردد (fallback ایمن). - فیلتر وضعیت پزشک باید با `GET /doctors` هم‌خوان باشد تا «N پزشک» با لیست واقعی بخواند. - یک کوئری grouped (نه N+1). - پاسخ‌ها از `BaseController`؛ migration لازم نیست. - تست: `GET /api/v1/specialties/doctor-counts?city_id=` → آرایه‌ای از تخصص‌ها که هر کدام `number_of_doctors` دارند؛ مجموع/نمونه را با شمارش مستقیم `doctor_specialties⋈doctor_cities` راستی‌آزمایی کن.