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 - так ловляться помилки, які інакше знайде користувач на продакшені.