Свой пакет для Laravel: 6 ошибок, которые ломают установку
Разбор частых просчётов при разработке пакета для Laravel — от жёсткой привязки к контейнеру до забытого тега публикации конфига. С фрагментами кода для Laravel 11.x и путями, как их избежать.

Пакет ставится через composer require, но конфиг не публикуется, миграции не находятся, а на CI падает тест из-за жёсткой привязки к app(). Всё это — не редкие баги, а типовые грабли, на которые наступают почти все, кто первый раз выносит код из app/ в отдельный composer-пакет. Разберём шесть ошибок, которые чаще всего всплывают уже после публикации, и что писать вместо них. Ориентируюсь на Laravel 11.x и пакеты, устанавливаемые через Composer с автодискавери.
Речь именно о пакете-библиотеке (тип library), который подключается в чужие приложения. Разработку модулей внутри монолита через локальный path-репозиторий не трогаем — там свои правила.
Ошибка 1. ServiceProvider делает слишком много в register()
Метод register() сервис-провайдера предназначен только для привязок в контейнер. Частый промах — обращаться там к конфигу, роутам, событиям или другим сервисам. На момент вызова register() другие провайдеры могут быть ещё не зарегистрированы, и вы получите поведение, которое ломается в зависимости от порядка загрузки.
Разделение простое: связывание — в register(), всё остальное (публикация, роуты, миграции, вьюхи, команды) — в boot().
<?php
namespace Acme\Billing;
use Illuminate\Support\ServiceProvider;
class BillingServiceProvider extends ServiceProvider
{
// Только привязки. Никакого чтения конфига и роутов.
public function register(): void
{
// Сначала подмешиваем дефолты пакета в общий конфиг,
// чтобы config('billing.key') работал даже без публикации.
$this->mergeConfigFrom(__DIR__ . '/../config/billing.php', 'billing');
$this->app->singleton(BillingClient::class, function ($app) {
return new BillingClient(
config('billing.api_url'),
config('billing.timeout')
);
});
}
// Здесь уже можно всё: приложение полностью собрано.
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
}
}
mergeConfigFrom() — исключение, которое живёт в register() по историческим причинам: он подмешивает дефолтные значения, чтобы обращения к config('billing.*') не падали, пока пользователь не опубликовал свой конфиг.
Ошибка 2. Забыли зарегистрировать публикацию конфига
Пользователь ставит пакет, запускает php artisan vendor:publish и не видит вашего конфига в списке. Причина — вы вызвали mergeConfigFrom(), но не publishes(). Это две независимые вещи: первая даёт дефолты, вторая позволяет скопировать файл в приложение для правки.
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/billing.php' => config_path('billing.php'),
], 'billing-config');
}
}
Тег ('billing-config') обязателен, если хотите, чтобы работало vendor:publish --tag=billing-config. Без тега пользователю придётся публиковать всё скопом через --provider, что раздражает. Оборачивание в runningInConsole() — не косметика: публикация имеет смысл только в консоли, и это снимает лишнюю работу при обычных HTTP-запросах.
Ошибка 3. Хардкод путей и namespace вместо composer.json
Пакет не подхватывается автоматически, и пользователь вынужден вручную дописывать провайдер в конфиг приложения. В Laravel это лечится секцией extra.laravel в composer.json пакета — механизм package discovery.
{
"name": "acme/billing",
"type": "library",
"autoload": {
"psr-4": {
"Acme\\Billing\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Acme\\Billing\\BillingServiceProvider"
],
"aliases": {
"Billing": "Acme\\Billing\\Facades\\Billing"
}
}
}
}
После этого провайдер и фасад регистрируются сами при установке. Обратная сторона: если пользователь хочет отключить автодискавери именно вашего пакета, он добавляет его в extra.laravel.dont-discover в своём приложении — предусмотрите это в документации, а не в коде.
Правило, которое экономит часы поддержки: всё, что пользователь должен сделать руками после установки, — это баг вашего пакета, а не его обязанность. Если можно сделать за него автоматически и безопасно — делайте.
Ошибка 4. Тесты гоняются на реальном приложении, а не на orchestra/testbench
Пакет — это не приложение, у него нет bootstrap/app.php и полного окружения Laravel. Попытка тестировать через Illuminate\Foundation\Testing\TestCase напрямую приведёт к ошибкам инициализации контейнера. Стандартное решение — пакет orchestra/testbench, который поднимает минимальное окружение Laravel в памяти под ваш пакет.
<?php
namespace Acme\Billing\Tests;
use Acme\Billing\BillingServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
class TestCase extends Orchestra
{
// Подключаем провайдер пакета в тестовое окружение.
protected function getPackageProviders($app): array
{
return [BillingServiceProvider::class];
}
// Настраиваем окружение под тест, а не полагаемся на .env.
protected function defineEnvironment($app): void
{
$app['config']->set('billing.api_url', 'https://example.test');
}
}
Версию testbench подбирайте под версию Laravel: у пакета мажорные версии идут в связке с релизами фреймворка. Точное соответствие смотрите в README репозитория orchestral/testbench — не фиксируйте здесь номер вслепую, он меняется с каждым релизом Laravel.
Ошибка 5. Жёсткая привязка к фасадам и хелперам внутри пакета
Внутри бизнес-логики пакета соблазнительно писать Cache::get(), DB::table(), config(). Это работает, но привязывает пакет к глобальному состоянию Laravel и превращает юнит-тесты в интеграционные: чтобы протестировать один класс, приходится поднимать половину фреймворка.
Внедряйте зависимости через конструктор и контракты (интерфейсы Illuminate\Contracts\*), а не через фасады.
<?php
namespace Acme\Billing;
use Illuminate\Contracts\Cache\Repository as Cache;
class RateProvider
{
// Зависимость явная — её легко подменить моком в тесте.
public function __construct(private Cache $cache) {}
public function current(string $currency): float
{
return $this->cache->remember("rate:{$currency}", 60, function () use ($currency) {
return $this->fetchFromApi($currency);
});
}
}
Цена: чуть больше кода на связывание в провайдере. Выигрыш: класс тестируется изолированно и не тащит за собой весь фреймворк. Для пакета, который ставят в разные приложения, это важнее, чем краткость.
Ошибка 6. Миграции пакета мешают миграциям приложения
Через loadMigrationsFrom() миграции пакета выполняются автоматически при php artisan migrate. Проблема — имена файлов. Если вы назвали миграцию create_transactions_table.php без даты, а в приложении уже есть таблица с таким же именем, будет конфликт или неожиданное поведение при откате.
- Давайте таблицам префикс вендора:
billing_transactions, а неtransactions. - Именуйте файлы миграций с датой, как в стандартном Laravel:
2024_01_01_000000_create_billing_transactions_table.php. - Если пользователю нужен контроль над миграциями — предложите публикацию через
publishes()с тегом вместо автозагрузки, чтобы он мог править файлы до запуска.
Сводка: где что размещать
| Что | Где регистрируется | Метод |
|---|---|---|
| Привязки в контейнер | register() | bind / singleton |
| Дефолты конфига | register() | mergeConfigFrom() |
| Публикация конфига | boot() | publishes() с тегом |
| Миграции | boot() | loadMigrationsFrom() |
| Роуты | boot() | loadRoutesFrom() |
| Провайдер и фасад | composer.json | extra.laravel |
Что делать дальше
Перед первым релизом прогоните установку в чистом Laravel-приложении: composer require из локального path-репозитория, затем проверьте, что провайдер подхватился без ручной регистрации, vendor:publish видит ваши теги, а migrate проходит без конфликтов. Настройте GitHub Actions с матрицей версий PHP и Laravel, которые вы заявляете в composer.json — иначе «поддержка Laravel 10 и 11» останется обещанием. Семантическое версионирование пакета читайте на getcomposer.org, а требования к структуре сервис-провайдеров — в разделе Package Development официальной документации Laravel.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: Laravel Documentation — Package Development, Orchestra Testbench
Частые вопросы
У меня Laravel 10, а не 11 — эти советы подойдут?
Что делать, если vendor:publish не видит мой конфиг?
Обязательно ли использовать фасад в своём пакете?
Пакет ставится, но провайдер не регистрируется автоматически — почему?
Тесты пакета падают с ошибкой о неинициализированном приложении. Что не так?
Стоит ли автоматически запускать миграции пакета или давать их публиковать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.