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

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

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

3 питання

Користувачі пакета оновлюють Laravel у різний час. Пакет, що підтримує лише найновішу версію, блокує їм оновлення - або змушує шукати альтернативу.

Обмеження версій у composer.json:

"require": {
    "php": "^8.3",
    "illuminate/support": "^12.0|^13.0",
    "illuminate/database": "^12.0|^13.0"
},
"require-dev": {
    "orchestra/testbench": "^10.0|^11.0",
    "pestphp/pest": "^4.0|^5.0"
}

Матриця в CI - кожна комбінація перевіряється окремо:

strategy:
  matrix:
    php: [8.3, 8.4, 8.5]
    laravel: [12.*, 13.*]
    stability: [prefer-lowest, prefer-stable]
    include:
      - laravel: 12.*
        testbench: 10.*
      - laravel: 13.*
        testbench: 11.*
    exclude:
      - laravel: 13.*
        php: 8.3      # якщо Laravel 13 вимагає новішу версію PHP
steps:
  - run: composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-update
  - run: composer update --${{ matrix.stability }} --prefer-dist

prefer-lowest ловить випадки, коли код використовує метод, якого ще немає в мінімальній заявленій версії.

Як писати код для кількох версій:

  • спільний знаменник API: нові можливості фреймворку використовувати лише тоді, коли мінімальна версія їх містить;
  • якщо дуже треба - перевірка можливостей, а не номера версії: method_exists(Builder::class, 'whereVectorSimilarTo'), class_exists(...);
  • не покладатися на внутрішні класи й поведінку, не описану в документації, - вони змінюються в мінорних версіях;
  • version_compare(app()->version(), ...) - крайній засіб.

Семантичне версіонування пакета:

Зміна Версія
виправлення без зміни API патч 1.4.1
нова можливість, сумісна назад мінорна 1.5.0
видалено чи змінено публічний API, ключ конфігурації, підпис методу, структуру міграцій мажорна 2.0.0
зняття підтримки старої версії Laravel чи PHP зазвичай мажорна (хоча Composer сам не встановить пакет на непідтримуваній версії, багато супровідників вважають це breaking)

Що вважається публічним API пакета: публічні класи й методи, ключі конфігурації, імена подій, теги публікації, структура таблиць, назви команд. Позначайте внутрішнє @internal і робіть класи final, щоб звузити поверхню, яку доведеться зберігати.

Політика підтримки: записати в README, які версії Laravel і PHP підтримуються і скільки часу стара мажорна версія пакета отримує виправлення безпеки.

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

Пакет живе в чужому застосунку. Що менше він нав'язує, то легше його встановити, налаштувати й замінити.

Залежності в коді пакета - через інтерфейси й контейнер:

namespace Acme\Invoices\Contracts;

interface PdfRenderer
{
    public function render(string $html): string;
}
// InvoicesServiceProvider::register()
$this->app->singleton(PdfRenderer::class, fn ($app) => new DompdfRenderer(
    $app['config']->get('invoices.pdf'),
));

$this->app->singleton(InvoiceGenerator::class);
final class InvoiceGenerator
{
    public function __construct(
        private PdfRenderer $renderer,
        private Filesystem $files,     // Illuminate\Contracts\Filesystem\Filesystem
    ) {}
}

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

$this->app->singleton(PdfRenderer::class, BrowsershotRenderer::class);

Фасади:

  • фасад - зручність для користувача, а не спосіб писати внутрішній код пакета. Усередині пакета краще впровадження залежностей: код тестується без фасадів і не залежить від глобального стану;
  • фасад пакета вказує на прив'язку в контейнері (getFacadeAccessor повертає клас чи ключ), тож заміна реалізації працює й для нього;
  • реєструвати аліас через extra.laravel.aliases - необов'язково: багато користувачів імпортують клас фасаду напряму.

Необов'язкові залежності:

"require": { "illuminate/support": "^12.0|^13.0" },
"suggest": { "spatie/browsershot": "Для рендерингу PDF через Chrome" }
if (! class_exists(\Spatie\Browsershot\Browsershot::class)) {
    throw new MissingDependency('Install spatie/browsershot to use the browsershot driver.');
}

Важка залежність (headless Chrome, SDK хмари) не має ставитися всім, кому потрібна лише частина пакета.

Моделі застосунку:

  • не імпортувати App\Models\User - модель береться з конфігурації: config('invoices.user_model', config('auth.providers.users.model'));
  • власні моделі пакета дозволяти розширювати: config('invoices.models.invoice') і використовувати їх через цей ключ.

Події замість жорстких викликів: InvoicePaid дозволяє застосунку реагувати (лист, вебхук, бухгалтерія), не змінюючи код пакета.

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

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

На продакшені застосунок кешує конфігурацію, маршрути, події й представлення (php artisan optimize). Пакет, який цього не враховує, працює локально й ламається після деплою.

Що змінюється, коли кеші ввімкнені:

Кеш Наслідок для пакета
config:cache mergeConfigFrom пропускається - значення беруться з кешу; env() у коді пакета повертає null
route:cache loadRoutesFrom пропускається; замикання в маршрутах пакета раніше ламали кешування - краще контролери
event:cache слухачі, знайдені автоматично, кешуються; динамічна реєстрація в рантаймі може не потрапити
view:cache компілює представлення, зокрема пакетів, зареєстровані через loadViewsFrom

Типові помилки пакетів:

  • env() поза файлом конфігурації пакета - після config:cache повертає null. Усе, що залежить від оточення, має проходити через config/<пакет>.php;
  • конфігурація, змінена в рантаймі (config(['invoices.x' => ...]) у boot залежно від запиту) - не потрапляє в кеш і поводиться по-різному з кешем і без;
  • несеріалізовані значення в конфігурації: замикання чи об'єкти в config/*.php ламають config:cache («Your configuration files are not serializable»). Замість замикання - ім'я класу;
  • важка робота в boot: запити до бази чи HTTP при кожному завантаженні застосунку, зокрема в консольних командах і в package:discover під час composer install (коли бази ще може не бути).

Інтеграція з командами застосунку:

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        // викликаються з optimize / optimize:clear
        $this->optimizes(
            optimize: 'invoices:cache',
            clear: 'invoices:clear',
        );

        // викликається з php artisan reload (перезапуск довгоживучих процесів)
        $this->reloads('invoices:restart-workers');
    }

    // рядок у php artisan about
    AboutCommand::add('Invoices', fn () => [
        'Version' => '2.3.0',
        'PDF driver' => config('invoices.pdf.driver'),
    ]);
}

Тоді власний кеш пакета (наприклад, скомпільовані шаблони чи маніфест) створюється й очищується разом зі стандартними кешами, і розробнику не треба пам'ятати окрему команду в скрипті деплою.

Octane і довгоживучі процеси: пакет не має зберігати стан запиту в синглтонах чи статичних властивостях - у воркері Octane він «протікає» між запитами. Для даних запиту - scoped() прив'язки замість singleton().

Тест у CI пакета: прогнати набір тестів ще й після config:cache і route:cache у Testbench - так ловляться помилки, які інакше знайде користувач на продакшені.

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