تخمین زمان مطالعه


بر اساس آمار گزارش‌شده، میانگین سرعت مطالعه‌ی افراد در اینترنت بین ۲۰۰ تا ۲۵۰ واژه در هر دقیقه است. احتمالاً وبلاگ‌هایی را دیده‌اید که در آن‌ها زمان تقریبی مطالعه‌ی هر پست زیر عنوان آن نوشته شده است. از شما می‌خواهیم این قابلیت را پیاده‌سازی کنید.

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

تابعی با نام estimateReadingTime پیاده‌سازی کنید که یک رشته از کاربر دریافت کرده و زمان مطالعه‌ی پست را برحسب دقیقه برگرداند. این تابع باید تعداد کلمات موجود در متن را تقسیم بر ۲۰۰ کرده و سقف آن را برگرداند. هم‌چنین، کاراکترهای .، ?، !، ,، ; و : باید نادیده گرفته شوند. کلمات با یک یا چند کاراکتر ' '، '\t' یا '\n' از یکدیگر جدا شده‌اند. هم‌چنین، تفاوتی برای طول کلمات قائل نمی‌شویم.

امضای تابع estimateReadingTime به‌صورت زیر خواهد بود:

function estimateReadingTime(string $text): int
{
    // TODO: Implement
}
PHP

مثال🔗

<?php

function estimateReadingTime(string $text): int
{
    // TODO: Implement
}

function randomText(int $wordsCount): string
{
    $words = [];
    for ($i = 0; $i < $wordsCount; $i++) {
        $words[] = randomWord();
    }
    $text = implode(" ", $words);

    return $text;
}

function randomWord(): string
{  
    $vowels = ["a","e","i","o","u"];  
    $consonants = [
        "b", "c", "d", "f", "g", "h", "j", "k", "l", "m", 
        "n", "p", "r", "s", "t", "v", "w", "x", "y", "z"
    ];  
    $string = "";
    $max = intdiv(rand(4, 8), 2);
    for ($i = 1; $i <= $max; $i++) {
        $string .= $consonants[array_rand($consonants)];
        $string .= $vowels[array_rand($vowels)];
    }

    return $string;
}

echo estimateReadingTime(randomText(200)); // 1
echo "\n";
echo estimateReadingTime(randomText(201)); // 2
echo "\n";
echo estimateReadingTime(randomText(50)); // 1
echo "\n";
echo estimateReadingTime(randomText(1895)); // 10
PHP

خروجی موردانتظار:

1
2
1
10
Plain text

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

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

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

یک روش برای پیاده‌سازی تابع estimateReadingTime، حذف علائم نگارشی، جداسازی متن نهایی بر اساس whitespace و در نهایت شمارش تعداد کلمات است:

<?php

function estimateReadingTime(string $text): int
{
    $text = str_replace([".", "?", "!", ",", ";", ":"], "", $text);
    $text = trim($text);
    if (empty($text)) {
        return 0;
    }
    $wordsCount = count(preg_split("/\s+/", $text));

    // TODO: return ceil of $wordsCount divided by 200
}
PHP
راهنمایی ۲

روش دیگر برای پیاده‌سازی تابع estimateReadingTime، استفاده از تابع str_word_count برای محاسبه‌ی تعداد کلمات موجود در متن است:

<?php

function estimateReadingTime(string $text): int
{
    return ceil(str_word_count($text) / 200);
}
PHP

صرافی حسن


  • محدودیت زمان: ۳ ثانیه
  • محدودیت حافظه: ۶۴ مگابایت

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

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

نکته: برای دریافت ورودی، می‌توانید از تابع readline استفاده کنید.

ورودی🔗

در خط اول ورودی استاندارد (stdin)، عدد صحیح nn که بیانگر تعداد نوسان‌های قیمت است وارد می‌شود.

0n<3600 \leq n < 360

در nn خط بعدی، تاریخ (با فرمت mm/dd) و قیمت دلار در تاریخ ذکرشده وارد می‌شود. برای سادگی، فرض کنید همه‌ی ماه‌ها ۳۰ روزه هستند. هم‌چنین، تضمین می‌شود که تاریخ‌ها از یکدیگر متمایز هستند.

توجه: قیمت دلار را در تاریخ 01/01 برابر با 14900 در نظر بگیرید.

در خط بعدی، عدد صحیح mm که بیانگر تعداد مشتری‌های حسن است وارد می‌شود.

0m1050 \leq m \leq 10^5

در mm خط بعدی، در هر خط یک تاریخ آمده است.

خروجی🔗

به ازای هر مشتری، قیمت دلار در آن روز را چاپ کنید.

مثال🔗

ورودی نمونه🔗

4
05/12 22000
06/06 24100
05/20 17000
06/05 23300
4
06/07
05/12
05/21
06/05
Plain text

خروجی نمونه🔗

24100
22000
17000
23300
Plain text

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

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

<?php

$n = readline();
$prices = [
    1 => 14900
];
for ($i = 0; $i < $n; $i++) {
    [$date, $price] = explode(" ", readline());
    $dayNumber = getDayNumberOfDate($date);
    $prices[$dayNumber] = $price;
}

// TODO: complete
PHP
راهنمایی ۲

در ادامه، باید قیمت تاریخ‌هایی که در ورودی نیامده‌اند را بر اساس سایر تاریخ‌ها مشخص کرد. قیمت در تاریخ dd برابر است با قیمت در بزرگ‌ترین تاریخی که کوچک‌تر یا مساوی dd است.

<?php

function fillEmptyDates(&$prices)
{
    $currentDate = 1;
    for ($i = 2; $i <= 360; $i++) {
        if (!array_key_exists($i, $prices)) {
            $prices[$i] = $prices[$currentDate];
        } else {
            $currentDate = $i;
        }
    }
}

function getDayNumberOfDate($date)
{
    [$m, $d] = explode("/", $date);
    $dayNumber = (($m - 1) * 30) + $d;

    return $dayNumber;
}

$n = readline();
$prices = [
    1 => 14900
];
for ($i = 0; $i < $n; $i++) {
    [$date, $price] = explode(" ", readline());
    $dayNumber = getDayNumberOfDate($date);
    $prices[$dayNumber] = $price;
}
fillEmptyDates($prices);
PHP

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

$m = readline();
for ($i = 0; $i < $m; $i++) {
    $date = readline();
    $dayNumber = getDayNumberOfDate($date);
    echo $prices[$dayNumber]."\n";
}
PHP

RBAC


محدود کردن دسترسی بر اساس نقش (Role Based Access Control) نوعی مدیریت دسترسی است که در آن هر کاربر دارای یک نقش (Role) بوده و هر نقش دارای یک مجموعه دسترسی است. در این سؤال قرار است این سیستم را پیاده‌سازی کنید.

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

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های این پروژه به‌صورت زیر است:

rbac_inital
├── app
│   ├── Console
│   │   └── Kernel.php
│   ├── Exceptions
│   │   └── Handler.php
│   ├── Http
│   │   ├── Controllers
│   │   │   ├── Controller.php
│   │   │   └── RBACController.php
│   │   ├── Middleware
│   │   │   ├── Authenticate.php
│   │   │   ├── EncryptCookies.php
│   │   │   ├── PreventRequestsDuringMaintenance.php
│   │   │   ├── RedirectIfAuthenticated.php
│   │   │   ├── TrimStrings.php
│   │   │   ├── TrustHosts.php
│   │   │   ├── TrustProxies.php
│   │   │   └── VerifyCsrfToken.php
│   │   └── Kernel.php
│   ├── Models
│   │   ├── Permission.php
│   │   ├── Role.php
│   │   └── User.php
│   └── Providers
│       ├── AppServiceProvider.php
│       ├── AuthServiceProvider.php
│       ├── BroadcastServiceProvider.php
│       ├── EventServiceProvider.php
│       └── RouteServiceProvider.php
├── bootstrap
│   ├── cache
│   │   ├── packages.php
│   │   └── services.php
│   └── app.php
├── config
│   ├── app.php
│   ├── auth.php
│   ├── broadcasting.php
│   ├── cache.php
│   ├── cors.php
│   ├── database.php
│   ├── filesystems.php
│   ├── hashing.php
│   ├── logging.php
│   ├── mail.php
│   ├── queue.php
│   ├── services.php
│   ├── session.php
│   └── view.php
├── database
│   ├── factories
│   │   └── UserFactory.php
│   ├── migrations
│   │   ├── 2014_10_12_000000_create_users_table.php
│   │   ├── 2014_10_12_100000_create_password_resets_table.php
│   │   ├── 2019_08_19_000000_create_failed_jobs_table.php
│   │   ├── 2020_09_27_092338_create_roles_table.php
│   │   ├── 2020_09_27_092758_create_permissions_table.php
│   │   ├── 2020_09_27_092917_create_permission_role_table.php
│   │   └── 2020_09_27_093704_create_role_user_table.php
│   ├── seeders
│   │   └── DatabaseSeeder.php
│   └── database.db
├── public
│   ├── favicon.ico
│   ├── index.php
│   ├── robots.txt
│   └── web.config
├── resources
│   ├── css
│   │   └── app.css
│   ├── js
│   │   ├── app.js
│   │   └── bootstrap.js
│   ├── lang
│   │   └── en
│   │       ├── auth.php
│   │       ├── pagination.php
│   │       ├── passwords.php
│   │       └── validation.php
│   └── views
│       └── welcome.blade.php
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php
├── storage
│   ├── app
│   │   └── public
│   ├── framework
│   │   ├── cache
│   │   │   └── data
│   │   ├── sessions
│   │   ├── testing
│   │   └── views
│   └── logs
│       └── laravel.log
├── tests
│   ├── Feature
│   │   └── RBACSampleTest.php
│   ├── Unit
│   ├── CreatesApplication.php
│   └── TestCase.php
├── 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 مایگریشن‌ها را اجرا کرده و پایگاه داده را به طور کامل داشته باشید. در این پروژه از پایگاه داده‌ی SQLite استفاده شده است. در ادامه، لیست مدل‌ها و توضیحات آمده است:

  1. User: مدل کاربر که دارای فیلدهای name، email و password است؛ فیلدهای name و password را می‌توانید دلخواه مقدار دهید.

  2. Role: مدل نقش که دارای فیلد name است

  3. Permission: مدل دسترسی که دارای فیلد name است

علاوه بر این مدل‌ها، دو جدول permission_role و role_user نیز وجود دارند که جدول‌های رابطه هستند.

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

  1. createRole($name): این متد با ورودی گرفتن نام نقش آن را اضافه می‌کند.

  2. addPermissionToRole($class, $function, $role): این متد با گرفتن نام کلاس، نام متد و نام نقش، اجازه دسترسی یک نقش به یک متد از یک کلاس را می‌دهد.

  3. addUser($email): این متد یک کاربر با ایمیل داده شده را ایجاد می‌کند.

  4. addRoleToUser($role, $email): این متد با با دریافت نام نقش و ایمیل کاربر، آن نقش را به کاربر مورد نظر می‌دهد. هر کاربر می‌تواند چند نقش داشته باشد.

برنامه‌ی شما به این صورت داوری می‌شود که کلاسی با ارث‌بری از کنترلر اصلی لاراول (App\Http\Controllers\Controller) به کد شما اضافه می‌شود. شما باید چهار متد create، update، store و edit را در کلاس Controller طوری پیاده‌سازی کنید که به‌ترتیب چهار دسترسی create، update، store و edit برای کلاسی که از Controller ارث‌بری کرده است را بررسی کند. در صورتی که برای کاربر لاگین‌شده دسترسی موردنظر وجود نداشته باشد، متد موجود در Controller باید عدد 403 را برگرداند (return 403;). در غیر این‌صورت، این متدها نباید مقداری را برگردانند. تضمین می‌شود که هنگام تست این متدها، کاربر در سایت لاگین کرده است (با استفاده از Auth::user() می‌توانید اطلاعات کاربر را دریافت کنید).

توجه: شما تنها مجاز به تغییر فایل‌های موجود در پوشه‌ی /app هستید.

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

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

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

برای پیاده‌سازی متد createRole، می‌توان از متد create در مدل Role استفاده کرد. متد addUser نیز مشابه این متد پیاده‌سازی می‌شود:

public function createRole($name)
{
    Role::create([
        'name' => $name
    ]);

    return response()->json();
}
PHP

برای پیاده‌سازی متد addRoleToUser، می‌توان ابتدا Role متناظر با نام ورودی و User متناظر با ایمیل ورودی را دریافت کرد و سپس Role را به User attach کرد:

public function addRoleToUser($role, $email)
{
    $role = Role::where('name', $role)->first();
    $user = User::where('email', $email)->first();
    $user->roles()->attach($role);

    return response()->json();
}
PHP

برای پیاده‌سازی متد addPermissionToRole، یک روش برای ذخیره‌سازی نام کلاس و نام متد در فیلد name دسترسی، جداسازی آن‌ها با استفاده از یک کاراکتر space است:

public function addPermissionToRole($class, $function, $role)
{
    $permission = Permission::firstOrCreate([
        'name' => $class . ' ' . $function
    ]);
    $role = Role::where('name', $role)->first();
    $role->permissions()->attach($permission);

    return response()->json();
}
PHP
راهنمایی ۲

برای پیاده‌سازی متد create در کلاس Controller، باید ابتدا Roleهای کاربر و سپس Permissionهای متناظر با Roleها را دریافت کرد. نام کلاسی که این متد را صدا می‌زند با استفاده از کلاس ReflectionClass یا get_called_class قابل دریافت است. در صورتی که دسترسی موردنظر وجود نداشته باشد، باید عدد 403 را برگرداند.

برای جلوگیری از مشکل N+1N+1، می‌توان از join استفاده کرد:

public function create()
{
    $className = str_replace(__NAMESPACE__ . '\\', '', get_called_class());
    $hasPermission = Auth::user()
        ->roles()
        ->join('permission_role', 'roles.id', '=', 'permission_role.role_id')
        ->join('permissions', 'permission_role.permission_id', '=', 'permissions.id')
        ->where('permissions.name', $className . ' create')
        ->exists();
    if (!$hasPermission) {
        return 403;
    }
}
PHP

سایر متدهای کلاس Controller نیز مشابه متد create پیاده‌سازی می‌شوند.

بلاگ خیلی ساده


در این سؤال، شما باید یک بلاگ خیلی ساده را پیاده‌سازی کنید.

جزئیات🔗

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های این پروژه به‌صورت زیر است:

simple_blog_initial
├── app
│   ├── Console
│   │   └── Kernel.php
│   ├── Exceptions
│   │   └── Handler.php
│   ├── Http
│   │   ├── Controllers
│   │   │   ├── Controller.php
│   │   │   └── PostController.php
│   │   ├── Middleware
│   │   │   ├── Authenticate.php
│   │   │   ├── EncryptCookies.php
│   │   │   ├── PreventRequestsDuringMaintenance.php
│   │   │   ├── RedirectIfAuthenticated.php
│   │   │   ├── TrimStrings.php
│   │   │   ├── TrustHosts.php
│   │   │   ├── TrustProxies.php
│   │   │   └── VerifyCsrfToken.php
│   │   └── Kernel.php
│   ├── Models
│   │   ├── FavoritePost.php
│   │   ├── Post.php
│   │   └── User.php
│   ├── Override
│   │   ├── Connection.php
│   │   └── QueryBuilder.php
│   └── Providers
│       ├── AppServiceProvider.php
│       ├── AuthServiceProvider.php
│       ├── BroadcastServiceProvider.php
│       ├── DatabaseServiceProvider.php
│       ├── EventServiceProvider.php
│       └── RouteServiceProvider.php
├── bootstrap
│   ├── cache
│   │   ├── packages.php
│   │   └── services.php
│   └── app.php
├── config
│   ├── app.php
│   ├── auth.php
│   ├── broadcasting.php
│   ├── cache.php
│   ├── cors.php
│   ├── database.php
│   ├── filesystems.php
│   ├── hashing.php
│   ├── logging.php
│   ├── mail.php
│   ├── queue.php
│   ├── services.php
│   ├── session.php
│   └── view.php
├── database
│   ├── factories
│   │   └── UserFactory.php
│   ├── migrations
│   │   ├── 2014_10_12_000000_create_users_table.php
│   │   ├── 2014_10_12_100000_create_password_resets_table.php
│   │   ├── 2019_08_19_000000_create_failed_jobs_table.php
│   │   ├── 2020_09_27_124807_create_table_posts.php
│   │   └── 2020_09_27_133130_create_table_favorite_posts.php
│   ├── seeders
│   │   └── DatabaseSeeder.php
│   └── database.sqlite
├── public
│   ├── favicon.ico
│   ├── index.php
│   ├── robots.txt
│   └── web.config
├── resources
│   ├── css
│   │   └── app.css
│   ├── js
│   │   ├── app.js
│   │   └── bootstrap.js
│   ├── lang
│   │   └── en
│   │       ├── auth.php
│   │       ├── pagination.php
│   │       ├── passwords.php
│   │       └── validation.php
│   └── views
│       ├── posts
│       │   ├── create.blade.php
│       │   ├── edit.blade.php
│       │   ├── index.blade.php
│       │   └── show.blade.php
│       └── welcome.blade.php
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php
├── storage
│   ├── app
│   │   └── public
│   ├── framework
│   │   ├── cache
│   │   │   └── data
│   │   ├── sessions
│   │   ├── testing
│   │   └── views
│   └── logs
├── tests
│   ├── Feature
│   │   ├── BlogSampleTest.php
│   │   ├── BlogTest.php
│   │   └── OrderByCustomTest.php
│   ├── Unit
│   ├── CreatesApplication.php
│   └── TestCase.php
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
Plain text
راه‌اندازی پروژه

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

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

این بلاگ شامل صفحات زیر است:

  1. صفحه‌ی نوشتن یک پست جدید (/post/create): در این صفحه با وارد کردن یک تیتر و متن محتوای یک پست، آن پست ساخته می‌شود. پس از ساخت پست، کاربر به صفحه‌ی نمایش آن پست هدایت می‌شود.
  2. صفحه‌ی ویرایش یک پست (/post/edit/{id}): در این صفحه، تیتر و محتوای فعلی پست نمایش داده می‌شود و قابل ویرایش است. پس از ویرایش، کاربر به صفحه‌ی نمایش آن پست هدایت می‌شود.
  3. صفحه‌ی نمایش یک پست (/post/show/{id}): در این صفحه، تیتر و محتوای پست نمایش داده می‌شود.
  4. صفحه لیست تمام پست‌ها (/posts): در این صفحه، تمامی پست‌ها نمایش داده می‌شوند و با کلیک بر روی تیتر هر پست، کاربر‌ وارد صفحه‌ی نمایش آن پست می‌شود.

هر پست شامل یک تیتر و یک متن محتوا است.

هر پست پس از نوشته شدن به لیست پست‌ها در صفحه لیست تمام پست‌ها اضافه می‌شود.

مدل‌ها🔗

مدل‌ها در پروژه‌ی اولیه داده شده است. هم‌چنین می‌توانید با اجرای دستور php artisan migrate مایگریشن‌ها را اجرا کنید و پایگاه داده را به‌طور کامل داشته باشید. در این پروژه از پایگاه داده‌ی SQLite استفاده شده است. در ادامه، لیست مدل‌ها و توضیحات آمده است:

  1. Post: مدل پست‌ها که دارای دو فیلد تیتر (title) و محتوا (contents) است
  2. FavoritePost: مدل پست‌های مورد علاقه که دارای یک فیلد post_id است و مشخص‌کننده‌ی پست‌های موردعلاقه است. با کلیک روی لینک «اضافه به علاقه‌مندی‌ها»، در صورتی که پست موردنظر از قبل موردعلاقه نباشد، یک ردیف به این جدول اضافه می‌شود.

شما باید PostController و viewهای مربوطه را طوری کامل کنید که عملیات ساخت، ویرایش و نمایش پست‌ها (به‌صورت تکی و در لیست) به درستی کار کند.

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

کنترلرها🔗

متدهای زیر را در کنترلر PostController پیاده‌سازی کنید:

  1. index(): این متد باید لیست پست‌ها را با ترتیبی که قبلاً ذکر شد با نام posts به viewposts.index ارسال کند.
  2. store(Request $request): این متد باید یک پست جدید با اطلاعات ورودی (title و contents) به پایگاه داده اضافه کرده و کاربر را به صفحه‌ی /post/show/{id} هدایت کند.
  3. edit($id): این متد باید اطلاعات پست با شناسه‌ی $id را با نام post به view posts.edit ارسال کند.
  4. update($id, Request $request): این متد باید پست با شناسه‌ی $id را با اطلاعات ورودی (title و contents) به‌روزرسانی کرده و کاربر را به صفحه‌ی /post/show/{id} هدایت کند.
  5. show($id): این متد باید اطلاعات پست با شناسه‌ی $id را با نام post به view posts.show ارسال کند.
  6. favorite($id): این متد باید پست با شناسه‌ی $id را به جدول علاقه‌مندی‌ها اضافه کرده و کاربر را به صفحه‌ی قبلی‌اش هدایت کند.

برای نمایش پست‌های اضافه شده به علاقه‌مندی‌ها، شما باید در کلاس \App\Providers\DatabaseServiceProvider یک macro جدید با نام orderByCustom به QueryBuilder اضافه کنید. این macro باید به این‌صورت کار کند:

User::orderByCustom('id', 3, 7, 2, 8)->get();
PHP

کوئری معادل فراخوانی متد بالا:

select * from "users" order by (case "id" when 3 then 0 when 7 then 1 when 2 then 2 when 8 then 3 else 4 end)
SQL

هم‌چنین اگر فقط یک آرگومان به این متد داده شود (یا آرگومان‌های دوم به بعد خالی باشند)، نباید تغییری در کوئری اعمال شود.

توجه: شما تنها مجاز به تغییر فایل‌های موجود در پوشه‌ی /app هستید.

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

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

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

برای پیاده‌سازی macro orderByCustom باید از عملگر ... استفاده کرد:

Builder::macro('orderByCustom', function ($column, ...$values) {
    if (empty($values)) {
        return $this;
    }
    $orderByQuery = '(case "' . $column . '"';
    $i = 0;
    foreach ($values as $value) {
        $orderByQuery .= ' when ' . $value . ' then ' . ($i++);
    }
    $orderByQuery .= ' else ' . $i . ' end)';

    return $this->orderByRaw($orderByQuery);
});
PHP
راهنمایی ۲

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

public function index()
{
    $favorites = FavoritePost::pluck('post_id')->flatten()->toArray();
    $posts = Post::orderByCustom('id', ...$favorites)->orderBy('created_at', 'desc')->get();
    return view('posts.index')->with('posts', $posts);
}
PHP

حالت تعمیر سفارشی


محمدرضا در شرکت عدالتخانه به‌عنوان توسعه‌دهنده‌ی بک‌اند مشغول به کار است. او اخیراً با یک چالش جدید مواجه شده است. همان‌طور که مطلع هستید، در Laravel 8 قابلیت‌های جدیدی به حالت تعمیر (maintenance mode) اضافه شده است. یکی از این قابلیت‌ها، امکان تعریف کلید secret برای حالت تعمیر است که با استفاده از آن می‌توان سایت را خارج از حالت تعمیر مشاهده کرد. این قابلیت در واقع یک کوکی با نام laravel_maintenance در مرورگر ذخیره کرده و با استفاده از آن، حالت تعمیر را برای کاربر فعلی غیرفعال می‌کند.

زمان اعتبار این کوکی ۱۲ ساعت است، اما تیم فنی شرکت عدالتخانه قصد دارد یک آرگومان به دستور down موجود در Artisan اضافه کند که در صورت مقداردهی آن، کوکی laravel_maintenance با زمان اعتبار واردشده در این آرگومان ست شود.

محمدرضا این task را برعهده گرفته، اما از پس آن برنیامده است. از شما می‌خواهیم این task را برای او انجام دهید.

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

پروژه‌ی اولیه را از این لینک دانلود کنید. ساختار فایل‌های این پروژه به‌صورت زیر است:

maintenance_initial
├── app
│   ├── Console
│   │   └── Kernel.php
│   ├── Exceptions
│   │   └── Handler.php
│   ├── Http
│   │   ├── Controllers
│   │   │   └── Controller.php
│   │   ├── Middleware
│   │   │   ├── Authenticate.php
│   │   │   ├── EncryptCookies.php
│   │   │   ├── PreventRequestsDuringMaintenance.php
│   │   │   ├── RedirectIfAuthenticated.php
│   │   │   ├── TrimStrings.php
│   │   │   ├── TrustHosts.php
│   │   │   ├── TrustProxies.php
│   │   │   └── VerifyCsrfToken.php
│   │   └── Kernel.php
│   ├── Models
│   │   └── User.php
│   └── Providers
│       ├── AppServiceProvider.php
│       ├── AuthServiceProvider.php
│       ├── BroadcastServiceProvider.php
│       ├── EventServiceProvider.php
│       └── RouteServiceProvider.php
├── bootstrap
│   ├── cache
│   │   ├── packages.php
│   │   └── services.php
│   └── app.php
├── config
│   ├── app.php
│   ├── auth.php
│   ├── broadcasting.php
│   ├── cache.php
│   ├── cors.php
│   ├── database.php
│   ├── filesystems.php
│   ├── hashing.php
│   ├── logging.php
│   ├── mail.php
│   ├── queue.php
│   ├── services.php
│   ├── session.php
│   └── view.php
├── database
│   ├── factories
│   │   └── UserFactory.php
│   ├── migrations
│   │   ├── 2014_10_12_000000_create_users_table.php
│   │   ├── 2014_10_12_100000_create_password_resets_table.php
│   │   └── 2019_08_19_000000_create_failed_jobs_table.php
│   └── seeders
│       └── DatabaseSeeder.php
├── public
│   ├── favicon.ico
│   ├── index.php
│   ├── robots.txt
│   └── web.config
├── resources
│   ├── css
│   │   └── app.css
│   ├── js
│   │   ├── app.js
│   │   └── bootstrap.js
│   ├── lang
│   │   └── en
│   │       ├── auth.php
│   │       ├── pagination.php
│   │       ├── passwords.php
│   │       └── validation.php
│   └── views
│       └── welcome.blade.php
├── routes
│   ├── api.php
│   ├── channels.php
│   ├── console.php
│   └── web.php
├── storage
│   ├── app
│   │   └── public
│   ├── framework
│   │   ├── cache
│   │   │   └── data
│   │   ├── sessions
│   │   ├── testing
│   │   └── views
│   └── logs
├── tests
│   ├── Feature
│   │   └── ExampleTest.php
│   ├── Unit
│   │   └── ExampleTest.php
│   ├── CreatesApplication.php
│   └── TestCase.php
├── README.md
├── artisan
├── composer.json
├── composer.lock
├── package.json
├── phpunit.xml
├── server.php
└── webpack.mix.js
Plain text
راه‌اندازی پروژه

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

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

آرگومانی با نام time و مقدار پیش‌فرض 12 به دستور down در Artisan اضافه کنید که در صورت مقداردهی شدن، زمان کوکی laravel-maintenance برابر با مقدار آرگومان time باشد. مقدار آرگومان time یک عدد صحیح بوده و بیانگر زمان کوکی laravel-maintenance برحسب ساعت است. تضمین می‌شود که عدد صفر و اعداد منفی به این آرگومان داده نمی‌شوند.

دستور down به‌صورت زیر اجرا خواهد شد:

php artisan down --secret=MvYgEH651d3X4JRcys --time=24
Plain text

در این‌صورت، کاربر با ارسال درخواست به آدرس /MvYgEH651d3X4JRcys می‌تواند سایت را به مدت ۲۴ ساعت خارج از حالت تعمیر مشاهده کند.

نکات🔗

  • برای پیاده‌سازی این قابلیت، نیازمند جست‌وجو در سورس‌کد Laravel خواهید بود.
  • شما تنها مجاز به ایجاد تغییرات در پوشه‌های app و config هستید.

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

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

قسمت آموزشی🔗

در این قسمت راهنمایی‌های سوال، به مرور اضافه می‌شود. مشکلات‌تان در راستای حل سوال را می‌توانید از بخش "سوال بپرسید" مطرح کنید.

راهنمایی ۱

برای تغییر رفتار دستور down، باید کلاس جدیدی نظیر App\Console\Commands\ExtendedDownCommand تعریف کرد. این کلاس می‌تواند از کلاس Illuminate\Foundation\Console\DownCommand ارث‌بری کند. تنها تفاوت این کلاس با کلاس DownCommand در signature و بدنه‌ی متد getDownFilePayload است.

class ExtendedDownCommand extends DownCommand
{
    protected $signature = 'down {--redirect= : The path that users should be redirected to}
                                 {--render= : The view that should be prerendered for display during maintenance mode}
                                 {--retry= : The number of seconds after which the request may be retried}
                                 {--secret= : The secret phrase that may be used to bypass maintenance mode}
                                 {--time=12 : Bypass cookie expiration time (hours)}
                                 {--status=503 : The status code that should be used when returning the maintenance mode response}';

    protected function getDownFilePayload()
    {
        return [
            'redirect' => $this->redirectPath(),
            'retry' => $this->getRetryTime(),
            'secret' => $this->option('secret'),
            'time' => (int) $this->option('time'),
            'status' => (int) $this->option('status', 503),
            'template' => $this->option('render') ? $this->prerenderView() : null,
        ];
    }
}
PHP

برای پیاده‌سازی متدی جهت ساخت کوکی bypass، می‌توان کلاسی مشابه کلاس Illuminate\Foundation\Http\MaintenanceModeBypassCookie تعریف کرد، با این تفاوت که متد create آن یک آرگومان جدید با نام time نیز می‌پذیرد:

<?php

namespace App\Http;

use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Cookie;

class MaintenanceModeBypassCookie
{
    public static function create(string $key, int $time = 12)
    {
        if ($time < 1) {
            $time = 1;
        }
        $expiresAt = Carbon::now()->addHours($time);

        return new Cookie('laravel_maintenance', base64_encode(json_encode([
            'expires_at' => $expiresAt->getTimestamp(),
            'mac' => hash_hmac('SHA256', $expiresAt->getTimestamp(), $key),
        ])), $expiresAt);
    }

    public static function isValid(string $cookie, string $key)
    {
        $payload = json_decode(base64_decode($cookie), true);

        return is_array($payload) &&
            is_numeric($payload['expires_at'] ?? null) &&
            isset($payload['mac']) &&
            hash_equals(hash_hmac('SHA256', $payload['expires_at'], $key), $payload['mac']) &&
            (int) $payload['expires_at'] >= Carbon::now()->getTimestamp();
    }
}
PHP
راهنمایی ۲

برای تغییر رفتار secret bypass route باید یک middleware مشابه Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance تعریف کرد و بخش ساخت کوکی آن را تغییر داد:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Foundation\Application;
use \App\Http\MaintenanceModeBypassCookie;
use Symfony\Component\HttpKernel\Exception\HttpException;

class PreventRequestsDuringMaintenance
{
    protected $app;

    protected $except = [];

    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    public function handle($request, Closure $next)
    {
        if ($this->app->isDownForMaintenance()) {
            $data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);

            if (isset($data['secret']) && isset($data['time']) && $request->path() === $data['secret']) {
                return $this->bypassResponse($data['secret'], $data['time']);
            }

            if ($this->hasValidBypassCookie($request, $data) ||
                $this->inExceptArray($request)) {
                return $next($request);
            }

            if (isset($data['redirect'])) {
                $path = $data['redirect'] === '/'
                            ? $data['redirect']
                            : trim($data['redirect'], '/');

                if ($request->path() !== $path) {
                    return redirect($path);
                }
            }

            if (isset($data['template'])) {
                return response(
                    $data['template'],
                    $data['status'] ?? 503,
                    isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
                );
            }

            throw new HttpException(
                $data['status'] ?? 503,
                'Service Unavailable',
                null,
                isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
            );
        }

        return $next($request);
    }

    protected function hasValidBypassCookie($request, array $data)
    {
        return isset($data['secret']) &&
                $request->cookie('laravel_maintenance') &&
                MaintenanceModeBypassCookie::isValid(
                    $request->cookie('laravel_maintenance'),
                    $data['secret']
                );
    }

    protected function inExceptArray($request)
    {
        foreach ($this->except as $except) {
            if ($except !== '/') {
                $except = trim($except, '/');
            }

            if ($request->fullUrlIs($except) || $request->is($except)) {
                return true;
            }
        }

        return false;
    }

    protected function bypassResponse(string $secret, int $time)
    {
        return redirect('/')->withCookie(
            MaintenanceModeBypassCookie::create($secret, $time)
        );
    }
}
PHP

در نهایت، باید یک service provider برای override کردن دستور پیش‌فرض down ایجاد کرد. بدنه‌ی متد register این کلاس به‌صورت زیر خواهد بود:

public function register()
{
    $this->commands([
        ExtendedDownCommand::class
    ]);
}
PHP

با افزودن این service provider به فایل app/config.php، رفتار دستور پیش‌فرض down تغییر می‌کند.