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.
This commit is contained in:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
# معماری — تسک ۰۴: ماژول بلاگ
## ساختار فایل‌ها
```
src/Module/Blog/
├── Controller/
│ ├── BlogController.php ← CRUD بلاگ
│ └── BlogImageController.php ← آپلود تصویر
├── Service/
│ ├── BlogService.php
│ └── ImageUploadService.php
├── Repository/
│ └── BlogRepository.php
├── Entity/
│ └── Blog.php
├── DTO/
│ ├── Request/
│ │ ├── CreateBlogRequest.php
│ │ └── UpdateBlogRequest.php
│ └── Response/
│ ├── BlogResponse.php
│ └── BlogListResponse.php
└── Voter/
└── BlogVoter.php
```
## Entity: Blog
```php
#[ORM\Entity]
#[ORM\Table(name: 'blogs')]
class Blog
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private User $author;
#[ORM\Column(length: 300)]
private string $title;
#[ORM\Column(length: 300, unique: true)]
private string $slug;
#[ORM\Column(type: 'text')]
private string $body;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $summary;
#[ORM\Column(length: 20, default: 'draft')]
private string $status; // draft, published, archived
#[ORM\Column(type: 'integer', default: 0)]
private int $viewCount = 0;
#[ORM\Column(length: 255, nullable: true)]
private ?string $imagePath;
#[ORM\ManyToMany(targetEntity: Category::class)]
#[ORM\JoinTable(name: 'blog_tags')]
private Collection $tags;
// TimestampableTrait
}
```
+50
View File
@@ -0,0 +1,50 @@
# پایگاه داده — تسک ۰۴: ماژول بلاگ
## ساختار واقعی از DB backup
جدول `blog` در Drupal base fields را دارد + ۳ custom field:
## جدول: blogs
_(entity_type=blog — از DB backup تأیید شده)_
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
| user_id | INT FK → users.id NOT NULL | uid | نویسنده |
| title | VARCHAR(255) NULL | label | عنوان (base field Drupal) |
| slug | VARCHAR(300) UNIQUE NOT NULL | — | اضافه‌شده در Symfony (در Drupal نیست!) |
| body | LONGTEXT NULL | description__value | محتوا (HTML) |
| status | TINYINT(1) DEFAULT 0 | status | منتشرشده/پیش‌نویس |
| is_top | TINYINT(1) DEFAULT 0 | field_top | نمایش در صفحه اول |
| image_id | INT FK → files.id NULL | field_image | تصویر شاخص (entity ref → file) |
| created_at | INT NOT NULL | created | Unix timestamp |
| updated_at | INT NOT NULL | changed | Unix timestamp |
## جدول: blog_tags (ManyToMany)
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| blog_id | INT FK → blogs.id CASCADE | | |
| category_id | INT FK → categories.id CASCADE | field_tag | تگ/دسته‌بندی (entity ref → category/tag bundle) |
## نمونه داده واقعی از DB backup
```
id=14, label='روش های خانگی محافظت از پوست در تابستان', uid=22, status=1
id=15, 16, ... (22 مطلب)
field_image: target_id=97 (file entity)
```
## ایندکس‌ها
```sql
CREATE INDEX idx_blogs_status ON blogs(status);
CREATE INDEX idx_blogs_user ON blogs(user_id);
CREATE INDEX idx_blogs_created ON blogs(created_at DESC);
CREATE INDEX idx_blogs_top ON blogs(is_top);
CREATE UNIQUE INDEX idx_blogs_slug ON blogs(slug);
```
## نکات مهم
- `slug` در Drupal وجود ندارد — در Symfony باید auto-generate شود از `title`
- `image` در Drupal یک entity reference به file/media است — در Symfony مسیر فایل ذخیره می‌شود
- `body` در Drupal با نام `description__value` ذخیره می‌شود (base field)
- `status=1` = منتشرشده، `status=0` = پیش‌نویس
@@ -0,0 +1,139 @@
# نکات پیاده‌سازی — تسک ۰۴: ماژول بلاگ
## نگاشت فیلدهای Request → Response (مهم!)
در Drupal نام فیلدهای **ارسالی** با نام فیلدهای **دریافتی** متفاوت است:
| فیلد در Request | فیلد در Response | توضیح |
|----------------|-----------------|-------|
| `label` | `title` | عنوان مقاله |
| `description` (string) | `body: {value, format}` | متن مقاله — در response به object تبدیل می‌شود |
| `field_image: [{target_id}]` | `images: [{url, fid, filename, filemime, filesize}]` | تصاویر — ID ارسال، object دریافت |
| — | `author` | نام نویسنده (computed از realname کاربر) |
| — | `uuid` | شناسه یکتا |
| — | `status` | وضعیت انتشار (string: "1") |
| — | `created` / `changed` | Unix timestamp به صورت string |
## فرمت واقعی Response بلاگ (از سرور Drupal — endpoint 93)
```json
{
"uuid": "95f6acb0-3331-4141-801e-004e8460edac",
"title": "روش های خانگی محافظت از پوست در تابستان",
"status": "1",
"body": {
"value": "متن کامل مقاله...",
"format": "full_html"
},
"created": "1763536163",
"changed": "1763537699",
"author": "single doctor",
"images": [
{
"url": "https://domain.com/sites/default/files/blog/image.png",
"fid": "97",
"filename": "image.png",
"filemime": "image/png",
"filesize": 320787
}
],
"tag": [
{
"uuid": "24926497-fc2d-47d7-82ae-26cbbc4d6468",
"id": "2501",
"name": "مجله"
},
{
"uuid": "6c6a488b-1051-43a2-887b-0ec7a05e52d5",
"id": "2500",
"name": "سلامتی"
}
]
}
```
> **نکات مهم response:**
> - `body` یک **object** است با کلیدهای `value` و `format` — نه string ساده!
> - `format` معمولاً `"full_html"` یا `"basic_html"` است
> - `author` از `realname` کاربر نویسنده computed می‌شود
> - `created` و `changed` به صورت **string** برگردانده می‌شوند (نه integer)
> - `status` به صورت string `"1"` برگردانده می‌شود (نه boolean)
> - `images` و `tag` ممکن است آرایه خالی `[]` باشند
> - response دارای `id` نیست — فقط `uuid`
## فرمت Request ایجاد/ویرایش بلاگ
### POST /api/v1/blog/
```json
{
"label": "عنوان مقاله",
"description": "متن کامل مقاله...",
"field_image": [
{ "target_id": 97 },
{ "target_id": 98 }
]
}
```
### PATCH /api/v1/blog/{uuid}
```json
{
"label": "عنوان ویرایش‌شده",
"description": "متن ویرایش‌شده"
}
```
## آپلود تصویر بلاگ (endpoint 99)
قبل از ایجاد بلاگ، تصویر باید آپلود شود و `fid` آن در `field_image` استفاده شود:
```
POST /file/upload/blog/blog/field_image
Headers:
Content-Type: application/octet-stream
Content-Disposition: file; filename="image.png"
X-CSRF-Token: {token}
Authorization: Bearer {token}
Response → fid که در field_image استفاده می‌شود
```
## تولید Slug
- از عنوان فارسی slug تولید کن
- پکیج `cocur/slugify` را نصب کن:
```bash
ddev composer require cocur/slugify
```
- اگر slug تکراری بود، عدد به انتهای آن اضافه کن: `rahnamai-diabet-2`
## بلاگ‌های برتر (Top Blogs)
- endpoint: `GET /api/v1/blogs/top` (نیاز به auth دارد)
- پاسخ: آرایه مستقیم (نه object با pagination) از بلاگ‌هایی که `is_top = 1` هستند
- فرمت هر آیتم دقیقاً مشابه response معمولی بلاگ است
## لیست بلاگ‌ها با فیلتر
```
GET /api/v1/blogs?page=1&limit=10&title=نشانه&tag=3637
```
- `title`: جستجو در عنوان
- `tag`: فیلتر بر اساس ID تگ
## Pagination
```php
$offset = ($page - 1) * $limit;
// در Repository با limit/offset
```
## مجوزها
```
POST → احراز هویت الزامی (ROLE_DOCTOR یا ROLE_ADMIN)
PATCH → احراز هویت الزامی (owner یا ROLE_ADMIN)
DELETE → احراز هویت الزامی (owner یا ROLE_ADMIN)
GET → عمومی (بدون auth)
GET /api/v1/blogs/top → احراز هویت الزامی
```
## بهینه‌سازی
- view_count با یک query atomic آپدیت کن تا race condition نباشد:
```php
$this->em->createQuery('UPDATE Blog b SET b.viewCount = b.viewCount + 1 WHERE b.id = :id')
->setParameter('id', $blog->getId())
->execute();
```
+71
View File
@@ -0,0 +1,71 @@
# تسک ۰۴: ماژول بلاگ
## توضیح
پیاده‌سازی سیستم مدیریت مقالات (بلاگ) شامل ایجاد، ویرایش، حذف،
نمایش لیست، بلاگ‌های برتر و آپلود تصویر.
## Endpoint ها
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/blog/` | ایجاد بلاگ جدید | بله (Admin/Doctor) |
| PATCH | `/api/v1/blog/{uuid}` | ویرایش بلاگ | بله (Owner/Admin) |
| DELETE | `/api/v1/blog/{uuid}` | حذف بلاگ | بله (Owner/Admin) |
| GET | `/api/v1/blog/{uuid}` | دریافت یک بلاگ | خیر |
| GET | `/api/v1/blogs` | لیست بلاگ‌ها (با pagination) | خیر |
| GET | `/api/v1/blogs/top` | بلاگ‌های برتر | خیر |
| POST | `/api/v1/blog/image` | آپلود تصویر بلاگ | بله |
## پیش‌نیازها
- تسک ۰۱ و ۰۲
## خروجی‌های مورد انتظار
- [ ] CRUD کامل برای بلاگ
- [ ] pagination در لیست
- [ ] آپلود و ذخیره تصویر
- [ ] بلاگ‌های برتر (براساس بازدید یا لایک)
- [ ] slug برای SEO
## زمان تخمینی
۶ تا ۸ ساعت
## نمونه Request/Response
### POST /api/v1/blog/
```json
// Request
{
"title": "راهنمای کامل دیابت",
"body": "<p>محتوای مقاله...</p>",
"summary": "خلاصه مقاله",
"tags": [1, 2, 3],
"status": "published",
"image_uuid": "abc-..."
}
// Response 201
{
"data": {
"uuid": "72522a1d-...",
"title": "راهنمای کامل دیابت",
"slug": "rahnamai-kamel-diabet",
"status": "published",
"created_at": "2024-01-01T00:00:00Z"
}
}
```
### GET /api/v1/blogs
```
Query params: page=1&limit=10&category=1&tag=2
```
### GET /api/v1/blogs/top
```json
// Response
{
"data": [
{ "uuid": "...", "title": "...", "views": 1250, "image": "..." }
]
}
```