feat: implement specialty hierarchy handling in doctor and representation APIs, add backfill command and tests
This commit is contained in:
@@ -490,9 +490,10 @@ class AdminApiController extends BaseController
|
||||
if (!empty($data['activity_time'])) $doctor->setActivityTime((int) $data['activity_time']);
|
||||
|
||||
if (!empty($data['specialties']) && is_array($data['specialties'])) {
|
||||
foreach ($data['specialties'] as $id) {
|
||||
$s = $this->em->getRepository(Specialty::class)->find((int) $id);
|
||||
if ($s !== null) $doctor->getSpecialties()->add($s);
|
||||
$specialtyRepo = $this->em->getRepository(Specialty::class);
|
||||
foreach ($specialtyRepo->expandWithAncestors(array_map('intval', $data['specialties'])) as $id) {
|
||||
$s = $specialtyRepo->find($id);
|
||||
if ($s !== null && !$doctor->getSpecialties()->contains($s)) $doctor->getSpecialties()->add($s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Doctor\Command;
|
||||
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Specialty\Entity\Specialty;
|
||||
use App\Specialty\Repository\SpecialtyRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* backfill: پزشکانی که فقط تخصص فرزند دارند (مثلاً «گوارش و کبد» بدون «داخلی»)
|
||||
* تمام تخصصهای والد تا ریشهٔ درخت را میگیرند. idempotent؛ با --dry-run فقط گزارش میدهد.
|
||||
*
|
||||
* php bin/console app:doctors:backfill-specialty-parents --dry-run
|
||||
* php bin/console app:doctors:backfill-specialty-parents
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'app:doctors:backfill-specialty-parents',
|
||||
description: 'Attach every ancestor specialty to doctors that only have the child one (idempotent, supports --dry-run)',
|
||||
)]
|
||||
class BackfillDoctorSpecialtyParentsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly SpecialtyRepository $specialtyRepo,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report only, change nothing');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = (bool) $input->getOption('dry-run');
|
||||
|
||||
/** @var Doctor[] $doctors */
|
||||
$doctors = $this->em->getRepository(Doctor::class)->findAll();
|
||||
|
||||
$touched = 0;
|
||||
$added = 0;
|
||||
|
||||
foreach ($doctors as $doctor) {
|
||||
$current = array_map(
|
||||
static fn (Specialty $s) => $s->getId(),
|
||||
$doctor->getSpecialties()->toArray()
|
||||
);
|
||||
if ($current === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing = array_diff($this->specialtyRepo->expandWithAncestors($current), $current);
|
||||
if ($missing === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->text(sprintf(
|
||||
'%s doctor #%d — adding specialty %s',
|
||||
$dryRun ? '[dry-run]' : '[update]',
|
||||
$doctor->getId(),
|
||||
implode(', ', $missing)
|
||||
));
|
||||
|
||||
if (!$dryRun) {
|
||||
foreach ($missing as $id) {
|
||||
$s = $this->specialtyRepo->find($id);
|
||||
if ($s !== null) {
|
||||
$doctor->getSpecialties()->add($s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$touched++;
|
||||
$added += count($missing);
|
||||
}
|
||||
|
||||
if (!$dryRun && $touched > 0) {
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
$io->success(sprintf(
|
||||
'%d doctor(s) %s, %d specialty link(s) added (of %d scanned)',
|
||||
$touched,
|
||||
$dryRun ? 'would be updated' : 'updated',
|
||||
$added,
|
||||
count($doctors)
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -792,8 +792,8 @@ class DoctorController extends BaseController
|
||||
// Specialties
|
||||
if (array_key_exists('specialties', $data) && is_array($data['specialties'])) {
|
||||
$doctor->getSpecialties()->clear();
|
||||
foreach ($data['specialties'] as $id) {
|
||||
$s = $this->specialtyRepo->find((int) $id);
|
||||
foreach ($this->specialtyRepo->expandWithAncestors(array_map('intval', $data['specialties'])) as $id) {
|
||||
$s = $this->specialtyRepo->find($id);
|
||||
if ($s !== null) $doctor->getSpecialties()->add($s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,12 @@ class DoctorImportService
|
||||
if (array_key_exists('info', $data)) $doctor->setInfo($data['info']);
|
||||
|
||||
// روابط بر پایهٔ شناسههای مرجع (تخصص/استان/شهر)
|
||||
$this->syncRefCollection($doctor->getSpecialties(), $data['specialties'] ?? null, Specialty::class);
|
||||
$specialtyIds = $data['specialties'] ?? null;
|
||||
if (is_array($specialtyIds)) {
|
||||
$specialtyIds = $this->em->getRepository(Specialty::class)
|
||||
->expandWithAncestors(array_map('intval', $specialtyIds));
|
||||
}
|
||||
$this->syncRefCollection($doctor->getSpecialties(), $specialtyIds, Specialty::class);
|
||||
$this->syncRefCollection($doctor->getProvinces(), $data['states'] ?? null, Province::class);
|
||||
$this->syncRefCollection($doctor->getCities(), $data['cities'] ?? null, City::class);
|
||||
|
||||
|
||||
@@ -289,9 +289,10 @@ class RepresentationActionController extends BaseController
|
||||
if (!empty($data['activity_time'])) $doctor->setActivityTime((int) $data['activity_time']);
|
||||
|
||||
if (!empty($data['specialties']) && is_array($data['specialties'])) {
|
||||
foreach ($data['specialties'] as $id) {
|
||||
$s = $this->em->getRepository(Specialty::class)->find((int) $id);
|
||||
if ($s !== null) $doctor->getSpecialties()->add($s);
|
||||
$specialtyRepo = $this->em->getRepository(Specialty::class);
|
||||
foreach ($specialtyRepo->expandWithAncestors(array_map('intval', $data['specialties'])) as $id) {
|
||||
$s = $specialtyRepo->find($id);
|
||||
if ($s !== null && !$doctor->getSpecialties()->contains($s)) $doctor->getSpecialties()->add($s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class SpecialtyRepository extends ServiceEntityRepository
|
||||
{
|
||||
/** @var array<int,?int>|null */
|
||||
private ?array $parentMap = null;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Specialty::class);
|
||||
@@ -62,6 +65,52 @@ class SpecialtyRepository extends ServiceEntityRepository
|
||||
return $this->findOneBy(['slug' => $slug]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialty ids plus every ancestor up to the root, unique and sorted.
|
||||
* Unknown ids are dropped; a cyclic parent chain stops at the repeated id.
|
||||
*
|
||||
* @param int[] $ids
|
||||
* @return int[]
|
||||
*/
|
||||
public function expandWithAncestors(array $ids): array
|
||||
{
|
||||
$map = $this->parentMap();
|
||||
$out = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$cur = (int) $id;
|
||||
$seen = [];
|
||||
while (array_key_exists($cur, $map) && !isset($seen[$cur])) {
|
||||
$seen[$cur] = true;
|
||||
$out[$cur] = true;
|
||||
$cur = $map[$cur] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
$out = array_keys($out);
|
||||
sort($out);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @return array<int,?int> id => parentId for every specialty */
|
||||
private function parentMap(): array
|
||||
{
|
||||
if ($this->parentMap === null) {
|
||||
$rows = $this->createQueryBuilder('s')
|
||||
->select('s.id AS id', 'IDENTITY(s.parent) AS parent')
|
||||
->getQuery()
|
||||
->getArrayResult();
|
||||
|
||||
$this->parentMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$this->parentMap[(int) $row['id']] = $row['parent'] !== null ? (int) $row['parent'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parentMap;
|
||||
}
|
||||
|
||||
public function save(Specialty $specialty, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($specialty);
|
||||
|
||||
Reference in New Issue
Block a user