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

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

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

12 питань

  • composer install ставить рівно ті версії, що записані в composer.lock. Нічого не оновлює.
  • composer update шукає найновіші версії, дозволені обмеженнями з composer.json, ставить їх і переписує composer.lock.

Звідси правило:

  • На проді, в CI і після git pull - лише install. Так у всіх однакові версії, і те, що перевірили тести, саме те, що поїде на сервер.
  • update - свідома дія розробника, щоб оновити залежності. Після неї ганяють тести і комітять новий composer.lock.
composer install --no-dev --optimize-autoloader   # прод
composer update laravel/framework --with-dependencies   # оновити один пакет

Пастка: composer update без аргументів оновлює все одразу. Якщо після нього щось зламалося, важко зрозуміти, який пакет винен. Краще оновлювати пакети поштучно чи невеликими групами.

Якщо composer.lock немає, install поводиться як update і створює його.

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

composer.json описує дозволені версії (^11.0 - будь-яка 11.x). composer.lock фіксує точні версії всіх пакетів, разом із залежностями залежностей, і хеші їхнього вмісту.

Для застосунку - комітити обов'язково. Інакше кожен composer install у колеги, в CI чи на сервері поставить «найновіше, що підходить», і версії поступово розійдуться. Баг «у мене працює» часто саме звідси.

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

Що ще дає lock-файл:

  • composer install швидший, бо не треба розв'язувати залежності.
  • composer audit перевіряє на відомі вразливості саме встановлені версії.
  • Diff composer.lock у pull request показує, що реально оновилося.

Конфлікти в lock-файлі при злитті гілок не правлять руками: беруть версію однієї гілки й заново виконують ту composer require чи update, яка була в іншій.

Докладніше в документації: composer.lock у системі контролю версій

  • require - залежності, без яких застосунок не працює: фреймворк, бібліотеки для бізнес-логіки, SDK.
  • require-dev - потрібні лише розробникам і CI: тестовий фреймворк (Pest, PHPUnit), статичний аналіз (PHPStan, Larastan), форматування (Pint), Faker, налагоджувальні панелі.
composer require guzzlehttp/guzzle
composer require --dev pestphp/pest

На продакшені dev-залежності не ставлять:

composer install --no-dev --optimize-autoloader

Це:

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

Типова помилка: клас з dev-пакета використовується в робочому коді. Локально все працює, бо dev-пакети встановлено, а на проді - Class not found. Приклад - фабрики й Faker у сидерах, які запускають на проді, або налагоджувальний пакет, підключений у сервіс-провайдері без перевірки оточення.

Як ловити: CI-крок, що встановлює залежності з --no-dev і запускає застосунок (або хоча б php artisan чи тести, що не потребують dev-пакетів), а також composer-require-checker для перевірки, що весь код покритий оголошеними залежностями.

Для бібліотек require-dev ще важливіший: споживачі бібліотеки отримують лише її require, тож тестові інструменти бібліотеки до них не потрапляють.

Докладніше в документації: require-dev

Скрипти в composer.json - іменовані команди, які запускають через composer run (або коротко composer <назва>), і хуки на події Composer.

"scripts": {
    "test": "pest --parallel",
    "lint": ["pint --test", "phpstan analyse"],
    "dev": [
        "Composer\\Config::disableProcessTimeout",
        "npx concurrently \"php artisan serve\" \"php artisan queue:listen\" \"npm run dev\""
    ],
    "post-autoload-dump": [
        "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
        "@php artisan package:discover --ansi"
    ]
}
composer test
composer lint
composer dev

Навіщо:

  • Єдині команди для команди й CI. Не треба пам'ятати прапорці кожного інструмента - composer test однаковий локально й у пайплайні.
  • Хуки на події: post-install-cmd, post-update-cmd, post-autoload-dump, post-create-project-cmd. Laravel через post-autoload-dump виявляє пакети (package:discover), а через post-create-project-cmd - генерує APP_KEY.

Що варто знати:

  • @php у скрипті використовує той самий PHP, яким запущено Composer; @composer - той самий Composer; @other-script - викликати інший скрипт.
  • Виконувані файли з vendor/bin доступні в скриптах без повного шляху.
  • За замовчуванням скрипт обмежений 300 секундами; для довгих (dev-сервер) - Composer\Config::disableProcessTimeout.
  • composer install --no-scripts вимикає хуки - корисно в Docker-збиранні, поки код застосунку ще не скопійовано.

Безпека: скрипти пакетів-залежностей Composer не виконує - лише скрипти кореневого composer.json. Плагіни ж пакетів потребують явного дозволу в allow-plugins.

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

PSR-4 зіставляє префікс простору імен з каталогом. Решта імені класу перетворюється на шлях до файлу:

"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
}

App\Services\Billing\Invoice → app/Services/Billing/Invoice.php.

Як це працює: vendor/autoload.php реєструє функцію через spl_autoload_register(). Коли код уперше звертається до невідомого класу, PHP викликає її, вона обчислює шлях і робить require. Класи, які не знадобилися в запиті, не завантажуються взагалі.

Що треба знати:

  • Регістр важливий: на Linux invoice.php для класу Invoice не знайдеться, хоча на macOS усе працюватиме. Класична причина «на проді клас не знайдено».
  • Один клас - один файл, ім'я файлу збігається з ім'ям класу.
  • Після зміни секції autoload у composer.json потрібен composer dump-autoload. Нові класи в уже налаштованому каталозі підхоплюються без нього.
  • Для файлів з функціями (не класами) є секція "files" - вони підключаються на кожному запиті.

Докладніше в документації: PSR-4 у composer.json

Обидва дозволяють оновлення, які за семантичним версіонуванням не мають ламати сумісність:

  • ^1.2.3 - від 1.2.3 до (не включно) 2.0.0. Дозволені всі мінорні й патч-оновлення.
  • ~1.2.3 - від 1.2.3 до (не включно) 1.3.0. Дозволені лише патчі.
  • ~1.2 - від 1.2 до 2.0. Остання вказана цифра може рости.

Нюанс з нулем: для версій 0.x мінорна версія вважається ламкою. ^0.3.1 - це від 0.3.1 до 0.4.0, а не до 1.0.

Що обирати:

  • ^ - стандарт для більшості залежностей. Саме його пише composer require.
  • Точна версія (1.2.3) - лише коли пакет відомий тим, що ламає сумісність у мінорних версіях, і оновлювати його хочеться тільки вручну.
  • * чи dev-master у застосунку - ні: сьогоднішній composer update може принести будь-що.

Важливо: обмеження в composer.json визначають, що дозволено. Що встановлено - записано в composer.lock, і на сервер їде саме воно.

Докладніше в документації: Версії й обмеження

За замовчуванням Composer шукає пакети на Packagist. Інші джерела додають у секцію repositories.

Git-репозиторій (приватний пакет):

"repositories": [
    { "type": "vcs", "url": "git@github.com:acme/billing-sdk.git" }
],
"require": {
    "acme/billing-sdk": "^2.1"
}

Composer читає теги репозиторію як версії. Доступ - через SSH-ключ або токен у auth.json (composer config --global github-oauth.github.com <token>), а в CI - через змінну COMPOSER_AUTH.

Локальна папка (розробка пакета поруч із застосунком):

"repositories": [
    { "type": "path", "url": "../packages/billing-sdk", "options": { "symlink": true } }
]

Зміни в пакеті одразу видно в застосунку, без публікації нових версій. Для монорепозиторіїв - "url": "packages/*".

Приватний реєстр: для кількох пакетів і великої команди зручніше Private Packagist, Satis (статичний реєстр) або реєстр пакетів GitLab/GitHub - ними керують як Packagist, з версіями й правами доступу.

Що варто знати:

  • Порядок важливий: репозиторії з repositories перевіряються перед Packagist. Пакет з тією ж назвою з вашого репозиторію матиме пріоритет - цим користуються для тимчасових форків з виправленням ("type": "vcs" на свій форк і версія dev-fix-branch).
  • Форк - тимчасовий захід. Виправлення варто віддати в оригінальний пакет і повернутися на нього, інакше ви самі підтримуєте форк.
  • path-репозиторій на проді зазвичай не використовують: локальний шлях там може не існувати.

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

PER Coding Style - стандарт форматування PHP-коду від PHP-FIG, наступник PSR-12 (а той - PSR-2). Описує відступи (4 пробіли), розташування дужок, пробіли навколо операторів, порядок use, оформлення сигнатур, нові конструкції мови (enum, match, атрибути, property hooks).

Навіщо єдиний стиль: код-рев'ю обговорює суть, а не пробіли; diff-и не засмічуються переформатуванням; новий розробник читає код без звикання до особистих звичок кожного автора.

Інструменти, що форматують автоматично:

  • Laravel Pint - обгортка над PHP-CS-Fixer з пресетом Laravel (є й пресет per). У Laravel-проєктах - стандарт.
vendor/bin/pint            # виправити
vendor/bin/pint --test     # лише перевірити (для CI)
vendor/bin/pint --dirty    # лише змінені файли
  • PHP-CS-Fixer - гнучке налаштування правил.
  • PHP_CodeSniffer (phpcs/phpcbf) - перевірка й виправлення за стандартами.

Як впровадити, щоб працювало:

  1. Один конфіг у репозиторії (pint.json) - а не налаштування в IDE кожного розробника.
  2. Перевірка в CI - злиття неможливе, якщо стиль порушено.
  3. Автоформатування при збереженні в IDE чи pre-commit хук - щоб CI рідко падав.
  4. Одноразове переформатування всього коду окремим комітом, а його хеш - у .git-blame-ignore-revs, щоб git blame не показував цей коміт як автора кожного рядка.

Форматування ≠ якість коду: стиль ловить пробіли й дужки, а помилки логіки й типів - статичний аналіз (PHPStan/Larastan) і тести.

Докладніше в документації: PER Coding Style

За замовчуванням PSR-4 автозавантажувач для кожного класу обчислює шлях і перевіряє, чи існує файл. На тисячах класів ці перевірки файлової системи помітні.

Рівень 1 - classmap (composer install --optimize-autoloader або -o). Composer заздалегідь будує мапу «клас → файл» для всіх PSR-4/PSR-0 класів. Знайти клас - це один пошук у масиві, який до того ж лежить в OPcache.

Рівень 2 - authoritative classmap (--classmap-authoritative або -a). Те саме, але якщо класу немає в мапі, Composer не шукає його у файловій системі, а одразу вирішує, що класу немає. Швидше для перевірок class_exists() на неіснуючих класах.

Рівень 2/B - APCu (--apcu-autoloader). Знайдені шляхи кешуються в APCu. Альтернатива для випадків, коли authoritative не підходить.

composer install --no-dev --classmap-authoritative

Обмеження authoritative: класи, згенеровані під час виконання (проксі, скомпільовані шаблони в нестандартних місцях), не знайдуться, бо їх не було в мапі під час збирання. Тому його вмикають і перевіряють на стейджингу.

--no-dev теж допомагає: dev-залежності не потрапляють у мапу й не встановлюються на сервер.

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

Це спільні інтерфейси PHP-FIG для роботи з HTTP. Вони дозволяють бібліотекам не залежати від конкретного фреймворку чи HTTP-клієнта:

  • PSR-7 - інтерфейси HTTP-повідомлень: RequestInterface, ResponseInterface, UriInterface, потоки. Об'єкти незмінні: withHeader() повертає нову копію.
  • PSR-17 - фабрики для створення цих об'єктів, щоб бібліотека не викликала new конкретного класу.
  • PSR-15 - серверні обробники й middleware: RequestHandlerInterface, MiddlewareInterface.
  • PSR-18 - HTTP-клієнт: ClientInterface::sendRequest().
final class GithubApi
{
    public function __construct(
        private ClientInterface $http,             // Guzzle, Symfony HttpClient...
        private RequestFactoryInterface $requests,
    ) {}
}

Що це дає:

  • SDK пише код один раз, а застосунок підставляє клієнт, який уже використовує.
  • Middleware, написане за PSR-15, працює в Slim, Mezzio та інших PSR-сумісних фреймворках.
  • У тестах легко підставити фейковий клієнт.

Laravel використовує власні класи Request/Response на базі Symfony HttpFoundation, а не PSR-7. Але конвертувати можна (пакет symfony/psr-http-message-bridge), а Guzzle під HTTP-клієнтом Laravel реалізує PSR-18.

Докладніше в документації: Стандарти PHP-FIG

Composer розв'язує обмеження версій усіх пакетів одночасно. Якщо для потрібного пакета немає версії, сумісної з рештою, він відмовляє з довгим повідомленням «Your requirements could not be resolved».

Інструменти для розслідування:

composer why vendor/package             # хто залежить від пакета і з якими обмеженнями
composer why-not laravel/framework 13.0 # що заважає встановити цю версію
composer outdated --direct              # які прямі залежності застаріли
composer show vendor/package --all      # усі доступні версії
composer update vendor/package -W --dry-run   # що оновиться разом із залежностями

why-not - найкорисніша: показує конкретний пакет і обмеження, що блокує версію. Типова відповідь: «пакет X вимагає illuminate/support ^11.0» - тобто X ще не оновився під нову версію Laravel.

Типові причини й рішення:

  • Старий пакет не підтримує нову версію фреймворку чи PHP. Оновити пакет, знайти форк чи альтернативу, або відкласти оновлення.
  • Залежності залежностей зафіксовані lock-файлом. composer update vendor/package оновлює лише його; -W (--with-all-dependencies) дозволяє оновити й залежності, які йому заважають.
  • Платформа: версія PHP чи розширення на машині не задовольняє require пакета. config.platform.php у composer.json фіксує цільову версію PHP прод-сервера, щоб локально не поставити пакети, які там не запрацюють.
  • Конфлікт conflict-обмежень: пакет явно оголосив несумісність з певними версіями іншого.

Чого не робити: --ignore-platform-reqs на проді «щоб встановилося» - пакети, що вимагають іншої версії PHP чи відсутнього розширення, впадуть під час виконання.

Профілактика: оновлювати залежності регулярно й невеликими порціями (Dependabot, Renovate), а не раз на два роки разом із мажорною версією фреймворку.

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

PSR-11 - мінімальний спільний інтерфейс контейнера залежностей. Усього два методи:

namespace Psr\Container;

interface ContainerInterface
{
    public function get(string $id);   // отримати запис або кинути NotFoundExceptionInterface
    public function has(string $id): bool;
}

Навіщо такий мінімалізм: стандарт описує лише отримання сервісів, а не їхню реєстрацію. Конфігурація (прив'язки, автовпровадження, синглтони) у кожного контейнера своя, а споживання - однакове.

Що це дало екосистемі:

  • Фреймворко-незалежні бібліотеки й фреймворки. Slim, Mezzio, роутери й диспетчери middleware приймають будь-який PSR-11-контейнер: PHP-DI, Symfony DependencyInjection, League Container, Laravel.
  • Laravel реалізує PSR-11: Illuminate\Container\Container - це ContainerInterface, тож сторонні бібліотеки, що очікують PSR-контейнер, працюють з ним.
  • Композиція контейнерів - делегування пошуку іншому контейнеру.

Застереження - Service Locator. PSR-11 прямо не радить передавати контейнер у бізнес-класи, щоб вони самі діставали залежності:

// Погано: прихована залежність, важко тестувати
final class ReportService
{
    public function __construct(private ContainerInterface $container) {}

    public function build(): void
    {
        $mailer = $this->container->get(Mailer::class);
    }
}

// Добре: залежність явна
final class ReportService
{
    public function __construct(private Mailer $mailer) {}
}

Контейнер - для «склеювання» застосунку на верхньому рівні (фабрики, фреймворк), а не для використання всередині логіки. Виняток - фабрики, що за природою створюють різні об'єкти за ідентифікатором.

Докладніше в документації: PSR-11: Container interface