ttbooking/formster

Laravel Formster.

Maintainers

Package info

github.com/ttbooking/formster

pkg:composer/ttbooking/formster

Transparency log

Statistics

Installs: 170

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v1.4.2 2026-07-02 10:20 UTC

README

Latest Version on Packagist Tests PHP Version Total Downloads License

Formster — это Laravel-библиотека, которая автоматически генерирует HTML-формы и таблицы-просмотры из любого PHP-объекта или Eloquent-модели, опираясь на типы свойств. Вам не нужно вручную описывать каждое поле формы: Formster читает структуру объекта из PHPDoc-аннотаций (@property), нативных типов PHP или PHP-атрибутов, подбирает подходящий виджет ввода для каждого свойства и берёт на себя обработку отправленных данных.

{{-- Вся форма — со всеми полями, подписями и кнопкой «Сохранить» — генерируется одной строкой --}}
<x-formster::form :object="$user" action="{{ route('users.update', $user) }}" />
// Обработка отправки формы — тоже одна строка
Route::put('/users/{user}', function (Request $request, User $user) {
    ActionHandler::update($request, $user)->save();

    return back();
});

Содержание

Возможности

  • 🚀 Формы без шаблонов. Объявите модель — Formster сам построит редактируемую форму или таблицу-просмотр.
  • 🧠 Несколько источников метаданных. Свойства извлекаются из PHPDoc (@property), нативных типов PHP (рефлексия) и PHP-атрибутов #[Aura] / #[AuraProperty]. Источники можно комбинировать.
  • 🧩 Богатая система типов. Поддержка union (A|B), intersection (A&B), nullable, дженериков (Collection<int, User>, list<File>, class-string<User>) и рекурсивного разбора вложенных классов.
  • 🎛️ Готовые виджеты для строк, чисел, дробных, булевых, enum, дат, часовых поясов, цветов, файлов и изображений.
  • 🖼️ Псевдотипы файлов и изображений с загрузкой через Storage, автоматическими превью (Intervention Image) и очисткой старых файлов.
  • 🔒 Интеграция с Laravel Gate. Просмотр и редактирование каждого свойства управляются политиками (viewPolicy / updatePolicy).
  • 🌍 Локализация подписей полей, описаний и enum-значений (из коробки английский и русский).
  • Кэширование результатов парсинга.
  • 🛠️ Расширяемость — собственные обработчики типов генерируются Artisan-командой.

Требования

  • PHP 8.2+ (тестируется на 8.2–8.5)
  • Laravel ^12.17 || ^13.0
  • intervention/image-laravel ^1.5 || ^4.0 (для превью изображений)

Установка

composer require ttbooking/formster

Пакет использует автообнаружение, поэтому сервис-провайдер и фасады регистрируются автоматически.

При необходимости опубликуйте конфигурацию и/или шаблоны представлений:

# конфигурация
php artisan vendor:publish --tag=formster-config

# Blade-шаблоны виджетов (для кастомизации вёрстки)
php artisan vendor:publish --tag=formster-views

Быстрый старт

1. Опишите модель

Минимально достаточно PHPDoc-аннотаций @property. Никаких $fillable, кастов или ручного объявления полей формы не требуется — тип в аннотации определяет виджет.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

/**
 * @property string $text
 * @property int $integer
 * @property bool $flag
 */
class Frankenstein extends Model
{
    protected $table = 'frankenstein';
}

2. Отрендерите форму или таблицу

{{-- Редактируемая форма (метод PUT, кнопка «Сохранить») --}}
<x-formster::form :object="$model" action="{{ route('update', $model) }}" />

{{-- Таблица только для просмотра --}}
<x-formster::form.table :object="$model" />

3. Обработайте отправку

use Illuminate\Http\Request;
use TTBooking\Formster\Facades\ActionHandler;
use App\Models\Frankenstein;

Route::get('/formster/{model}',      fn (Frankenstein $model) => view('table', compact('model')))->name('view');
Route::get('/formster/{model}/edit', fn (Frankenstein $model) => view('form', compact('model')))->name('edit');

Route::put('/formster/{model}', function (Request $request, Frankenstein $model) {
    ActionHandler::update($request, $model)->save();

    return redirect()->back();
})->name('update');

ActionHandler::update() сам обходит все свойства модели, применяет нужный обработчик к каждому полю из запроса, учитывает политики доступа и возвращает изменённый объект — остаётся лишь вызвать ->save().

Как это работает

Полный цикл «модель → форма → отправка» состоит из трёх этапов.

                ┌─────────────────────┐
   объект  ───► │   PropertyParser    │ ──► Aura { properties: AuraProperty[] }
                │  (phpstan,reflection)│
                └─────────────────────┘
                           │
                           ▼  для каждого свойства
                ┌─────────────────────┐
                │   HandlerFactory    │ ──► PropertyHandler (по типу свойства)
                └─────────────────────┘
                           │
            ┌──────────────┴───────────────┐
            ▼                              ▼
   component() → Blade-виджет     handle($obj, $request) → запись значения
   (рендеринг формы)              (обработка отправки)
  1. Парсинг. PropertyParser::parse($object) разбирает объект и возвращает агрегат Aura — описание класса со списком свойств AuraProperty (имя, тип, читаемость/записываемость, значение по умолчанию, политики доступа).
  2. Подбор обработчика. Для каждого свойства HandlerFactory::for($property) подбирает первый PropertyHandler, чей статический метод satisfies() соответствует типу свойства.
  3. Рендеринг и обработка. При выводе формы обработчик через component() указывает Blade-виджет. При отправке handle() приводит значение из Request и записывает в объект.

Ключевые сущности

Сущность Назначение
Aura «Аура» класса: краткое (summary) и полное (description) описание, коллекция свойств (проиндексирована по имени) и политики по умолчанию (viewPolicy, updatePolicy). Одновременно является PHP-атрибутом уровня класса #[Aura].
AuraProperty Описание одного свойства: readable, writable, type, variableName, description, hasDefaultValue/defaultValue, viewPolicy, updatePolicy. Одновременно является атрибутом свойства #[AuraProperty].
AuraType Система типов: AuraNamedType (именованный/дженерик-тип), AuraUnionType (`A

Описание свойств модели

Formster поддерживает три способа объявить свойства, и их можно сочетать (см. Парсеры).

Способ 1. PHPDoc-аннотации (рекомендуется)

/**
 * @property string $name              Имя пользователя
 * @property int $age
 * @property ?string $bio              Может быть null
 * @property \App\Enums\Status $status
 * @property-read int $id              Только для чтения
 * @property-write string $password    Только для записи
 */
class User extends Model {}
  • @property — свойство доступно и для чтения, и для записи;
  • @property-read — только чтение (в форме отображается, но не редактируется);
  • @property-write — только запись.

Текст после типа и имени становится описанием поля.

Способ 2. Нативные типы PHP (рефлексия)

class Dto
{
    public string $name;
    public int $age = 18;            // значение по умолчанию подхватывается автоматически
    public readonly string $id;      // readonly → только для чтения
    public ?Color $color = null;
}

Способ 3. PHP-атрибуты

Для полного контроля над метаданными можно навесить атрибуты #[Aura] и #[AuraProperty] напрямую:

use TTBooking\Formster\Entities\Aura;
use TTBooking\Formster\Entities\AuraProperty;
use TTBooking\Formster\Entities\AuraNamedType;

#[Aura(summary: 'Профиль', description: 'Данные пользователя')]
class Profile
{
    #[AuraProperty(
        readable: true,
        writable: true,
        type: new AuraNamedType('string'),
        variableName: 'nickname',
        description: 'Псевдоним',
    )]
    public string $nickname;
}

Парсеры свойств

За извлечение метаданных отвечают парсеры. Активные парсеры и их порядок задаются опцией formster.property_parser (по умолчанию phpstan,reflection).

Драйвер Источник данных
aura PHP-атрибуты #[Aura] / #[AuraProperty]
reflection Нативные типизированные public-свойства
phpdoc PHPDoc-блок класса через phpdocumentor/reflection-docblock
phpstan PHPDoc через phpstan/phpdoc-parser — поддерживает дженерики, const-выражения и рекурсивный разбор вложенных классов
aggregate Композит: объединяет несколько парсеров
(внутренний) caching Декоратор, кэширующий результат любого парсера

Агрегация

Если в property_parser указано несколько драйверов через запятую, автоматически используется драйвер aggregate. Он последовательно прогоняет объект через каждый парсер и сливает результаты через Aura::merge().

Порядок важен: парсеры, идущие позже в списке, имеют приоритет — их непустые значения перекрывают данные предыдущих. Например, при phpstan,reflection данные из PHPDoc дополняются и при совпадении перекрываются информацией из нативных типов.

Кэширование

Результат парсинга кэшируется автоматически (декоратор CachingParser). Хранилище и TTL настраиваются через formster.property_cache. Ключ кэша: formster:properties:{драйвер}:{класс}.

Обработчики свойств (Handlers)

Обработчик (PropertyHandler) связывает тип свойства с виджетом и логикой записи. Контракт:

interface PropertyHandler
{
    public static function satisfies(AuraProperty $property): bool; // подходит ли тип
    public function component(): string;                            // Blade-виджет
    public function handle(object $object, Request $request): void; // запись значения
    public function validate(Request $request): bool;               // валидация
}

HandlerFactory::for($property) перебирает обработчики из конфига formster.property_handlers и возвращает первый, для которого satisfies() вернул true. Если ни один не подошёл, используется FallbackHandler.

Поддерживаемые типы и виджеты

Тип свойства Обработчик Виджет (Blade) HTML-поле
bool BooleanHandler form.checkbox <input type="checkbox">
int IntegerHandler form.number <input type="number">
float FloatHandler form.decimal <input type="number" step="0.01">
string StringHandler form.text <input type="text">
BackedEnum EnumHandler form.radio / form.select переключатели или выпадающий список
DateTimeInterface DateTimeHandler form.datetime <input type="datetime-local">
DateTimeZone DateTimeZoneHandler form.timezone <select> с часовыми поясами
Color ColorHandler form.color <input type="color">
File / list<File> FileHandler form.file <input type="file">
Image ImageHandler form.image <input type="file"> + превью
прочее FallbackHandler form.disclaimer сообщение «тип не поддерживается»

Enum. EnumHandler рендерит переключатели (radio), если число вариантов не превышает порог buttonLimit (по умолчанию 2), и выпадающий список (select) в противном случае.

Описания вариантов enum локализуются (см. Локализация); при отсутствии перевода берётся PHPDoc-комментарий кейса или его «человекочитаемое» имя.

Псевдотипы и касты

Помимо скалярных типов Formster предоставляет четыре псевдотипа в пространстве TTBooking\Formster\Types. Они реализуют Castable, поэтому достаточно объявить их в casts() модели — Eloquent сам подберёт нужный каст, а Formster выведет соответствующий виджет.

use TTBooking\Formster\Types\{Color, DateTimeZone, File, Image};

class Product extends Model
{
    /**
     * @property ?Color $brand_color
     * @property ?DateTimeZone $timezone
     * @property ?File $manual
     * @property ?Image $photo
     */
    protected function casts(): array
    {
        return [
            'brand_color' => Color::class,
            'timezone'    => DateTimeZone::class,
            'manual'      => File::class,
            'photo'       => Image::class,
        ];
    }
}

Color — цвет

HEX-цвет в формате #RRGGBB. Рендерится как <input type="color">, в режиме просмотра — цветная плашка.

$product->brand_color = new Color('#3366ff');

Конструктор валидирует формат (/^#[a-zA-Z0-9]{6}$/) и при ошибке бросает InvalidArgumentException.

DateTimeZone — часовой пояс

Расширяет нативный \DateTimeZone, рендерится в <select>, сгруппированный по регионам. Группой можно управлять через параметры псевдотипа в аннотации:

/**
 * Все пояса, сгруппированные по регионам (по умолчанию):
 * @property ?DateTimeZone $tz
 *
 * Только пояса России (двухбуквенный ISO-код страны):
 * @property ?DateTimeZone<'RU'> $tz_ru
 */

File — файл

Загрузка файла через Storage. В режиме редактирования — <input type="file">, в режиме просмотра — ссылка на скачивание/открытие.

Параметры псевдотипа задаются дженерик-аннотацией: File<TAccept, TDisposition, TDisk>.

/**
 * PDF-документы, заголовок «attachment» (скачивание), диск «documents»:
 * @property ?File<'application/pdf', 'attachment', 'documents'> $contract
 *
 * Несколько файлов (поле получит атрибут multiple):
 * @property list<File> $attachments
 */
  • TAccept — фильтр MIME-типов для атрибута accept (по умолчанию */*);
  • TDispositionattachment (скачивание) или inline (открытие в браузере);
  • TDisk — диск файловой системы (по умолчанию из конфига).

Имена сохраняемых файлов. По умолчанию используется hashName(). Логику можно переопределить глобально, например в boot() сервис-провайдера:

use TTBooking\Formster\Types\File;

File::generateStorableNamesUsing(function ($object, $property, $uploadedFile, $disk) {
    return 'uploads/'.$uploadedFile->getClientOriginalName();
});

// вернуть поведение по умолчанию:
File::generateStorableNamesNormally();

При загрузке нового файла старый автоматически удаляется (если он не «статический» и не совпадает со значением по умолчанию). Статическими считаются файлы, чьё имя начинается с / — они хранятся на отдельном static_disk и не удаляются.

Image — изображение

Наследует File, но по умолчанию принимает image/*, открывается inline и показывает превью.

Превью генерируется через Intervention Image: изображение уменьшается до размеров formster.preview.width × height. SVG и файлы меньше порога scale_down_threshold отдаются как есть. Поддерживаются обе мажорные версии Intervention Image.

Blade-компоненты

Все компоненты доступны в пространстве имён formster::.

Структурные компоненты

Компонент Назначение Основные параметры
<x-formster::form> Полноценная <form> (POST + @method('PUT'), кнопка «Сохранить») :object, action, :show-defaults
<x-formster::form.table> Таблица свойств (просмотр или редактирование) :object, action, :editable, :show-defaults
<x-formster::form.row> Строка таблицы для одного свойства :property
<x-formster::form.input> Виджет ввода для свойства (выбирает компонент через обработчик) :property, :object

Примеры:

{{-- Готовая форма с кнопкой «Сохранить» --}}
<x-formster::form :object="$model" action="{{ route('users.update', $model) }}" />

{{-- Только таблица, без столбца значений по умолчанию --}}
<x-formster::form.table :object="$model" :editable="false" :show-defaults="false" />

{{-- Своя кнопка вместо стандартной (через слот) --}}
<x-formster::form :object="$model" action="{{ route('users.update', $model) }}">
    <x-slot:buttons>
        <button type="submit">Обновить профиль</button>
    </x-slot:buttons>
</x-formster::form>

Если среди свойств есть файл или изображение, форма автоматически получает enctype="multipart/form-data".

Компоненты-виджеты (анонимные)

Каждый виджет можно вызвать и напрямую: form.text, form.number, form.decimal, form.checkbox, form.radio, form.select, form.datetime, form.color, form.timezone, form.file, form.image, form.disclaimer.

<x-formster::form.text :property="$property" />

Виджеты используют @aware для наследования контекста (object, editable, action) от родительской таблицы и показывают либо редактируемое поле, либо представление «только для чтения».

Чтобы изменить вёрстку, опубликуйте шаблоны (vendor:publish --tag=formster-views) и отредактируйте файлы в resources/views/vendor/formster.

Контроль доступа (политики)

Видимость и редактируемость каждого свойства проверяются через Laravel Gate. У Aura и AuraProperty есть политики:

  • viewPolicy (по умолчанию view) — можно ли показывать свойство;
  • updatePolicy (по умолчанию update) — можно ли редактировать свойство.

При рендеринге таблицы и при обработке отправки Formster вызывает соответствующий метод политики модели, передавая модель и имя свойства:

class UserPolicy
{
    // $property — имя свойства, например 'email'
    public function view(?User $authUser, User $model, string $property): bool
    {
        return $property !== 'secret_field';
    }

    public function update(?User $authUser, User $model, string $property): bool
    {
        return $authUser?->isAdmin() ?? false;
    }
}

Логика «fail-open» (gate_check). Доступ предоставляется автоматически, если:

  • для объекта не определена политика, или
  • в политике отсутствует метод под проверяемую способность.

Иначе результат определяется Gate::check(). Это позволяет пользоваться Formster без написания политик, добавляя ограничения постепенно — только там, где они нужны.

Локализация

Подписи интерфейса и описания свойств переводятся. Пакет поставляется с английскими и русскими строками; их можно опубликовать и дополнить.

Подписи интерфейса

Файл lang/vendor/formster/{locale}/form.php:

Ключ RU
description Параметр
value Значение
default По умолч.
na н/д
null NULL
on / off ✔️ / ❌
open / download / uploaded открыть / скачать / загружен
save Сохранить

Описания свойств и enum-значений

Описание поля ищется по ключам перевода (с приоритетом строк приложения над пакетными):

formster.{model|object}.{alias}.{свойство_в_snake_case}

Для enum-значений:

formster.enum.{alias}.{кейс_в_snake_case}

Например, для модели App\Models\User и свойства firstName:

// lang/{locale}/formster.php
return [
    'model' => [
        'user' => [
            'first_name' => 'Имя',
            '_summary'     => 'Профиль пользователя',   // заголовок таблицы
            '_description' => 'Основные данные',         // описание таблицы
        ],
    ],
];

Если перевод не найден, описание берётся из текста PHPDoc-аннотации, а в крайнем случае генерируется из имени свойства (Str::headline).

Алиасы

alias — это ключ, под которым модель/enum фигурирует в строках локализации. По умолчанию он вычисляется из имени класса (с отбрасыванием namespace App\Models\ / App\Enums\ и переводом остатка в snake_case). Зафиксировать собственный алиас можно атрибутом #[Alias]:

use TTBooking\Formster\Attributes\Alias;

#[Alias('user')]
class Customer extends Model {}

Конфигурация

После публикации (vendor:publish --tag=formster-config) доступен файл config/formster.php:

return [

    // Парсер(ы) свойств. Несколько — через запятую (включает агрегацию).
    'property_parser' => env('FORMSTER_PROPERTY_PARSER', 'phpstan,reflection'),

    // Кэш результатов парсинга.
    'property_cache' => [
        'store' => env('FORMSTER_PROPERTY_CACHE_STORE'),      // хранилище кэша (по умолчанию — стандартное)
        'ttl'   => (int) env('FORMSTER_PROPERTY_CACHE_TTL') ?: null, // время жизни (null — бессрочно)
    ],

    // Активные обработчики свойств (порядок = приоритет проверки satisfies()).
    'property_handlers' => [
        TTBooking\Formster\Handlers\BooleanHandler::class,
        TTBooking\Formster\Handlers\IntegerHandler::class,
        TTBooking\Formster\Handlers\FloatHandler::class,
        TTBooking\Formster\Handlers\StringHandler::class,
        TTBooking\Formster\Handlers\EnumHandler::class,
        TTBooking\Formster\Handlers\DateTimeHandler::class,
        TTBooking\Formster\Handlers\DateTimeZoneHandler::class,
        TTBooking\Formster\Handlers\ColorHandler::class,
        TTBooking\Formster\Handlers\ImageHandler::class,
        TTBooking\Formster\Handlers\FileHandler::class,
    ],

    // Настройки псевдотипа File.
    'file' => [
        'disk'                => env('FORMSTER_DISK'),                         // диск для загрузок
        'static_disk'         => env('FORMSTER_STATIC_DISK', env('FORMSTER_DISK')), // диск для статических файлов
        'content_disposition' => env('FORMSTER_CONTENT_DISPOSITION', 'attachment'),
        'show_uploaded_name'  => (bool) env('FORMSTER_SHOW_FILENAME', true),   // показывать имя файла в ссылке
    ],

    // Настройки превью для псевдотипа Image.
    'preview' => [
        'width'                => (int) env('FORMSTER_PREVIEW_WIDTH', 100),
        'height'               => (int) env('FORMSTER_PREVIEW_HEIGHT', 100),
        'scale_down_threshold' => (int) env('FORMSTER_PREVIEW_SCALE_DOWN_THRESHOLD', 10_240), // байт
    ],

];

Переменные окружения

Переменная Назначение По умолчанию
FORMSTER_PROPERTY_PARSER Парсер(ы) свойств phpstan,reflection
FORMSTER_PROPERTY_CACHE_STORE Хранилище кэша стандартное
FORMSTER_PROPERTY_CACHE_TTL TTL кэша (сек) бессрочно
FORMSTER_DISK Диск для загрузок диск по умолчанию
FORMSTER_STATIC_DISK Диск для статических файлов FORMSTER_DISK
FORMSTER_CONTENT_DISPOSITION Поведение файлов attachment
FORMSTER_SHOW_FILENAME Показывать имя файла true
FORMSTER_PREVIEW_WIDTH / _HEIGHT Размер превью 100 / 100
FORMSTER_PREVIEW_SCALE_DOWN_THRESHOLD Порог уменьшения превью 10240

Создание собственного обработчика

Чтобы добавить поддержку нового типа, сгенерируйте обработчик командой:

php artisan make:formster-handler MoneyHandler --type=Money
  • --type (-t) — обрабатываемый тип или класс;
  • --force (-f) — перезаписать существующий класс.

Команда интерактивна: при отсутствии аргументов спросит имя и предложит выбрать тип из каталога app/Formster/Types. Класс создаётся в namespace App\Formster\Handlers.

Сгенерированный обработчик:

namespace App\Formster\Handlers;

use App\Formster\Types\Money;
use Illuminate\Http\Request;
use TTBooking\Formster\Contracts\PropertyHandler;
use TTBooking\Formster\Entities\AuraProperty;

class MoneyHandler implements PropertyHandler
{
    public function __construct(public AuraProperty $property) {}

    public static function satisfies(AuraProperty $property): bool
    {
        return $property->type->contains(Money::class);
    }

    public function component(): string
    {
        return 'formster::form.money';
    }

    public function handle(object $object, Request $request): void
    {
        $object->{$this->property->variableName} = new Money($request->{$this->property->variableName});
    }

    public function validate(Request $request): bool
    {
        return true;
    }
}

Зарегистрируйте обработчик в config/formster.php (в массиве property_handlers, до FileHandler/FallbackHandler) и создайте Blade-виджет form.money.

Стаб генератора можно опубликовать и кастомизировать, поместив stubs/handler.stub в корень приложения.

Очистка осиротевших файлов

Чтобы загруженные файлы удалялись при удалении модели, подключите обсервер OrphanedFileCollector:

use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use TTBooking\Formster\Observers\OrphanedFileCollector;

#[ObservedBy(OrphanedFileCollector::class)]
class Product extends Model {}

При удалении модели обсервер удаляет все привязанные файлы (File/Image), кроме статических (имя начинается с /). При мягком удалении (SoftDeletes без force-delete) файлы сохраняются.

Фасады и публичный API

Фасад Класс Назначение
PropertyParser PropertyParserManager parse($objectOrClass): Aura — разбор объекта/класса в метаданные
PropertyHandler HandlerFactory for(AuraProperty $property): PropertyHandler — подбор обработчика
ActionHandler ActionHandler update(Request $request, object $object): object — применение данных запроса к объекту
use TTBooking\Formster\Facades\PropertyParser;

$aura = PropertyParser::parse(App\Models\User::class);

foreach ($aura->properties as $property) {
    echo $property->variableName.': '.$property->type.PHP_EOL;
}

Тестирование и качество кода

Пакет использует Pest, PHPStan (larastan, level max) и Laravel Pint. Доступные composer-скрипты:

composer test      # запуск тестов (Pest)
composer analyse   # статический анализ (PHPStan)
composer lint      # проверка стиля (Pint --test)
composer serve     # запуск демо-приложения (workbench)

CI прогоняет матрицу PHP 8.2–8.5 × Laravel 12.17 / 13.0 (prefer-lowest и prefer-stable).

Лицензия

Formster распространяется по лицензии MIT. Подробности — в файле LICENSE.md.