Свой пакет для Laravel: приёмы из практики
Выносите повторяющийся код в отдельный пакет — но делайте это так, чтобы его можно было тестировать без всего фреймворка, публиковать конфиг и не ломать проекты при обновлении.

Вы копируете один и тот же сервис-класс, миграцию и пару хелперов из проекта в проект. На третий раз стоит вынести это в пакет: подключается через Composer, обновляется одной командой, тестируется отдельно от приложения. Но между «работает у меня в app/» и «работает как установленный через composer require пакет» лежит несколько ловушек — автозагрузка, публикация конфига, слияние настроек, порядок регистрации провайдеров. Разберём приёмы, которые экономят часы отладки. Ориентируюсь на Laravel 11.x и Composer 2.x.
Структура: что кладём в пакет, а что оставляем в приложении
Минимальный пакет — это composer.json, один сервис-провайдер и код. Всё остальное добавляется по мере надобности. Типичное дерево:
vendor-name/package-name/
├── composer.json
├── config/
│ └── package.php
├── database/
│ └── migrations/
├── src/
│ ├── PackageServiceProvider.php
│ └── Services/
└── tests/
└── ExampleTest.php
Главная развилка: миграции и конфиг — публикуемые ресурсы. Пользователь пакета должен иметь право поменять таблицу или значение настройки, не редактируя код в vendor/ (изменения там затрёт следующий composer update). Поэтому конфиг и миграции публикуются в проект, а логика остаётся в пакете.
composer.json: autoload и discovery
Два ключевых блока — PSR-4 автозагрузка и секция extra.laravel для автоматической регистрации провайдера. Без второго блока пользователю пришлось бы вручную дописывать провайдер в bootstrap/providers.php.
{
"name": "acme/report-builder",
"description": "Генератор отчётов для внутренних сервисов",
"require": {
"php": "^8.2",
"illuminate/support": "^11.0"
},
"require-dev": {
"orchestra/testbench": "^9.0"
},
"autoload": {
"psr-4": {
"Acme\\ReportBuilder\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\ReportBuilder\\Tests\\": "tests/"
}
},
"extra": {
"laravel": {
"providers": [
"Acme\\ReportBuilder\\ReportBuilderServiceProvider"
]
}
}
}
Обратите внимание: в зависимостях указан illuminate/support, а не laravel/framework. Пакет не должен тащить весь фреймворк — только те компоненты, которые реально использует. Если нужен роутинг, добавляйте illuminate/routing; для очередей — illuminate/queue. Это снижает вероятность конфликта версий в проекте пользователя.
Сервис-провайдер: register против boot
Самая частая ошибка новичка в пакетах — путать, что делать в register(), а что в boot(). Правило простое: в register() только биндинги в контейнер и слияние конфига, ничего больше. Внутри register() нельзя обращаться к другим сервисам — они могут быть ещё не зарегистрированы. Всё остальное (публикация, миграции, роуты, вьюхи) — в boot(), который вызывается после регистрации всех провайдеров.
<?php
namespace Acme\ReportBuilder;
use Illuminate\Support\ServiceProvider;
use Acme\ReportBuilder\Services\ReportService;
class ReportBuilderServiceProvider extends ServiceProvider
{
public function register(): void
{
// Слияние дефолтного конфига с пользовательским.
// Пользователь переопределяет только нужные ключи.
$this->mergeConfigFrom(
__DIR__ . '/../config/report.php',
'report'
);
// Биндинг сервиса как singleton в контейнер.
$this->app->singleton(ReportService::class, function ($app) {
return new ReportService(
config('report.format')
);
});
}
public function boot(): void
{
// Миграции загружаются напрямую — их не обязательно публиковать.
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// Публикуемые ресурсы регистрируются только в консоли.
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/report.php' => config_path('report.php'),
], 'report-config');
}
}
}
Проверка runningInConsole() вокруг publishes() не обязательна для работы, но экономит память при обработке HTTP-запросов: публикация нужна только для команды artisan vendor:publish, в вебе она мёртвый груз.
mergeConfigFrom и его подвох
Метод mergeConfigFrom() сливает массивы только на верхнем уровне. Если у вас вложенная структура конфига, а пользователь опубликовал файл и удалил из него один вложенный ключ, при слиянии этот ключ не восстановится из дефолтного — потому что весь вложенный массив пользователя перезапишет ваш целиком. Для плоского конфига проблемы нет; для глубоко вложенного придётся либо документировать все ключи, либо писать своё рекурсивное слияние.
Пакет, который нельзя протестировать без поднятого приложения, — это не пакет, а вырезанный кусок проекта. Testbench существует именно чтобы дать пакету минимальное окружение Laravel в изоляции.
Тесты через Testbench
Пакет живёт вне приложения, значит у него нет bootstrap/app.php, конфигов и контейнера. Пакет orchestra/testbench поднимает облегчённое приложение Laravel специально для тестов пакета. Базовый тест-кейс наследуется от его класса и объявляет, какие провайдеры регистрировать.
<?php
namespace Acme\ReportBuilder\Tests;
use Orchestra\Testbench\TestCase as Orchestra;
use Acme\ReportBuilder\ReportBuilderServiceProvider;
class TestCase extends Orchestra
{
// Регистрируем провайдер пакета в тестовом приложении.
protected function getPackageProviders($app): array
{
return [
ReportBuilderServiceProvider::class,
];
}
// Переопределяем окружение: например, БД в памяти.
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
}
}
Названия методов getPackageProviders() и defineEnvironment() относятся к Testbench 9.x (совместим с Laravel 11). В старых версиях метод окружения назывался иначе — если у вас Testbench 8 или ниже, сверьте имя в документации, оно менялось.
Локальная разработка: path-репозиторий
Пока пакет не опубликован в Packagist, подключайте его в тестовый проект через локальный путь. В composer.json приложения:
{
"repositories": [
{
"type": "path",
"url": "../packages/report-builder"
}
],
"require": {
"acme/report-builder": "@dev"
}
}
Composer создаст симлинк на директорию пакета, и правки в его коде сразу видны в приложении без переустановки. Это главный приём при разработке: не публикуете в Packagist на каждый чих, а работаете с живым каталогом.
Где что размещать: сводка
| Ресурс | Метод в провайдере | Публикуется? |
|---|---|---|
| Конфиг | mergeConfigFrom + publishes | Да, тег -config |
| Миграции | loadMigrationsFrom | Опционально |
| Роуты | loadRoutesFrom | Нет |
| Вьюхи | loadViewsFrom | Опционально |
| Команды artisan | commands (в консоли) | Нет |
Типичные ошибки
- Провайдер не подхватывается. Забыли секцию
extra.laravel.providersвcomposer.jsonпакета, либо после её добавления не пересобрали кеш пакетов приложения командойcomposer dump-autoload. - Конфиг публикуется, но значения не читаются. Ключ во втором аргументе
mergeConfigFromдолжен совпадать с тем, что вы дёргаете черезconfig('report.xxx'). Опечатка в имени ключа — молчаливыйnull. - Тесты падают на «class not found». Не описали
autoload-devили после его добавления не выполнилиcomposer dump-autoloadв каталоге пакета. - Миграции запускаются дважды. Одновременно и
loadMigrationsFrom, и опубликованные в проект копии. Выберите одно: либо загружаете из пакета, либо публикуете.
Что дальше
Когда пакет стабилен, публикуйте его: тег версии по SemVer через git, регистрация репозитория на Packagist, дальше подключение через обычный composer require. Заведите GitHub Actions с матрицей PHP и версий Laravel — Testbench как раз позволяет прогнать тесты против нескольких версий фреймворка сразу. И зафиксируйте в composer.json честный диапазон illuminate/*: если пакет реально работает на Laravel 10 и 11, ставьте ^10.0|^11.0, а не только текущую — иначе отрежете половину пользователей.
Таможенный брокер (ВЭД): Коды ТН ВЭД, маркировка «Честный ЗНАК», сертификация.
Спросить за 20 ₽Источники: Laravel Package Development — официальная документация, Orchestra Testbench — документация
Частые вопросы
Что делать, если у меня Laravel 10, а не 11?
Провайдер не регистрируется автоматически, что проверить?
Нужно ли класть код в register() или в boot()?
Как разрабатывать пакет, не публикуя его в Packagist каждый раз?
Стоит ли зависеть от laravel/framework целиком?
Почему опубликованный пользователем конфиг теряет часть настроек?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.