contextResolver->resolve($user); if (!$context->isResolved()) { return null; } return $context->isClinic() ? $this->secretaryRepo->findActiveBySecretaryForClinic($user, $context->clinic) : $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $context->doctor); } public function can(User $user, string $resource, string $action): bool { $relation = $this->activeRelation($user); return $relation !== null && $this->permissions->can($relation, $resource, $action); } /** * جفتِ [entityType, entityId] مالکِ محیطِ فعالِ منشی — کلینیک یا پزشک. * برای کنترلرهایی که دادهٔ tenant را با این جفت واکشی می‌کنند. مجوز جدا با * denyUnlessGranted بررسی می‌شود؛ این متد فقط owner را حل می‌کند. * * @return array{0: string, 1: int|null} ['clinic'|'doctor'|'unknown', id|null] */ public function resolveOwnerEntity(User $user): array { return $this->contextResolver->resolve($user)->toEntityPair(); } /** * آیا منشی در محیطِ فعالِ خود، روی این پزشکِ مشخص (و کلینیکِ همان نوبت/تنظیم) * مجاز به resource/action است؟ ترکیبِ «اسکوپِ پزشکِ تخصیص‌یافته» و «توگلِ مجوز». * قرینهٔ AppointmentAccessChecker::secretaryCan اما برای هر resource. */ public function canForDoctor( User $user, \App\Doctor\Entity\Doctor $doctor, ?\App\Clinic\Entity\Clinic $clinic, string $resource, string $action ): bool { $context = $this->contextResolver->resolve($user); if (!$context->isResolved()) { return false; } if ($context->isClinic()) { // محیطِ کلینیک: تنظیم باید در همان کلینیک باشد و پزشکش جزو پزشکانِ منشی. if ($clinic === null || $context->id !== $clinic->getId()) { return false; } $relation = $this->secretaryRepo->findActiveClinicRow($user, $context->clinic, $doctor); return $relation !== null && $this->permissions->can($relation, $resource, $action); } // محیطِ مطب شخصی: تنظیم هم باید شخصی باشد (clinic == null). if ($clinic !== null || $context->id !== $doctor->getId()) { return false; } $relation = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor); return $relation !== null && $this->permissions->can($relation, $resource, $action); } /** * idهای پزشکانِ تخصیص‌یافته به این منشی در این کلینیک — برای محدودکردنِ * لیست‌هایی که پیش‌فرض همهٔ پزشکانِ کلینیک را برمی‌گردانند. اگر منشی نیست یا * محیطش این کلینیک نیست → آرایهٔ خالی. * * @return int[] */ public function assignedClinicDoctorIds(User $user, \App\Clinic\Entity\Clinic $clinic): array { if (!$user->hasRole('ROLE_SECRETARY')) { return []; } return array_map( static fn(\App\Doctor\Entity\Doctor $d) => $d->getId(), $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $clinic), ); } /** * آیا منشی در محیطِ فعالِ خود — که باید همین کلینیک باشد — مجاز به resource/action است؟ * برای منابعِ کلینیک‌سطح مثل clinic_doctors که tenant لزوماً کلینیک است. */ public function canForClinic(User $user, \App\Clinic\Entity\Clinic $clinic, string $resource, string $action): bool { [$type, $id] = $this->resolveOwnerEntity($user); return $type === 'clinic' && $id === $clinic->getId() && $this->can($user, $resource, $action); } /** * برای مسیرهایی که چند نقش دارند: فقط منشی را محدود کن. سایر نقش‌ها true. */ public function canOrNonSecretary(User $user, string $resource, string $action): bool { if (!$user->hasRole('ROLE_SECRETARY')) { return true; } return $this->can($user, $resource, $action); } /** 403 اگر منشی مجاز نباشد؛ نقش‌های دیگر بدون تغییر عبور می‌کنند. */ public function denyUnlessGranted(User $user, string $resource, string $action): void { if (!$this->canOrNonSecretary($user, $resource, $action)) { throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403); } } }