vasichmen / laravel-handbooks
Package Handbooks
Requires
- php: ^8.4
- ext-json: *
- laravel/framework: ^12.0|^13.0
- vasichmen/laravel-foundation: ^2.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 10:12:25 UTC
README
Пакет для быстрого подключения справочников (CRUD + списки для выбора) в Laravel-сервисы. Достаточно описать конфиг — и пакет сам сгенерирует REST API: пагинированные списки, детальные карточки, создание/обновление/удаление, short- и dynamic-списки для фронтенда и отдачу enum'ов.
Поддерживаются Laravel 12 и Laravel 13.
Подключение
- установить пакет
composer require vasichmen/laravel-handbooks
- подключить
\Laravel\Handbooks\LaravelHandbooksServiceProvider::classвconfig/app.php - опубликовать конфиг
php artisan vendor:publish --provider="Laravel\Handbooks\LaravelHandbooksServiceProvider"
- заполнить заготовку конфига
handbooks.php(см. разделы ниже)
Структура конфига
| Секция | Назначение |
|---|---|
route_prefix |
префикс роутов справочников (после api/v1/{service_name}) |
api_prefix |
префикс API, по умолчанию api/v1 |
global_middleware |
мидлвары на все роуты справочников, например ['user-auth', 'local-auth'] |
defaults.middleware |
мидлвары по умолчанию для create/update/delete/view CRUD-справочников |
dynamic |
настройки short- и dynamic-списков (см. Dynamic- и short-списки) |
crud |
справочники с полным CRUD (см. ниже) |
enums |
список enum-классов, которые будут отданы через API |
Секция crud
Ключ — kebab-код справочника (используется в роутах), значение — настройки:
'crud' => [ 'business-block' => [ 'repository' => BusinessBlockRepository::class, 'resource' => BusinessBlockResource::class, 'create_request' => CreateBusinessBlockRequest::class, // обязателен для create, должен подключать DTO 'update_request' => UpdateBusinessBlockRequest::class, // обязателен для update, должен подключать DTO 'readonly' => false, // только для чтения 'searchable_fields' => ['name', 'code'], // поля для поиска по q 'with' => [ // связи, загружаемые методами list/detail/create/update 'detail' => ['system', 'architectManagers'], 'list' => ['system'], ], // 'view_middleware' => [...], 'create_middleware' => [...], // 'update_middleware' => [...], 'delete_middleware' => [...] — иначе defaults.middleware.* ], ],
Если у справочника указан readonly => true, методы создания, изменения и удаления будут выбрасывать HandbookAccessException.
Если для create или update не задан соответствующий реквест, будет выброшено HandbookException.
Секция enums
Простой список классов:
'enums' => [
ComponentTypeEnum::class,
MicroserviceCodeEnum::class,
],
Либо массив EnumClass::class => ['filter' => fn($enum) => ...], если нужна фильтрация значений.
API-методы
Все роуты строятся по схеме {api_prefix}/{service_name}/{route_prefix}, по умолчанию — api/v1/{service_name}. Название сервиса берется из SERVICE_NAME (.env) или app.service_name.
Справочники с CRUD (handbooks.crud)
| Метод | Роут | Описание |
|---|---|---|
GET |
/handbook/{code} |
Пагинированный список. Параметры: q, filters, пагинация, сортировка |
GET |
/handbook/{code}/{id} |
Детальная карточка |
POST |
/handbook/{code} |
Создание. Валидация через create_request, выполняется в транзакции, BelongsToMany-связи синхронизируются автоматически |
POST |
/handbook/{code}/{id} |
Обновление. Валидация через update_request, выполняется в транзакции, BelongsToMany-связи синхронизируются автоматически |
DELETE |
/handbook/{code}/{id} |
Удаление. При foreign key violation (код 23503) бросается KeyViolationException |
Код справочника ({code}) определяется из имени роута.
Списки для выбора
| Метод | Роут | Описание |
|---|---|---|
GET |
/handbook/short/{model} |
Short-список модели (см. ниже) |
POST |
/handbook/short-pack |
Пакетный short-запрос: несколько списков за один вызов |
GET |
/handbook/dynamic/{model}/{field} |
Dynamic-список: уникальные значения поля или связанной модели (см. ниже) |
Enum'ы
| Метод | Роут | Описание |
|---|---|---|
GET |
/enum/{kebab-name} |
Значения enum-класса. Префикс строится из имени класса: ComponentTypeEnum → /enum/component-type |
Dynamic- и short-списки
Эти методы позволяют фронтенду получать данные для селектов и фильтров без написания отдельных эндпоинтов — модель и поле передаются прямо в URL.
Short-список — GET /handbook/short/{model}
Возвращает «короткие» записи модели (по умолчанию id + name) для выпадающих списков.
Как резолвится модель:
- если в
handbooks.dynamic.custom.{model}есть настройка — берутся еёmodel,repository,resource,select,with,searchable_fields; - иначе модель ищется по конвенции:
dynamic.model_base_namespace+Model, репозиторий —dynamic.repository_base_namespace+ModelRepository, ресурс —HandbookShortResource.
Поддерживаемые query-параметры: q (поиск по searchable_fields), filters, queries, sort.
Пример настройки:
'dynamic' => [ 'model_base_namespace' => '\\App\\Models\\', 'repository_base_namespace' => '\\App\\Repositories\\', 'default_select' => ['id', 'name'], 'default_with' => [], 'default_searchable_fields' => ['name'], 'custom' => [ 'system' => [ 'resource' => SystemShortResource::class, 'select' => ['id', 'name', 'code', 'it_asset_code'], 'with' => ['organization'], ], 'user' => [ 'select' => ['id', 'name', 'login'], 'resource' => UserShortResource::class, ], ], ],
Пакетный short-запрос — POST /handbook/short-pack
Позволяет получить несколько списков одним запросом (удобно при открытии формы с несколькими селектами):
{
"items": [
{ "key": "vendors", "model": "vendor", "params": "filters[active]=1" },
{ "key": "systemTypes", "model": "system-type" },
{ "key": "systemStatuses", "model": "system-status" }
]
}
key— ключ, под которым вернется список в ответе (уникальный);model— код модели изdynamic.custom, код enum'а, либо{model}.{field}для dynamic-списка;params— query-строка с параметрами (q,filters,queries,sort).
Ответ:
{
"data": {
"vendors": { "data": [ { "id": 1, "name": "Vendor A" } ] },
"systemTypes": { "data": [ { "id": 1, "name": "Информационная система" } ] }
}
}
Dynamic-список — GET /handbook/dynamic/{model}/{field}
Возвращает уникальные значения (id + name) по указанному полю или связи модели с учетом переданных filters. Используется для построения «каскадных» фильтров: например, получить список вендоров, у которых есть компоненты, подходящие под текущие фильтры.
Логика работы DynamicService::getDynamicList:
{field}— обычное поле модели (проверка поgetFillable()): выполняетсяSELECT DISTINCT {field}по отфильтрованному запросу. Если поле кастится вarray/collection(JSON-массив), значения схлопываются и берутся уникальные.{field}— BelongsTo-связь: берутся уникальные внешние ключи из отфильтрованных записей родительской модели, затем выбираются связанные записи.{field}— BelongsToMany-связь: id записей родительской модели по фильтрам подставляются подзапросом в pivot-таблицу, из неё достаются уникальные id связанных записей, затем выбираются сами записи.- Иначе — исключение
Поле или связь {field} не найдена в модели {model}.
Во всех случаях результат сортируется по name.
Если используется кастомная связь BelongsToMany с дополнительными условиями (например, wherePivot), она должна реализовать интерфейс ProvidesUniqueRelatedKeys, чтобы dynamic-список учитывал эти условия.
final class EntityArchitectsRelation extends BelongsToMany implements ProvidesUniqueRelatedKeys { public function getUniqueRelatedKeysQuery(): \Illuminate\Database\Query\Builder { return DB::query()->where('type', $this->architectType); } }
Пример реквеста создания
class CreateBusinessBlockRequest extends AbstractRequest { protected ?string $dtoClassName = BusinessBlockHandbookDTO::class; public function rules() { return [ 'name' => 'required|string|max:255', 'active' => 'required|bool', ]; } }
Пример реквеста обновления
class UpdateBusinessBlockRequest extends CreateBusinessBlockRequest { protected ?string $dtoClassName = BusinessBlockHandbookDTO::class; public function rules() { return [ ...parent::rules(), 'name' => [ 'sometimes', 'string', 'max:255', (new Unique(BusinessBlock::class, 'name'))->ignore($this->getRouteParameter('handbookId')) ], ]; } }
Реквесты создания/обновления обязательно должны подключать DTO ($dtoClassName) — через него сервис передает данные в репозиторий.