- Added seed_realistic_data.php to clean existing data and populate the database with realistic entries for doctors, clinics, and secretaries. - Created a structured approach to generate 100 doctors per city with diverse specialties and services. - Implemented database cleanup routines to ensure a fresh start for data seeding. - Enhanced the DoctorSecretaryRepository with improved comments for clarity.
14 KiB
منطق scope منشی: تفکیک کلینیک از مطب شخصی
زمینه
در حال حاضر DoctorSecretary فقط یک رابطه (doctor_id, secretary_id) دارد. اگر منشی A به دکتر X اختصاص داده شود — فرق نمیکند از طرف کلینیک باشد یا از طرف خود دکتر — در همه جا یکسان رفتار میشود. این باعث میشود:
- منشیای که کلینیک برای دکتر X تعریف کرده، به نوبتهای مطب شخصی همان دکتر هم دسترسی داشته باشد.
- منشیای که خود دکتر تعریف کرده، در داشبورد همه روابط کلینیکی او را هم میبیند.
- کلینیک میتواند منشیهایی را ببیند و حذف کند که دکتر شخصاً تعریف کرده است.
هدف
اضافه کردن فیلد owner_type ('doctor' | 'clinic') و clinic_id (nullable FK) به DoctorSecretary تا هر رابطه منشی-پزشک به یک scope تعلق داشته باشد.
قوانین:
| ایجادکننده | owner_type |
clinic_id |
|---|---|---|
| خود دکتر | 'doctor' |
null |
| کلینیک | 'clinic' |
<id کلینیک> |
دسترسیها، داشبورد، و فیلتر نوبتها باید فقط در scope مربوطه اعمال شوند.
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Secretary/Entity/DoctorSecretary.php |
entity اصلی — باید owner_type و clinic_id اضافه شود |
src/Secretary/Controller/SecretaryController.php |
ایجاد و لیست منشی — باید scope را تنظیم کند |
src/Secretary/Repository/DoctorSecretaryRepository.php |
query ها — باید scope-aware شوند |
src/Appointment/Controller/MyAppointmentsController.php |
فیلتر نوبتهای منشی — باید scope-aware شود |
src/Dashboard/Controller/DashboardController.php |
داشبورد منشی — باید scope-aware شود |
src/Patient/Controller/PatientController.php |
دسترسی منشی به پرونده بیمار — باید scope-aware شود |
src/Auth/Controller/AuthController.php |
/api/v1/me context builder — باید scope را در context نشان دهد |
migrations/ |
migration جدید برای owner_type + clinic_id |
docs/api/secretary.md |
مستندات API |
وضعیت فعلی
Entity (بدون scope)
// src/Secretary/Entity/DoctorSecretary.php
// هیچ فیلد owner_type یا clinic_id وجود ندارد
#[ORM\UniqueConstraint(name: 'idx_doctor_secretaries_pair', columns: ['doctor_id', 'secretary_id'])]
class DoctorSecretary
{
#[ORM\ManyToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
#[ORM\ManyToOne(targetEntity: User::class)]
private User $secretary;
// ...
}
Repository — scope-blind
public function findActiveBySecretary(User $user): ?DoctorSecretary
{
// فقط اولین رابطه فعال را برمیگرداند — scope در نظر گرفته نمیشود
return $this->findOneBy(['secretary' => $user, 'active' => true]);
}
Controller — ایجاد بدون scope
// create() در SecretaryController
// هیچ owner_type یا clinic_id ست نمیشود
$secretary = new DoctorSecretary($doctor, $secretaryUser);
Auth context builder — scope-blind
// AuthController::buildContexts()
foreach ($this->secretaryRepo->findAllActiveBySecretary($user) as $rel) {
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $rel->getDoctor()->getUuid(),
'name' => 'مطب ' . $rel->getDoctor()->getName(),
'role' => 'secretary',
'permissions' => $rel->getPermissions(),
];
}
// اگر منشی هم در کلینیک هم در مطب شخصی یک دکتر باشد، دو context با db_uuid یکسان میسازد
// و هیچکدام نشان نمیدهند که کدام scope است
Appointment فیلتر — فقط یک رابطه
// MyAppointmentsController — منشی فقط به نوبتهای یک دکتر دسترسی دارد
} elseif (in_array('ROLE_SECRETARY', $roles, true)) {
$rel = $this->secretaryRepo->findActiveBySecretary($user); // اولین رابطه
$qb->andWhere('a.doctor = :doctor')
->setParameter('doctor', $rel->getDoctor());
}
وظایف
۱. Entity: اضافه کردن owner_type و clinic_id
// src/Secretary/Entity/DoctorSecretary.php
// unique constraint باید scope را هم در نظر بگیرد:
// یک منشی میتواند هم از طرف کلینیک هم از طرف خود دکتر به همان پزشک متصل باشد
#[ORM\UniqueConstraint(
name: 'idx_doctor_secretary_scope',
columns: ['doctor_id', 'secretary_id', 'owner_type', 'clinic_id']
)]
#[ORM\Column(name: 'owner_type', type: 'string', length: 10)]
private string $ownerType; // 'doctor' | 'clinic'
#[ORM\ManyToOne(targetEntity: Clinic::class)]
#[ORM\JoinColumn(name: 'clinic_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Clinic $clinic = null;
public function __construct(Doctor $doctor, User $secretary, string $ownerType = 'doctor', ?Clinic $clinic = null)
{
// ...
$this->ownerType = $ownerType;
$this->clinic = $clinic;
}
public function getOwnerType(): string { return $this->ownerType; }
public function getClinic(): ?Clinic { return $this->clinic; }
در toArray() هم اضافه شود:
'owner_type' => $this->ownerType,
'clinic_uuid' => $this->clinic?->getUuid(),
۲. Migration
بعد از تغییر Entity:
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
unique constraint قدیمی idx_doctor_secretaries_pair باید drop شود و جدید جای آن را بگیرد.
۳. Repository: query های scope-aware
// src/Secretary/Repository/DoctorSecretaryRepository.php
/** منشی در context مطب شخصی (owner_type='doctor') */
public function findActiveBySecretaryForDoctor(User $user, Doctor $doctor): ?DoctorSecretary
{
return $this->findOneBy([
'secretary' => $user,
'doctor' => $doctor,
'ownerType' => 'doctor',
'active' => true,
]);
}
/** منشی در context کلینیک */
public function findActiveBySecretaryForClinic(User $user, Clinic $clinic): ?DoctorSecretary
{
// یک منشی میتواند به چند دکتر در یک کلینیک متصل باشد — اولین را برگردان برای چک دسترسی کلی
return $this->createQueryBuilder('s')
->where('s.secretary = :user')
->andWhere('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->andWhere('s.active = true')
->setParameter('user', $user)
->setParameter('clinic', $clinic)
->setParameter('type', 'clinic')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
/** همه روابط فعال یک منشی — برای auth context builder */
public function findAllActiveBySecretary(User $user): array
{
return $this->findBy(['secretary' => $user, 'active' => true]);
}
/** findActiveBySecretary قدیمی — نگه داشته شود برای backward compat اما deprecated */
public function findActiveBySecretary(User $user): ?DoctorSecretary
{
return $this->findOneBy(['secretary' => $user, 'active' => true]);
}
/** همه دکترهای یک کلینیک که این منشی به آنها متصل است */
public function findDoctorsBySecretaryInClinic(User $user, Clinic $clinic): array
{
return $this->createQueryBuilder('s')
->select('d')
->join('s.doctor', 'd')
->where('s.secretary = :user')
->andWhere('s.clinic = :clinic')
->andWhere('s.ownerType = :type')
->andWhere('s.active = true')
->setParameter('user', $user)
->setParameter('clinic', $clinic)
->setParameter('type', 'clinic')
->getQuery()
->getResult();
}
همچنین findByClinic باید فقط منشی ها با owner_type='clinic' را برگرداند:
public function findByClinic(Clinic $clinic): array
{
return $this->createQueryBuilder('s')
->join('s.doctor', 'd')
->where('s.clinic = :clinic') // اصلاح: به جای d.id IN (:ids)
->andWhere('s.ownerType = :type')
->setParameter('clinic', $clinic)
->setParameter('type', 'clinic')
->orderBy('s.createdAt', 'DESC')
->getQuery()
->getResult();
}
۴. SecretaryController: ایجاد با scope
در create():
- اگر
currentUserدارایROLE_CLINICبود →ownerType = 'clinic'،clinic = clinicRepo->findByUser($currentUser) - اگر
ROLE_DOCTORبود →ownerType = 'doctor'،clinic = null
if ($currentUser->hasRole('ROLE_CLINIC')) {
$clinic = $this->clinicRepo->findByUser($currentUser);
$secretary = new DoctorSecretary($doctor, $secretaryUser, 'clinic', $clinic);
} else {
$secretary = new DoctorSecretary($doctor, $secretaryUser, 'doctor', null);
}
همچنین canManageDoctor() باید scope را چک کند:
ROLE_DOCTORفقط میتواند روابطowner_type='doctor'را مدیریت کندROLE_CLINICفقط میتواند روابطowner_type='clinic'متعلق به کلینیک خودش را مدیریت کند
۵. Auth context builder: scope-aware
در AuthController::buildContexts() هر رابطه باید context مجزا با scope داشته باشد:
foreach ($this->secretaryRepo->findAllActiveBySecretary($user) as $rel) {
if ($rel->getOwnerType() === 'clinic' && $rel->getClinic() !== null) {
// context کلینیک — اگر قبلاً اضافه نشده
$clinicUuid = $rel->getClinic()->getUuid();
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinicUuid && $c['role'] === 'secretary_clinic');
if (empty($alreadyAdded)) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinicUuid,
'name' => 'کلینیک ' . $rel->getClinic()->getName(),
'role' => 'secretary',
'scope' => 'clinic',
'permissions' => $rel->getPermissions(),
];
}
} else {
// context مطب شخصی
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $rel->getDoctor()->getUuid(),
'name' => 'مطب ' . $rel->getDoctor()->getName(),
'role' => 'secretary',
'scope' => 'doctor',
'permissions' => $rel->getPermissions(),
];
}
}
۶. Appointment و Dashboard: scope-aware
در MyAppointmentsController و DashboardController به جای findActiveBySecretary:
- اگر
db_uuidدر JWT به یک کلینیک اشاره دارد → همه دکترهای کلینیک که این منشی به آنها متصل است - اگر به یک دکتر اشاره دارد → فقط همان دکتر با
owner_type='doctor'
برای تشخیص context فعلی در runtime: JWT payload را parse کن (یا db_uuid را از request header بگیر اگر پیادهسازی شده) و سپس:
// اگر db_uuid کلینیک بود:
$doctors = $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $clinic);
$qb->andWhere('a.doctor IN (:doctors)')->setParameter('doctors', $doctors);
// اگر db_uuid دکتر بود:
$rel = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);
$qb->andWhere('a.doctor = :doctor')->setParameter('doctor', $rel->getDoctor());
۷. PatientController: scope-aware
// PatientController::resolveEntity()
if ($user->hasRole('ROLE_SECRETARY')) {
// تعیین scope از db_uuid در JWT
// اگر کلینیک: entityType='clinic'
// اگر مطب شخصی: entityType='doctor'
}
نکات مهم
- Unique constraint جدید:
(doctor_id, secretary_id, owner_type, clinic_id)— یک منشی میتواند هم از کلینیک هم از خود دکتر به پزشک مشابه متصل باشد (دو ردیف جداگانه باowner_typeمتفاوت). - Migration: فیلد
owner_typeباید default'doctor'داشته باشد تا رکوردهای قبلی break نشوند.clinic_idnullable است. - تشخیص scope در runtime: JWT یا header باید
db_uuidفعلی را حمل کند. ابتدا بررسی کن آیا این uuid متعلق به یک کلینیک است یا دکتر — باclinicRepo->findByUuid()وdoctorRepo->findByUuid(). canManageدر SecretaryController: رابطهowner_type='clinic'را فقط صاحب آن کلینیک میتواند ویرایش/حذف کند، نه خود دکتر. و بالعکس.findByClinicبایدclinic_id = :clinicرا به جایd.id IN (:ids)استفاده کند تا صحیح باشد.- Frontend: فیلد
scopeرا در context برگشتی از/meنمایش بده. درauthStoreنگه دار تا صفحات بدانند منشی در چه scopeای است.