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

Middle: питання на співбесіді з теми «Розробка пакетів»

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

3 питання

Пакет має працювати одразу після встановлення з розумними значеннями за замовчуванням, але дозволяти змінити їх застосунку.

mergeConfigFrom (у register) - злиття конфігурації пакета з конфігурацією застосунку:

public function register(): void
{
    $this->mergeConfigFrom(__DIR__.'/../config/invoices.php', 'invoices');
}

Тепер config('invoices.currency') працює, навіть якщо застосунок нічого не публікував.

publishes (у boot) - дозволяє скопіювати файл у застосунок для редагування:

public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/invoices.php' => config_path('invoices.php'),
    ], 'invoices-config');
}
php artisan vendor:publish --tag=invoices-config

Як працює злиття - і головна пастка:

// всередині mergeConfigFrom
array_merge(require $packageConfig, config('invoices', []));
  • значення застосунку перемагають значення пакета;
  • злиття неглибоке - лише перший рівень ключів. Якщо пакет має 'pdf' => ['size' => 'A4', 'orientation' => 'portrait'], а застосунок у своєму файлі вказав 'pdf' => ['size' => 'Letter'], ключ orientation зникне повністю;
  • для рекурсивного злиття є replaceConfigRecursivelyFrom(), але воно має свої наслідки для масивів-списків.

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

config:cache: коли конфігурацію закешовано, mergeConfigFrom нічого не робить - значення вже в кеші. Тому змінюючи конфігурацію пакета в розробці, не забувайте config:clear.

Теги публікації:

  • конвенція - <пакет>-config, <пакет>-migrations, <пакет>-views;
  • vendor:publish --provider="Acme\Invoices\InvoicesServiceProvider" публікує все від провайдера;
  • міграції краще публікувати через publishesMigrations() - Laravel оновить мітку часу в імені файлу, щоб міграція виконалася після наявних;
  • не змушуйте публікувати конфігурацію для базової роботи: якщо без vendor:publish пакет падає - це погана інтеграція.

Значення з оточення: у конфігурації пакета можна використовувати env('INVOICES_CURRENCY', 'UAH'), а в коді пакета - лише config(), бо після config:cache виклики env() поза конфігураційними файлами повертають null.

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

Усе підключається в методі boot сервіс-провайдера пакета:

public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
    $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'invoices');
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'invoices');
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');

    if ($this->app->runningInConsole()) {
        $this->commands([InstallCommand::class]);
    }
}

Використання з простором імен пакета:

return view('invoices::pdf', ['invoice' => $invoice]);
__('invoices::messages.paid');
<x-invoices::status-badge :status="$invoice->status" />

Як застосунок перевизначає представлення без форку пакета: loadViewsFrom реєструє два шляхи - спершу resources/views/vendor/invoices застосунку, потім каталог пакета. Достатньо опублікувати чи створити файл з тим самим іменем:

$this->publishes([
    __DIR__.'/../resources/views' => resource_path('views/vendor/invoices'),
], 'invoices-views');

Переклади працюють так само: файли в lang/vendor/invoices застосунку мають пріоритет.

Особливості кожного ресурсу:

Ресурс На що зважати
маршрути loadRoutesFrom нічого не робить, якщо маршрути закешовано; дайте можливість вимкнути маршрути чи змінити префікс і middleware через конфігурацію
міграції loadMigrationsFrom запускає міграції пакета разом з міграціями застосунку - зручно, але застосунок не може їх змінити. Альтернатива - лише публікація
представлення ім'я простору - унікальне, щоб не зіткнутися з іншими пакетами
компоненти Blade::componentNamespace('Acme\\Invoices\\Views', 'invoices') чи анонімні компоненти з каталогу представлень
команди реєструвати лише в консолі - runningInConsole()

Маршрути пакета - найбільша зона конфліктів: пакет, що безумовно реєструє /login чи /admin, може перекрити маршрути застосунку. Гарна практика:

if (config('invoices.routes.enabled', true)) {
    Route::prefix(config('invoices.routes.prefix', 'invoices'))
        ->middleware(config('invoices.routes.middleware', ['web', 'auth']))
        ->group(__DIR__.'/../routes/web.php');
}

Міграції з моделями користувача: таблиці, що посилаються на users, мають враховувати, що модель користувача й тип ключа (int, UUID) у застосунку можуть бути іншими - краще брати їх із конфігурації.

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

Пакет не має власного застосунку - немає bootstrap/app.php, контейнера, конфігурації. Orchestra Testbench створює мінімальний Laravel-застосунок для тестів пакета.

composer require --dev orchestra/testbench pestphp/pest

Версії Testbench відповідають версіям Laravel:

Testbench Laravel
9.x 11.x
10.x 12.x
11.x 13.x

Базовий тест-кейс:

namespace Acme\Invoices\Tests;

use Acme\Invoices\InvoicesServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;

abstract class TestCase extends Orchestra
{
    protected function getPackageProviders($app): array
    {
        return [InvoicesServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('database.default', 'testing');
        $app['config']->set('invoices.currency', 'UAH');
    }

    protected function defineDatabaseMigrations(): void
    {
        $this->loadLaravelMigrations();
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
    }
}
// tests/Pest.php
uses(Acme\Invoices\Tests\TestCase::class)->in(__DIR__);

Тепер у тестах працює все, що й у застосунку: HTTP-тести маршрутів пакета, фабрики, Mail::fake(), artisan():

it('generates an invoice pdf', function () {
    $this->get('/invoices/1/pdf')->assertOk()->assertHeader('content-type', 'application/pdf');
});

it('publishes the config', function () {
    $this->artisan('vendor:publish', ['--tag' => 'invoices-config'])->assertSuccessful();
});

Workbench - для ручної перевірки: vendor/bin/testbench workbench:install створює каталог workbench/ з маршрутами й моделями, а vendor/bin/testbench serve запускає вебсервер з підключеним пакетом.

Що обов'язково протестувати в пакеті:

  • пакет працює без опублікованої конфігурації;
  • реєстрація провайдера не падає за відсутності необов'язкових залежностей;
  • міграції застосовуються й відкочуються;
  • поведінку на всіх підтримуваних версіях Laravel і PHP - матриця в CI (laravel: [12.*, 13.*] з відповідними версіями Testbench);
  • --prefer-lowest у CI - що пакет справді працює з мінімальними версіями, вказаними в composer.json.

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