Search by

vasichmen / laravel-handbooks

vasichmen

Package Handbooks

Package info

github.com/vasichmen/laravel-handbooks

pkg:composer/vasichmen/laravel-handbooks

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-10-31 10:12 UTC

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) для выпадающих списков.

Как резолвится модель:

  1. если в handbooks.dynamic.custom.{model} есть настройка — берутся её model, repository, resource, select, with, searchable_fields;
  2. иначе модель ищется по конвенции: 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:

  1. {field} — обычное поле модели (проверка по getFillable()): выполняется SELECT DISTINCT {field} по отфильтрованному запросу. Если поле кастится в array/collection (JSON-массив), значения схлопываются и берутся уникальные.
  2. {field} — BelongsTo-связь: берутся уникальные внешние ключи из отфильтрованных записей родительской модели, затем выбираются связанные записи.
  3. {field} — BelongsToMany-связь: id записей родительской модели по фильтрам подставляются подзапросом в pivot-таблицу, из неё достаются уникальные id связанных записей, затем выбираются сами записи.
  4. Иначе — исключение Поле или связь {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) — через него сервис передает данные в репозиторий.