Anophel-آنوفل فرآیندها و دستورات Artisan در لاراول : بررسی عمیق

فرآیندها و دستورات Artisan در لاراول : بررسی عمیق

انتشار:
3

رابط خط فرمان (CLI) می تواند ابزار قدرتمندی برای توسعه دهندگان باشد. می توانید از آن به عنوان بخشی از گردش کار توسعه خود برای افزودن ویژگی های جدید به برنامه خود و انجام وظایف در محیط تولید استفاده کنید. لاراول به شما این امکان را می دهد که "دستورهای Artisan" را ایجاد کنید تا عملکرد سفارشی را به برنامه خود اضافه کنید. همچنین یک فساد فرآیند ارائه می دهد که می توانید از آن برای اجرای فرآیندهای سیستم عامل از برنامه لاراول خود برای انجام کارهایی مانند اجرای اسکریپت های shell سفارشی استفاده کنید.


در این مقاله، دستورات Artisan را بررسی خواهیم کرد و نکات و ترفندهایی را برای ایجاد و تست آنها ارائه خواهیم کرد. همچنین نحوه اجرای فرآیندهای سیستم عامل (OS) را از برنامه لاراول خود و تست صحیح فراخوانی آنها بررسی خواهیم کرد.


دستورات Artisan چیست؟

دستورات Artisan را می توان از CLI برای انجام بسیاری از وظایف در یک برنامه لاراول اجرا کرد. آنها می توانند فرآیند توسعه ساده تری را امکان پذیر کنند و برای انجام وظایف در یک محیط تولید مورد استفاده قرار گیرند.


به‌عنوان یک توسعه‌دهنده لاراول، احتمالاً قبلاً از برخی دستورات Artisan داخلی مانند php artisan make:model، php artisan migrate و php artisan down استفاده کرده‌اید.


به عنوان مثال، برخی از دستورات Artisan را می توان به عنوان بخشی از فرآیند توسعه استفاده کرد، مانند php artisan make:model و php artisan make:controller. به طور معمول، این ها در یک محیط تولید اجرا نمی شوند و صرفا برای سرعت بخشیدن به فرآیند توسعه با ایجاد فایل های دیگ بخار استفاده می شوند.


برخی از دستورات Artisan را می توان برای انجام وظایف در یک محیط تولید استفاده کرد، مانند php artisan migrate و php artisan down. اینها برای انجام کارهایی مانند اجرای انتقال پایگاه داده و آفلاین کردن برنامه شما در حین انجام تعمیرات یا به‌روزرسانی استفاده می‌شوند.
بنابراین، دستورات Artisan را می توان برای انجام کارهای مختلف مورد استفاده قرار داد و در محیط های توسعه و تولید قابل استفاده است.

ایجاد دستورات Artisan خود

اکنون که درک بهتری از دستورات Artisan داریم، بیایید ببینیم چگونه می‌توانیم دستورات خود را ایجاد کنیم.


دریافت ورودی از کاربران

برای ارائه چند نمونه از کارهایی که می‌توانیم با دستورات Artisan انجام دهیم، اجازه دهید نگاهی به یک مورد معمول برای آنها بیندازیم که ممکن است در پروژه‌های خود مشاهده کنید: ایجاد یک super admin جدید در پایگاه داده. در این مثال، برای ایجاد یک super admin جدید به اطلاعات زیر نیاز داریم:


+ نام
+ آدرس ایمیل
+ کلمه عبور


بیایید دستوری برای این کار ایجاد کنیم. دستور CreateSuperAdmin را فراخوانی می کنیم و با اجرای دستور زیر آن را ایجاد می کنیم:

php artisan make:command CreateSuperAdmin

این دستور یک فایل app/Console/Commands/CreateSuperAdmin.php جدید ایجاد می کند. ما فرض می کنیم که به یک متد createSuperAdmin در کلاس UserService دسترسی داریم. برای اهداف این مقاله، نیازی نیست که بدانیم چه کاری انجام می‌دهد یا چگونه کار می‌کند، زیرا تمرکز ما بر نحوه عملکرد دستورات است.


کلاس Command ایجاد شده توسط دستور make:command چیزی شبیه به این خواهد بود:

namespace App\Console\Commands;
 
use Illuminate\Console\Command;
 
class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';
 
    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        //
    }
}

حالا می خواهیم آرگومان های خود را به دستور اضافه کنیم تا بتوانیم نام، ایمیل و رمز عبور هر کاربر جدید را بپذیریم. ما می توانیم این کار را با به روز رسانی ویژگی امضای کلاس فرمان انجام دهیم. ویژگی signature برای تعریف نام دستور، آرگومان ها و گزینه هایی که دستور می پذیرد استفاده می شود.


ویژگی signature$ باید چیزی شبیه به این باشد:

protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=}';

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

protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';

همچنین مهم است که ویژگی توضیحات فرمان را نیز به روز کنید تا آنچه را که انجام می دهد توصیف کنید. زمانی که کاربر لیست php artisan یا دستورات راهنمای php artisan را اجرا می کند، نمایش داده می شود.


اکنون که گزینه های خود را برای پذیرش ورودی پیکربندی کرده ایم، می توانیم این گزینه ها را به متد createSuperAdmin خود منتقل کنیم. بیایید نگاهی بیندازیم که کلاس Command ما اکنون چگونه خواهد بود:

namespace App\Console\Commands;
 
use App\Services\UserService;
use Illuminate\Console\Command;
 
class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';
 
    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $userService->createSuperAdmin(
            email: $this->option('email'),
            password: $this->option('password'),
            name: $this->option('name'),
            sendEmail: $this->option('send-email'),
        );
 
        $this->components->info('Super admin created successfully!');
 
        return self::SUCCESS;
    }
}

دستور ما اکنون باید آماده اجرا باشد. می توانیم با استفاده از دستور زیر آن را اجرا کنیم:

php artisan app:create-super-admin --email="hello@example.com" --name="John Doe" --password="password" --send-email

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


اگر بخواهیم دستور خود را برای استفاده از سؤالات به روز کنیم، دستور ما اکنون ممکن است چیزی شبیه به این باشد:

namespace App\Console\Commands;
 
use App\Services\UserService;
use Illuminate\Console\Command;
 
class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';
 
    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $userService->createSuperAdmin(
            email: $this->getInput('email'),
            password: $this->getInput('password'),
            name: $this->getInput('name'),
            sendEmail: $this->getInput('send-email'),
        );
 
        $this->components->info('Super admin created successfully!');
 
        return self::SUCCESS;
    }
 
    public function getInput(string $inputKey): string
    {
        return match($inputKey) {
            'email' => $this->option('email') ?? $this->ask('Email'),
            'password' => $this->option('password') ?? $this->secret('Password'),
            'name' => $this->option('name') ?? $this->ask('Name'),
            'send-email' => $this->option('send-email') === true
                ? $this->option('send-email')
                : $this->confirm('Send email?'),
            default => null,
        };
    }
}

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


اجرای دستور تنها با آرگومان ایمیل به خروجی زیر منجر می شود:

❯ php artisan app:create-super-admin --email="hello@example.com"
Password:
>
Name:
> Anophel com
Send email? (yes/no) [no]:
> yes

پیش بینی ورودی

اگر در حال ساختن یک command برای کاربر و ارائه چندین گزینه برای انتخاب از یک مجموعه داده شناخته شده (مثلاً ردیف ها در پایگاه داده)، ممکن است گاهی بخواهید ورودی کاربر را پیش بینی کنید. با انجام این کار، می تواند گزینه های تکمیل خودکار را به کاربر پیشنهاد دهد تا از بین آنها انتخاب کند.


به عنوان مثال، بیایید تصور کنیم که شما دستوری دارید که می توان از آن برای ایجاد یک کاربر جدید استفاده کرد و به شما اجازه می دهد نقش کاربر را مشخص کنید. ممکن است بخواهید نقش های احتمالی را که کاربر ممکن است انتخاب کند، پیش بینی کنید. با استفاده از متد anticipate می توانید این کار را انجام دهید:

$roles = [
    'super-admin',
    'admin',
    'manager',
    'user',
];
 
$role = $this->anticipate('What is the role of the new user?', $roles);

هنگامی که از کاربر خواسته می شود نقش را انتخاب کند، دستور سعی می کند فیلد را به صورت خودکار تکمیل کند. به عنوان مثال، اگر کاربر شروع به تایپ sup کند، دستور er-admin را به عنوان تکمیل خودکار پیشنهاد می کند.


اجرای دستور به خروجی زیر منجر می شود:

What is the role of the new user?:
> super-admin

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


آرگومان های متعدد و ورودی های انتخاب

ممکن است مواقعی پیش بیاید که در حال ساخت یک دستور Artisan هستید و بخواهید کاربران را قادر کنید تا چندین گزینه را از یک لیست وارد کنند. اگر از Laravel Sail استفاده کرده‌اید، زمانی که دستور php artisan sail:install را اجرا می‌کنید و از شما خواسته می‌شود سرویس‌های مختلفی را که می‌خواهید نصب کنید، انتخاب کنید، از این ویژگی استفاده کرده‌اید.


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

protected $signature = 'app:install-services {services?*}';

اکنون می‌توانیم این دستور را برای نصب سرویس‌های mysql و redis صدا کنیم:

php artisan app:install-services mysql redis

در دستور Artisan، $this->argument('services') یک آرایه حاوی دو آیتم را برمی گرداند: mysql و redis.


همچنین می‌توانیم این قابلیت را به دستور خود اضافه کنیم تا در صورتی که کاربر هیچ آرگومانی را ارائه نکرد، با استفاده از متد choice، گزینه‌ها را به کاربر نمایش دهد:

$installableServices = [
    'mysql',
    'redis',
    'mailpit',
];
 
$services = $this->choice(
    question: 'Which services do you want to install?',
    choices: $installableServices,
    multiple: true,
);

با اجرای این دستور بدون هیچ آرگومان، خروجی زیر نمایش داده می شود:

Which services do you want to install?:
 [0] mysql
 [1] redis
 [2] mailpit
> mysql,redis

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


اعتبار سنجی ورودی

مشابه روشی که ورودی یک درخواست HTTP را تأیید می کنید، ممکن است بخواهید ورودی دستورات Artisan خود را نیز تأیید کنید. با انجام این کار می توانید اطمینان حاصل کنید که ورودی صحیح است و می تواند به قسمت های دیگر کد برنامه شما منتقل شود.


بیایید نگاهی به یک راه ممکن برای اعتبار سنجی ورودی بیندازیم. ما با بررسی کد شروع می کنیم و سپس آن را تجزیه می کنم.


برای تایید ورودی خود، می توانید کاری شبیه به این انجام دهید:

namespace App\Console\Commands;
 
use App\Services\UserService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\MessageBag;
use InvalidArgumentException;
 
class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';
 
    private bool $inputIsValid = true;
 
    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $input = $this->validateInput();
 
        if (!$this->inputIsValid) {
            return self::FAILURE;
        }
 
        $userService->createSuperAdmin(
            email: $input['email'],
            password: $input['password'],
            name: $input['name'],
            sendEmail: $input['send-email'],
        );
 
        $this->components->info('Super admin created successfully!');
 
        return self::SUCCESS;
    }
 
    /**
     * Validate and return all the input from the command. If any of the input
     * was invalid, an InvalidArgumentException will be thrown. We catch this
     * and report it so it's still logged or sent to a bug-tracking system.
     * But we don't display it to the console. Only the validation error
     * messages will be displayed in the console.
     *
     * @return array
     */
    private function validateInput(): array
    {
        $input = [];
 
        try {
            foreach (array_keys($this->rules()) as $inputKey) {
                $input[$inputKey] = $this->validated($inputKey);
            }
        } catch (InvalidArgumentException $e) {
            $this->inputIsValid = false;
 
            report($e);
        }
 
        return $input;
    }
 
    /**
     * Validate the input and then return it. If the input is invalid, we will
     * display the validation messages and then throw an exception.
     *
     * @param string $inputKey
     * @return string
     */
    private function validated(string $inputKey): string
    {
        $input = $this->getInput($inputKey);
 
        $validator = Validator::make(
            data: [$inputKey => $input],
            rules: [$inputKey => $this->rules()[$inputKey]]
        );
 
        if ($validator->passes()) {
            return $input;
        }
 
        $this->handleInvalidData($validator->errors());
    }
 
    /**
     * Loop through each of the error messages and output them to the console.
     * Then throw an exception so we can prevent the rest of the command
     * from running. We will catch this in the "validateInput" method.
     *
     * @param MessageBag $errors
     * @return void
     */
    private function handleInvalidData(MessageBag $errors): void
    {
        foreach ($errors->all() as $error) {
            $this->components->error($error);
        }
 
        throw new InvalidArgumentException();
    }
 
    /**
     * Define the rules used to validate the input.
     *
     * @return array<string,string>
     */
    private function rules(): array
    {
        return [
            'email' => 'required|email',
            'password' => 'required|min:8',
            'name' => 'required',
            'send-email' => 'boolean',
        ];
    }
 
    /**
     * Attempt to get the input from the command options. If the input wasn't passed
     * to the command, ask the user for the input.
     *
     * @param string $inputKey
     * @return string|null
     */
    private function getInput(string $inputKey): ?string
    {
        return match($inputKey) {
            'email' => $this->option('email') ?? $this->ask('Email'),
            'password' => $this->option('password') ?? $this->secret('Password'),
            'name' => $this->option('name') ?? $this->ask('Name'),
            'send-email' => $this->option('send-email') === true
                ? $this->option('send-email')
                : $this->confirm('Send email?'),
            default => null,
        };
    }
}

بیایید کد بالا را تجزیه کنیم.

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


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


با اجرای کد بالا و ارائه یک آدرس ایمیل نامعتبر، خروجی زیر حاصل می شود:

❯ php artisan app:create-super-admin
Email:
> INVALID
ERROR  The email field must be a valid email address.

پنهان کردن دستورات از لیست

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


برای مخفی کردن یک دستور از لیست، می‌توانید از ویژگی setHidden استفاده کنید تا مقدار خاصیت مخفی کامند را روی true تنظیم کنید. به عنوان مثال، اگر در حال ساخت یک کامند نصب به عنوان بخشی از بسته ای هستید که برخی از دارایی ها را منتشر می کند، ممکن است بخواهید بررسی کنید که آیا آن دارایی ها از قبل در سیستم فایل وجود دارند یا خیر. اگر این کار را انجام دهند، احتمالاً می توانید فرض کنید که دستور قبلاً یک بار اجرا شده است و نیازی به نمایش در خروجی دستور لیست نیست.


بیایید نگاهی بیندازیم که چگونه می توانیم این کار را انجام دهیم. ما تصور می کنیم که بسته ما یک فایل پیکربندی my-new-package.php را در زمان نصب منتشر می کند. اگر این فایل وجود داشته باشد، دستور را از لیست مخفی می کنیم. کد دستور ما ممکن است چیزی شبیه به این باشد:

namespace App\Console\Commands;
 
use Illuminate\Console\Command;
 
class PackageInstall extends Command
{
    protected $signature = 'app:package-install';
 
    protected $description = 'Install the package and publish the assets';
 
    public function __construct()
    {
        parent::__construct();
 
        if (file_exists(config_path('my-new-package.php'))) {
            $this->setHidden();
        }
    }
 
    public function handle()
    {
        // Run the command here as usual...
    }
}

شایان ذکر است که پنهان کردن یک فرمان مانع از اجرای دستور نمی شود. اگر کسی نام دستور را بداند، همچنان می تواند آن را اجرا کند.


دستور کمک

هنگام ساخت دستورات خود، استفاده از نام های واضح برای آرگومان ها و گزینه های خود بسیار مهم است. این کار باعث می شود تا سایر توسعه دهندگان درک کنند که دستور چه کاری انجام می دهد و چگونه از آن استفاده کنند.


با این حال، ممکن است مواقعی پیش بیاید که بخواهید اطلاعات کمکی اضافی ارائه دهید تا با اجرای دستور php artisan help، آنها را در کنسول نمایش دهید.


به عنوان مثال، اگر بخواهیم نمونه CreateSuperAdmin خود را از قبل در این راهنما بگیریم، می توانیم امضا را به موارد زیر به روز کنیم:

protected $signature = 'app:create-super-admin
                        {--email= : The email address of the new user}
                        {--password= : The password of the new user}
                        {--name= : The name of the new user}
                        {--send-email : Send a welcome email after creating the user}';

حالا اگر بخواهیم php artisan help app:create-super-admin را اجرا کنیم، خروجی زیر را خواهیم دید:

❯ php artisan help app:create-super-admin
Description:
  Store a new super admin in the database
Usage:
  app:create-super-admin [options]
Options:
      --email[=EMAIL]        The email address of the new user
      --password[=PASSWORD]  The password of the new user
      --name[=NAME]          The name of the new user
      --send-email           Send a welcome email after creating the user
  -h, --help                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                Do not output any message
  -V, --version              Display this application version
      --ansi|--no-ansi       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction       Do not ask any interactive question
      --env[=ENV]            The environment the command should run under
  -v|vv|vvv, --verbose       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

کارها را با استفاده از schedule خودکار کنید

دستورات Artisan همچنین راهی برای خودکارسازی وظایف در برنامه شما فراهم می کند.


به عنوان مثال، فرض کنید در روز اول هر ماه، می خواهید یک گزارش پی دی اف برای فعالیت کاربران خود در ماه قبل بسازید و آن را برای آنها ایمیل کنید. برای خودکار کردن این فرآیند، می‌توانید یک دستور Artisan سفارشی ایجاد کنید و آن را به schedule برنامه خود اضافه کنید. با فرض اینکه شما یک schedule روی سرور خود تنظیم کرده اید، این دستور به طور خودکار در روز اول هر ماه اجرا می شود و گزارش را برای کاربران شما ارسال می کند.


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

php /path/to/artisan schedule:run >> /dev/null 2>&1

در اصل فقط دستور php artisan schedule:run را برای پروژه شما فراخوانی می کند، که به لاراول اجازه می دهد دستورات برنامه ریزی شده برای اجرا آماده باشند یا خیر. بخش dev/null/ دستور خروجی دستور را به dev/null/ هدایت می‌کند که آن را کنار می‌گذارد، بنابراین نمایش داده نمی‌شود. قسمت 1&<2 دستور، خروجی خطای دستور را به خروجی استاندارد هدایت می کند. این مورد استفاده قرار می گیرد تا خطاهایی که هنگام اجرای دستور رخ می دهد نیز نادیده گرفته شود.


حالا بیایید نگاهی به نحوه اضافه کردن یک دستور به scheduleدر لاراول بیاندازیم.


بیایید تصور کنیم که می خواهیم مثال خود را از بالا پیاده سازی کنیم و در روز اول هر ماه به کاربران خود ایمیل ارسال کنیم. برای انجام این کار، باید یک دستور SendMonthlyReport Artisan ایجاد کنیم:

namespace App\Console\Commands;
 
use Illuminate\Console\Command;
 
class SendMonthlyReport extends Command
{
    protected $signature = 'app:send-monthly-report';
 
    protected $description = 'Send monthly report to each user';
 
    public function handle(): void
    {
        // Send the monthly reports here...
    }
}

پس از ایجاد دستور شما، می‌توانیم آن را به زمان‌بندی برنامه شما اضافه کنیم. برای انجام این کار، باید آن را در متد زمانبندی فایل app/Console/Kernel.php ثبت کنیم. کلاس کرنل شما ممکن است چیزی شبیه به این باشد:

namespace App\Console;
 
use App\Console\Commands\SendMonthlyReport;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 
class Kernel extends ConsoleKernel
{
    /**
     * Define the application's command schedule.
     */
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('app:send-monthly-report')->monthly();
    }
 
    // ...
 
}

اگر schedule شما به درستی بر روی سرور شما تنظیم شده باشد، این دستور اکنون باید به طور خودکار در ابتدای هر ماه اجرا شود. در لاراول 11 این مورد تغییر کرده و برای آشنایی بیشتر این مقاله را بررسی کنید.


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

تست دستورات Artisan

مانند هر کد دیگری که می نویسید، مهم است که دستورات Artisan خود را تست کنید. این امر مخصوصاً در صورتی صادق است که بخواهید از آنها در یک محیط تولید استفاده کنید یا وظایف را از طریق schedule خودکار انجام دهید. برای آشنایی با نحوه تست نویسی در لاراول با Pest که در لاراول 11 به صورت پیش فرض است این مقاله را بررسی کنید.


اعلام خروجی از دستورات

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


بیایید تصور کنیم که دستور مثال زیر را داریم که می خواهیم آزمایش کنیم:

namespace App\Console\Commands;
 
use App\Services\UserService;
use Illuminate\Console\Command;
 
class CreateUser extends Command
{
    protected $signature = 'app:create-user {email} {--role=}';
 
    protected $description = 'Create a new user';
 
    public function handle(UserService $userService): int
    {
        $userService->createUser(
            email: $this->argument('email'),
            role: $this->option('role'),
        );
 
        $this->info('User created successfully!');
 
        return self::SUCCESS;
    }
}

اگر می‌خواهیم آزمایش کنیم که با موفقیت اجرا شده و متن مورد انتظار را خروجی می‌دهد، می‌توانیم کارهای زیر را انجام دهیم:

namespace Tests\Feature\Console\Commands;
 
use Tests\TestCase;
 
class CreateUserTest extends TestCase
{
    /** @test */
    public function user_can_be_created(): void
    {
        $this->artisan('app:create-user', [
            'email' => 'hello@example.com',
            '--role' => 'super-admin'
        ])
            ->expectsOutput('User created successfully!')
            ->assertSuccessful();
 
        // Run extra assertions to check the user was created with the correct role...
    }

سوالات مطرح شده مطرح می شود

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


به عنوان مثال، بیایید تصور کنیم که می خواهیم دستور زیر را آزمایش کنیم:

protected $signature = 'app:create-user {email}';
 
protected $description = 'Create a new user';
 
public function handle(UserService $userService): int
{
    $roles = [
        'super-admin',
        'admin',
        'manager',
        'user',
    ];
 
    $role = $this->choice('What is the role of the new user?', $roles);
 
    if (!in_array($role, $roles, true)) {
        $this->error('The role is invalid!');
 
        return self::FAILURE;
    }
 
    $userService->createUser(
        email: $this->argument('email'),
        role: $role,
    );
 
    $this->info('User created successfully!');
 
    return self::SUCCESS;
}

برای تست این دستور می توانیم موارد زیر را انجام دهیم:

/** @test */
public function user_can_be_created_with_choice_for_role(): void
{
    $this->artisan('app:create-user', [
        'email' => 'hello@example.com',
    ])
        ->expectsQuestion('What is the role of the new user?', 'super-admin')
        ->expectsOutput('User created successfully!')
        ->assertSuccessful();
 
    // Run extra assertions to check the user was created with the correct role...
}
 
/** @test */
public function error_is_returned_if_the_role_is_invalid(): void
{
    $this->artisan('app:create-user', [
        'email' => 'hello@example.com',
    ])
        ->expectsQuestion('What is the role of the new user?', 'INVALID')
        ->expectsOutput('The role is invalid!')
        ->assertFailed();
 
    // Run extra assertions to check the user wasn't created...
}

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


اجرای فرآیندهای سیستم عامل در لاراول

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


این می تواند مفید باشد، برای مثال، هنگام اجرای اسکریپت های پوسته سفارشی، اسکن آنتی ویروس، یا مبدل فایل.


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


فرآیندهای در حال اجرا

برای اجرای دستور بر روی سیستم عامل می توانید از متد run در فساد Process استفاده کنید. بیایید تصور کنیم که یک shell اسکریپت به نام install.sh در دایرکتوری اصلی پروژه خود داریم. ما می توانیم این اسکریپت را از کد خود اجرا کنیم:

use Illuminate\Support\Facades\Process;
 
$process = Process::path(base_path())->run('./install.sh');
$output = $process->output();

برای ارائه زمینه اضافی، ممکن است بخواهیم این اسکریپت را از متد handle یک کامند Artisan اجرا کنیم که چندین کار را انجام می دهد (به عنوان مثال، خواندن از پایگاه داده یا برقراری تماس های API). می‌توانیم اسکریپت shell را از دستور خود فراخوانی کنیم:

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
 
class RunInstallShellScript extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:run-install-shell-script';
 
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Install the package and publish the assets';
 
    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->info('Starting installation...');
 
        $process = Process::path(base_path())->run('./install.sh');
 
        $this->info($process->output());
 
        // Make calls to API here...
 
        // Publish assets here...
 
        // Add new rows to the database here...
 
        $this->info('Installation complete!');
    }
}

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


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


اجرای فرآیند از دایرکتوری دیگر

به طور پیش فرض، فساد Process دستور را در همان دایرکتوری کاری که فایل PHP در حال اجرا است اجرا می کند. به طور معمول، این بدان معنی است که اگر فرآیند را با استفاده از دستور Artisan اجرا می کنید، این فرآیند در دایرکتوری ریشه پروژه شما اجرا می شود زیرا فایل artisan در آنجا قرار دارد. با این حال، اگر فرآیند از یک کنترلر HTTP اجرا شود، این فرآیند در فهرست عمومی پروژه شما اجرا می‌شود، زیرا این نقطه ورود به سرور وب است.


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


به عنوان مثال، بیایید تصور کنیم که یک اسکریپت shell به نام custom-script.sh در پوشه scripts/custom در پروژه خود داریم. برای اجرای این اسکریپت می‌توانیم کارهای زیر را انجام دهیم:

use Illuminate\Support\Facades\Process;
 
$process = Process::path(base_path('/scripts/custom'))->run('./install.sh');
$output = $process->output();

تعیین زمان پایان فرآیند

به‌طور پیش‌فرض، فساد Process به فرآیندها اجازه می‌دهد تا حداکثر 60 ثانیه قبل از اتمام زمان اجرا شوند. این برای جلوگیری از اجرای نامحدود فرآیندها است (به عنوان مثال، اگر در یک حلقه نامحدود گیر کنند).


با این حال، اگر می‌خواهید فرآیندی را اجرا کنید که ممکن است بیشتر از زمان پیش‌فرض طول بکشد، می‌توانید با استفاده از متد timeout، یک بازه زمانی سفارشی را در چند ثانیه تعیین کنید. برای مثال، بیایید تصور کنیم که می‌خواهیم فرآیندی را اجرا کنیم که ممکن است تا 5 دقیقه (300 ثانیه) طول بکشد:

use Illuminate\Support\Facades\Process;
 
$process = Process::timeout(300)->run('./install.sh');
$output = $process->output();

دریافت خروجی در زمان واقعی

بسته به فرآیندی که در حال اجرا هستید، ممکن است ترجیح دهید متن را در حین اجرا به جای منتظر ماندن برای پایان فرآیند، خروجی بگیرید. اگر یک اسکریپت طولانی مدت (به عنوان مثال، یک اسکریپت نصب) دارید که می خواهید مراحلی را که اسکریپت طی می کند را ببینید، ممکن است بخواهید این کار را انجام دهید.


برای انجام این کار، می‌توانید یک closure را به عنوان دومین پارامتر متد run ارسال کنید تا مشخص کنید که هنگام دریافت خروجی چه کاری باید انجام دهید. به عنوان مثال، بیایید تصور کنیم که یک اسکریپت به نام install.sh داریم و می‌خواهیم خروجی را در زمان واقعی ببینیم نه اینکه منتظر بمانیم تا تمام شود. ما می توانیم اسکریپت را به این صورت اجرا کنیم:

use Illuminate\Support\Facades\Process;
 
$process = Process::run('./install.sh', function (string $type, string $output): void {
    $type === 'out' ? $this->line($output) : $this->error($output);
});

اجرای فرآیندها به صورت ناهمزمان

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


برای این کار می توانید از متد start استفاده کنید. بیایید نگاهی به مثالی بیندازیم که چگونه ممکن است این کار را انجام دهیم. بیایید تصور کنیم که یک اسکریپت به نام install.sh داریم و می‌خواهیم مراحل نصب دیگری را اجرا کنیم در حالی که منتظر هستیم تا اسکریپت اجرا شود:

use Illuminate\Support\Facades\Process;
 
public function handle(): void
{
    $this->info('Starting installation...');
 
    $extraCodeHasBeenRun = false;
 
    $process = Process::start('./install.sh');
 
    while ($process->running()) {
        if (!$extraCodeHasBeenRun) {
            $extraCodeHasBeenRun = true;
 
            $this->runExtraCode();
        }
    }
 
    $result = $process->wait();
 
    $this->info($process->output());
 
    $this->info('Installation complete!');
}
 
private function runExtraCode(): void
{
    // Make calls to API here...
 
    // Publish assets here...
 
    // Add new rows to the database here...
}

همانطور که ممکن است متوجه شده باشید، ما در حلقه while بررسی می کنیم که هنوز اجرای کد اضافی را شروع نکرده ایم. ما این کار را انجام می دهیم زیرا نمی خواهیم کد اضافی را چندین بار اجرا کنیم. ما فقط می خواهیم آن را یک بار در حالی که فرآیند در حال اجرا است اجرا کنیم.


پس از اتمام فرآیند، می‌توانیم نتیجه فرآیند (با استفاده از روش خروجی) را دریافت کرده و آن را به کنسول ارسال کنیم.


اجرای همزمان فرآیندها

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


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


برای این کار می توانید از روش همزمان استفاده کنید. بیایید نگاهی به مثالی از نحوه استفاده از آن بیندازیم.


تصور می کنیم که می خواهیم یک اسکریپت convert.sh را برای سه فایل در یک دایرکتوری اجرا کنیم. برای هدف این مثال، ما این نام فایل ها را کدگذاری می کنیم. با این حال، در یک سناریوی واقعی، احتمالاً این نام فایل ها را به صورت پویا از سیستم فایل، پایگاه داده یا درخواست دریافت خواهید کرد.


اگر اسکریپت ها را به صورت متوالی اجرا کنیم، کد ما ممکن است چیزی شبیه به این باشد:

// 15 seconds to run...
 
$firstOutput = Process::path(base_path())->run('./convert.sh file-1.png');
$secondOutput = Process::path(base_path())->run('./convert.sh file-1.png');
$thirdOutput = Process::path(base_path())->run('./convert.sh file-1.png');

با این حال، اگر اسکریپت ها را همزمان اجرا کنیم، کد ما ممکن است چیزی شبیه به این باشد:

// 5 seconds to run...
 
$commands = Process::concurrently(function (Pool $pool) {
    $pool->path(base_path())->command('./convert.sh file-1.png');
    $pool->path(base_path())->command('./convert.sh file-2.png');
    $pool->path(base_path())->command('./convert.sh file-3.png');
});
 
foreach ($commands->collect() as $command) {
    $this->info($command->output());
}

تست فرآیندهای سیستم عامل

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


برای شروع تست فرآیندهای خود، بیایید تصور کنیم که یک مسیر وب در برنامه خود داریم که یک فایل را می پذیرد و آن را به فرمت دیگری تبدیل می کند. اگر فایل ارسال شده یک تصویر باشد، فایل را با استفاده از اسکریپت convert-image.sh تبدیل می کنیم. اگر فایل ویدیویی است، آن را با استفاده از convert-video.sh تبدیل می کنیم. فرض می کنیم که یک متد isImage در کنترلر خود داریم که اگر فایل تصویری باشد true و اگر یک ویدئو باشد false را برمی گرداند.


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


تصور می‌کنیم که کنترلر ما می‌تواند از طریق یک درخواست POST برای conver/filet/ (با مسیری به نام file.convert) دسترسی داشته باشد و چیزی شبیه به این است:

namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
 
class ConvertFileController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        // Temporarily the file so it can be converted.
        $tempFilePath = 'tmp/'.Str::random();
 
        Storage::put(
            $tempFilePath,
            $request->file('uploaded_file')
        );
 
        // Determine which command to run.
        $command = $this->isImage($request->file('uploaded_file'))
            ? 'convert-image.sh'
            : 'convert-video.sh';
 
        $command .= ' '.$tempFilePath;
 
        // The conversion command.
        $process = Process::timeout(30)
            ->path(base_path())
            ->run($command);
 
        // If the process fails, report the error.
        if ($process->failed()) {
            // Report the error here to the logs or bug tracking system...
 
            return response()->json([
                'message' => 'Something went wrong!',
            ], 500);
        }
 
        return response()->json([
            'message' => 'File converted successfully!',
            'path' => trim($process->output()),
        ]);
    }
 
    // ...
}

برای تست اینکه فرآیندها به درستی ارسال شده اند، ممکن است بخواهیم تست های زیر را بنویسیم:

namespace Tests\Feature\Http\Controllers;
 
use Illuminate\Http\UploadedFile;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;
 
class ConvertFileControllerTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();
 
        Storage::fake();
 
        Process::preventStrayProcesses();
 
        // Determine how the random strings should be built.
        // We do this so we can assert the correct command
        // is run.
        Str::createRandomStringsUsing(static fn (): string => 'random');
    }
 
    /** @test */
    public function image_can_be_converted(): void
    {
        Process::fake([
            'convert-image.sh tmp/random' => Process::result(
                output: 'tmp/converted.webp',
            )
        ]);
 
        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->image('dummy.png'),
        ])
            ->assertOk()
            ->assertExactJson([
                'message' => 'File converted successfully!',
                'path' => 'tmp/converted.webp',
            ]);
 
        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-image.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }
 
    /** @test */
    public function video_can_be_converted(): void
    {
        Process::fake([
            'convert-video.sh tmp/random' => Process::result(
                output: 'tmp/converted.mp4',
            )
        ]);
 
        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->create('dummy.mp4'),
        ])
            ->assertOk()
            ->assertExactJson([
                'message' => 'File converted successfully!',
                'path' => 'tmp/converted.mp4',
            ]);
 
        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-video.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }
 
    /** @test */
    public function error_is_returned_if_the_file_cannot_be_converted(): void
    {
        Process::fake([
            'convert-video.sh tmp/random' => Process::result(
                errorOutput: 'Something went wrong!',
                exitCode: 1 // Error exit code
            )
        ]);
 
        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->create('dummy.mp4'),
        ])
            ->assertStatus(500)
            ->assertExactJson([
                'message' => 'Something went wrong!',
            ]);
 
        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-video.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }
}

تست های فوق موارد زیر را تضمین می کند:


اگر فایلی یک تصویر باشد، فقط اسکریپت convert-image.sh اجرا می شود.
اگر فایلی ویدیویی باشد، فقط اسکریپت convert-video.sh اجرا می شود.
اگر اسکریپت تبدیل ناموفق باشد، یک خطا برگردانده می شود.


ممکن است متوجه شده باشید که ما از متد preventStrayProcesses استفاده کرده ایم. این تضمین می کند که فقط دستوراتی که ما مشخص کرده ایم اجرا می شوند. اگر هر دستور دیگری اجرا شود، آزمایش ناموفق خواهد بود. این برای اطمینان از عدم اجرای تصادفی اسکریپت ها در سیستم شما مفید است.


ما همچنین به صراحت تست کرده‌ایم که دستورات با مسیر و زمان مورد انتظار اجرا می‌شوند. این برای اطمینان از اجرای دستورات با تنظیمات صحیح است.


نتیجه

امیدواریم این مقاله به شما نشان داده باشد که چگونه دستورات Artisan خود را ایجاد و آزمایش کنید. همچنین باید به شما نشان می داد که چگونه فرآیندهای سیستم عامل را از برنامه لاراول خود اجرا کنید و آنها را آزمایش کنید. اکنون باید بتوانید هر دوی این ویژگی ها را در پروژه های خود پیاده سازی کنید تا قابلیت های بیشتری را به برنامه های خود اضافه کنید.

در آنوفل ثبت نام کنید تا از جدید ترین های دنیای برنامه نویسی آگاه شوید. آنوفل را در شبکه های مجازی دنبال کنید!

#laravel#laravel_artisan#artisan#laravel_processes#artisan_command
نظرات ارزشمند شما :

در حال دریافت...

مقاله های مشابه

در حال دریافت...