محیط فعالِ ذخیره‌شدهٔ کاربر > نقش کاربر. * * نقش به‌تنهایی برای کاربری که هم پزشک است و هم مالک کلینیک جواب نمی‌دهد: چنین * کاربری همیشه به‌عنوان پزشک حل می‌شد و هرگز به سرویس‌های کلینیک خودش نمی‌رسید. * UserActiveContext تعیین‌کننده است و نقش فقط fallback آخر. * * این کلاس فقط به «کدام محیط» جواب می‌دهد. «چه کاری در آن محیط مجاز است» کار * ClinicDoctorPermissionChecker و SecretaryPermissionChecker است و اینجا بررسی نمی‌شود. * * برای مسیر رزرو عمومی (که کاربر پنل ندارد و ورودی‌اش Doctor است، نه User) * قرینه‌ای جدا وجود دارد: {@see \App\Appointment\Service\BookingContextResolver}. */ class EntityContextResolver { public function __construct( private readonly DoctorRepository $doctorRepo, private readonly ClinicRepository $clinicRepo, private readonly UserActiveContextRepository $activeContextRepo, private readonly DoctorSecretaryRepository $secretaryRepo, private readonly ClinicStaffRepository $staffRepo, ) {} /** * @param string|null $clinicUuid اگر داده شود، محیط کلینیک اجباری می‌شود و در * صورت نداشتن دسترسی، خطای ۴۰۳ پرتاب می‌شود. */ public function resolve(User $user, ?string $clinicUuid = null): EntityContext { if ($clinicUuid !== null && $clinicUuid !== '') { $clinic = $this->clinicRepo->findByUuid($clinicUuid); if ($clinic === null) { throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404); } $this->assertCanActInClinic($user, $clinic); return EntityContext::forClinic($clinic)->asChosen(); } $fromActive = $this->fromActiveContext($user); if ($fromActive !== null) { return $fromActive; } return $this->fromRole($user); } /** * محیط را بدون پرتاب خطا حل می‌کند؛ اگر کاربر به کلینیکِ خواسته‌شده دسترسی * نداشته باشد null برمی‌گرداند. برای مسیرهایی که خودشان authorization جدا دارند. */ public function tryResolve(User $user, ?string $clinicUuid = null): ?EntityContext { try { return $this->resolve($user, $clinicUuid); } catch (AppException) { return null; } } /** * محیطی که کاربر **صاحبش** است، مستقل از محیط فعال و از نقش‌هایش. * * برای خریدهایی است که به حساب خودِ صاحب می‌نشیند (اشتراک): آنجا «کجا ایستاده‌ام» * مهم نیست، «چه چیزی دارم» مهم است. تنها مرجعِ این پرسش همین متد است تا پرداختِ * اشتراک و خودِ اشتراک هرگز روی دو محیط متفاوت ننشینند. * * استثنا: کاربری که هر دو محیط را دارد. آنجا محیط فعال تعیین می‌کند کدام‌یک، * وگرنه یکی از دو محیطِ صاحب‌شده هرگز اشتراک نمی‌گیرد. */ public function ownedEntity(User $user): EntityContext { $doctor = $this->doctorRepo->findByUser($user); $clinic = $this->clinicRepo->findByUser($user); // کاربری که هم پزشک است و هم مالک کلینیک، دو محیطِ صاحب‌شده دارد و «چه چیزی // دارم» به‌تنهایی جواب یکتا ندارد. محیط فعال گره را باز می‌کند: اشتراک همان // جایی می‌نشیند که کاربر ایستاده و آن را می‌بیند. بدون این، اعطای اشتراک به // کلینیک برای چنین کاربری بی‌اثر می‌ماند، چون پنل همیشه پزشک را می‌خواند. if ($doctor !== null && $clinic !== null) { $active = $this->fromActiveContext($user); if ($active !== null) { if ($active->isClinic() && $active->toEntityPair()[1] === $clinic->getId()) { return EntityContext::forClinic($clinic); } if (!$active->isClinic() && $active->toEntityPair()[1] === $doctor->getId()) { return EntityContext::forDoctor($doctor); } } } if ($doctor !== null) { return EntityContext::forDoctor($doctor); } return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown(); } /** * آیا این کاربر **صاحبِ** همین محیط است؟ (ادمین همیشه بله.) * * «صاحب» با «می‌تواند در آن بایستد» فرق دارد: پزشکِ مهمانِ یک کلینیک در محیط آن * می‌ایستد ولی صاحبش نیست. تصمیم‌هایی که قراردادِ کلِ مجموعه را عوض می‌کنند — * مثل الگوی شمارهٔ پرونده — به این پرسش وصل‌اند، نه به مجوزهای per-resource. */ public function owns(User $user, string $entityType, ?int $entityId): bool { if ($user->hasRole('ROLE_ADMIN')) { return true; } [$ownedType, $ownedId] = $this->ownedEntity($user)->toEntityPair(); return $ownedId !== null && $ownedType === $entityType && $ownedId === $entityId; } /** * مالک کلینیک، ادمین، پزشکِ عضو همان کلینیک، منشیِ دارای رابطهٔ فعال در آن، یا * پرسنلِ فعالِ همان کلینیک. * * «می‌تواند در این محیط بایستد» یعنی محیطش حل می‌شود — نه اینکه هر کاری در آن * مجاز است؛ محدودهٔ پرسنل را StaffRouteGuardSubscriber تعیین می‌کند. */ public function canActInClinic(User $user, Clinic $clinic): bool { if ($user->hasRole('ROLE_ADMIN') || $clinic->getUser()->getId() === $user->getId()) { return true; } $doctor = $this->doctorRepo->findByUser($user); if ($doctor !== null && $clinic->hasDoctor($doctor)) { return true; } if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_CLINIC, $clinic->getId()) !== null) { return true; } return $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic) !== null; } public function assertCanActInClinic(User $user, Clinic $clinic): void { if (!$this->canActInClinic($user, $clinic)) { throw new AppException(ErrorCodes::ERR_ACCESS_DENIED, 'به این کلینیک دسترسی ندارید', 403); } } /** محیط فعالِ ذخیره‌شده؛ db_type می‌گوید db_uuid کدام موجودیت را آدرس می‌دهد. */ private function fromActiveContext(User $user): ?EntityContext { $active = $this->activeContextRepo->findByUser($user); if ($active === null) { return null; } if ($active->isClinic()) { $clinic = $this->clinicRepo->findByUuid($active->getDbUuid()); return $clinic !== null && $this->canActInClinic($user, $clinic) ? EntityContext::forClinic($clinic)->asChosen() : null; } $doctor = $this->doctorRepo->findByUuid($active->getDbUuid()); return $doctor !== null && $this->canActForDoctor($user, $doctor) ? EntityContext::forDoctor($doctor)->asChosen() : null; } /** * محیطِ مطب شخصی: خودِ پزشک، یا منشیِ دارای رابطهٔ فعال با او. * قرینهٔ canActInClinic برای شاخهٔ غیرکلینیکی؛ ادمین عمداً اینجا نیست چون * مسیرهای ادمین سراسری‌اند و از محیط فعالِ یک پزشک عبور نمی‌کنند. */ private function canActForDoctor(User $user, Doctor $doctor): bool { if ($doctor->getUser()->getId() === $user->getId()) { return true; } if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_DOCTOR, $doctor->getId()) !== null) { return true; } return $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor) !== null; } private function fromRole(User $user): EntityContext { if ($user->hasRole('ROLE_DOCTOR')) { return EntityContext::forDoctor($this->doctorRepo->findByUser($user)); } if ($user->hasRole('ROLE_CLINIC')) { $clinic = $this->clinicRepo->findByUser($user); return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown(); } // منشی و پرسنل fallback نقشی ندارند: محیطشان فقط از UserActiveContext می‌آید، // چون هر دو می‌توانند هم‌زمان به چند پزشک و کلینیک وصل باشند و نقش تنها، // انتخاب بین آن‌ها را تعیین نمی‌کند. return EntityContext::unknown(); } }