Свой пакет для Laravel: структура, автозагрузка, тесты
Разбираем, как собрать пакет для Laravel так, чтобы его можно было переиспользовать и опубликовать в Packagist: структура каталогов, service provider, автодискавери и публикация конфига.

Вы третий раз копируете один и тот же класс интеграции с платёжкой между проектами. Пора вынести его в отдельный пакет. Задача звучит просто, но очевидный путь — сложить файлы в app/Services и подключить через composer.json с path-репозиторием — разваливается, как только пакет нужен во втором проекте или в Packagist. Ниже — рабочие приёмы для сборки пакета под Laravel 11.x с PHP 8.2+: как разложить каталоги, зарегистрировать сервисы, настроить автодискавери и тесты, чтобы не переделывать через месяц.
Минимальная структура, которая не мешает
Пакет — это отдельный composer-проект с собственным composer.json, а не подкаталог приложения. Типовая раскладка:
my-vendor/laravel-invoicer/
├── composer.json
├── config/
│ └── invoicer.php
├── src/
│ ├── InvoicerServiceProvider.php
│ ├── Invoicer.php
│ └── Facades/
│ └── Invoicer.php
├── database/
│ └── migrations/
├── routes/
│ └── web.php
└── tests/
└── InvoicerTest.php
Каталог src и есть корень PSR-4 автозагрузки. Мигрировать структуру потом больно, поэтому решите сразу: код пакета живёт в src, а не в app. Смешивать не нужно.
composer.json пакета
Ключевые части — секция autoload с PSR-4 и секция extra.laravel для автодискавери. Вторая позволяет Laravel подхватить service provider без ручной регистрации в конфиге приложения.
{
"name": "my-vendor/laravel-invoicer",
"description": "Генерация счетов для Laravel",
"require": {
"php": "^8.2",
"illuminate/support": "^11.0"
},
"require-dev": {
"orchestra/testbench": "^9.0",
"phpunit/phpunit": "^11.0"
},
"autoload": {
"psr-4": {
"MyVendor\\Invoicer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MyVendor\\Invoicer\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"MyVendor\\Invoicer\\InvoicerServiceProvider"
],
"aliases": {
"Invoicer": "MyVendor\\Invoicer\\Facades\\Invoicer"
}
}
}
}
Зависите от illuminate/support, а не от laravel/framework: тянуть весь фреймворк в требования пакета не нужно, и это ломает установку в проектах на других минорных версиях. Указывайте диапазон версий через ^, а не жёсткую версию.
Service provider: где регистрировать, а где загружать
Главная ошибка новичка — сваливать всё в один метод. В service provider два метода с разной семантикой, и путать их нельзя.
register()— только привязки в контейнере и слияние конфига. Никаких обращений к другим сервисам: на этом этапе не все провайдеры загружены.boot()— всё остальное: публикация ресурсов, загрузка миграций, маршрутов, вью, слушателей событий.
<?php
namespace MyVendor\Invoicer;
use Illuminate\Support\ServiceProvider;
class InvoicerServiceProvider extends ServiceProvider
{
public function register(): void
{
// Слияние конфига по умолчанию с конфигом приложения
$this->mergeConfigFrom(
__DIR__ . '/../config/invoicer.php',
'invoicer'
);
// Синглтон основного сервиса
$this->app->singleton('invoicer', function ($app) {
return new Invoicer(config('invoicer.currency'));
});
}
public function boot(): void
{
// Загрузка миграций пакета
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// Публикация конфига по команде vendor:publish
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/invoicer.php' => config_path('invoicer.php'),
], 'invoicer-config');
}
}
}
Разница между mergeConfigFrom и publishes частая причина путаницы. Первый даёт значения по умолчанию, чтобы пакет работал сразу после установки. Второй позволяет пользователю скопировать конфиг к себе и переопределить. Нужны оба.
Если пакет требует ручной правки конфига приложения для запуска — это плохой пакет. Он должен работать сразу после composer require, а тонкая настройка — уже опционально через публикацию конфига.
Автодискавери и что делать, когда он мешает
Секция extra.laravel.providers включает автодискавери: Laravel сам зарегистрирует провайдер при установке. Это удобно, но иногда мешает — например, при тестировании конфликтующих провайдеров. Пользователь может отключить автодискавери конкретного пакета в своём composer.json:
{
"extra": {
"laravel": {
"dont-discover": [
"my-vendor/laravel-invoicer"
]
}
}
}
Тогда провайдер регистрируют вручную в bootstrap/providers.php (в Laravel 11 массив провайдеров переехал туда из config/app.php). Держите это в голове при написании документации к пакету.
Тесты без реального приложения
Пакет тестируют не внутри готового проекта, а через orchestra/testbench — он поднимает минимальное Laravel-окружение в памяти. Базовый тест-кейс наследуется от Orchestra\Testbench\TestCase и объявляет провайдер вашего пакета.
<?php
namespace MyVendor\Invoicer\Tests;
use MyVendor\Invoicer\InvoicerServiceProvider;
use Orchestra\Testbench\TestCase;
class InvoicerTest extends TestCase
{
// Регистрируем провайдер пакета в тестовом окружении
protected function getPackageProviders($app): array
{
return [InvoicerServiceProvider::class];
}
public function test_service_resolves_from_container(): void
{
$invoicer = $this->app->make('invoicer');
$this->assertInstanceOf(
\MyVendor\Invoicer\Invoicer::class,
$invoicer
);
}
}
Метод getPackageProviders — то, что отличает тест пакета от теста приложения. Без него ваш провайдер просто не загрузится, и привязки в контейнере не будет. Точные имена методов и версии Testbench под ваш минор Laravel сверяйте в документации пакета — соответствие версий Testbench и фреймворка жёсткое.
Соответствие версий Testbench и Laravel
Testbench версионируется в привязке к минорам фреймворка. Ставить произвольную версию нельзя — тесты либо не соберутся, либо будут проверять не то окружение.
| Laravel | Testbench (ветка) | PHP минимум |
|---|---|---|
| 10.x | 8.x | 8.1 |
| 11.x | 9.x | 8.2 |
Соответствие для новых версий сверяйте в README репозитория Testbench — таблица там ведётся авторами и обновляется с каждым релизом фреймворка.
Локальная разработка через path-репозиторий
Пока пакет не в Packagist, подключайте его в тестовый проект как локальный репозиторий. В composer.json приложения:
{
"repositories": [
{
"type": "path",
"url": "../laravel-invoicer"
}
],
"require": {
"my-vendor/laravel-invoicer": "@dev"
}
}
Composer создаст симлинк на каталог пакета, и правки в src видны сразу без переустановки. Это удобнее, чем гонять изменения через Git на каждый чих.
Когда что-то не собирается
- Class "MyVendor\Invoicer\..." not found — не совпадает namespace в PSR-4 и в файле, либо не выполнен
composer dump-autoload. Проверьте, что ключ вautoload.psr-4точно соответствует пространству имён с обратными слешами. - Провайдер не загружается — опечатка в
extra.laravel.providersили пакет попал вdont-discover. Проверьте выводphp artisan package:discover. - Конфиг null при обращении — забыли
mergeConfigFromвregister(), значений по умолчанию нет. Пользователь ещё не публиковал конфиг, а дефолтов вы не задали. - vendor:publish ничего не копирует — тег в
publishes()не совпадает с тем, что передаёте в--tag. Тег регистронезависимой магии не имеет, пишите точно.
Что делать дальше
Когда пакет собирается и тесты зелёные — заведите README с примером установки и разделом про dont-discover, поставьте семантическое версионирование через теги Git и опубликуйте в Packagist, привязав репозиторий. Дальше подключайте GitHub Actions с матрицей версий: прогоняйте тесты на нескольких минорах Laravel и версиях PHP, чтобы ^11.0 в требованиях не оказался пустым обещанием. Инвалидация кеша конфига при обновлении пакета — отдельная тема, здесь её не разворачиваем.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Laravel Documentation — Package Development, Orchestral Testbench (GitHub)
Частые вопросы
А если у меня Laravel 10, а не 11?
Обязательно ли публиковать пакет в Packagist?
Тесты не видят мой service provider, что не так?
Почему зависеть от illuminate/support, а не от laravel/framework?
Изменения в пакете не подхватываются в проекте, хотя я подключил через path
Куда класть миграции и как их не публиковать принудительно?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.