چالش‌های نام ایران‌سرور


  • محدودیت زمان: ۱ ثانیه
  • محدودیت حافظه: ۲۵۶ مگابایت

در دنیای توسعه نرم‌افزار، ایران‌سرور (IranServer) همواره به دنبال بهترین راه‌حل‌ها بود تا پروژه‌هایش را منظم‌تر و حرفه‌ای‌تر مدیریت کند. یکی از چالش‌های اخیر تیم فنی، ایجاد یک تابع slug بود که بتواند متن‌ها را به قالبی استاندارد و یکنواخت تبدیل کند. تابعی که تمام حروف را کوچک کند، علائم نگارشی را حذف کند و فاصله‌ها را با خط تیره جایگزین کند. اما مشکلی وجود داشت؛ در برخی از متون، نام ایران‌سرور به صورت "Iran Server" نوشته شده بود و این باعث می‌شد که خروجی نهایی یکپارچه نباشد.

توضیح تصویر

تیم توسعه پس از بررسی‌های فراوان، تصمیم گرفت این مشکل را یک بار برای همیشه حل کند. آن‌ها از شما می‌خواهند که این تابع را پیاده‌سازی کنید. یک رشته‌ی متنی حاوی کلمات انگلیسی، اعداد، و علائم نگارشی داریم. هدف این است که رشته را به یک slug تبدیل کنیم.

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

شما باید تابع slugify را به شکل زیر پیاده‌سازی کنید تا این کار را انجام دهد:

extensionFromNamesolution.php
<?php

function slugify(string $input): string { 
    // TODO
} 
PHP

ورودی تابع:🔗

  • یک رشته‌ی متنی (حاوی حروف انگلیسی کوچک و بزرگ، اعداد، فاصله، و علائم نگارشی).

خروجی تابع:🔗

  • یک رشته به‌صورت slug که مطابق شرایط زیر پردازش شده است:
    • تمامی حروف به حروف کوچک تبدیل شوند.
    • تمام علائم نگارشی از متن حذف شوند.
    • فاصله‌ها با یک خط تیره (-) جایگزین شوند.
    • اگر کلمات "iran" و "server" به صورت متوالی و به‌ترتیب با یک فاصله ظاهر شوند، به عبارت "iranserver" ادغام شوند.

مثال‌ها🔗

نمونه ورودی ۱:🔗

>> 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
Bash

آن‌چه باید آپلود کنید🔗

یک فایل PHP با نام solution.php که تابع slugify در آن پیاده‌سازی شده است آپلود کنید.

میزگرد بزرگ


  • محدودیت زمان: ۱ ثانیه
  • محدودیت حافظه: ۲۵۶ مگابایت

ایران‌سرور (IranServer) تصمیم گرفته که برای رونمایی از خدمات جدید خود، یک جلسه مهم میزگردی با حضور مشتری‌های کلیدی‌اش برگزار کند. هدف این جلسه، بحث و بررسی درباره نیازها و تجربه‌های مشتریان است تا تیم ایران‌سرور بتواند خدماتش را بهینه‌تر کند.

اما یک مشکل وجود دارد؛ ایران‌سرور طی تحقیقاتش متوجه شده که اگر مشتری‌هایی که با هم آشنایی قبلی دارند کنار هم بنشینند، بحث‌هایشان بیشتر حالت تعارفی پیدا می‌کند و بازخورد صادقانه‌ای ارائه نمی‌دهند. برای اینکه بتواند بهترین تحلیل‌ها را داشته باشد، باید اطمینان حاصل کند که هیچ دو نفری که با هم آشنا هستند کنار هم ننشینند.

ایران‌سرور می‌خواهد مشتری‌ها را به شکلی روی دو میز تقسیم کند که:

تصویر

  1. هیچ دو نفری که آشنا هستند، روی یک میز نباشند.
  2. تعداد افراد روی دو میز باید تا حد امکان متعادل باشد تا هر دو میز فرصت برابری برای بحث‌های مفید داشته باشند. (منظور از متعادل این است که اختلاف تعداد افراد روی دو میز کمترین مقدار ممکن باشد!)

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

تابع iranServerRoundTable را طوری کامل کنید که خروجی خواسته شده را برگرداند:

extensionFromNamesolution.php
<?php

function iranServerRoundTable(int $n, int $m, array $connections): string {
    // TODO
}
PHP

ورودی تابع:🔗

  • مقدار n، نشان‌دهنده تعداد افراد حاضر در مهمانی است.
  • مقدار m، نشان‌دهنده تعداد آشنایی‌های قبلی که میان افراد وجود دارد، است.
  • مقدار connections، لیستی از m آشنایی‌ که هر یک به صورت یک جفت مانند [A, B] است که نشان دهنده وجود آشنایی قبلی میان دو فرد A و B است.

خروجی تابع:🔗

  • در صورتی که امکان تقسیم‌بندی افراد بین دو میز مطابق شرایط داده شده وجود داشته باشد، یک آرایه که در آن مقدار کلید possible برابر با "YES" و دو کلید table_1 و table_2 که نشان‌دهنده لیستی از افراد دور هر میز هستند را بازگرداندید.
  • در غیر این صورت، آرایه‌ای شامل کلید possible با مقدار "NO" بازگرداندید.
  • در نهایت خروجی برنامه‌ی شما باید به فرمت JSON باشد.
نکته

مقدار کلید possible در هر یک از شرایط ذکر شده، باید دقیقا به شکلی که نوشته شده است باشد. (بزرگی و کوچکی حروف مهم است!)

توجه: این سؤال از نوع ساختاری (Constructive) است، بنابراین پاسخ‌های ممکن لزوماً یکتا نیستند. با این حال، سیستم داوری پاسخ‌های شما را بررسی می‌کند تا اطمینان حاصل شود که شرایط مسئله به‌درستی رعایت شده‌اند. تنها در صورتی نمره دریافت خواهید کرد که پیاده‌سازی شما تمامی این شرایط را برآورده کند و یکی از پاسخ‌های معتبر مسئله باشد.

مثال‌ها🔗

نمونه ورودی ۱:🔗

>>  iranServerRoundTable(6, 6, [
        [1, 2],
        [2, 3],
        [3, 4],
        [4, 5],
        [5, 6],
        [6, 1],
    ]);
PHP

نمونه خروجی ۱:🔗

{
    "possible": "YES",
    "table_1": [1, 3, 5],
    "table_2": [2, 4, 6],
}
JSON
توضیحات نمونه

این مثال شامل ۶ نفر است که هر یک از آن‌ها دقیقا با دو فرد دیگر آشنایی قبلی دارد؛ به شکل زیر توجه کنید:

تصویر

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

تصویر ۲

Table1:[1,3,5],Table2:[2,4,6] Table_1 : [1, 3, 5], Table_2 : [2, 4, 6]

نمونه ورودی ۲:🔗

>>  iranServerRoundTable(6, 5, [
        [1, 2],
        [3, 4],
        [3, 5],
        [4, 5],
        [1, 6],
    ]);
PHP

نمونه خروجی ۲:🔗

{
    "possible": "NO"
}
JSON
توضیحات نمونه

این مثال شامل ۶ نفر است و رابط آشنایی میان‌ آن‌ها به شکل زیر است:

تصویر

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

تصویر ۲

نمونه ورودی ۳:🔗

>>  iranServerRoundTable(6, 3, [
        [1, 2],
        [3, 4],
        [3, 5],
    ]);
PHP

نمونه خروجی ۳:🔗

{
    "possible": "YES",
    "table_1": [1, 4, 5],
    "table_2": [2, 3, 6],
}
JSON
توضیحات نمونه

این مثال شامل ۶ نفر است و رابط آشنایی میان‌ آن‌ها به شکل زیر است:

تصویر

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

تصویر ۲

حالت دیگری هم وجود دارد که می‌توان این کار را انجام داد اما با شرط دوم (متعادل بودن میز‌ها) را ندارد!

تصویر زیر یکی از حالاتی است که برای این مسئله موردقبول نیست:

تصویر ۳

آن‌چه باید آپلود کنید🔗

یک فایل PHP با نام solution.php که تابع iranServerRoundTable در آن پیاده‌سازی شده است آپلود کنید.

معین سخنور


معین پس از مدت‌ها حضور در کوئرا، تصمیم گرفت برای ایجاد تنوع، در مصاحبه استخدامی جونیورا شرکت کند؛ در این مصاحبه از او خواسته‌اند که پروژه‌ای با استفاده از ORM مخصوص Laravel انجام دهد؛ اما او معتقد است هرکسی می‌تواند با کمی آموزش، رانندگی را یاد بگیرد؛ ولی فقط یک مکانیک دقیقاً می‌داند ماشین چگونه کار می‌کند. بنابراین، تصمیم گرفت با هدف نشان‌دادن تبحر واقعی خود، به‌جای استفاده از ORM آماده، یک ORM اختصاصی به زبان PHP بنویسد و آن را به افتخار خودش معین سخنور (Moein Eloquent) نام‌گذاری کند. مشکل اینجاست که پروژه اصلی به‌اندازه کافی وقت‌گیر است و معین برای پیاده‌سازی ORM اختصاصی‌اش به زمان بیشتری نیاز دارد؛ از همین رو، او از شما کمک می‌خواهد تا در تکمیل این ORM همراهش باشید.

معین سخنور

جزئیات پروژه🔗

پروژه اولیه را از این لینک دانلود کنید.

ساختار فایل‌ها
moein-eloquent
├── Database.php
├── > QueryBuilder.php <
├── > Model.php <
├── User.php
├── autoload.php
└── test
    └── EloquentSampleTest.php
Plain text
راه‌اندازی پروژه

برای راه‌اندازی پروژه، به پایگاه‌داده MySQL نیاز است. در این بخش، به راه‌اندازی MySQL در داکر می‌پردازیم. اگر تاکنون داکر را نصب نکرده‌اید، به‌کمک این لینک آن را نصب کنید.

برای نصب و راه‌اندازی کانتینر MySQL به‌کمک داکر، دستور زیر را در ترمینال وارد کنید تا ایمیج MySQL دانلود شود و کانتینر راه‌اندازی شود:

extensionFromNameterminal
docker run --name mysql-container \
    -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
    -e MYSQL_DATABASE=testdb \
    -p 3306:3306 \
    -d mysql:latest
Bash

وقتی مراحل نصب کامل شد، دستور زیر را در ترمینال وارد کنید تا با سرویس MySQL در کانتینر ساخته‌شده ارتباط برقرار کنید:

extensionFromNameterminal
docker exec -it mysql-container mysql -u root
Bash

حال دستور زیر را در محیط تعاملی MySQL وارد کنید تا پایگاه‌داده testdb استفاده شود:

extensionFromNamesql
USE testdb;
SQL

اکنون می‌توانیم جدول users را ایجاد کنیم؛ دستور زیر را وارد کنید تا جدول users در پایگاه‌داده testdb ساخته شود:

extensionFromNamesql
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    age INT NOT NULL
);
SQL

این پروژه شامل ۵ فایل است که ۳ فایل از قبل تکمیل شده‌اند. شما باید فایل‌های Model.php و QueryBuilder.php را پیاده‌سازی کنید؛ در ادامه، جزئیات هرکدام از فایل‌ها و متد‌های آن‌ها را می‌توانید مشاهده کنید:

فایل Database.php

این فایل شامل یک کلاس به‌نام Database است که یک شی pdo را مقداردهی اولیه می‌کند و می‌توان با استفاده از Database::getInstance() به آن دسترسی پیدا کرد. این فایل از قبل پیاده‌سازی شده و شما نیازی به‌ تغییر در آن ندارید؛ در پروژه ارسالی شما، این فایل جایگزین می‌شود، بنابراین هر تغییری در آن ایجاد کنید، نادیده گرفته خواهد شد. اما اگر می‌خواهید نتایج کوئری‌هایتان ببینید، باید مقادیر موجود در این فایل را تغییر دهید تا بتوانید پروژه را روی سیستم خودتان اجرا کنید.

extensionFromNameDatabase.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;
    }
}

?>
PHP
فایل QueryBuilder.php

کلاس QueryBuilder برای ساخت و اجرای کوئری‌های پایگاه‌داده طراحی شده است. این کلاس امکاناتی مانند SELECT، WHERE، GROUP BY، HAVING، ORDER BY، LIMIT و عملیات CRUD را فراهم می‌کند. در پیاده‌سازی متدهای این کلاس، از دیزاین پترن Builder استفاده کنید.

متد select()

این متد تعیین می‌کند که کدام ستون‌ها در نتایج SELECT آورده شوند.

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();
PHP
متد where()

این متد برای افزودن شرط‌ها به کوئری‌های WHERE استفاده می‌شود.

  • امکان استفاده از عملگرهای مقایسه‌ای (=, >, <, LIKE، و ...) وجود دارد.
  • در صورتی که فقط دو آرگومان به متد داده شود، عملگر مقایسه‌ای پیش‌فرض (=) در نظر گرفته می‌شود.
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();
PHP
متد groupBy()

این متد کوئری را براساس یک یا چند ستون گروه‌بندی (GROUP BY) می‌کند.

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();
PHP
متد having()

این متد برای فیلترکردن نتایج پس از GROUP BY استفاده می‌شود.

public function having(string $column, string $operator, mixed $value): self 
{
    // TODO
}
PHP

مثال🔗

User::select('age', 'COUNT(*) AS count')->groupBy('age')->having('age', '>', 32)->get();
PHP
متد orderBy()

این متد چیدمان نتایج را براساس یک ستون و ترتیب ASC یا DESC تعیین می‌کند.

public function orderBy(string $column, string $direction = 'ASC'): self 
{
    // TODO
}
PHP

مثال🔗

User::orderBy('age')->get();
User::orderBy('age')->orderBy('name', 'DESC')->get();
PHP
متد limit()

این متد تعداد نتایج بازگشتی را محدود (LIMIT) می‌کند.

public function limit(int $limit): self 
{
    // TODO
}
PHP

مثال🔗

User::orderBy('age')->limit(1)->get();
PHP
متد offset

این متد تعدادی از نتایج بازگشتی را رد (skip) می‌کند.

public function offset(int $offset): self
{
    // TODO
}
PHP

مثال🔗

User::orderBy('age')->offset(1)->limit(1)->get();
PHP
متد get()

این متد کوئری نهایی را اجرا کرده و نتایج را به‌صورت آرایه‌ای از اشیا مدل برمی‌گرداند.

public function get(): array 
{
    // TODO
}
PHP
متد first()

این متد اولین نتیجه‌ی کوئری را دریافت کرده و مقدار null را در صورت خالی‌بودن بازمی‌گرداند.

public function first() 
{
    // TODO
}
PHP

مثال🔗

User::orderBy('age', 'DESC')->first();
PHP
متد count()

این متد تعداد کل ردیف‌های خروجی کوئری را برمی‌گرداند (COUNT(*)).

public function count(): int 
{
    // TODO
}
PHP

مثال🔗

User::count();
User::where('age', 24)->count()
PHP
متدهای sum(), avg(), min(), max()

این متدها مقدار مجموع، میانگین، حداقل و حداکثر یک ستون را محاسبه می‌کنند.

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');
PHP
متد insert()

این متد یک رکورد جدید در پایگاه‌داده درج (INSERT) کرده و id آن را برمی‌گرداند.

public function insert(array $data): int 
{
    // TODO
}
PHP

مثال🔗

User::insert(
    [
        'id' => 10, 
        'name' => 'New User', 
        'age' => 50, 
        'email' => 'newuser@email.com'
    ]
)
PHP
متد update()

این متد داده‌های جدول را بر اساس WHERE به‌روزرسانی می‌کند.

  • اگر شرط WHERE تعیین نشده باشد، خطا (Exception) ایجاد می‌شود.
public function update(array $data): bool 
{
    // TODO
}
PHP

مثال🔗

User::where('id', 10)->update(['name' => 'Newer Name']);
PHP
متد delete()

این متد رکوردهای پایگاه‌داده را حذف (DELETE) می‌کند.

  • اگر WHERE تعیین نشده باشد، عملیات متوقف شده و خطا (Exception) صادر می‌شود.
public function delete(): bool 
{
    // TODO
}
PHP

مثال🔗

User::where('id', 10)->delete();
PHP
فایل Model.php

این فایل باید پیاده‌سازی شود. در این فایل یک کلاس انتزاعی (abstract) به‌نام Model وجود دارد که مدل‌های پایگاه‌داده از آن ارث‌بری می‌کنند. این کلاس دو پراپرتی دارد؛ نام جدول و آرایه attributes. نام جدول باید توسط مدل‌هایی که از این کلاس ارث‌بری می‌کنند Overwrite شود. آرایه attributes برای ست‌کردن پراپرتی‌های dynamic است. این کلاس دارای چند متد است که باید پیاده‌سازی شوند:

متد query()

این متد یک شیء از کلاس QueryBuilder ایجاد کرده و برای اجرای کوئری‌های پایگاه‌داده مورد استفاده قرار می‌گیرد.

public static function query(): QueryBuilder
{
    // TODO
}
PHP
متد allQuery()

این متد تمام رکوردهای موجود در جدول مرتبط با مدل را دریافت کرده و به‌صورت آرایه‌ای از اشیا برمی‌گرداند.

public static function allQuery(): array
{
    // TODO
}
PHP

مثال🔗

User::all();
PHP
متد findQuery()

این متد یک رکورد خاص را بر اساس مقدار id جست‌وجو می‌کند.

  • اگر رکوردی یافت شود، نمونه‌ای از مدل را برمی‌گرداند.
  • در غیر این صورت مقدار null برمی‌گردد.
public static function findQuery(int $id): ?self
{
    // TODO
}
PHP

مثال🔗

User::find(1);
PHP
متد createQuery()

این متد یک رکورد جدید در جدول مرتبط با مدل ایجاد می‌کند.

  • داده‌های جدید را در پایگاه‌داده درج کرده و نمونه‌ای از مدل را که شامل این داده‌ها است، برمی‌گرداند.
public static function createQuery(array $data): ?self
{
    // TODO
}
PHP

مثال🔗

User::create([
    'id' => 20,
    'name' => 'Jason Voorhees',
    'email' => 'jason@gmail.com',
    'age' => 60
]);
PHP
متد firstQuery()

این متد اولین رکورد موجود در جدول را دریافت کرده و به‌عنوان نمونه‌ای از مدل برمی‌گرداند.

public static function firstQuery(): ?self
{
    // TODO
}
PHP

مثال🔗

User::first();
PHP
متد hydrateQuery()

این متد یک نمونه از مدل را با داده‌های ورودی مقداردهی می‌کند.

  • برای پر کردن شیء مدل از یک آرایه داده استفاده می‌شود.
public static function hydrateQuery(array $data): self
{
    // TODO
}
PHP
متد updateQuery()

این متد مقدار فیلدهای یک رکورد را به‌روزرسانی می‌کند.

  • رکورد مورد نظر باید در object و پایگاه‌داده همزمان به‌روزرسانی شود.
  • اگر به‌روزرسانی موفقیت‌آمیز باشد، مقدار true برمی‌گردد.
  • در غیر این صورت مقدار false بازمی‌گردد.
public function updateQuery(array $data): bool
{
    // TODO
}
PHP

مثال🔗

$user = User::find(1);
$user->update(['age' => 30]);
PHP
متد deleteQuery()

این متد رکورد مربوطه را از پایگاه‌داده حذف می‌کند.

  • اگر حذف موفقیت‌آمیز باشد، مقدار true برمی‌گردد.
  • در غیر این صورت مقدار false بازمی‌گردد.
public function deleteQuery(): bool
{
    // TODO
}
PHP

مثال🔗

$user = User::find(1);
$user->delete();
PHP
فایل User.php

این فایل شامل یک کلاس User است که از کلاس Model ارث‌بری می‌کند. در این کلاس، صرفا مقادیر مورد نیاز تعریف یا مقداردهی شده‌اند. شما نیازی به پیاده‌سازی این فایل ندارید.

extensionFromNameUser.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;
}

?>
PHP
فایل autoload.php

این فایل صرفا شامل یک رجیستر است که کلاس‌های مورد نیاز را از فایل‌های دیگر فراخوانی می‌کند. شما نیازی به پیاده‌سازی این فایل ندارید.

extensionFromNameautoload.php
<?php

spl_autoload_register(function ($class_name) {
    require __DIR__ . "/$class_name.php";
});

?>
PHP

نکات🔗

  • برای حل این سوال باید از دیزاین پترن Builder استفاده کنید.
  • دقت کنید که صرفا تغییراتی که در فایل‌های QueryBuilder.php و Model.php ایجاد می‌کنید بررسی خواهند شد؛ تغییراتی که در فایل‌های دیگر پروژه ایجاد می‌کنید در سیستم داوری نادیده گرفته خواهند شد.

آن‌چه باید آپلود کنید🔗

پس از پیاده‌سازی فایل‌های مورد نیاز، فایل‌های QueryBuilder.php و Model.php را به‌صورت فایل Zip ارسال کنید.

میعین فیراری


میعین (Miein) که به تازگی از کوئرا (Quera) به جونیورا (Juniora) فرار کرده است، پس از مدتی متوجه شده است که حتی جونیورا نیز نمی‌تواند میزبان رویا‌های بلند‌پروازانه‌ی او باشد! او که پس از دیدن بنر مسابقه استخدامی ایران‌سرور (IranServer) در کوئرا و مراجعه به وب‌سایت آن، با شعار "سریع، امن و همیشه همراه در مسیر رشد کسب و کار" مواجه شده است حالا مطمئن شده که ایران‌سرور همان سرزمین رویا‌های او خواهد بود.

از آن‌جایی که رسیدن به هیچ سرزمین‌ رویا‌هایی بدون چالش و دشواری امکان پذیر نیست، میعین باید چالش ایران‌سروری زیر را حل کند تا بتواند مراحل استخدام در این مجموعه را با موفقیت طی کند!

تصویر سوال میعین فیراری

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

ساختار فایل‌ها
miein-farari
├── app
│   ├── Http
│   │   ├── Controllers
│   │   │   └── UserController.php
│   ├── Models
│   │   ├── User.php
│   │   ├── Service.php
│   │   ├── Invoice.php
│   │   └── Ticket.php
├── database
│   ├── factories
│   │   ├── UserFactory.php
│   │   ├── ServiceFactory.php
│   │   ├── InvoiceFactory.php
│   │   └── TicketFactory.php
│   ├── migrations
│   │   ├── 2024_02_26_000000_create_users_table.php
│   │   ├── 2024_02_26_000001_create_services_table.php
│   │   ├── 2024_02_26_000002_create_invoices_table.php
│   │   └── 2024_02_26_000003_create_tickets_table.php
│   ├── seeders
│   │   └── DatabaseSeeder.php
├── routes
│   └── web.php
├── tests
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
Plain text
راه‌اندازی پروژه

برای اجرای پروژه، باید php و composer را از قبل نصب کرده باشید.

  • ابتدا پروژه‌ی اولیه را دانلود و از حالت فشرده خارج کنید.
  • دستور composer install را در پوشه‌ی اصلی پروژه برای نصب نیازمندی‌ها اجرا کنید.
  • برای اجرای مایگریشن‌ها از دستور php artisan migrate استفاده کنید.

میعین در چالش ایران‌سروری باید بخشی از مدل‌ها، مایگریشن‌ها، سیدر‌ و فکتوری‌های ایران‌سرور را در یک پروژه لاراولی پیاده‌سازی کند. همچنین او در نهایت قرار است تا یک مشکل رایج در کار با دیتابیس‌ها را در بخشی از یکی از کنترلرهایی که از قبل در وبسایت ایران‌سرور وجود دارد را برطرف کند! توضیح مواردی که باید پیاده‌سازی شوند به شکل زیر می‌باشد:

پیاده‌سازی جداول و نوشتن مایگریشن‌ها (Migrations)🔗

در این بخش میعین باید مایگریشن‌های مربوط به ایجاد جداول users، services، invoices و tickets را که به ترتیب مربوط به اطلاعات کاربران، سرویس‌های ارائه‌شده توسط ایران‌سرور به هر کاربر، صورت‌حساب‌های سرویس‌ها و تیکت‌های پشتیبانی ارسالی کاربران می‌باشند را پیاده‌سازی کند. ساختار مربوط به هر کدام از جداول در بخش زیر مشخص است:

ساختار جدول users و پیاده‌سازی مایگریشن create_users_table

ساختار جدول users🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
name string نام کاربر
email string ایمیل (منحصربه‌فرد)
email_verified_at timestamp زمان تأیید ایمیل
password string رمز عبور
remember_token string توکن احراز هویت
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول services و پیاده‌سازی مایگریشن create_services_table

جدول services🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
user_id bigint کلید خارجی به جدول users
name string نام سرویس
price decimal(8,2) قیمت سرویس
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول invoices و پیاده‌سازی مایگریشن create_invoices_table

جدول invoices🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
service_id bigint کلید خارجی به جدول services
amount decimal(8,2) مبلغ صورت‌حساب
paid_at timestamp زمان پرداخت
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول tickets و پیاده‌سازی مایگریشن create_tickets_table

جدول tickets🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
user_id bigint کلید خارجی به جدول users
subject string موضوع تیکت
description text توضیحات تیکت
status string وضعیت (open یا closed)
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی

پیاده‌سازی مدل‌ها🔗

لیست بخشی از مدل‌های ایران‌سرور به شرح زیر است که باید به همراه روابطشان در پروژه پیاده‌سازی شوند:

  • مدل User: کاربران ایران‌سرور
  • مدل Service: سرویس‌هایی که ایران‌سرور به هر کاربر ارائه می‌دهد
  • مدل Invoice: صورت‌حساب‌های پرداختی مربوط به سرویس‌های ارائه شده
  • مدل Ticket: تیکت‌های پشتیبانی ارسال شده توسط کاربران

پیاده‌سازی روابط بین مدل‌ها🔗

  • هر کاربر می‌تواند چندین سرویس داشته باشد.
  • هر سرویس به یک کاربر تعلق دارد.
  • هر سرویس می‌تواند چندین صورت‌حساب داشته باشد.
  • هر صورت‌حساب به یک سرویس تعلق دارد.
  • هر کاربر می‌تواند چندین تیکت پشتیبانی ایجاد کند.
  • هر تیکت پشتیبانی متعلق به یک کاربر است.

پیاده‌سازی فکتوری‌ها‌ (Factories)🔗

میعین باید فکتوری‌های زیر را در این چالش پیاده‌سازی کند تا در مرحله بعد با استفاده از یک دیتابیس سیدر (Seeder) داده‌های تصادفی برای تست برنامه‌ نوشته شده ایجاد کند. برای بررسی بیشتر هر کدام از فکتوری‌ها روی بخش‌های زیر کلیک کنید:

ساختار فکتوری InvoiceFactory

فکتوری InvoiceFactory🔗

نام ستون توضیحات مقادیر ممکن
service_id کلید خارجی به جدول services مقدار یک service_id معتبر
amount مبلغ صورت حساب عدد اعشاری بین ۱۰ تا ۱۰۰
paid_at زمان پرداخت null یا یک تاریخ تصادفی
ساختار فکتوری ServiceFactory

فکتوریServiceFactory🔗

نام ستون توضیحات مقادیر ممکن
user_id کلید خارجی به جدول users مقدار یک user_id معتبر
name نام سرویس یک کلمه تصادفی
price قیمت سرویس عدد اعشاری بین ۱۰ تا ۱۰۰
ساختار فکتوری TicketFactory

فکتوری TicketFactory🔗

نام ستون توضیحات مقادیر ممکن
user_id کلید خارجی به جدول users مقدار یک user_id معتبر
subject موضوع تیکت یک جمله تصادفی
description توضیحات تیکت یک پاراگراف تصادفی
status وضعیت تیکت open یا closed
ساختار فکتوری UserFactory

فکتوری UserFactory🔗

نام ستون توضیحات مقادیر ممکن
name نام کاربر نام تصادفی
email ایمیل کاربر (یونیک) ایمیل تصادفی معتبر
email_verified_at زمان تأیید ایمیل زمان فعلی
password رمز عبور کلمه password به صورت هش شده
remember_token توکن یادآوری رشته ۱۰ کاراکتری تصادفی

پیاده‌سازی سیدر (Seeder)🔗

یک سیدر (Seeder) با عنوان DatabaseSeeder در پروژه اولیه وجود دارد که باید به شکل زیر برای ایجاد داده‌ها در دیتابیس با استفاده از فکتوری‌های پیاده‌سازی شده مورد استفاده قرار بگیرد:

  1. دقیقا ۱۰ کاربر با استفاده از فکتوری User ایجاد می‌شوند.
  2. هر کاربر دقیقا ۵ سرویس با استفاده از فکتوری Service خواهد داشت، پس در نهایت باید ۵۰ سرویس ثبت شود.
  3. هر کاربر به صورت تصادفی بین ۳ تا ۵ صورت‌حساب با استفاده از فکتوری Invoice برای سرویس‌های مختلف خود دارد. توجه داشته باشید که تعداد کل صورت‌حساب‌های کاربر بین ۳ تا ۵ صورت‌حساب می‌باشد و این مقدار به ازای هر کدام از سرویس‌های کاربر نیست.
  4. هر کاربر به صورت تصادفی بین ۰ تا ۱۰ تیکت با استفاده از فکتوری Ticket دارد.

بهینه‌سازی یک مشکل اساسی در کنترلر UserController🔗

کنترلر UserController به شکل زیر از قبل در فایل‌های پروژه اولیه پیاده‌سازی شده است که با دریافت تمامی کاربران موجود در برنامه در ویو users.index لیست کاربران به همراه هر کدام از سرویس‌های مربوط به آن‌ها و هر کدام از صورت‌حساب‌های مربوط به هر کدام از آن سرویس‌ها را نمایش می‌دهد. پیاده‌سازی انجام شده به شکل بهینه‌ای نیست و دارای مشکل است که میعین باید این مشکل را نیز در پروژه ارسالی خود برطرف سازد:

extensionFromNameApp\Http\Controllers
<?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'));
    }
}
PHP

آن‌چه باید آپلود کنید🔗

  • توجه: پس از پیاده‌سازی موارد خواسته شده، کل فایل‌های پروژه به‌جز پوشه‌ی vendor را زیپ کرده و ارسال کنید.
  • توجه: شما مجاز به افزودن فایل جدیدی در این ساختار نیستید و تنها باید تغییرات را در فایل‌های موجود اعمال کنید.
  • توجه: که نام فایل Zip اهمیتی ندارد.

ایران‌گیت


پس از تلاش‌‌های فراوان معین بالاخره به سرزمین رویا‌هایش ایران‌سرور (IranServer) رسید! از آن‌جایی که ایران‌سرور قصد دارد به زودی سرویس‌های ابری جدیدی را به کاربرانش ارائه دهد، معین قرار است همراه با سایر ایران‌سروریون به توسعه پنل کاربری جدیدی با عنوان "ایران‌گیت" مشغول شود که بر پایه لاراول ۱۲ است که به تازگی معرفی شده است!

از آن‌جایی که ایران‌گیت به زودی قرار است به کاربران معرفی شود، در این سوال شما نیز قرار است تا به کمک معین و ایران‌سروریون بروید تا با استفاده از لاراول ۱۲ و استارتر کیت Jetstream بخش‌هایی از پنل کاربری ایران‌گیت را پیاده‌سازی کنید.

تصویر سوال ایران‌گیت

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

ساختار فایل‌ها
irangate
├── app
│   ├── Http
│   │   ├── Middleware
│   │   │   └── CheckFeatureAccess.php
│   ├── Jobs
│   │   ├── DowngradePendingInvoicesJob.php
│   │   └── ExpireSubscriptionsJob.php
│   ├── Models
│   │   ├── Coupon.php
│   │   ├── Feature.php
│   │   ├── Invoice.php
│   │   ├── Log.php
│   │   ├── Plan.php
│   │   ├── Referral.php
│   │   ├── Subscription.php
│   │   ├── SubscriptionLog.php
│   │   └── User.php
│   ├── Observers
│   │   └── SubscriptionObserver.php
│   ├── Policies
│   │   ├── FeaturePolicy.php
│   │   └── SubscriptionPolicy.php
│   ├── Providers
│   │   ├── AppServiceProvider.php
│   │   └── AuthServiceProvider.php
├── bootstrap
│   ├── app.php
│   └── providers.php
├── database
│   ├── factories
│   │   ├── CouponFactory.php
│   │   ├── FeatureFactory.php
│   │   ├── InvoiceFactory.php
│   │   ├── LogFactory.php
│   │   ├── PlanFactory.php
│   │   ├── ReferralFactory.php
│   │   ├── SubscriptionFactory.php
│   │   ├── SubscriptionLogFactory.php
│   │   └── UserFactory.php
│   ├── migrations
│   │   ├── 2025_02_26_164235_create_plans_table.php
│   │   ├── 2025_02_26_164344_create_features_table.php
│   │   ├── 2025_02_26_164408_create_subscriptions_table.php
│   │   ├── 2025_02_26_164432_create_subscription_logs_table.php
│   │   ├── 2025_02_26_164501_create_referrals_table.php
│   │   ├── 2025_02_26_164538_create_coupons_table.php
│   │   ├── 2025_02_26_164610_create_invoices_table.php
│   │   ├── 2025_02_26_164639_create_logs_table.php
│   │   └── 2025_02_26_173343_create_plan_feature_table.php
│   ├── seeders
│   │   ├── CouponSeeder.php
│   │   ├── DatabaseSeeder.php
│   │   ├── FeatureSeeder.php
│   │   ├── InvoiceSeeder.php
│   │   ├── LogSeeder.php
│   │   ├── PlanSeeder.php
│   │   ├── ReferralSeeder.php
│   │   ├── SubscriptionLogSeeder.php
│   │   ├── SubscriptionSeeder.php
│   │   └── UserSeeder.php
├── routes
│   └── console.php
├── tests
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
Plain text
راه‌اندازی پروژه

برای اجرای پروژه، باید php، composer و npm را از قبل نصب کرده باشید.

  • ابتدا پروژه‌ی اولیه را دانلود و از حالت فشرده خارج کنید.
  • دستور composer install را در پوشه‌ی اصلی پروژه برای نصب نیازمندی‌ها اجرا کنید.
  • دستور npm install را در پوشه‌ی اصلی پروژه برای نصب نیازمندی‌ها اجرا کنید. (توجه کنید که این پروژه از Tailwindcss استفاده می‌کند)
  • دستور npm run dev را در مسیر پوشه اصلی پروژه اجرا کنید. در صورتی که این دستور را اجرا نکنید، نمی‌توانید ویو‌های ساخته شده با Tailwindcss را مشاهده کنید.
  • برای اجرای مایگریشن‌ها از دستور php artisan migrate استفاده کنید.
  • برای اجرای تست‌های نمونه، می‌توانید از دستور php artisan test استفاده کنید.

شما در این سوال قرار است تا در پیاده‌سازی یک سیستم پنل کاربری بر پایه Jetstream که امکانات شخصی‌سازی شده‌ای دارد به معین و ایران‌سروریون کمک کنید. توجه داشته باشید که برخی مواردی که در این سوال از شما خواسته شده تا پیاده‌سازی کنید ممکن است تا در این سوال به صورت عملیاتی مورد استفاده قرار نگیرند، اما در داوری سوال مورد ارزیابی قرار خواهند گرفت. توضیح مواردی که باید پیاده‌سازی شوند به شکل زیر می‌باشد:

تصویر اول سوال ایران‌گیت

معرفی کلی پروژه ایران‌گیت (IranGate) و موارد پیاده‌سازی شده🔗

در این بخش به معرفی پروژه ایران‌گیت (IranGate) خواهیم پرداخت. بخش‌هایی از ایران‌گیت در این پروژه اولیه این سوال از قبل پیاده‌سازی شده‌اند و نیازی به پیاده‌سازی دوباره ندارند. پایه ایران‌گیت همانطور که پیش‌تر گفته شد بر اساس استارتر کیت Jetstream می‌باشد و امکانات پیشفرضی که این استارتر کیت در اختیار توسعه‌دهندگان قرار می‌دهد را شامل می‌شود. موارد پیاده‌سازی شده در پروژه به شکل زیر هستند:

معرفی امکانات پیش‌فرض بر پایه Jetstream

این پروژه بر اساس امکانات پیش‌فرض پیاده‌سازی شده در Jetstream از ویو‌ها، کامپوننت‌ها، روت‌ها و ... این استارتر کیت ارث‌بری می‌کند. صفحه پیش‌فرض پروژه، صفحه پیش‌فرض لاراول ۱۲ می‌باشد که به شما برای شروع حل این سوال خوش‌آمد می‌گوید:

تصویر اول سوال ایران‌گیت

بخشی از روت‌های (Routes) پیاده‌سازی شده در پروژه نیز به مانند روت‌های پیشفرض Jetstream به شکل زیر تعریف شده‌اند:

extensionFromNameroutes/web.php
<?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');
});
PHP

شما می‌توانید همانند تصاویر زیر از امکانات پیشفرض پیاده‌سازی شده در پروژه برای ورود، عضویت، فراموشی رمز عبور و پنل کاربری پیشفرض استفاده کنید:

تصویر دوم سوال ایران‌گیت

تصویر سوم سوال ایران‌گیت

تصویر چهارم سوال ایران‌گیت

معرفی کنترلر FeatureController

این کنترلر برای مدیریت دسترسی به ویژگی‌های مختلف استفاده می‌شود. میدلور CheckFeatureAccess که قرار است توسط شما پیاده‌سازی شود، بررسی می‌کند که کاربر دسترسی به یک ویژگی خاص با عنوان featureSlug را دارد یا نه و پیامی مبنی بر مجوز دسترسی به آن ویژگی برمی‌گرداند. کنترلر و روت (Route) مربوط به آن به شکل زیر از قبل پیاده‌سازی شده‌اند:

extensionFromNameapp/Http/Controllers/FeatureController.php
<?php

namespace App\Http\Controllers;

class FeatureController extends Controller
{
    public function access($featureSlug)
    {
        return response('Access granted to feature: ' . $featureSlug, 200);
    }
}
PHP
extensionFromNameroutes/web.php
<?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');
PHP
معرفی کنترلر InvoiceController و ویو‌ی invoices.index

تصویر پنجم سوال ایران‌گیت

این کنترلر برای نمایش صورتحساب‌های کاربران است. متد index تمام فاکتورها را از طریق ارتباط با مدل Invoice و برای کاربر وارد شده (auth()->user()) بدست می‌آورد و در ویوی invoices.index به نمایش می‌گذارد. کنترلر مربوط به آن به شکل زیر از قبل پیاده‌سازی شده است:

extensionFromNameapp/Http/Controllers/InvoiceController.php
<?php

namespace App\Http\Controllers;

class InvoiceController extends Controller
{
    public function index()
    {
        $invoices = auth()->user()->invoices()->latest()->get();

        return view('invoices.index', compact('invoices'));
    }
}
PHP
معرفی کنترلر PlanController و ویو‌ی profile.plans

تصویر هفتم سوال ایران‌گیت

این کنترلر برای مدیریت اشتراک‌های کاربری و تغییرات آنها استفاده می‌شود. متد index تمام اشتراک‌های کاربری موجود را نمایش می‌دهد و متد switch به کاربر این امکان را می‌دهد که اشتراک کاربری خود را تغییر دهد. اگر اشتراک جدید هزینه داشته باشد، یک صورت‌حساب برای پرداخت ایجاد می‌شود که شما باید در بخشی از پیاده‌سازی‌های خود آن‌ها را مدیریت کنید. کنترلر مربوط به آن به شکل زیر از قبل پیاده‌سازی شده است:

extensionFromNameapp/Http/Controllers/PlanController.php
<?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', 'اشتراک کاربری شما با موفقیت بروزرسانی شد.');
        }
    }
}
PHP

با تغییر اشتراک کاربری، پیام موفقیت در تغییر اشتراک کاربری به نمایش در می‌آید و شما می‌توانید در منوی کاربری بالای صفحه امکانات مختلفی که برای آن اشتراک کاربری تعریف شده است را ببینید و به آن‌ها دسترسی پیدا کنید.

تصویر پنجم سوال ایران‌گیت

تصویر ششم سوال ایران‌گیت

معرفی کنترلر SubscriptionLogController و ویو‌ی subscription-logs.index

تصویر نهم سوال ایران‌گیت

این کنترلر برای نمایش سوابق اشتراک‌های کاربری است. متد index تمام سوابق اشتراکی مربوط به کاربر فعلی را از طریق ارتباط با مدل SubscriptionLog بدست می‌آورد و در ویوی subscription-logs.index به نمایش می‌گذارد. کنترلر مربوط به آن به شکل زیر از قبل پیاده‌سازی شده است:

extensionFromNameapp/Http/Controllers/SubscriptionLogController.php
<?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'));
    }
}
PHP

پیاده‌سازی جداول و نوشتن مایگریشن‌ها (Migrations)🔗

در این بخش، مایگریشن‌های مربوط به ایجاد جداول users، plans، subscriptions، invoices، logs، coupons، features و سایر جداول مرتبط باید پیاده‌سازی شوند. این جداول برای مدیریت کاربران، اشتراک‌ها، پرداخت‌ها، ویژگی‌های پلن‌ها و گزارش‌های سیستم طراحی شده‌اند. ساختار مربوط به هر کدام از جداول در بخش زیر مشخص است:

ساختار جدول users و پیاده‌سازی مایگریشن create_users_table

ساختار جدول users:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
name string نام کاربر
email string ایمیل (منحصربه‌فرد)
password string رمز عبور
remember_token string توکن احراز هویت
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول plans و پیاده‌سازی مایگریشن create_plans_table

ساختار جدول plans:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
name string نام اشتراک کاربری
slug string عنوان یکتای اشتراک کاربری (منحصربه‌فرد)
price decimal قیمت اشتراک کاربری
duration integer مدت زمان اشتراک کاربری (به روز)
is_active boolean وضعیت فعال بودن اشتراک کاربری
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول subscriptions و پیاده‌سازی مایگریشن create_subscriptions_table

ساختار جدول subscriptions:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
user_id bigint کلید خارجی به users
plan_id bigint کلید خارجی به plans
expires_at timestamp تاریخ انقضای اشتراک کاربری
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول invoices و پیاده‌سازی مایگریشن create_invoices_table

ساختار جدول 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 زمان آخرین به‌روزرسانی
ساختار جدول logs و پیاده‌سازی مایگریشن create_logs_table

ساختار جدول logs:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
user_id bigint کلید خارجی به users
action string نوع فعالیت انجام‌شده
details text جزئیات فعالیت
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول coupons و پیاده‌سازی مایگریشن create_coupons_table

ساختار جدول coupons:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
code string کد تخفیف (منحصربه‌فرد)
discount decimal مقدار تخفیف
max_uses integer حداکثر دفعات استفاده
expires_at timestamp تاریخ انقضای کد
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول features و پیاده‌سازی مایگریشن create_features_table

ساختار جدول features:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
name string نام ویژگی
slug string شناسه یکتای ویژگی
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول plan_feature و پیاده‌سازی مایگریشن create_plan_feature_table

ساختار جدول plan_feature:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
plan_id bigint کلید خارجی به plans
feature_id bigint کلید خارجی به features
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول referrals و پیاده‌سازی مایگریشن create_referrals_table

ساختار جدول referrals:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
referrer_id bigint کاربری که معرف شخص دیگر است
referred_id bigint کاربری که معرفی شده است
code string کد ارجاع (منحصربه‌فرد)
reward decimal پاداش داده‌شده به معرف
is_used boolean وضعیت استفاده از کد (true/false)
created_at timestamp زمان ایجاد
updated_at timestamp زمان آخرین به‌روزرسانی
ساختار جدول subscription_logs و پیاده‌سازی مایگریشن create_subscription_logs_table

ساختار جدول subscription_logs:🔗

نام ستون نوع داده توضیحات
id bigint کلید اصلی
user_id bigint کلید خارجی به جدول کاربران
old_subscription_level string سطح قبلی اشتراک کاربر (nullable)
new_subscription_level string سطح جدید اشتراک کاربر (nullable)
created_at timestamp زمان ایجاد رکورد
updated_at timestamp زمان آخرین به‌روزرسانی رکورد

پیاده‌سازی مدل‌ها🔗

لیست برخی از مدل‌های پیاده‌سازی شده در سیستم به شرح زیر است که شما باید آن‌ها را همراه با روابط بینشان در این بخش پیاده‌سازی کنید:

  • مدل 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)🔗

در این بخش، فکتوری‌های تعریف شده برای تولید داده‌های تصادفی جهت تست برنامه معرفی شده‌اند. این فکتوری‌ها به منظور تولید داده‌های آزمایشی برای جداول مرتبط در پایگاه داده استفاده می‌شوند:

ساختار فکتوری CouponFactory

فکتوری CouponFactory🔗

نام ستون توضیحات مقادیر ممکن
code کد تخفیف (منحصربه‌فرد) یک UUID تصادفی
discount مقدار تخفیف عدد اعشاری بین ۵ تا ۵۰
max_uses حداکثر دفعات استفاده عدد صحیح بین ۱ تا ۱۰۰
expires_at تاریخ انقضا تاریخی بین حالا تا ۱ سال آینده
ساختار فکتوری FeatureFactory

فکتوریFeatureFactory🔗

نام ستون توضیحات مقادیر ممکن
name نام ویژگی یک کلمه تصادفی
slug شناسه یکتا (slug) مقدار یکتای تصادفی
ساختار فکتوری InvoiceFactory

فکتوری InvoiceFactory🔗

نام ستون توضیحات مقادیر ممکن
user_id کلید خارجی به جدول users مقدار یک user_id معتبر
plan_id کلید خارجی به جدول plans مقدار یک plan_id معتبر
payment_id شناسه پرداخت یک UUID تصادفی
amount مبلغ صورت‌حساب عدد اعشاری بین ۱۰ تا ۱۰۰
status وضعیت پرداخت paid یا failed
ساختار فکتوری LogFactory

فکتوری LogFactory🔗

نام ستون توضیحات مقادیر ممکن
action نوع فعالیت یک کلمه تصادفی
details جزئیات فعالیت یک آرایه JSON
user_id کلید خارجی به جدول users مقدار یک user_id معتبر
ساختار فکتوری PlanFactory

فکتوری PlanFactory🔗

نام ستون توضیحات مقادیر ممکن
name نام اشتراک یک کلمه تصادفی
slug شناسه یکتا (slug) مقدار یکتای تصادفی
price قیمت عدد اعشاری بین ۱۰ تا ۱۰۰
duration مدت زمان (روز) عدد صحیح بین ۳۰ تا ۳۶۵
is_active وضعیت فعال/غیرفعال بودن اشتراک مقدار true یا false
ساختار فکتوری ReferralFactory

فکتوری ReferralFactory🔗

نام ستون توضیحات مقادیر ممکن
referrer_id کلید خارجی به جدول users مقدار یک user_id معتبر
referred_id کلید خارجی به جدول users مقدار یک user_id معتبر
code کد ارجاع (منحصربه‌فرد) یک UUID تصادفی
reward مقدار جایزه ارجاع عدد اعشاری بین ۰ تا ۱۰۰
is_used وضعیت استفاده از کد مقدار true یا false
ساختار فکتوری UserFactory

فکتوری UserFactory🔗

نام ستون توضیحات مقادیر ممکن
name نام کاربر یک نام تصادفی
email ایمیل کاربر (یونیک) ایمیل تصادفی معتبر
email_verified_at زمان تأیید ایمیل now() یا null
password رمز عبور password هش‌شده
remember_token توکن یادآوری رشته ۱۰ کاراکتری تصادفی
ساختار فکتوری SubscriptionFactory

فکتوری SubscriptionFactory🔗

نام ستون توضیحات مقادیر ممکن
user_id شناسه کاربر مرتبط مقدار تصادفی از UserFactory
plan_id شناسه پلن مرتبط مقدار تصادفی از PlanFactory
expires_at تاریخ انقضای اشتراک تاریخ تصادفی بین now و +1 year
created_at زمان ایجاد رکورد now()
updated_at زمان آخرین به‌روزرسانی رکورد now()
ساختار فکتوری SubscriptionLogFactory

فکتوری SubscriptionLogFactory🔗

نام ستون توضیحات مقادیر ممکن
user_id شناسه کاربر مرتبط مقدار تصادفی از UserFactory
old_subscription_level سطح قبلی اشتراک کاربر یک کلمه تصادفی
new_subscription_level سطح جدید اشتراک کاربر یک کلمه تصادفی
created_at زمان ایجاد رکورد now()
updated_at زمان آخرین به‌روزرسانی رکورد now()

پیاده‌سازی سیدر (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 را برای مدیریت دسترسی به ویژگی‌های خاص و اشتراک‌های کاربری خاص به صورت زیر پیاده‌سازی کنید:

ساختار پالیسی FeaturePolicy

پالیسی FeaturePolicy به صورت زیر در مسیر app/Policies/FeaturePolicy.php پیاده‌سازی شده است که با استفاده از تابعی با نام accessFeature و دریافت دو مقدار $user و $feature به صورت پویا مشخص می‌کند که آیا کاربر $user در اشتراک کاربری فعلی‌اش می‌تواند می‌تواند به قابلیت $feature دسترسی داشته باشد یا نه.

extensionFromNameapp/Policies/FeaturePolicy.php
<?php

namespace App\Policies;

use App\Models\User;
use App\Models\Feature;

class FeaturePolicy
{
    public function accessFeature(User $user, Feature $feature)
    {
        // TODO
    }
}
PHP
  • از این پالیسی برای مدیریت دسترسی بر اساس قابلیت‌ها (Features) استفاده خواهد شد.

‍‍‍

ساختار پالیسی SubscriptionPolicy

پالیسی SubscriptionPolicy به صورت زیر در مسیر app/Policies/SubscriptionPolicy.php پیاده‌سازی شده است که برای مدیریت دسترسی‌های کاربران با اشتراک‌های کاربری پیش‌فرض basic، premium و vip به بخش‌های مختلف پنل‌کاربری مورد استفاده قرار می‌گیرد. این پالیسی با استفاده از هر کدام از سه تابع accessVIP، accessPremium و accessBasic بررسی می‌کند که آیا اشتراک کاربری $subscription که متعلق به کاربر $user می‌باشد به ترتیب از نوع vip، premium و basic می‌باشد یا نه.

extensionFromNameapp/Policies/FeaturePolicy.php
<?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
    }
}
PHP
  • از این پالیسی برای مدیریت دسترسی بر اساس اشتراک‌های کاربری (Subscriptions) استفاده خواهد شد.

پیاده‌سازی Observer SubscriptionObserver🔗

در این پروژه شما باید یک Observer را برای مدل Subscription به صورتی پیاده‌سازی کنید که هنگام بروزرسانی (Update) شناسه اشتراک کاربری (plan_id) برای یک کاربر، یک SubscriptionLog جدید ایجاد کند که مقدار user_id آن برابر با شناسه کاربری است که اشتراک کاربری آن تغییر کرده است، مقدار old_subscription_level آن برابر با مقدار عنوان (Slug) اشتراک کاربری قبلی کاربر و مقدار new_subscription_level آن برابر با مقدار عنوان (Slug) اشتراک جدید کاربر می‌باشد.

پیاده‌سازی وظایف (Jobs) و اسکجول (Schedule) کردن🔗

در این پروژه شما باید دو جاب ExpireSubscriptionsJob و DowngradePendingInvoicesJob را برای انجام بخشی از پردازش‌های مربوط به پنل کاربری به صورت زیر پیاده‌سازی کنید:

ساختار جاب DowngradePendingInvoicesJob

جاب DowngradePendingInvoicesJob را به صورت زیر در مسیر app/Jobs/DowngradePendingInvoicesJob.php پیاده‌سازی کنید تا اشتراک کاربری تمام کاربرانی را حداقل یک صورت‌حساب (Invoice) با وضعیت (Status) باز (pending) دارند که بیش‌تر مساوی یک هفته از صادر شدن‌شان گذشته است، به اشتراک کاربری basic تغییر دهد که زمان انقضایی هم نداشته باشد.

extensionFromNameapp/Jobs/DowngradePendingInvoicesJob.php
<?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
    }
}
PHP
  • از این پالیسی برای مدیریت دسترسی بر اساس قابلیت‌ها (Features) استفاده خواهد شد.

‍‍‍

ساختار جاب ExpireSubscriptionsJob

جاب ExpireSubscriptionsJob را به صورت زیر در مسیر app/Jobs/ExpireSubscriptionsJob.php پیاده‌سازی کنید تا اشتراک کاربری تمام کاربرانی را که تاریخ انقضای اشتراک کاربری‌شان (expires_at) به پایان رسیده است (تاریخ انقضای اشتراک کاربری‌شان کوچک‌تر مساوی زمان حال است)، به اشتراک کاربری basic تغییر دهد که زمان انقضایی هم نداشته باشد.

extensionFromNameapp/Jobs/ExpireSubscriptionsJob.php
<?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
    }
}
PHP
  • از این پالیسی برای مدیریت دسترسی بر اساس اشتراک‌های کاربری (Subscriptions) استفاده خواهد شد.

در نهایت پس از تعریف وظایف (Jobs) های مشخص شده، شما باید این دو وظیفه (Job) را طوری در لاراول اسکجول (Schedule) کنید تا به صورت خودکار و هر دقیقه یک بار پردازش شوند.

پیاده‌سازی میدلور (Middleware)🔗

در نهایت شما قرار است تا میدلور (Middleware) CheckFeatureAccess را برای پردازش دسترسی در درخواست (Request) کاربر به صورت زیر پیاده‌سازی کنید:

extensionFromNameapp/Http/Middleware/CheckFeatureAccess.php
<?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
    }
}
PHP

این میدلور با استفاده از پالیسی‌های تعریف شده در مراحل قبل بررسی می‌کند که آیا کاربر فعلی می‌تواند به مسیر مشخص شده که از این میدلور استفاده می‌کند و مشخص کننده مسیر یکی از قابلیت‌های موجود در برنامه است دسترسی دارد یا نه. در صورتی که کاربر در تلاش بود تا به مسیر مربوط به قابلیتی دسترسی پیدا کند که در برنامه به کل وجود ندارد باید خطای 404 به همراه پیام "این صفحه یافت نشد." و در صورتی که در تلاش بود تا به مسیر مربوط به قابلیتی دسترسی پیدا کند که اشتراک کاربری‌اش اجازه دسترسی به آن را ندارد، باید خطای 403 به همراه پیام "شما دسترسی به این بخش را ندارید." نمایش داده شود. در غیر این صورت ادامه پردازش درخواست کاربر انجام می‌شود.

آن‌چه باید آپلود کنید🔗

  • توجه: پس از پیاده‌سازی موارد خواسته شده، کل فایل‌های پروژه به‌جز پوشه‌ی vendor را زیپ کرده و ارسال کنید.
  • توجه: شما مجاز به افزودن فایل جدیدی در این ساختار نیستید و تنها باید تغییرات را در فایل‌های موجود اعمال کنید.
  • توجه: که نام فایل Zip اهمیتی ندارد.