Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді: Artisan

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

8 питань

Контролер - це клас, що групує логіку обробки запитів. Створюють його генератором Artisan; файл з'являється в app/Http/Controllers.

php artisan make:controller PostController              # порожній
php artisan make:controller PostController --resource   # 7 CRUD-методів
php artisan make:controller PostController --model=Post # з type-hint моделі
php artisan make:controller Api/PostController --api    # без create/edit
php artisan make:controller PhotoController --invokable # один метод __invoke

Згенерований resource-контролер містить методи, що відповідають RESTful-конвенції:

class PostController extends Controller
{
    public function index() {}                  // GET  /posts
    public function create() {}                 // GET  /posts/create
    public function store(Request $request) {}  // POST /posts
    public function show(Post $post) {}         // GET  /posts/{post}
    public function edit(Post $post) {}         // GET  /posts/{post}/edit
    public function update(Request $request, Post $post) {} // PUT/PATCH
    public function destroy(Post $post) {}      // DELETE
}

Прапорець --resource поєднується з Route::resource('posts', PostController::class), яка реєструє всі ці маршрути одним рядком.

Докладніше в документації: Контролери

Artisan - CLI Laravel. Він прискорює рутину: генерацію класів, міграції, очищення кешу, запуск черг тощо.

php artisan list # усі команди
php artisan make:model Post -mfsc # модель + міграція, фабрика, сідер, контролер
php artisan migrate
php artisan queue:work
php artisan tinker # інтерактивна REPL-консоль

Під капотом Artisan побудований на Symfony Console. Можна писати власні команди через make:command.

Докладніше в документації: Artisan Console

Згенерувати клас і описати логіку в handle():

php artisan make:command SendReports
class SendReports extends Command
{
    protected $signature = 'reports:send {--month=}';
    protected $description = 'Розіслати місячні звіти';

    public function handle(): int
    {
        $this->info('Відправка...');
        return self::SUCCESS;
    }
}

Аргументи й опції описуються в $signature. Команду можна запускати вручну або ставити в розклад через Schedule::command('reports:send')->monthly().

Докладніше в документації: Власні команди Artisan

Через властивість $signature: аргументи - у фігурних дужках, опції - з --.

protected $signature = 'reports:send
                        {user : ID користувача}
                        {period=month : day, week або month}
                        {--queue : Поставити надсилання в чергу}
                        {--format=pdf : Формат файлу}
                        {--tag=* : Можна передати кілька разів}';
php artisan reports:send 7 week --queue --format=csv --tag=sales --tag=q3

Що означають позначки:

  • {user} - обов'язковий аргумент, {user?} - необов'язковий, {period=month} - зі значенням за замовчуванням;
  • {user*} - масив аргументів;
  • {--queue} - перемикач (true, якщо передали), {--format=} чи {--format=pdf} - опція зі значенням;
  • {--tag=*} - масив значень опції;
  • текст після : - опис для php artisan help reports:send.

Читання в handle():

$userId = $this->argument('user');
$asCsv  = $this->option('format') === 'csv';

Залежності беруть параметрами handle() - контейнер їх впровадить. Для обов'язкових аргументів, які зручно запитати інтерактивно, є інтерфейс PromptsForMissingInput.

Докладніше в документації: Опис очікуваного вводу

$exitCode = Artisan::call('reports:send', ['user' => 7, '--queue' => true]);
$output   = Artisan::output();

Artisan::queue('reports:send', ['user' => 7])->onQueue('reports');   // у черзі

Artisan::call() виконує команду синхронно в поточному процесі й повертає код виходу. Artisan::queue() ставить виконання в чергу.

З іншої команди - $this->call('cache:clear') або $this->callSilently() без виводу.

Коли це доречно:

  • адмін-кнопка «перебудувати індекс», що запускає наявну команду;
  • команда-оркестратор, яка по черзі викликає інші;
  • тести консольних команд.

Коли краще інакше: якщо логіку з команди потрібно викликати з контролера, її виносять у клас (дію чи сервіс), який викликають і команда, і контролер. Команда - це інтерфейс для CLI з парсингом аргументів і виводом у консоль; тягнути цей шар у веб-запит означає зайве перетворення параметрів у рядки й назад.

Обережно з Artisan::call() у запиті: важка команда заблокує запит на весь час виконання. Для довгого - черга.

Докладніше в документації: Програмне виконання команд

Фасад Process - обгортка над компонентом Symfony Process для запуску зовнішніх програм: git, ffmpeg, pg_dump, скриптів. Він замінює exec() і shell_exec() зручним API з тайм-аутами й тестуванням.

Запуск і результат:

use Illuminate\Support\Facades\Process;

$result = Process::run(['git', 'log', '--oneline', '-5']);

$result->successful();   // код завершення 0
$result->output();       // stdout
$result->errorOutput();  // stderr
$result->exitCode();

Масив аргументів замість рядка - головне правило безпеки. З масивом кожен аргумент передається окремо, без участі shell, і ін'єкція команд неможлива:

Process::run(['convert', $uploadedPath, '-resize', '800x', $outputPath]);   // безпечно
Process::run("convert {$uploadedPath} -resize 800x {$outputPath}");         // ін'єкція через назву файлу

Параметри:

Process::path(storage_path('exports'))
    ->timeout(120)              // за замовчуванням 60 секунд
    ->idleTimeout(30)           // без виводу довше - зупинити
    ->env(['PGPASSWORD' => $password])
    ->input($csv)
    ->run(['psql', '-c', '\copy items from stdin csv']);

Process::run($command)->throw();   // виняток при ненульовому коді

Асинхронно й паралельно:

$process = Process::start(['ffmpeg', '-i', $in, $out]);
// ... інша робота ...
$result = $process->wait();

[$first, $second] = Process::concurrently(function (Pool $pool) {
    $pool->path(base_path())->command(['npm', 'run', 'lint']);
    $pool->path(base_path())->command(['vendor/bin/pint', '--test']);
});

Тестування - без справжнього запуску:

Process::fake([
    '*ffmpeg*' => Process::result(output: 'done'),
    '*git*' => Process::result(errorOutput: 'fatal', exitCode: 128),
]);

// ... виклик коду ...

Process::assertRan(fn (PendingProcess $process) => $process->command[0] === 'ffmpeg');

Шаблони порівнюються з повним командним рядком, де аргументи масиву взяті в лапки, тому зручні шаблони з * з обох боків. Process::preventStrayProcesses() змушує тест падати, якщо запущено процес, для якого немає підробки, - тести ніколи не запустять справжній rm чи git push.

Де запускати: довгі процеси (конвертація відео, бекапи) - у черзі, а не у вебзапиті; тайм-аут процесу має бути меншим за тайм-аут задачі.

Докладніше в документації: Процеси

Довгу команду переривають деплой, зупинка контейнера, OOM. Вона має завершуватися чисто й продовжувати, а не починати все з нуля чи дублювати роботу.

1. Реагувати на сигнал:

public function handle(): int
{
    $this->trap([SIGTERM, SIGINT], fn () => $this->shouldStop = true);

    Order::query()->where('synced', false)->lazyById(500)->each(function (Order $order) {
        if ($this->shouldStop) {
            return false;            // вийти після поточного запису
        }
        $this->sync($order);
        $order->update(['synced' => true]);
    });

    return self::SUCCESS;
}

2. Пам'ятати прогрес у даних, а не в пам'яті: позначка synced, останній оброблений id у таблиці чи кеші. Перезапуск продовжить з місця зупинки.

3. Ідемпотентність кроку: повторна обробка того самого запису не має шкодити - перевірка стану чи унікальний ключ.

4. Один екземпляр: Isolatable і --isolated або withoutOverlapping() у планувальнику.

5. Обходити дані порціями (lazyById, chunkById) - щоб пам'ять не росла, а зміни не зсували сторінки.

Альтернатива для дуже великих обсягів - команда лише розбиває роботу на завдання черги (батчі), а повтори й паралельність бере на себе черга.

Докладніше в документації: Обробка сигналів

Команду запускають cron, CI, скрипти деплою - не люди. Їй треба повідомляти результат так, щоб це зрозуміла машина.

Коди виходу: 0 - успіх, будь-що інше - збій. Скрипт деплою й CI зупиняються саме на ненульовому коді.

if ($failed > 0) {
    $this->error("{$failed} записів не оброблено");
    return self::FAILURE;
}
return self::SUCCESS;

// або з будь-якого місця
$this->fail('Немає підключення до API');

Команда, що ловить винятки й завжди повертає 0, приховує збої від усього навколо.

Інше, що варто мати:

  • неінтерактивність: без confirm() і ask() у шляху виконання за розкладом; підтвердження руйнівних дій - через --force і перевірку isProduction();
  • журнал, а не лише консоль: $this->info() бачить лише той, хто дивиться в термінал; підсумок і помилки - ще й у Log;
  • --dry-run для команд, що змінюють дані, - показати, що буде змінено;
  • прогрес і підсумок (оброблено, пропущено, помилки) - щоб було видно, що команда не зависла;
  • тести: $this->artisan('reports:send', ['user' => 7])->assertSuccessful()->expectsOutputToContain('Надіслано').

Докладніше в документації: Коди виходу