+ محدودیت زمان: ۱ ثانیه
+ محدودیت حافظه: ۲۵۶ مگابایت
----------
در دنیای توسعه نرمافزار، [**ایرانسرور** *(IranServer)*](https://www.iranserver.com/) همواره به دنبال **بهترین راهحلها** بود تا پروژههایش را منظمتر و حرفهایتر مدیریت کند. یکی از چالشهای اخیر تیم فنی، ایجاد یک تابع *slug* بود که بتواند متنها را به **قالبی استاندارد و یکنواخت** تبدیل کند. تابعی که **تمام حروف** را کوچک کند، **علائم نگارشی** را **حذف** کند و **فاصلهها** را با **خط تیره** جایگزین کند. اما مشکلی وجود داشت؛ در برخی از متون، نام **ایرانسرور** به صورت `"Iran Server"` نوشته شده بود و این باعث میشد که خروجی نهایی یکپارچه **نباشد**.

تیم توسعه پس از بررسیهای فراوان، تصمیم گرفت این مشکل را یک بار برای همیشه حل کند. آنها از شما میخواهند که این تابع را پیادهسازی کنید. یک رشتهی متنی حاوی کلمات انگلیسی، اعداد، و علائم نگارشی داریم. هدف این است که رشته را به یک *slug* تبدیل کنیم.
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/81560/download_problem_initial_project/279968/) دانلود کنید.
شما باید **تابع** `slugify` را به شکل زیر پیادهسازی کنید تا این کار را انجام دهد:
```php solution.php php
<?php
function slugify(string $input): string {
// TODO
}
```
### **ورودی تابع:**
+ **یک رشتهی متنی** *(حاوی حروف انگلیسی کوچک و بزرگ، اعداد، فاصله، و علائم نگارشی).*
### **خروجی تابع:**
+ یک **رشته** بهصورت *slug* که مطابق شرایط زیر پردازش شده است:
+ **تمامی حروف به حروف کوچک تبدیل شوند.**
+ **تمام علائم نگارشی از متن حذف شوند.**
+ **فاصلهها با یک خط تیره** (`-`) **جایگزین شوند.**
+ اگر کلمات `"iran"` و `"server"` **به صورت متوالی و بهترتیب با یک فاصله** ظاهر شوند، به عبارت `"iranserver"` **ادغام** شوند.
# مثالها
### نمونه ورودی ۱:
```bash
>> slugify("HeLLo Iran ServEr! PHP / Laravel ConTeSt 2025!");
```
### نمونه خروجی ۱:
```bash
>> hello-iranserver-php-laravel-contest-2025
```
### نمونه ورودی ۲:
```bash
>> slugify(" Hello #World FROM @IRan SerVeR! ");
```
### نمونه خروجی ۲:
```bash
>> hello-world-from-iranserver
```
# آنچه باید آپلود کنید
یک فایل *PHP* با نام `solution.php` که **تابع** `slugify` در آن پیادهسازی شده است آپلود کنید.
چالشهای نام ایرانسرور
+ محدودیت زمان: ۱ ثانیه
+ محدودیت حافظه: ۲۵۶ مگابایت
----------
[**ایرانسرور** *(IranServer)*](https://www.iranserver.com/) تصمیم گرفته که برای **رونمایی از خدمات جدید** خود، **یک جلسه مهم** میزگردی با حضور مشتریهای کلیدیاش برگزار کند. هدف این جلسه، بحث و بررسی درباره نیازها و تجربههای مشتریان است تا تیم ایرانسرور بتواند خدماتش را **بهینهتر** کند.
اما یک مشکل وجود دارد؛ ایرانسرور طی تحقیقاتش متوجه شده که اگر مشتریهایی که با هم **آشنایی قبلی** دارند کنار هم بنشینند، بحثهایشان بیشتر حالت **تعارفی** پیدا میکند و **بازخورد صادقانهای ارائه نمیدهند.** برای اینکه بتواند بهترین تحلیلها را داشته باشد، باید اطمینان حاصل کند که **هیچ دو نفری که با هم آشنا هستند کنار هم ننشینند.**
ایرانسرور میخواهد مشتریها را به شکلی **روی دو میز تقسیم کند** که:

1. **هیچ دو نفری که آشنا هستند، روی یک میز نباشند.**
2. تعداد افراد روی دو میز باید تا حد امکان **متعادل** باشد تا هر دو میز فرصت برابری برای بحثهای مفید داشته باشند. (منظور از متعادل این است که اختلاف تعداد افراد روی دو میز **کمترین مقدار ممکن** باشد!)
# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/81560/download_problem_initial_project/279967/) دانلود کنید.
**تابع** `iranServerRoundTable` را طوری کامل کنید که خروجی خواسته شده را برگرداند:
```php solution.php php
<?php
function iranServerRoundTable(int $n, int $m, array $connections): string {
// TODO
}
```
### **ورودی تابع:**
+ مقدار `n`، نشاندهنده **تعداد افراد حاضر در مهمانی** است.
+ مقدار `m`، نشاندهنده **تعداد آشناییهای قبلی که میان افراد وجود دارد،** است.
+ مقدار `connections`، **لیستی** از `m` **آشنایی** که هر یک **به صورت یک جفت** مانند `[A, B]` است که نشان دهنده وجود آشنایی قبلی میان دو فرد *A* و *B* است.
### **خروجی تابع:**
+ در صورتی که **امکان تقسیمبندی افراد** بین دو میز مطابق شرایط داده شده وجود داشته باشد، یک آرایه که در آن مقدار کلید `possible` برابر با `"YES"` و دو کلید `table_1` و `table_2` که نشاندهنده لیستی از افراد دور هر میز هستند را بازگرداندید.
+ در غیر این صورت، آرایهای شامل کلید `possible` با مقدار `"NO"` بازگرداندید.
+ در نهایت **خروجی برنامهی شما** باید به **فرمت `JSON`** باشد.
<details class="red">
<summary>**نکته**</summary>
مقدار کلید `possible` در هر یک از شرایط ذکر شده، باید دقیقا به شکلی که نوشته شده است باشد. *__(بزرگی و کوچکی حروف مهم است!)__*
</details>
**توجه:** این سؤال از نوع ساختاری *(Constructive)* است، بنابراین پاسخهای ممکن لزوماً یکتا نیستند. با این حال، سیستم داوری پاسخهای شما را بررسی میکند تا اطمینان حاصل شود که شرایط مسئله بهدرستی رعایت شدهاند. تنها در صورتی نمره دریافت خواهید کرد که پیادهسازی شما تمامی این شرایط را برآورده کند و **یکی از پاسخهای معتبر مسئله باشد.**
# مثالها
### نمونه ورودی ۱:
```php
>> iranServerRoundTable(6, 6, [
[1, 2],
[2, 3],
[3, 4],
[4, 5],
[5, 6],
[6, 1],
]);
```
### نمونه خروجی ۱:
```json
{
"possible": "YES",
"table_1": [1, 3, 5],
"table_2": [2, 4, 6],
}
```
<details class="blue">
<summary>***توضیحات نمونه***</summary>
این مثال شامل ۶ نفر است که هر یک از آنها **دقیقا با دو فرد** دیگر آشنایی قبلی دارد؛ به شکل زیر توجه کنید:

حال میتوانیم آنها را به صوزت زیر **به دو گروه مجزا** تقسیم بندی کرد:

$$ Table_1 : [1, 3, 5], Table_2 : [2, 4, 6] $$
</details>
### نمونه ورودی ۲:
```php
>> iranServerRoundTable(6, 5, [
[1, 2],
[3, 4],
[3, 5],
[4, 5],
[1, 6],
]);
```
### نمونه خروجی ۲:
```json
{
"possible": "NO"
}
```
<details class="blue">
<summary>***توضیحات نمونه***</summary>
این مثال شامل ۶ نفر است و رابط آشنایی میان آنها به شکل زیر است:

در این مثال **نمیتوان** افراد را طوری برا سر دو میز نشاند که شرایط خواسته شده را حفظ کند؛ **چرا که در تصویر زیر فرد شماره ۵ را نمیتوان دور هیچیک از میزها نشاند!**

</details>
### نمونه ورودی ۳:
```php
>> iranServerRoundTable(6, 3, [
[1, 2],
[3, 4],
[3, 5],
]);
```
### نمونه خروجی ۳:
```json
{
"possible": "YES",
"table_1": [1, 4, 5],
"table_2": [2, 3, 6],
}
```
<details class="blue">
<summary>***توضیحات نمونه***</summary>
این مثال شامل ۶ نفر است و رابط آشنایی میان آنها به شکل زیر است:

**یکی از حالات مطلوب** جهت تقسیم افراد به دو میز موجود به شکل زیر است:

حالت دیگری هم وجود دارد که میتوان این کار را انجام داد اما با شرط دوم *(متعادل بودن میزها)* را **ندارد!**
تصویر زیر یکی از حالاتی است که برای این مسئله **موردقبول نیست:**

</details>
# آنچه باید آپلود کنید
یک فایل *PHP* با نام `solution.php` که تابع `iranServerRoundTable` در آن پیادهسازی شده است آپلود کنید.
میزگرد بزرگ
معین پس از مدتها حضور در **کوئرا،** تصمیم گرفت برای ایجاد تنوع، در مصاحبه استخدامی **جونیورا** شرکت کند؛ در این مصاحبه از او خواستهاند که پروژهای با استفاده از [_ORM_](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) مخصوص _Laravel_ انجام دهد؛ اما او معتقد است *هرکسی میتواند با کمی آموزش، رانندگی را یاد بگیرد؛ ولی فقط یک مکانیک دقیقاً میداند ماشین چگونه کار میکند.* بنابراین، تصمیم گرفت با هدف نشاندادن تبحر واقعی خود، بهجای استفاده از _ORM_ آماده، **یک _ORM_ اختصاصی به زبان _PHP_ بنویسد** و آن را به افتخار خودش **معین سخنور (_Moein Eloquent_)** نامگذاری کند. مشکل اینجاست که پروژه اصلی بهاندازه کافی وقتگیر است و معین برای پیادهسازی _ORM_ اختصاصیاش به زمان بیشتری نیاز دارد؛ از همین رو، او از شما کمک میخواهد تا در تکمیل این _ORM_ همراهش باشید.

# جزئیات پروژه
پروژه اولیه را از [این لینک](/contest/assignments/81560/download_problem_initial_project/279970/) دانلود کنید.
<details class="yellow">
<summary>**ساختار فایلها**</summary>
```
moein-eloquent
├── Database.php
├── <mark class="orange" title="این فایل باید پیادهسازی شود">> QueryBuilder.php <</mark>
├── <mark class="orange" title="این فایل باید پیادهسازی شود">> Model.php <</mark>
├── User.php
├── autoload.php
└── test
└── EloquentSampleTest.php
```
</details>
<details class="grey">
<summary>**راهاندازی پروژه**</summary>
برای راهاندازی پروژه، به پایگاهداده _MySQL_ نیاز است. در این بخش، به راهاندازی _MySQL_ در داکر میپردازیم. اگر تاکنون داکر را نصب نکردهاید، بهکمک [این لینک](https://www.docker.com/get-started/) آن را نصب کنید.
برای نصب و راهاندازی کانتینر _MySQL_ بهکمک داکر، دستور زیر را در ترمینال وارد کنید تا ایمیج _MySQL_ دانلود شود و کانتینر راهاندازی شود:
``` bash terminal terminal
docker run --name mysql-container \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-e MYSQL_DATABASE=testdb \
-p 3306:3306 \
-d mysql:latest
```
وقتی مراحل نصب کامل شد، دستور زیر را در ترمینال وارد کنید تا با سرویس _MySQL_ در کانتینر ساختهشده ارتباط برقرار کنید:
``` bash terminal terminal
docker exec -it mysql-container mysql -u root
```
حال دستور زیر را در محیط تعاملی _MySQL_ وارد کنید تا پایگاهداده `testdb` استفاده شود:
``` sql sql sql
USE testdb;
```
اکنون میتوانیم جدول `users` را ایجاد کنیم؛ دستور زیر را وارد کنید تا جدول `users` در پایگاهداده `testdb` ساخته شود:
``` sql sql sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
age INT NOT NULL
);
```
</details>
این پروژه **شامل ۵ فایل است که ۳ فایل از قبل تکمیل شدهاند**. شما باید فایلهای `Model.php` و `QueryBuilder.php` را پیادهسازی کنید؛ در ادامه، جزئیات هرکدام از فایلها و متدهای آنها را میتوانید مشاهده کنید:
<details class="blue">
<summary>**فایل** `Database.php`</summary>
این فایل شامل یک کلاس بهنام `Database` است که یک شی `pdo` را مقداردهی اولیه میکند و میتوان با استفاده از `Database::getInstance()` به آن دسترسی پیدا کرد. **این فایل از قبل پیادهسازی شده و شما نیازی به تغییر در آن ندارید**؛ در پروژه ارسالی شما، این فایل جایگزین میشود، بنابراین هر تغییری در آن ایجاد کنید، نادیده گرفته خواهد شد. اما اگر میخواهید نتایج کوئریهایتان ببینید، باید مقادیر موجود در این فایل را تغییر دهید تا بتوانید پروژه را روی سیستم خودتان اجرا کنید.
``` php Database.php php
<?php
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
$this->pdo = new PDO("mysql:host=127.0.0.1;dbname=testdb", "root", "");
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance->pdo;
}
}
?>
```
</details>
<details class="red">
<summary>**فایل** `QueryBuilder.php`</summary>
کلاس `QueryBuilder` برای **ساخت و اجرای کوئریهای پایگاهداده** طراحی شده است. این کلاس امکاناتی مانند `SELECT`، `WHERE`، `GROUP BY`، `HAVING`، `ORDER BY`، `LIMIT` و عملیات [**CRUD**](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) را فراهم میکند. در پیادهسازی متدهای این کلاس، از [**دیزاین پترن Builder**](https://refactoring.guru/design-patterns/builder) استفاده کنید.
<details class="yellow">
<summary>متد `select()`</summary>
این متد تعیین میکند که **کدام ستونها** در نتایج `SELECT` آورده شوند.
``` php
public function select(...$columns): self
{
// TODO
}
```
### مثال
``` php
User::select('name', 'age')->get();
User::select(['name', 'age'])->get();
User::select('*')->get();
User::select('age', 'COUNT(*) AS count')->groupBy('age')->get();
```
</details>
<details class="yellow">
<summary>متد `where()`</summary>
این متد برای **افزودن شرطها به کوئریهای `WHERE`** استفاده میشود.
- امکان استفاده از عملگرهای مقایسهای (`=`, `>`, `<`, `LIKE`، و ...) وجود دارد.
- در صورتی که فقط دو آرگومان به متد داده شود، عملگر مقایسهای پیشفرض (`=`) در نظر گرفته میشود.
``` php
public function where(string $column, mixed $operator = null, mixed $value = null): self
{
// TODO
}
```
### مثال
``` php
User::where('age', '>=', '32')->get();
User::where('age', '=', '40')->get();
User::where('age', '22')->get();
User::where('name', 'LIKE', 'M%')->get();
```
</details>
<details class="yellow">
<summary>متد `groupBy()`</summary>
این متد کوئری را براساس یک یا چند ستون گروهبندی (`GROUP BY`) میکند.
``` php
public function groupBy(...$columns): self
{
// TODO
}
```
### مثال
``` php
User::select('age', 'COUNT(*) AS count')->groupBy('age')->get();
User::select('age', 'email', 'COUNT(*) AS count')->groupBy('age', 'email')->get();
```
</details>
<details class="yellow">
<summary>متد `having()`</summary>
این متد برای فیلترکردن نتایج پس از `GROUP BY` استفاده میشود.
``` php
public function having(string $column, string $operator, mixed $value): self
{
// TODO
}
```
### مثال
``` php
User::select('age', 'COUNT(*) AS count')->groupBy('age')->having('age', '>', 32)->get();
```
</details>
<details class="yellow">
<summary>متد `orderBy()`</summary>
این متد **چیدمان نتایج** را براساس یک ستون و ترتیب `ASC` یا `DESC` تعیین میکند.
``` php
public function orderBy(string $column, string $direction = 'ASC'): self
{
// TODO
}
```
### مثال
``` php
User::orderBy('age')->get();
User::orderBy('age')->orderBy('name', 'DESC')->get();
```
</details>
<details class="yellow">
<summary>متد `limit()`</summary>
این متد تعداد نتایج بازگشتی را محدود (`LIMIT`) میکند.
``` php
public function limit(int $limit): self
{
// TODO
}
```
### مثال
``` php
User::orderBy('age')->limit(1)->get();
```
</details>
<details class="yellow">
<summary>متد `offset`</summary>
این متد تعدادی از نتایج بازگشتی را رد (_skip_) میکند.
``` php
public function offset(int $offset): self
{
// TODO
}
```
### مثال
``` php
User::orderBy('age')->offset(1)->limit(1)->get();
```
</details>
<details class="yellow">
<summary>متد `get()`</summary>
این متد کوئری نهایی را **اجرا** کرده و نتایج را **بهصورت آرایهای از اشیا مدل** برمیگرداند.
``` php
public function get(): array
{
// TODO
}
```
</details>
<details class="yellow">
<summary>متد `first()`</summary>
این متد **اولین نتیجهی** کوئری را دریافت کرده و مقدار `null` را در صورت خالیبودن بازمیگرداند.
``` php
public function first()
{
// TODO
}
```
### مثال
``` php
User::orderBy('age', 'DESC')->first();
```
</details>
<details class="yellow">
<summary>متد `count()`</summary>
این متد تعداد کل ردیفهای خروجی کوئری را برمیگرداند (`COUNT(*)`).
``` php
public function count(): int
{
// TODO
}
```
### مثال
``` php
User::count();
User::where('age', 24)->count()
```
</details>
<details class="yellow">
<summary>متدهای `sum()`, `avg()`, `min()`, `max()`</summary>
این متدها مقدار **مجموع**، **میانگین**، **حداقل** و **حداکثر** یک ستون را محاسبه میکنند.
``` php
private function aggregate($function, $column)
{
// TODO
}
public function sum($column)
{
// TODO
}
public function avg($column)
{
// TODO
}
public function min($column)
{
// TODO
}
public function max($column)
{
// TODO
}
```
### مثال
``` php
User::sum('age');
User::avg('age');
User::min('age');
User::max('age');
```
</details>
<details class="yellow">
<summary>متد `insert()`</summary>
این متد یک **رکورد جدید** در پایگاهداده درج (`INSERT`) کرده و `id` آن را برمیگرداند.
``` php
public function insert(array $data): int
{
// TODO
}
```
### مثال
``` php
User::insert(
[
'id' => 10,
'name' => 'New User',
'age' => 50,
'email' => 'newuser@email.com'
]
)
```
</details>
<details class="yellow">
<summary>متد `update()`</summary>
این متد دادههای جدول را بر اساس `WHERE` **بهروزرسانی** میکند.
- اگر شرط `WHERE` تعیین نشده باشد، خطا (`Exception`) ایجاد میشود.
``` php
public function update(array $data): bool
{
// TODO
}
```
### مثال
``` php
User::where('id', 10)->update(['name' => 'Newer Name']);
```
</details>
<details class="yellow">
<summary>متد `delete()`</summary>
این متد رکوردهای پایگاهداده را حذف (`DELETE`) میکند.
- اگر `WHERE` تعیین نشده باشد، عملیات متوقف شده و خطا (`Exception`) صادر میشود.
``` php
public function delete(): bool
{
// TODO
}
```
### مثال
``` php
User::where('id', 10)->delete();
```
</details>
</details>
<details class="red">
<summary>**فایل** `Model.php`</summary>
این فایل باید پیادهسازی شود. در این فایل یک **کلاس انتزاعی (_abstract_)** بهنام `Model` وجود دارد که **مدلهای پایگاهداده از آن ارثبری میکنند**. این کلاس دو پراپرتی دارد؛ نام جدول و آرایه `attributes`. نام جدول باید توسط مدلهایی که از این کلاس ارثبری میکنند _Overwrite_ شود. آرایه `attributes` برای ستکردن پراپرتیهای _dynamic_ است. این کلاس دارای چند متد است که باید پیادهسازی شوند:
<details class="yellow">
<summary>متد `query()`</summary>
این متد یک **شیء از کلاس `QueryBuilder`** ایجاد کرده و برای اجرای کوئریهای پایگاهداده مورد استفاده قرار میگیرد.
``` php
public static function query(): QueryBuilder
{
// TODO
}
```
</details>
<details class="yellow">
<summary>متد `allQuery()`</summary>
این متد تمام رکوردهای موجود در جدول مرتبط با مدل را دریافت کرده و **بهصورت آرایهای از اشیا** برمیگرداند.
``` php
public static function allQuery(): array
{
// TODO
}
```
### مثال
``` php
User::all();
```
</details>
<details class="yellow">
<summary>متد `findQuery()`</summary>
این متد یک رکورد خاص را بر اساس مقدار `id` جستوجو میکند.
- اگر رکوردی یافت شود، **نمونهای از مدل** را برمیگرداند.
- در غیر این صورت مقدار `null` برمیگردد.
``` php
public static function findQuery(int $id): ?self
{
// TODO
}
```
### مثال
``` php
User::find(1);
```
</details>
<details class="yellow">
<summary>متد `createQuery()`</summary>
این متد یک رکورد جدید در جدول مرتبط با مدل ایجاد میکند.
- دادههای جدید را در پایگاهداده **درج** کرده و نمونهای از مدل را که شامل این دادهها است، برمیگرداند.
``` php
public static function createQuery(array $data): ?self
{
// TODO
}
```
### مثال
``` php
User::create([
'id' => 20,
'name' => 'Jason Voorhees',
'email' => 'jason@gmail.com',
'age' => 60
]);
```
</details>
<details class="yellow">
<summary>متد `firstQuery()`</summary>
این متد **اولین رکورد موجود** در جدول را دریافت کرده و بهعنوان نمونهای از مدل برمیگرداند.
``` php
public static function firstQuery(): ?self
{
// TODO
}
```
### مثال
``` php
User::first();
```
</details>
<details class="yellow">
<summary>متد `hydrateQuery()`</summary>
این متد یک نمونه از مدل را با دادههای ورودی مقداردهی میکند.
- برای **پر کردن شیء مدل از یک آرایه داده** استفاده میشود.
``` php
public static function hydrateQuery(array $data): self
{
// TODO
}
```
</details>
<details class="yellow">
<summary>متد `updateQuery()`</summary>
این متد مقدار فیلدهای یک رکورد را **بهروزرسانی** میکند.
- رکورد مورد نظر باید در _object_ و پایگاهداده همزمان بهروزرسانی شود.
- اگر بهروزرسانی موفقیتآمیز باشد، مقدار `true` برمیگردد.
- در غیر این صورت مقدار `false` بازمیگردد.
``` php
public function updateQuery(array $data): bool
{
// TODO
}
```
### مثال
``` php
$user = User::find(1);
$user->update(['age' => 30]);
```
</details>
<details class="yellow">
<summary>متد `deleteQuery()`</summary>
این متد رکورد مربوطه را از پایگاهداده **حذف** میکند.
- اگر حذف موفقیتآمیز باشد، مقدار `true` برمیگردد.
- در غیر این صورت مقدار `false` بازمیگردد.
``` php
public function deleteQuery(): bool
{
// TODO
}
```
### مثال
``` php
$user = User::find(1);
$user->delete();
```
</details>
</details>
<details class="blue">
<summary>**فایل** `User.php`</summary>
این فایل شامل یک کلاس `User` است که از کلاس `Model` ارثبری میکند. در این کلاس، صرفا مقادیر مورد نیاز تعریف یا مقداردهی شدهاند. شما نیازی به پیادهسازی این فایل ندارید.
``` php User.php php
<?php
require_once 'autoload.php';
class User extends Model {
protected static string $table = 'users';
public int $id;
public string $name;
public string $email;
public int $age;
}
?>
```
</details>
<details class="blue">
<summary>**فایل** `autoload.php`</summary>
این فایل صرفا شامل یک رجیستر است که کلاسهای مورد نیاز را از فایلهای دیگر فراخوانی میکند. شما نیازی به پیادهسازی این فایل ندارید.
``` php autoload.php php
<?php
spl_autoload_register(function ($class_name) {
require __DIR__ . "/$class_name.php";
});
?>
```
</details>
# نکات
- برای حل این سوال باید از [**دیزاین پترن Builder**](https://refactoring.guru/design-patterns/builder) استفاده کنید.
- دقت کنید که صرفا تغییراتی که در فایلهای `QueryBuilder.php` و `Model.php` ایجاد میکنید بررسی خواهند شد؛ **تغییراتی که در فایلهای دیگر پروژه ایجاد میکنید در سیستم داوری نادیده گرفته خواهند شد.**
# آنچه باید آپلود کنید
پس از پیادهسازی فایلهای مورد نیاز، فایلهای `QueryBuilder.php` و `Model.php` را بهصورت فایل _Zip_ ارسال کنید.
معین سخنور
**میعین** *(Miein)* که به تازگی از [**کوئرا** *(Quera)*](https://quera.org/) به [**جونیورا** *(Juniora)*](https://juniora.org/) فرار کرده است، پس از مدتی متوجه شده است که حتی **جونیورا** نیز **نمیتواند میزبان رویاهای بلندپروازانهی او باشد!** او که پس از دیدن بنر **مسابقه استخدامی** [**ایرانسرور** *(IranServer)*](https://www.iranserver.com/) در کوئرا و مراجعه به وبسایت آن، با شعار _**"سریع، امن و همیشه همراه در مسیر رشد کسب و کار"**_ مواجه شده است حالا مطمئن شده که ایرانسرور همان **سرزمین رویاهای او** خواهد بود.
از آنجایی که رسیدن به **هیچ سرزمین رویاهایی** بدون چالش و دشواری **امکان پذیر نیست،** میعین باید **چالش ایرانسروری** زیر را حل کند تا بتواند مراحل استخدام در این مجموعه را با موفقیت طی کند!

# جزئیات پروژه
پروژهی اولیه را از [این لینک](/contest/assignments/81560/download_problem_initial_project/279969/) دانلود کنید.
<details class="yellow">
<summary>
**ساختار فایلها**
</summary>
```
miein-farari
├── app
│ ├── Http
│ │ ├── Controllers
│ │ │ └── <mark class="orange" title="این فایل باید بهینه سازی شود">UserController.php</mark>
│ ├── Models
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">User.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Service.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Invoice.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">Ticket.php</mark>
├── database
│ ├── factories
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">UserFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">ServiceFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">InvoiceFactory.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">TicketFactory.php</mark>
│ ├── migrations
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2024_02_26_000000_create_users_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2024_02_26_000001_create_services_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2024_02_26_000002_create_invoices_table.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">2024_02_26_000003_create_tickets_table.php</mark>
│ ├── seeders
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">DatabaseSeeder.php</mark>
├── routes
│ └── web.php
├── tests
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
```
</details>
<details class="grey">
<summary>
**راهاندازی پروژه**
</summary>
**برای اجرای پروژه، باید `php` و `composer` را از قبل نصب کرده باشید.**
+ ابتدا پروژهی اولیه را دانلود و از حالت فشرده خارج کنید.
+ دستور `composer install` را در پوشهی اصلی پروژه برای نصب نیازمندیها اجرا کنید.
+ برای اجرای مایگریشنها از دستور `php artisan migrate` استفاده کنید.
</details>
**میعین** در چالش ایرانسروری باید بخشی از **مدلها**، **مایگریشنها**، **سیدر** و **فکتوریهای** ایرانسرور را در یک **پروژه لاراولی** پیادهسازی کند. همچنین او در نهایت قرار است تا **یک مشکل رایج** در کار با دیتابیسها را در بخشی از یکی از کنترلرهایی که از قبل در وبسایت ایرانسرور وجود دارد را **برطرف** کند! توضیح مواردی که باید پیادهسازی شوند به شکل زیر میباشد:
## **پیادهسازی جداول و نوشتن مایگریشنها** *(Migrations)*
در این بخش میعین باید مایگریشنهای مربوط به ایجاد جداول `users`، `services`، `invoices` و `tickets` را که به ترتیب مربوط به **اطلاعات کاربران**، **سرویسهای ارائهشده توسط ایرانسرور به هر کاربر**، **صورتحسابهای سرویسها** و **تیکتهای پشتیبانی ارسالی کاربران** میباشند را پیادهسازی کند. ساختار مربوط به هر کدام از جداول در بخش زیر مشخص است:
<details class="blue">
<summary>
**ساختار جدول** `users` و **پیادهسازی مایگریشن** `create_users_table`
</summary>
## **ساختار جدول** `users`
| نام ستون | نوع داده | توضیحات |
| ------------------- | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `name` | `string` | نام کاربر |
| `email` | `string` | ایمیل (منحصربهفرد) |
| `email_verified_at` | `timestamp` | زمان تأیید ایمیل |
| `password` | `string` | رمز عبور |
| `remember_token` | `string` | توکن احراز هویت |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `services` و **پیادهسازی مایگریشن** `create_services_table`
</summary>
## جدول `services`
| نام ستون | نوع داده | توضیحات |
| ------------ | -------------- | -------------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به جدول `users` |
| `name` | `string` | نام سرویس |
| `price` | `decimal(8,2)` | قیمت سرویس |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `invoices` و **پیادهسازی مایگریشن** `create_invoices_table`
</summary>
## جدول `invoices`
| نام ستون | نوع داده | توضیحات |
| ------------ | -------------- | ----------------------------- |
| `id` | `bigint` | کلید اصلی |
| `service_id` | `bigint` | کلید خارجی به جدول `services` |
| `amount` | `decimal(8,2)` | مبلغ صورتحساب |
| `paid_at` | `timestamp` | زمان پرداخت |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `tickets` و **پیادهسازی مایگریشن** `create_tickets_table`
</summary>
## جدول `tickets`
| نام ستون | نوع داده | توضیحات |
| ------------- | ----------- | -------------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به جدول `users` |
| `subject` | `string` | موضوع تیکت |
| `description` | `text` | توضیحات تیکت |
| `status` | `string` | وضعیت (`open` یا `closed`) |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
## **پیادهسازی مدلها**
لیست بخشی از مدلهای ایرانسرور به شرح زیر است که باید **به همراه روابطشان** در پروژه پیادهسازی شوند:
+ **مدل** `User`**:** کاربران *ایرانسرور*
+ **مدل** `Service`**:** سرویسهایی که *ایرانسرور* به هر کاربر ارائه میدهد
+ **مدل** `Invoice`**:** صورتحسابهای پرداختی مربوط به سرویسهای ارائه شده
+ **مدل** `Ticket`**:** تیکتهای پشتیبانی ارسال شده توسط کاربران
## **پیادهسازی روابط بین مدلها**
+ هر **کاربر** میتواند چندین **سرویس** داشته باشد.
+ هر **سرویس** به یک **کاربر** تعلق دارد.
+ هر **سرویس** میتواند چندین **صورتحساب** داشته باشد.
+ هر **صورتحساب** به یک **سرویس** تعلق دارد.
+ هر **کاربر** میتواند چندین **تیکت پشتیبانی** ایجاد کند.
+ هر **تیکت پشتیبانی** متعلق به یک **کاربر** است.
## **پیادهسازی فکتوریها** *(Factories)*
میعین باید فکتوریهای زیر را در این چالش پیادهسازی کند تا در مرحله بعد با استفاده از یک **دیتابیس سیدر** *(Seeder)* **دادههای تصادفی** برای تست برنامه نوشته شده ایجاد کند. برای بررسی بیشتر هر کدام از فکتوریها روی بخشهای زیر کلیک کنید:
<details class="red">
<summary>
**ساختار فکتوری** `InvoiceFactory`
</summary>
### **فکتوری** `InvoiceFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ---------- | ----------------------------- | --------------------------- |
| `service_id` | کلید خارجی به جدول `services` | مقدار یک `service_id` معتبر |
| `amount` | مبلغ صورت حساب | عدد اعشاری بین ۱۰ تا ۱۰۰ |
| `paid_at` | زمان پرداخت | `null` یا یک تاریخ تصادفی |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `ServiceFactory`
</summary>
### **فکتوری**`ServiceFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| -------- | -------------------------- | ------------------------ |
| `user_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
| `name` | نام سرویس | یک کلمه تصادفی |
| `price` | قیمت سرویس | عدد اعشاری بین ۱۰ تا ۱۰۰ |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `TicketFactory`
</summary>
### **فکتوری** `TicketFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ----------- | -------------------------- | ------------------------ |
| `user_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
| `subject` | موضوع تیکت | یک جمله تصادفی |
| `description` | توضیحات تیکت | یک پاراگراف تصادفی |
| `status` | وضعیت تیکت | `open` یا `closed` |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `UserFactory`
</summary>
### **فکتوری** `UserFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ----------------- | ------------------- | ----------------------- |
| `name` | نام کاربر | نام تصادفی |
| `email` | ایمیل کاربر (یونیک) | ایمیل تصادفی معتبر |
| `email_verified_at` | زمان تأیید ایمیل | زمان فعلی |
| `password` | رمز عبور | کلمه `password` به صورت هش شده |
| `remember_token` | توکن یادآوری | رشته ۱۰ کاراکتری تصادفی |
</details>
## **پیادهسازی سیدر** *(Seeder)*
یک **سیدر** *(Seeder)* با عنوان `DatabaseSeeder` در پروژه اولیه وجود دارد که باید به شکل زیر برای ایجاد دادهها در دیتابیس **با استفاده از فکتوریهای پیادهسازی شده** مورد استفاده قرار بگیرد:
1. دقیقا **۱۰ کاربر** با استفاده از **فکتوری** `User` ایجاد میشوند.
2. **هر کاربر دقیقا ۵ سرویس** با استفاده از **فکتوری** `Service` خواهد داشت، پس در نهایت باید **۵۰ سرویس** ثبت شود.
3. **هر کاربر به صورت تصادفی بین ۳ تا ۵ صورتحساب** با استفاده از **فکتوری** `Invoice` برای سرویسهای مختلف خود دارد. توجه داشته باشید که **تعداد کل صورتحسابهای کاربر بین ۳ تا ۵ صورتحساب میباشد** و این مقدار **به ازای هر کدام از سرویسهای کاربر نیست.**
4. **هر کاربر به صورت تصادفی بین ۰ تا ۱۰ تیکت** با استفاده از **فکتوری** `Ticket` دارد.
## **بهینهسازی یک مشکل اساسی در کنترلر** `UserController`
کنترلر `UserController` به شکل زیر از قبل در **فایلهای پروژه اولیه** پیادهسازی شده است که با دریافت تمامی کاربران موجود در برنامه در ویو `users.index` لیست کاربران به همراه هر کدام از سرویسهای مربوط به آنها و هر کدام از صورتحسابهای مربوط به هر کدام از آن سرویسها را نمایش میدهد. پیادهسازی انجام شده به شکل **بهینهای نیست** و **دارای مشکل است** که میعین باید این مشکل را نیز در پروژه ارسالی خود **برطرف** سازد:
```php App\Http\Controllers laravel
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
class UserController extends Controller
{
public function index()
{
$users = User::all();
return view('users.index', compact('users'));
}
}
```
# آنچه باید آپلود کنید
+ **توجه**: پس از پیادهسازی موارد خواسته شده، کل فایلهای پروژه بهجز پوشهی `vendor` را زیپ کرده و ارسال کنید.
+ **توجه**: شما مجاز به افزودن فایل جدیدی در این ساختار **نیستید** و تنها باید تغییرات را در فایلهای موجود اعمال کنید.
+ **توجه**: که نام فایل _Zip_ اهمیتی **ندارد**.
میعین فیراری
پس از تلاشهای فراوان **معین** بالاخره به سرزمین رویاهایش [**ایرانسرور** *(IranServer)*](https://www.iranserver.com/) رسید! از آنجایی که ایرانسرور قصد دارد به زودی **سرویسهای ابری جدیدی** را به کاربرانش ارائه دهد، معین قرار است همراه با سایر *ایرانسروریون* به توسعه پنل کاربری جدیدی با عنوان __"ایرانگیت"__ مشغول شود که بر پایه [**لاراول ۱۲**](https://laravel.com/docs/12.x/releases#laravel-12) است که به تازگی معرفی شده است!
از آنجایی که *ایرانگیت* به زودی قرار است به کاربران معرفی شود، در این سوال شما نیز قرار است تا به کمک معین و *ایرانسروریون* بروید تا با استفاده از [**لاراول ۱۲**](https://laravel.com/docs/12.x/releases#laravel-12) و [**استارتر کیت Jetstream**](https://jetstream.laravel.com/introduction.html) بخشهایی از پنل کاربری ایرانگیت را پیادهسازی کنید.

# جزئیات پروژه
**پروژهی اولیه** را از [این لینک](/contest/assignments/81560/download_problem_initial_project/279971/) دانلود کنید.
<details class="yellow">
<summary>
**ساختار فایلها**
</summary>
```
irangate
├── app
│ ├── Http
│ │ ├── Middleware
│ │ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">CheckFeatureAccess.php</mark>
│ ├── Jobs
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">DowngradePendingInvoicesJob.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">ExpireSubscriptionsJob.php</mark>
│ ├── Models
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Coupon.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Feature.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Invoice.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Log.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Plan.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Referral.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">Subscription.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionLog.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">User.php</mark>
│ ├── Observers
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionObserver.php</mark>
│ ├── Policies
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">FeaturePolicy.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionPolicy.php</mark>
│ ├── Providers
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">AppServiceProvider.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">AuthServiceProvider.php</mark>
├── bootstrap
│ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">app.php</mark>
│ └── <mark class="orange" title="این فایل باید پیاده سازی شود">providers.php</mark>
├── database
│ ├── factories
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">CouponFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">FeatureFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">InvoiceFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">LogFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">PlanFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">ReferralFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionFactory.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionLogFactory.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">UserFactory.php</mark>
│ ├── migrations
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164235_create_plans_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164344_create_features_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164408_create_subscriptions_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164432_create_subscription_logs_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164501_create_referrals_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164538_create_coupons_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164610_create_invoices_table.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_164639_create_logs_table.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">2025_02_26_173343_create_plan_feature_table.php</mark>
│ ├── seeders
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">CouponSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">DatabaseSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">FeatureSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">InvoiceSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">LogSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">PlanSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">ReferralSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionLogSeeder.php</mark>
│ │ ├── <mark class="orange" title="این فایل باید پیاده سازی شود">SubscriptionSeeder.php</mark>
│ │ └── <mark class="orange" title="این فایل باید پیاده سازی شود">UserSeeder.php</mark>
├── routes
│ └── <mark class="orange" title="این فایل باید پیاده سازی شود">console.php</mark>
├── tests
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
```
</details>
<details class="grey">
<summary>
**راهاندازی پروژه**
</summary>
**برای اجرای پروژه، باید `php`، `composer` و `npm` را از قبل نصب کرده باشید.**
+ ابتدا پروژهی اولیه را دانلود و از حالت فشرده خارج کنید.
+ دستور `composer install` را در **پوشهی اصلی پروژه** برای نصب نیازمندیها اجرا کنید.
+ دستور `npm install` را در **پوشهی اصلی پروژه** برای نصب نیازمندیها اجرا کنید. _(توجه کنید که این پروژه از_ [_Tailwindcss_](https://tailwindcss.com/) _استفاده میکند)_
+ دستور `npm run dev` را در **مسیر پوشه اصلی پروژه** اجرا کنید. در صورتی که این دستور را اجرا **نکنید،** **نمیتوانید** ویوهای ساخته شده با _Tailwindcss_ را مشاهده کنید.
+ برای **اجرای مایگریشنها** از دستور `php artisan migrate` استفاده کنید.
+ برای **اجرای تستهای نمونه،** میتوانید از دستور `php artisan test` استفاده کنید.
</details>
شما در این سوال قرار است تا در **پیادهسازی یک سیستم پنل کاربری** بر پایه *Jetstream* که **امکانات شخصیسازی شدهای** دارد به معین و ایرانسروریون کمک کنید. **توجه داشته باشید که برخی مواردی که در این سوال از شما خواسته شده تا پیادهسازی کنید ممکن است تا در این سوال به صورت عملیاتی مورد استفاده قرار نگیرند، اما در داوری سوال مورد ارزیابی قرار خواهند گرفت.** توضیح مواردی که باید پیادهسازی شوند به شکل زیر میباشد:

## **معرفی کلی پروژه ایرانگیت** *(IranGate)* **و موارد پیادهسازی شده**
در این بخش به معرفی پروژه **ایرانگیت** *(IranGate)* خواهیم پرداخت. بخشهایی از ایرانگیت در این پروژه اولیه این سوال از قبل پیادهسازی شدهاند و نیازی به پیادهسازی دوباره ندارند. پایه ایرانگیت همانطور که پیشتر گفته شد بر اساس [**استارتر کیت Jetstream**](https://jetstream.laravel.com/introduction.html) میباشد و امکانات پیشفرضی که این استارتر کیت در اختیار توسعهدهندگان قرار میدهد را شامل میشود. موارد پیادهسازی شده در پروژه به شکل زیر هستند:
<details class="yellow">
<summary>
**معرفی امکانات پیشفرض بر پایه** *Jetstream*
</summary>
این پروژه **بر اساس امکانات پیشفرض** پیادهسازی شده در *Jetstream* از **ویوها، کامپوننتها، روتها و ...** این استارتر کیت **ارثبری** میکند. **صفحه پیشفرض پروژه،** صفحه پیشفرض **لاراول ۱۲** میباشد که به شما برای شروع حل این سوال خوشآمد میگوید:

بخشی از **روتهای** *(Routes)* پیادهسازی شده در پروژه نیز به مانند روتهای پیشفرض *Jetstream* به شکل زیر تعریف شدهاند:
```php routes/web.php laravel
<?php
use App\Http\Controllers\FeatureController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\PlanController;
use App\Http\Controllers\SubscriptionLogController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::middleware([
'auth:sanctum',
config('jetstream.auth_session'),
'verified',
])->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
});
```
شما میتوانید همانند تصاویر زیر از **امکانات پیشفرض پیادهسازی شده** در پروژه برای **ورود، عضویت، فراموشی رمز عبور و پنل کاربری پیشفرض** استفاده کنید:



</details>
<details class="yellow">
<summary>
**معرفی کنترلر** `FeatureController`
</summary>
این کنترلر برای **مدیریت دسترسی به ویژگیهای مختلف** استفاده میشود. **میدلور** `CheckFeatureAccess` که قرار است توسط شما پیادهسازی شود، **بررسی میکند که کاربر دسترسی به یک ویژگی خاص با عنوان** `featureSlug` را دارد یا نه و پیامی مبنی بر مجوز دسترسی به آن ویژگی برمیگرداند. **کنترلر** و **روت** *(Route)* مربوط به آن به شکل زیر از قبل پیادهسازی شدهاند:
```php app/Http/Controllers/FeatureController.php laravel
<?php
namespace App\Http\Controllers;
class FeatureController extends Controller
{
public function access($featureSlug)
{
return response('Access granted to feature: ' . $featureSlug, 200);
}
}
```
```php routes/web.php laravel
<?php
use App\Http\Controllers\FeatureController;
use App\Http\Controllers\InvoiceController;
use App\Http\Controllers\PlanController;
use App\Http\Controllers\SubscriptionLogController;
use Illuminate\Support\Facades\Route;
Route::get('/feature/{featureSlug}', [FeatureController::class, 'access'])->middleware('feature.access')->name('feature');
```
</details>
<details class="yellow">
<summary>
**معرفی کنترلر** `InvoiceController` **و ویوی** `invoices.index`
</summary>

این **کنترلر** برای نمایش **صورتحسابهای کاربران** است. متد `index` تمام فاکتورها را از طریق ارتباط با مدل `Invoice` و برای کاربر وارد شده (`auth()->user()`) بدست میآورد و در ویوی `invoices.index` به نمایش میگذارد. **کنترلر** مربوط به آن به شکل زیر از قبل پیادهسازی شده است:
```php app/Http/Controllers/InvoiceController.php laravel
<?php
namespace App\Http\Controllers;
class InvoiceController extends Controller
{
public function index()
{
$invoices = auth()->user()->invoices()->latest()->get();
return view('invoices.index', compact('invoices'));
}
}
```
</details>
<details class="yellow">
<summary>
**معرفی کنترلر** `PlanController` **و ویوی** `profile.plans`
</summary>

این **کنترلر** برای **مدیریت اشتراکهای کاربری و تغییرات آنها** استفاده میشود. **متد** `index` **تمام اشتراکهای کاربری موجود را نمایش میدهد** و **متد** `switch` به کاربر این امکان را میدهد که اشتراک کاربری خود را تغییر دهد. **اگر اشتراک جدید هزینه داشته باشد، یک صورتحساب برای پرداخت ایجاد میشود** که شما باید در بخشی از پیادهسازیهای خود آنها را مدیریت کنید. **کنترلر** مربوط به آن به شکل زیر از قبل پیادهسازی شده است:
```php app/Http/Controllers/PlanController.php laravel
<?php
namespace App\Http\Controllers;
use App\Models\Plan;
use Illuminate\Support\Facades\Auth;
class PlanController extends Controller
{
public function index()
{
$plans = Plan::all();
return view('profile.plans', compact('plans'));
}
public function switch(Plan $plan)
{
$user = Auth::user();
$subscription = $user->subscription;
if (!$subscription) {
$subscription = $user->subscription()->create([
'plan_id' => $plan->id,
'expires_at' => now()->addDays($plan->duration),
]);
} else {
$subscription->plan_id = $plan->id;
$subscription->expires_at = now()->addDays($plan->duration);
}
if ($plan->price > 0) {
$invoice = $user->invoices()->create([
'plan_id' => $plan->id,
'amount' => $plan->price,
'status' => 'pending',
]);
$subscription->expires_at = now()->addDays($plan->duration);
$subscription->save();
return redirect()->route('plans.index')->with('success', 'اشتراک کاربری شما با موفقیت بروزرسانی شد. لطفا نسبت به پرداخت صورت حساب آن اقدام کنید.');
} else {
$subscription->save();
return redirect()->route('plans.index')->with('success', 'اشتراک کاربری شما با موفقیت بروزرسانی شد.');
}
}
}
```
**با تغییر اشتراک کاربری،** پیام موفقیت در تغییر اشتراک کاربری به نمایش در میآید و شما میتوانید در **منوی کاربری بالای صفحه امکانات مختلفی که برای آن اشتراک کاربری تعریف شده** است را ببینید و به آنها **دسترسی** پیدا کنید.


</details>
<details class="yellow">
<summary>
**معرفی کنترلر** `SubscriptionLogController` **و ویوی** `subscription-logs.index`
</summary>

این **کنترلر** برای نمایش **سوابق اشتراکهای کاربری** است. متد `index` **تمام سوابق اشتراکی مربوط به کاربر فعلی** را از طریق ارتباط با مدل `SubscriptionLog` بدست میآورد و در ویوی `subscription-logs.index` به نمایش میگذارد. **کنترلر** مربوط به آن به شکل زیر از قبل پیادهسازی شده است:
```php app/Http/Controllers/SubscriptionLogController.php laravel
<?php
namespace App\Http\Controllers;
class SubscriptionLogController extends Controller
{
public function index()
{
$subscriptionLogs = auth()->user()->subscriptionLogs()->latest()->get();
return view('subscription-logs.index', compact('subscriptionLogs'));
}
}
```
</details>
## **پیادهسازی جداول و نوشتن مایگریشنها** *(Migrations)*
در این بخش، مایگریشنهای مربوط به ایجاد جداول `users`، `plans`، `subscriptions`، `invoices`، `logs`، `coupons`، `features` و سایر جداول مرتبط باید پیادهسازی شوند. این جداول برای مدیریت **کاربران، اشتراکها، پرداختها، ویژگیهای پلنها و گزارشهای سیستم** طراحی شدهاند. ساختار مربوط به هر کدام از جداول در بخش زیر مشخص است:
<details class="blue">
<summary>
**ساختار جدول** `users` **و پیادهسازی مایگریشن** `create_users_table`
</summary>
## **ساختار جدول** `users`**:**
| نام ستون | نوع داده | توضیحات |
| ---------------- | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `name` | `string` | نام کاربر |
| `email` | `string` | ایمیل (منحصربهفرد) |
| `password` | `string` | رمز عبور |
| `remember_token` | `string` | توکن احراز هویت |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `plans` **و پیادهسازی مایگریشن** `create_plans_table`
</summary>
## **ساختار جدول** `plans`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ----------------------------- |
| `id` | `bigint` | کلید اصلی |
| `name` | `string` | نام اشتراک کاربری |
| `slug` | `string` | عنوان یکتای اشتراک کاربری (منحصربهفرد) |
| `price` | `decimal` | قیمت اشتراک کاربری |
| `duration` | `integer` | مدت زمان اشتراک کاربری (به روز) |
| `is_active` | `boolean` | وضعیت فعال بودن اشتراک کاربری |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `subscriptions` **و پیادهسازی مایگریشن** `create_subscriptions_table`
</summary>
## **ساختار جدول** `subscriptions`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به `users` |
| `plan_id` | `bigint` | کلید خارجی به `plans` |
| `expires_at` | `timestamp` | تاریخ انقضای اشتراک کاربری |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `invoices` **و پیادهسازی مایگریشن** `create_invoices_table`
</summary>
## **ساختار جدول** `invoices`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ---------------------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به `users` |
| `plan_id` | `bigint` | کلید خارجی به `plans` |
| `payment_id` | `string` | یک رشته که میتواند `null` باشد |
| `amount` | `decimal` | مبلغ پرداختی |
| `status` | `string` | وضعیت پرداخت (`paid` یا `pending`) |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `logs` **و پیادهسازی مایگریشن** `create_logs_table`
</summary>
## **ساختار جدول** `logs`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به `users` |
| `action` | `string` | نوع فعالیت انجامشده |
| `details` | `text` | جزئیات فعالیت |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `coupons` **و پیادهسازی مایگریشن** `create_coupons_table`
</summary>
## **ساختار جدول** `coupons`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `code` | `string` | کد تخفیف (منحصربهفرد) |
| `discount` | `decimal` | مقدار تخفیف |
| `max_uses` | `integer` | حداکثر دفعات استفاده |
| `expires_at` | `timestamp` | تاریخ انقضای کد |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `features` **و پیادهسازی مایگریشن** `create_features_table`
</summary>
## **ساختار جدول** `features`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ---------------------- |
| `id` | `bigint` | کلید اصلی |
| `name` | `string` | نام ویژگی |
| `slug` | `string` | شناسه یکتای ویژگی |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `plan_feature` **و پیادهسازی مایگریشن** `create_plan_feature_table`
</summary>
## **ساختار جدول** `plan_feature`**:**
| نام ستون | نوع داده | توضیحات |
| ------------ | ----------- | ------------------------ |
| `id` | `bigint` | کلید اصلی |
| `plan_id` | `bigint` | کلید خارجی به `plans` |
| `feature_id` | `bigint` | کلید خارجی به `features` |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `referrals` **و پیادهسازی مایگریشن** `create_referrals_table`
</summary>
## **ساختار جدول** `referrals`**:**
| نام ستون | نوع داده | توضیحات |
| ------------- | ----------- | ---------------------------------- |
| `id` | `bigint` | کلید اصلی |
| `referrer_id` | `bigint` | کاربری که معرف شخص دیگر است |
| `referred_id` | `bigint` | کاربری که معرفی شده است |
| `code` | `string` | کد ارجاع (منحصربهفرد) |
| `reward` | `decimal` | پاداش دادهشده به معرف |
| `is_used` | `boolean` | وضعیت استفاده از کد (`true/false`) |
| `created_at` | `timestamp` | زمان ایجاد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی |
</details>
<details class="blue">
<summary>
**ساختار جدول** `subscription_logs` **و پیادهسازی مایگریشن** `create_subscription_logs_table`
</summary>
## **ساختار جدول** `subscription_logs`**:**
| نام ستون | نوع داده | توضیحات |
| ------------------------ | ----------- | -------------------------------- |
| `id` | `bigint` | کلید اصلی |
| `user_id` | `bigint` | کلید خارجی به جدول کاربران |
| `old_subscription_level` | `string` | سطح قبلی اشتراک کاربر (nullable) |
| `new_subscription_level` | `string` | سطح جدید اشتراک کاربر (nullable) |
| `created_at` | `timestamp` | زمان ایجاد رکورد |
| `updated_at` | `timestamp` | زمان آخرین بهروزرسانی رکورد |
</details>
## **پیادهسازی مدلها**
لیست برخی از مدلهای پیادهسازی شده در سیستم به شرح زیر است که شما باید آنها را همراه با روابط بینشان در این بخش پیادهسازی کنید:
+ **مدل `User`**: مدیریت کاربران سیستم، شامل اطلاعات کاربری و روابط مرتبط.
+ **مدل `Plan`**: نمایشدهندهی اشتراکهای کاربری، شامل ویژگیها و مشخصات هر پلن.
+ **مدل `Subscription`**: ثبت اطلاعات اشتراک کاربران در اشتراکهای مختلف.
+ **مدل `Invoice`**: صورتحسابهای مربوط به خرید اشتراک و پرداختهای کاربران.
+ **مدل `Feature`**: ویژگیهایی که برای هر اشتراک کاربری قابل ارائه هستند.
+ **مدل `Coupon`**: کدهای تخفیف که کاربران میتوانند برای کاهش هزینه خرید اشتراک کاربری استفاده کنند.
+ **مدل `Referral`**: ثبت سیستم ارجاع کاربران و پاداشهای مربوط به معرفی کاربران جدید.
+ **مدل `Log`**: ثبت اقدامات کاربران در سیستم جهت نظارت و بررسی تاریخچه فعالیتها.
+ **مدل `SubscriptionLog`**: مدیریت تغییرات اشتراک کاربران و تاریخچه ارتقا یا کاهش پلن.
## **پیادهسازی روابط بین مدلها**
+ هر **کاربر (User)** میتواند دارای یک **اشتراک کاربری (Subscription)** باشد.
+ هر **اشتراک کاربری (Subscription)** به یک **اشتراک (Plan)** متصل است.
+ هر **اشتراک (Plan)** میتواند چندین **ویژگی (Feature)** داشته باشد.
+ هر **ویژگی (Feature)** میتواند به چندین **اشتراک (Plan)** مرتبط باشد.
+ هر **کاربر** میتواند چندین **صورتحساب (Invoice)** برای خرید پلنهای مختلف داشته باشد.
+ هر **صورتحساب (Invoice)** به دقیقا یک **اشتراک (Plan)** و دقیقا یک **کاربر (User)** مرتبط است.
+ هر **کاربر (User)** میتواند از **کدهای تخفیف (Coupon)** استفاده کند.
+ هر **کاربر (User)** میتواند از طریق سیستم ارجاع، کاربران جدید را معرفی کرده و پاداش دریافت کند.
+ هر **کاربر (User)** میتواند **چندین لاگ (Log)** از فعالیتهای خود داشته باشد. همچنین هر لاگ مربوط به دقیقا یک **کاربر (User)** است.
+ هر **کاربر (User)** میتواند چندین **تاریخچه تغییرات اشتراک (SubscriptionLogs)** داشته باشد.
## **پیادهسازی فکتوریها** *(Factories)*
در این بخش، فکتوریهای تعریف شده برای تولید دادههای تصادفی جهت تست برنامه معرفی شدهاند. این فکتوریها به منظور تولید دادههای آزمایشی برای جداول مرتبط در پایگاه داده استفاده میشوند:
<details class="red">
<summary>
**ساختار فکتوری** `CouponFactory`
</summary>
## **فکتوری** `CouponFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ---------- | ---------------------- | ------------------------------ |
| `code` | کد تخفیف (منحصربهفرد) | یک UUID تصادفی |
| `discount` | مقدار تخفیف | عدد اعشاری بین ۵ تا ۵۰ |
| `max_uses` | حداکثر دفعات استفاده | عدد صحیح بین ۱ تا ۱۰۰ |
| `expires_at` | تاریخ انقضا | تاریخی بین حالا تا ۱ سال آینده |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `FeatureFactory`
</summary>
### **فکتوری**`FeatureFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| -------- | ----------------- | ------------------ |
| `name` | نام ویژگی | یک کلمه تصادفی |
| `slug` | شناسه یکتا (slug) | مقدار یکتای تصادفی |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `InvoiceFactory`
</summary>
### **فکتوری** `InvoiceFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ---------- | -------------------------- | ------------------------ |
| `user_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
| `plan_id` | کلید خارجی به جدول `plans` | مقدار یک `plan_id` معتبر |
| `payment_id` | شناسه پرداخت | یک UUID تصادفی |
| `amount` | مبلغ صورتحساب | عدد اعشاری بین ۱۰ تا ۱۰۰ |
| `status` | وضعیت پرداخت | `paid` یا `failed` |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `LogFactory`
</summary>
### **فکتوری** `LogFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| -------- | -------------------------- | ------------------------ |
| `action` | نوع فعالیت | یک کلمه تصادفی |
| `details` | جزئیات فعالیت | یک آرایه JSON |
| `user_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `PlanFactory`
</summary>
### **فکتوری** `PlanFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| --------- | --------------------------- | ------------------------ |
| `name` | نام اشتراک | یک کلمه تصادفی |
| `slug` | شناسه یکتا (slug) | مقدار یکتای تصادفی |
| `price` | قیمت | عدد اعشاری بین ۱۰ تا ۱۰۰ |
| `duration` | مدت زمان (روز) | عدد صحیح بین ۳۰ تا ۳۶۵ |
| `is_active` | وضعیت فعال/غیرفعال بودن اشتراک | مقدار `true` یا `false` |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `ReferralFactory`
</summary>
### **فکتوری** `ReferralFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ----------- | -------------------------- | ------------------------ |
| `referrer_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
| `referred_id` | کلید خارجی به جدول `users` | مقدار یک `user_id` معتبر |
| `code` | کد ارجاع (منحصربهفرد) | یک UUID تصادفی |
| `reward` | مقدار جایزه ارجاع | عدد اعشاری بین ۰ تا ۱۰۰ |
| `is_used` | وضعیت استفاده از کد | مقدار `true` یا `false` |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `UserFactory`
</summary>
### **فکتوری** `UserFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ----------------- | ------------------- | ----------------------- |
| `name` | نام کاربر | یک نام تصادفی |
| `email` | ایمیل کاربر (یونیک) | ایمیل تصادفی معتبر |
| `email_verified_at` | زمان تأیید ایمیل | `now()` یا `null` |
| `password` | رمز عبور | `password` هششده |
| `remember_token` | توکن یادآوری | رشته ۱۰ کاراکتری تصادفی |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `SubscriptionFactory`
</summary>
### **فکتوری** `SubscriptionFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ------------ | ---------------------------- | ---------------------------------- |
| `user_id` | شناسه کاربر مرتبط | مقدار تصادفی از `UserFactory` |
| `plan_id` | شناسه پلن مرتبط | مقدار تصادفی از `PlanFactory` |
| `expires_at` | تاریخ انقضای اشتراک | تاریخ تصادفی بین `now` و `+1 year` |
| `created_at` | زمان ایجاد رکورد | `now()` |
| `updated_at` | زمان آخرین بهروزرسانی رکورد | `now()` |
</details>
<details class="red">
<summary>
**ساختار فکتوری** `SubscriptionLogFactory`
</summary>
### **فکتوری** `SubscriptionLogFactory`
| نام ستون | توضیحات | مقادیر ممکن |
| ------------------------ | ---------------------------- | ----------------------------- |
| `user_id` | شناسه کاربر مرتبط | مقدار تصادفی از `UserFactory` |
| `old_subscription_level` | سطح قبلی اشتراک کاربر | یک کلمه تصادفی |
| `new_subscription_level` | سطح جدید اشتراک کاربر | یک کلمه تصادفی |
| `created_at` | زمان ایجاد رکورد | `now()` |
| `updated_at` | زمان آخرین بهروزرسانی رکورد | `now()` |
</details>
## **پیادهسازی سیدر** *(Seeder)*
درون پروژه اولیه تعدادی **سیدر** *(Seeder)* وجود دارد که باید به شکل زیر پیادهسازی شوند، در نهایت قرار است تا این سیدرها با ترتیب درستی در سیدر `DatabaseSeeder` مورد استفاده قرار بگیرند. سیدرها باید به شکل زیر ایجاد شوند:
1. **دقیقا ۵۰ کد تخفیف** *(Coupon)* با استفاده از فکتوری `Coupon` ساخته شود.
2. **دقیقا ۲۰ قابلیت** *(Feature)* با استفاده از فکتوری `Feature` ساخته شود. همچنین یک اشتراک کاربری با **عنوان** *(Slug)* `vip` که توسط سیدر مربوطه ساخته شده **باید به تمام قابلیتهای ساخته شده توسط این سیدر** دسترسی داشته باشد.
3. **دقیقا ۱۰۰ صورت حساب** *(Invoice)* با استفاده از فکتوری `Invoice` ساخته شود.
4. **دقیقا ۲۰۰ لاگ** *(Log)* با استفاده از فکتوری `Log` ساخته شود.
5. **دقیقا ۱۰ اشتراک** *(Plan)* با استفاده از فکتوری `Plan` ساخته شود. همچنین باید سه اشتراک کاربری دیگر توسط این سیدر به صورت زیر ساخته شود:
- **اشتراک** پایه با **نام** *(Name)* `Basic`، **عنوان** *(Slug)* `basic`، **قیمت** *(Price)* `0` و **مدتزمان** *(Duration)* `30` روز
- **اشتراک** پریمیوم با **نام** *(Name)* `Premium`، **عنوان** *(Slug)* `premium`، **قیمت** *(Price)* `100` و **مدتزمان** *(Duration)* `30` روز
- **اشتراک** ویژه با **نام** *(Name)* `Vip`، **عنوان** *(Slug)* `vip`، **قیمت** *(Price)* `200` و **مدتزمان** *(Duration)* `30` روز
6. **دقیقا ۵۰ ارجاع** *(Referral)* با استفاده از فکتوری `Referral` ساخته شود.
7. **دقیقا ۱۵۰ گزارش اشتراک** *(SubscriptionLog)* با استفاده از فکتوری `SubscriptionLog` ساخته شود.
8. **دقیقا ۱۰۰ اشتراک کاربری** *(Subscription)* با استفاده از فکتوری `Subscription` ساخته شود.
9. **دقیقا ۵۰ کاربر** *(User)* با استفاده از فکتوری `User` ساخته شود. **تمامی این ۵۰ کاربر** باید در **اشتراک کاربری** `basic` که قبلا توسط سیدر مربوطه ساخته شده است، **مشترک** شوند.
## **پیادهسازی پالیسیها** *(Policies)*
در این سوال شما باید **دو پالیسی** `FeaturePolicy` و `SubscriptionPolicy` را برای **مدیریت دسترسی به ویژگیهای خاص** و **اشتراکهای کاربری خاص** به صورت زیر پیادهسازی کنید:
<details class="green">
<summary>
**ساختار پالیسی** `FeaturePolicy`
</summary>
**پالیسی** `FeaturePolicy` به صورت زیر در مسیر `app/Policies/FeaturePolicy.php` پیادهسازی شده است که با استفاده از تابعی با نام `accessFeature` و دریافت **دو مقدار** `$user` و `$feature` **به صورت پویا** مشخص میکند که آیا کاربر `$user` در **اشتراک کاربری فعلیاش** میتواند میتواند به **قابلیت** `$feature` **دسترسی داشته باشد یا نه.**
```php app/Policies/FeaturePolicy.php laravel
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Feature;
class FeaturePolicy
{
public function accessFeature(User $user, Feature $feature)
{
// TODO
}
}
```
- از این پالیسی برای مدیریت دسترسی بر اساس **قابلیتها** *(Features)* استفاده خواهد شد.
</details>
<details class="green">
<summary>
**ساختار پالیسی** `SubscriptionPolicy`
</summary>
**پالیسی** `SubscriptionPolicy` به صورت زیر در مسیر `app/Policies/SubscriptionPolicy.php` پیادهسازی شده است که برای **مدیریت دسترسیهای کاربران با اشتراکهای کاربری پیشفرض** `basic`، `premium` و `vip` به بخشهای مختلف پنلکاربری مورد استفاده قرار میگیرد. این پالیسی با استفاده از هر کدام از سه تابع `accessVIP`، `accessPremium` و `accessBasic` بررسی میکند که آیا **اشتراک کاربری** `$subscription` که متعلق به **کاربر** `$user` میباشد به ترتیب از نوع `vip`، `premium` و `basic` **میباشد یا نه.**
```php app/Policies/FeaturePolicy.php laravel
<?php
namespace App\Policies;
use App\Models\Subscription;
use App\Models\User;
use App\Models\Plan;
class SubscriptionPolicy
{
public function accessVip(User $user, Subscription $subscription)
{
// TODO
}
public function accessPremium(User $user, Subscription $subscription)
{
// TODO
}
public function accessBasic(User $user, Subscription $subscription)
{
// TODO
}
}
```
- از این پالیسی برای مدیریت دسترسی بر اساس **اشتراکهای کاربری** *(Subscriptions)* استفاده خواهد شد.
</details>
## **پیادهسازی** *Observer* `SubscriptionObserver`
در این پروژه شما باید یک *Observer* را برای مدل `Subscription` به صورتی پیادهسازی کنید که هنگام **بروزرسانی** *(Update)* شناسه اشتراک کاربری (`plan_id`) برای یک کاربر، یک `SubscriptionLog` **جدید** ایجاد کند که مقدار `user_id` آن برابر با **شناسه کاربری است که اشتراک کاربری آن تغییر کرده** است، مقدار `old_subscription_level` آن برابر با مقدار **عنوان** *(Slug)* **اشتراک کاربری قبلی کاربر** و مقدار `new_subscription_level` آن برابر با مقدار **عنوان** *(Slug)* **اشتراک جدید کاربر** میباشد.
## پیادهسازی **وظایف** *(Jobs)* و **اسکجول** *(Schedule)* **کردن**
در این پروژه شما باید **دو جاب** `ExpireSubscriptionsJob` **و** `DowngradePendingInvoicesJob` را برای انجام بخشی از پردازشهای مربوط به پنل کاربری به صورت زیر پیادهسازی کنید:
<details class="purple">
<summary>
**ساختار جاب** `DowngradePendingInvoicesJob`
</summary>
**جاب** `DowngradePendingInvoicesJob` را به صورت زیر در مسیر `app/Jobs/DowngradePendingInvoicesJob.php` پیادهسازی کنید تا اشتراک کاربری **تمام کاربرانی** را حداقل یک **صورتحساب** *(Invoice)* با **وضعیت** *(Status)* **باز** (`pending`) دارند که **بیشتر مساوی یک هفته** از صادر شدنشان گذشته است، به **اشتراک کاربری** `basic` تغییر دهد **که زمان انقضایی هم نداشته باشد.**
```php app/Jobs/DowngradePendingInvoicesJob.php laravel
<?php
namespace App\Jobs;
use App\Models\User;
use App\Models\Plan;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DowngradePendingInvoicesJob implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
// TODO
}
}
```
- از این پالیسی برای مدیریت دسترسی بر اساس **قابلیتها** *(Features)* استفاده خواهد شد.
</details>
<details class="purple">
<summary>
**ساختار جاب** `ExpireSubscriptionsJob`
</summary>
**جاب** `ExpireSubscriptionsJob` را به صورت زیر در مسیر `app/Jobs/ExpireSubscriptionsJob.php` پیادهسازی کنید تا اشتراک کاربری **تمام کاربرانی** را که **تاریخ انقضای اشتراک کاربریشان** (`expires_at`) به پایان رسیده است *(تاریخ انقضای اشتراک کاربریشان کوچکتر مساوی زمان حال است)،* به **اشتراک کاربری** `basic` تغییر دهد **که زمان انقضایی هم نداشته باشد.**
```php app/Jobs/ExpireSubscriptionsJob.php laravel
<?php
namespace App\Jobs;
use App\Models\Subscription;
use App\Models\Plan;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ExpireSubscriptionsJob implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public function handle()
{
// TODO
}
}
```
- از این پالیسی برای مدیریت دسترسی بر اساس **اشتراکهای کاربری** *(Subscriptions)* استفاده خواهد شد.
</details>
در نهایت پس از تعریف **وظایف** *(Jobs)* های مشخص شده، شما باید این دو **وظیفه** *(Job)* را طوری در لاراول **اسکجول** *(Schedule)* کنید تا **به صورت خودکار** و **هر دقیقه یک بار** پردازش شوند.
## **پیادهسازی میدلور** *(Middleware)*
در نهایت شما قرار است تا **میدلور** *(Middleware)* `CheckFeatureAccess` را برای پردازش دسترسی در **درخواست** *(Request)* کاربر به صورت زیر پیادهسازی کنید:
```php app/Http/Middleware/CheckFeatureAccess.php laravel
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckFeatureAccess
{
public function handle(Request $request, Closure $next): Response
{
// TODO
}
}
```
این **میدلور** با استفاده از **پالیسیهای تعریف شده** در مراحل قبل بررسی میکند که آیا **کاربر فعلی** میتواند به **مسیر مشخص شده** که از این **میدلور** استفاده میکند و مشخص کننده مسیر **یکی از قابلیتهای موجود در برنامه** است دسترسی دارد یا نه. در صورتی که کاربر در تلاش بود تا به مسیر مربوط به قابلیتی دسترسی پیدا کند که **در برنامه به کل وجود ندارد** باید **خطای** `404` به همراه پیام **"این صفحه یافت نشد."** و در صورتی که در تلاش بود تا به مسیر مربوط به قابلیتی دسترسی پیدا کند که اشتراک کاربریاش **اجازه دسترسی به آن را ندارد،** باید **خطای** `403` به همراه پیام **"شما دسترسی به این بخش را ندارید."** نمایش داده شود. **در غیر این صورت ادامه پردازش درخواست کاربر انجام میشود.**
# آنچه باید آپلود کنید
+ **توجه**: پس از پیادهسازی موارد خواسته شده، کل فایلهای پروژه بهجز پوشهی `vendor` را زیپ کرده و ارسال کنید.
+ **توجه**: شما مجاز به افزودن فایل جدیدی در این ساختار **نیستید** و تنها باید تغییرات را در فایلهای موجود اعمال کنید.
+ **توجه**: که نام فایل _Zip_ اهمیتی **ندارد**.