# تسک ۰۱: راه‌اندازی پروژه و زیرساخت ## توضیح راه‌اندازی اولیه پروژه Symfony 7 با DDEV، نصب پکیج‌ها، پیکربندی JWT، Doctrine ORM، CORS، ساختار Domain-Driven و الگوهای مشترک (BaseController، DTO، Error Codes). ## Endpoint ها | متد | مسیر | توضیح | نیاز به Auth | |-----|------|-------|-------------| | GET | `/health` | Health Check | خیر | ## پیش‌نیازها ندارد — اولین تسک است. ## خروجی‌های مورد انتظار - [ ] DDEV راه‌اندازی و در حال اجرا - [ ] پروژه Symfony 7 ایجاد شده - [ ] ساختار پوشه Domain-Driven تعریف شده - [ ] JWT authentication bundle پیکربندی شده - [ ] Doctrine ORM پیکربندی شده - [ ] CORS پیکربندی شده - [ ] `BaseController` با متدهای `success()` و `error()` پیاده شده - [ ] `BaseRepository` با متدهای مشترک پیاده شده - [ ] Error Codes و Error Response استاندارد تعریف شده - [ ] DTO pattern برای Input/Output تعریف شده - [ ] Rate Limiter پیکربندی شده - [ ] Symfony Messenger (Queue) راه‌اندازی شده - [ ] Structured Logging با Monolog پیکربندی شده - [ ] Swagger UI (NelmioApiDocBundle) راه‌اندازی شده — `/api/doc` - [ ] `GET /health` endpoint پیاده شده ## زمان تخمینی ۶ تا ۸ ساعت --- ## مراحل راه‌اندازی با DDEV ### ۱. ایجاد پروژه Symfony (قبل از DDEV) ```bash mkdir clinic-pro-symfony && cd clinic-pro-symfony # ابتدا Symfony، بعد DDEV composer create-project symfony/skeleton . "7.*" ddev config \ --project-type=symfony \ --php-version=8.3 \ --docroot=public \ --project-name=clinic-pro ddev start ``` ### ۲. نصب پکیج‌ها (همه در یک دستور) ```bash ddev composer require \ symfony/security-bundle \ symfony/validator \ symfony/serializer \ symfony/property-access \ symfony/property-info \ symfony/uid \ symfony/messenger \ lexik/jwt-authentication-bundle \ doctrine/doctrine-bundle \ doctrine/doctrine-migrations-bundle \ symfony/cache \ nelmio/cors-bundle \ symfony/rate-limiter \ symfony/http-client \ nelmio/api-doc-bundle \ zircote/swagger-php \ twig/twig \ symfony/asset ddev composer require --dev \ symfony/maker-bundle \ doctrine/data-fixtures \ symfony/debug-bundle ``` ### ۳. راه‌اندازی Redis با DDEV addon رسمی ```bash ddev get ddev/ddev-redis ddev restart ``` ### ۴. تولید کلیدهای JWT ```bash ddev exec php bin/console lexik:jwt:generate-keypair ``` ### ۵. پیکربندی LexikJWT فایل `config/packages/lexik_jwt_authentication.yaml`: ```yaml lexik_jwt_authentication: secret_key: '%env(resolve:JWT_SECRET_KEY)%' public_key: '%env(resolve:JWT_PUBLIC_KEY)%' pass_phrase: '%env(JWT_PASSPHRASE)%' token_ttl: 3600 ``` ### ۶. پیکربندی Symfony Messenger (Queue) فایل `config/packages/messenger.yaml`: ```yaml framework: messenger: transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: auto_setup: true routing: 'App\Shared\Message\SendSmsMessage': async 'App\Shared\Message\SendNotificationMessage': async ``` ### ۷. پیکربندی Swagger فایل `config/packages/nelmio_api_doc.yaml`: ```yaml nelmio_api_doc: documentation: info: title: ClinicPro API description: مستندات API سیستم کلینیک‌پرو version: 1.0.0 components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT security: - bearerAuth: [] areas: path_patterns: - ^/api - ^/oauth - ^/health ``` فایل `config/routes/nelmio_api_doc.yaml`: ```yaml app.swagger_ui: path: /api/doc methods: GET defaults: _controller: nelmio_api_doc.controller.swagger_ui app.swagger_json: path: /api/doc.json methods: GET defaults: _controller: nelmio_api_doc.controller.swagger ``` ### ۸. پیکربندی Structured Logging فایل `config/packages/monolog.yaml` (بخش prod): ```yaml monolog: handlers: main: type: stream path: '%kernel.logs_dir%/%kernel.environment%.log' level: info formatter: monolog.formatter.json security: type: stream path: '%kernel.logs_dir%/security.log' level: warning channels: [security] ``` --- ## ساختار پوشه Domain-Driven (الزامی) ``` src/ ├── Auth/ ← Identity Context │ ├── Controller/ │ ├── Service/ │ ├── DTO/ │ └── Message/ ← برای ارسال OTP async │ ├── Doctor/ ← Clinical Context │ ├── Controller/ │ ├── Entity/ │ │ ├── Doctor.php │ │ └── DoctorAddress.php │ ├── Repository/ │ ├── Service/ │ │ ├── DoctorService.php │ │ └── ScheduleService.php ← محاسبه free_turn و hours_of_work │ └── DTO/ │ ├── Clinic/ ← Clinical Context │ ├── Controller/ │ ├── Entity/ │ ├── Repository/ │ ├── Service/ │ └── DTO/ │ ├── Appointment/ ← Scheduling Context │ ├── Controller/ │ ├── Entity/ │ ├── Repository/ │ ├── Service/ │ │ ├── AppointmentService.php │ │ └── SlotService.php ← محاسبه اسلات‌های خالی │ └── DTO/ │ ├── Payment/ ← Financial Context │ ├── Controller/ │ ├── Entity/ │ ├── Repository/ │ ├── Service/ │ └── Gateway/ │ ├── PaymentGatewayInterface.php │ ├── MellatGateway.php │ └── SepGateway.php │ ├── Rating/ ← Community Context │ ├── Controller/ │ ├── Entity/ │ ├── Repository/ │ ├── Service/ │ │ └── RatingCalculatorService.php │ └── DTO/ │ ├── Blog/ ├── Category/ ← Catalog Context ├── Representation/ ← Tenant Context ├── Secretary/ ├── Insurance/ ├── Settlement/ ├── Sms/ │ └── Shared/ ← کدهای مشترک ├── Controller/ │ └── BaseController.php ├── Repository/ │ └── BaseRepository.php ├── Response/ │ ├── ApiResponse.php │ └── ApiError.php ├── DTO/ │ └── PaginationMeta.php ├── Exception/ │ └── AppException.php ├── Message/ │ ├── SendSmsMessage.php │ └── SendNotificationMessage.php └── Constant/ └── ErrorCodes.php ``` **قانون Dependency Direction — رعایت اجباری:** ``` Controller → Service → Repository → Entity ``` هیچ‌گاه: ``` Entity → Service ❌ Repository → Controller ❌ Service → Controller ❌ ``` --- ## Error Response استاندارد ### فرمت موفق ```json { "success": true, "data": { ... }, "meta": { "page": 1, "totalPages": 5, "totalRecords": 47 } } ``` ### فرمت خطا ```json { "success": false, "data": null, "errors": [ { "code": "ERR_VALIDATION_001", "field": "mobile_number", "message": "فرمت شماره موبایل نادرست است" } ] } ``` ### Error Codes — `src/Shared/Constant/ErrorCodes.php` ```php class ErrorCodes { // Auth const ERR_AUTH_001 = 'توکن JWT منقضی شده یا نامعتبر است'; const ERR_AUTH_002 = 'کد OTP نامعتبر است'; const ERR_AUTH_003 = 'کد OTP منقضی شده است'; const ERR_AUTH_004 = 'تعداد تلاش‌های OTP به حد مجاز رسیده است'; // Validation const ERR_VALIDATION_001 = 'ورودی نامعتبر است'; const ERR_VALIDATION_002 = 'فیلد الزامی وارد نشده است'; // Not Found const ERR_NOT_FOUND_001 = 'منبع درخواستی یافت نشد'; // Forbidden const ERR_FORBIDDEN_001 = 'دسترسی به این منبع مجاز نیست'; // Payment const ERR_PAYMENT_001 = 'درگاه پرداخت در دسترس نیست'; const ERR_PAYMENT_002 = 'مبلغ پرداخت نامعتبر است'; const ERR_PAYMENT_003 = 'وضعیت نوبت برای پرداخت مناسب نیست'; // Appointment const ERR_APPOINTMENT_001 = 'اسلات انتخاب‌شده در دسترس نیست'; const ERR_APPOINTMENT_002 = 'نوبت قابل لغو نیست'; // File const ERR_FILE_001 = 'فرمت فایل مجاز نیست'; const ERR_FILE_002 = 'حجم فایل بیش از حد مجاز است (حداکثر 5MB)'; } ``` ### BaseController — `src/Shared/Controller/BaseController.php` ```php abstract class BaseController extends AbstractController { protected function success(mixed $data, int $status = 200, array $meta = []): JsonResponse { $response = ['success' => true, 'data' => $data]; if (!empty($meta)) { $response['meta'] = $meta; } return new JsonResponse($response, $status); } protected function paginated(mixed $data, int $total, int $page, int $limit): JsonResponse { return $this->success($data, 200, [ 'totalRecords' => $total, 'totalPages' => (int) ceil($total / $limit), 'currentPage' => $page, ]); } protected function error(string $code, string $message, int $status = 400, ?string $field = null): JsonResponse { $err = ['code' => $code, 'message' => $message]; if ($field) { $err['field'] = $field; } return new JsonResponse(['success' => false, 'data' => null, 'errors' => [$err]], $status); } } ``` --- ## DTO Pattern هر endpoint باید DTO جداگانه داشته باشد: ```php // Input DTO (Request) class DoctorCreateRequest { #[Assert\NotBlank(message: 'نام دکتر الزامی است')] public string $title; #[Assert\Choice(['man', 'woman'])] public string $gender; #[Assert\Range(min: 0, max: 100)] public int $experience; } // Output DTO (Response) class DoctorResponse { public function __construct(private Doctor $doctor) {} public function toArray(): array { return [ 'id' => (string) $this->doctor->getId(), 'uuid' => $this->doctor->getUuid(), 'name' => $this->doctor->getName(), 'gender' => $this->doctor->getGender(), 'experience' => $this->doctor->getExperience(), // ... ]; } } ``` --- ## Health Check — `GET /health` ```json // Response 200: { "status": "ok", "checks": { "database": "ok", "redis": "ok" }, "timestamp": 1748000000 } // Response 503 (اگر یکی از سرویس‌ها down باشد): { "status": "degraded", "checks": { "database": "ok", "redis": "error" }, "timestamp": 1748000000 } ``` --- ## File Upload — محدودیت‌های مشترک و امنیتی ``` حداکثر حجم فایل: 5MB فرمت‌های مجاز: image/jpeg, image/png, image/webp هدرهای الزامی: Content-Type: application/octet-stream Content-Disposition: file; filename="name.jpg" Authorization: Bearer {token} ``` ### ⚠ اعتبارسنجی امنیتی فایل — بررسی محتوا، نه header ```php // src/Shared/Service/FileValidatorService.php class FileValidatorService { // Magic bytes برای تشخیص واقعی نوع فایل private const ALLOWED_SIGNATURES = [ 'image/jpeg' => ["\xFF\xD8\xFF"], 'image/png' => ["\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"], 'image/webp' => ["RIFF"], ]; public function validate(string $binaryContent, string $claimedFilename): void { // ۱. بررسی حجم if (strlen($binaryContent) > 5 * 1024 * 1024) { throw new AppException(ErrorCodes::ERR_FILE_002); } // ۲. بررسی magic bytes — نه MIME از header $detected = false; foreach (self::ALLOWED_SIGNATURES as $mime => $signatures) { foreach ($signatures as $sig) { if (str_starts_with($binaryContent, $sig)) { $detected = true; break 2; } } } if (!$detected) { throw new AppException(ErrorCodes::ERR_FILE_001); } // ۳. Sanitize filename — جلوگیری از path traversal $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '', basename($claimedFilename)); if (empty($safeName) || str_contains($safeName, '..')) { throw new AppException(ErrorCodes::ERR_FILE_001); } // ۴. پسوند باید با content مطابقت داشته باشد $ext = strtolower(pathinfo($safeName, PATHINFO_EXTENSION)); if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true)) { throw new AppException(ErrorCodes::ERR_FILE_001); } } } ``` --- ## Swagger — نحوه استفاده در Controller ```php use OpenApi\Attributes as OA; #[OA\Tag(name: 'Doctor')] class DoctorController extends BaseController { #[OA\Get( path: '/api/v1/doctor/{uuid}', summary: 'دریافت پروفایل دکتر', security: [['bearerAuth' => []]], parameters: [ new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')) ], responses: [ new OA\Response(response: 200, description: 'پروفایل کامل دکتر'), new OA\Response(response: 404, description: 'دکتر یافت نشد'), ] )] #[Route('/api/v1/doctor/{uuid}', methods: ['GET'])] public function show(string $uuid): JsonResponse { ... } } ``` --- ## متغیرهای محیطی (.env) ```dotenv APP_ENV=dev APP_SECRET=your-secret-key DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.0" REDIS_URL=redis://redis:6379 # Queue (از Redis استفاده می‌کند) MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE=your-passphrase # Refresh Token TTL (ثانیه) — 30 روز REFRESH_TOKEN_TTL=2592000 OTP_TTL=1200 # SMS Providers KAVENEGAR_API_KEY=your-key RANGINEH_API_KEY=your-key SMS_PROVIDER=kavenegar # File Upload MAX_FILE_SIZE_BYTES=5242880 ```