doctorRepo->findByUser($user) !== null) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'پروفایل دکتر قبلاً ایجاد شده است', 409); } $data = json_decode($request->getContent(), true) ?? []; $name = trim($data['title'] ?? $data['name'] ?? ''); if ($name === '') { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نام دکتر الزامی است', 422, 'title'); } $doctor = new Doctor($user, $name); $this->hydrateDoctor($doctor, $data); $this->doctorRepo->save($doctor); // Grant ROLE_DOCTOR to user $roles = $user->getRoles(); if (!in_array('ROLE_DOCTOR', $roles, true)) { $roles[] = 'ROLE_DOCTOR'; $user->setRoles(array_values(array_unique($roles))); $this->userRepo->save($user); } return $this->success(['data' => $doctor->toDetailArray()], 201); } #[Route('/api/v1/doctor/{uuid}', methods: ['GET'])] public function show(string $uuid): JsonResponse { $doctor = $this->doctorRepo->findByUuid($uuid); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } return $this->success(['data' => $doctor->toDetailArray()]); } #[Route('/api/v1/doctors', methods: ['GET'])] public function list(Request $request): JsonResponse { $filters = $request->query->all(); $result = $this->doctorRepo->findWithFilters($filters); return $this->paginated( array_map(fn(Doctor $d) => $d->toListArray(), $result['items']), $result['total'], $result['page'], $result['limit'] ); } #[Route('/api/v1/doctor/{uuid}', methods: ['PATCH'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function update(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse { $doctor = $this->doctorRepo->findByUuid($uuid); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } if ($doctor->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $data = json_decode($request->getContent(), true) ?? []; if (!empty($data['title'])) $doctor->setName($data['title']); $this->hydrateDoctor($doctor, $data); $this->doctorRepo->save($doctor); return $this->success(['data' => $doctor->toDetailArray()]); } #[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])] #[IsGranted('ROLE_ADMIN')] public function delete(string $uuid): JsonResponse { $doctor = $this->doctorRepo->findByUuid($uuid); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } $this->doctorRepo->remove($doctor); return $this->success(['message' => 'دکتر با موفقیت حذف شد']); } // ── File Upload ─────────────────────────────────────────────────────────── #[Route('/file/upload/clinic_pro/doctor/field_image', methods: ['POST'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function uploadImage(Request $request): JsonResponse { $content = $request->getContent(); $disposition = $request->headers->get('Content-Disposition', ''); preg_match('/filename=["\']?([^"\';\s]+)["\']?/i', $disposition, $m); $filename = $m[1] ?? 'upload.jpg'; // Write to a tmp file for magic bytes validation $tmpPath = sys_get_temp_dir() . '/' . uniqid('upload_', true); file_put_contents($tmpPath, $content); try { // validate() checks size, magic bytes, and sanitizes filename $safeFilename = $this->fileValidator->validate($content, $filename); $mime = $this->fileValidator->detectMimeType($tmpPath); $year = date('Y'); $month = date('m'); $dir = $this->projectDir . '/public/uploads/doctors/' . $year . '-' . $month; if (!is_dir($dir)) { mkdir($dir, 0755, true); } $storedName = uniqid('', true) . '_' . $safeFilename; $fullPath = $dir . '/' . $storedName; rename($tmpPath, $fullPath); $filesize = filesize($fullPath); $url = '/uploads/doctors/' . $year . '-' . $month . '/' . $storedName; return $this->success([ 'fid' => time(), 'uuid' => \Symfony\Component\Uid\Uuid::v4()->toRfc4122(), 'url' => $url, 'filename' => $safeFilename, 'filemime' => $mime, 'filesize' => $filesize, ]); } catch (\Throwable $e) { if (file_exists($tmpPath)) unlink($tmpPath); return $this->error(ErrorCodes::ERR_VALIDATION_001, $e->getMessage(), 422); } } // ── Doctor Addresses ────────────────────────────────────────────────────── #[Route('/api/v1/clinic-pro/doctor-address', methods: ['POST'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function createAddress(Request $request, #[CurrentUser] User $user): JsonResponse { $doctor = $this->doctorRepo->findByUser($user); if ($doctor === null && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'فقط دکتر می‌تواند آدرس اضافه کند', 403); } $data = json_decode($request->getContent(), true) ?? []; // Admin can specify doctor_id/doctor_uuid if ($doctor === null && $user->hasRole('ROLE_ADMIN')) { $doctorUuid = $data['doctor_uuid'] ?? null; if (!$doctorUuid) { return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_uuid الزامی است', 422); } $doctor = $this->doctorRepo->findByUuid($doctorUuid); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } } $address = new DoctorAddress($doctor); $this->hydrateAddress($address, $data); $this->addressRepo->save($address); return $this->success(['data' => $address->toArray()], 201); } #[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['GET'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function showAddress(int $id): JsonResponse { $address = $this->addressRepo->find($id); if ($address === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'آدرس یافت نشد', 404); } return $this->success(['data' => $address->toArray()]); } #[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['PATCH'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function updateAddress(int $id, Request $request, #[CurrentUser] User $user): JsonResponse { $address = $this->addressRepo->find($id); if ($address === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'آدرس یافت نشد', 404); } if ($address->getDoctor()->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $data = json_decode($request->getContent(), true) ?? []; $this->hydrateAddress($address, $data); $this->addressRepo->save($address); return $this->success(['data' => $address->toArray()]); } #[Route('/api/v1/clinic-pro/doctor-address/{id}', methods: ['DELETE'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function deleteAddress(int $id, #[CurrentUser] User $user): JsonResponse { $address = $this->addressRepo->find($id); if ($address === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'آدرس یافت نشد', 404); } if ($address->getDoctor()->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) { return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403); } $this->addressRepo->remove($address); return $this->success(['message' => 'آدرس با موفقیت حذف شد']); } #[Route('/api/v1/clinic-pro/doctor-addresses/{doctorId}', methods: ['GET'])] public function listAddresses(int $doctorId): JsonResponse { $doctor = $this->doctorRepo->find($doctorId); if ($doctor === null) { return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404); } $addresses = array_map(fn(DoctorAddress $a) => $a->toArray(), $doctor->getAddresses()->toArray()); return $this->success(['data' => $addresses]); } // ── Clinic/Doctor list (stub — implemented fully in Task 06) ────────────── #[Route('/api/v1/clinic/doctor-list/{clinicUuid}', methods: ['GET'])] public function clinicDoctorList(string $clinicUuid): JsonResponse { // Full implementation in Task 06 (Clinic entity not yet created) return $this->success(['data' => []]); } // ── Helpers ─────────────────────────────────────────────────────────────── private function hydrateDoctor(Doctor $doctor, array $data): void { if (array_key_exists('gender', $data)) $doctor->setGender($data['gender']); if (array_key_exists('medical_system_code', $data)) $doctor->setMedicalSystemCode($data['medical_system_code']); if (array_key_exists('mobile_number', $data)) $doctor->setMobileNumber($data['mobile_number']); if (array_key_exists('activity_time', $data)) $doctor->setActivityTime((int) $data['activity_time']); if (array_key_exists('degree', $data)) $doctor->setDegree($data['degree']); if (array_key_exists('info', $data)) $doctor->setInfo($data['info']); if (array_key_exists('detail', $data)) $doctor->setInfo($data['detail']); if (array_key_exists('active', $data)) $doctor->setActiveDoctorAppointment((bool) $data['active']); // Images array (from file upload response) if (array_key_exists('image_data', $data)) { $existing = $doctor->getImages() ?? []; $existing[] = $data['image_data']; $doctor->setImages($existing); } if (array_key_exists('images', $data) && is_array($data['images'])) { $doctor->setImages($data['images']); } // Specialties (array of category IDs) if (array_key_exists('specialties', $data) && is_array($data['specialties'])) { $doctor->getSpecialties()->clear(); foreach ($data['specialties'] as $catId) { $cat = $this->categoryRepo->find((int) $catId); if ($cat !== null) { $doctor->getSpecialties()->add($cat); } } } // Expertise / doctor_services if (array_key_exists('doctor_services', $data) && is_array($data['doctor_services'])) { $doctor->getExpertise()->clear(); foreach ($data['doctor_services'] as $catId) { $cat = is_numeric($catId) ? $this->categoryRepo->find((int) $catId) : $this->categoryRepo->findOneBy(['label' => $catId, 'bundle' => 'doctor_services']); if ($cat !== null) { $doctor->getExpertise()->add($cat); } } } if (array_key_exists('expertise', $data) && is_array($data['expertise'])) { $doctor->getExpertise()->clear(); foreach ($data['expertise'] as $catId) { $cat = $this->categoryRepo->find((int) $catId); if ($cat !== null) { $doctor->getExpertise()->add($cat); } } } // States if (array_key_exists('states', $data) && is_array($data['states'])) { $doctor->getStates()->clear(); foreach ($data['states'] as $catId) { $cat = $this->categoryRepo->find((int) $catId); if ($cat !== null) { $doctor->getStates()->add($cat); } } } // Cities if (array_key_exists('cities', $data) && is_array($data['cities'])) { $doctor->getCities()->clear(); foreach ($data['cities'] as $catId) { $cat = $this->categoryRepo->find((int) $catId); if ($cat !== null) { $doctor->getCities()->add($cat); } } } } private function hydrateAddress(DoctorAddress $address, array $data): void { if (array_key_exists('name', $data)) $address->setName($data['name']); if (array_key_exists('address', $data)) $address->setAddress($data['address']); if (array_key_exists('telephone', $data)) $address->setTelephone($data['telephone']); if (isset($data['map']['latitude'])) $address->setLatitude((float) $data['map']['latitude']); if (isset($data['map']['longitude'])) $address->setLongitude((float) $data['map']['longitude']); // Also support flat keys if (array_key_exists('latitude', $data)) $address->setLatitude((float) $data['latitude']); if (array_key_exists('longitude', $data)) $address->setLongitude((float) $data['longitude']); } }