Files
clinicpro/docs/tasks/task-01-project-setup/task.md
T
hamed de1a78a235 feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
2026-06-09 22:00:34 +03:30

563 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# تسک ۰۱: راه‌اندازی پروژه و زیرساخت
## توضیح
راه‌اندازی اولیه پروژه 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
```