PHPackages                             ttbooking/formster - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ttbooking/formster

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ttbooking/formster
==================

Laravel Formster.

v1.7.2(3w ago)02721MITPHPPHP ^8.2CI passing

Since Sep 5Pushed 1w agoCompare

[ Source](https://github.com/ttbooking/formster)[ Packagist](https://packagist.org/packages/ttbooking/formster)[ RSS](/packages/ttbooking-formster/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (10)Dependencies (81)Versions (31)Used By (0)

Laravel Formster
================

[](#laravel-formster)

[![Latest Version on Packagist](https://camo.githubusercontent.com/4e7eea806bb37e4bb389acefa1b33c6273db6faeb5625a6a11ec75057b6645d0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7474626f6f6b696e672f666f726d737465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ttbooking/formster)[![Tests](https://camo.githubusercontent.com/2d71fc1be66d7803eb92619a79d54ef9f97f376084feb4e812180f71e6af4141/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7474626f6f6b696e672f666f726d737465722f74657374732e796d6c3f6272616e63683d6d6173746572266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/ttbooking/formster/actions/workflows/tests.yml)[![PHP Version](https://camo.githubusercontent.com/42d3e6b5abf352035b392ed4cb5e108068049351ab71286bb2259ae43ac45dc8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f7474626f6f6b696e672f666f726d737465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ttbooking/formster)[![Total Downloads](https://camo.githubusercontent.com/5c9a060d88353f698cdb9b1d9949c83d904eecca3a422c13daaf04b2a91db6c2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7474626f6f6b696e672f666f726d737465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ttbooking/formster)[![License](https://camo.githubusercontent.com/6885d39002fca2d1df8532aff54a321ba51361a40a232584844d3d73afd7301a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f7474626f6f6b696e672f666f726d737465722e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

**English** · [Русский](README.ru.md)

**Formster** is a Laravel library that **automatically generates HTML forms and read-only tables from any PHP object or Eloquent model**, based on property types. You don't have to describe every form field by hand: Formster reads the object's structure from PHPDoc annotations (`@property`), native PHP types, or PHP attributes, picks a suitable input widget for each property, and takes care of processing the submitted data.

```
{{-- The whole form — with every field, label, and a "Save" button — is generated in a single line --}}

```

```
// Handling the form submission is a one-liner too
Route::put('/orders/{order}', function (Request $request, Order $order) {
    ActionHandler::update($request, $order)->save();

    return back();
});
```

---

Table of contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start](#quick-start)
- [How it works](#how-it-works)
- [Describing model properties](#describing-model-properties)
- [Property parsers](#property-parsers)
- [Property handlers](#property-handlers)
- [Supported types and widgets](#supported-types-and-widgets)
- [Validation](#validation)
- [Pseudotypes and casts](#pseudotypes-and-casts)
    - [Color](#color)
    - [DateTimeZone](#datetimezone)
    - [File](#file)
    - [Image](#image)
- [Blade components](#blade-components)
- [Access control (policies)](#access-control-policies)
- [Localization](#localization)
- [Aliases](#aliases)
- [Configuration](#configuration)
- [Writing your own handler](#writing-your-own-handler)
- [Cleaning up orphaned files](#cleaning-up-orphaned-files)
- [Facades and public API](#facades-and-public-api)
- [Testing and code quality](#testing-and-code-quality)
- [License](#license)

---

Features
--------

[](#features)

- 🚀 **Templateless forms.** Declare a model — Formster builds an editable form or a read-only table for it.
- 🧠 **Multiple metadata sources.** Properties are extracted from PHPDoc (`@property`), native PHP types (reflection), and the `#[Aura]` / `#[AuraProperty]` PHP attributes. Sources can be combined.
- 🧩 **Rich type system.** Support for union (`A|B`), intersection (`A&B`), nullable, generics (`Collection`, `list`, `class-string`), and recursive parsing of nested classes.
- 🎛️ **Ready-made widgets** for strings, integers, floats, booleans, enums, dates, time zones, colors, files, and images.
- ✅ **Automatic validation.** Every handler contributes default rules for its field; per-property rules are declared in the metadata and merged with the defaults via the `'...'` notation, and error messages are rendered next to the fields.
- 🖼️ **File and image pseudotypes** with `Storage` uploads, automatic previews (Intervention Image), and orphaned file cleanup.
- 🔒 **Laravel Gate integration.** Viewing and editing of every property is governed by policies (`viewPolicy` / `updatePolicy`), with a lenient mode by default and a switchable enforcing mode.
- 🌍 **Localization** of field labels, descriptions, and enum cases (English and Russian out of the box).
- ⚡ **Caching** of parsing results.
- 🛠️ **Extensibility** — custom type handlers are scaffolded by an Artisan command.

---

Requirements
------------

[](#requirements)

- PHP **8.2+** (tested on 8.2–8.5)
- Laravel **^12.17 || ^13.0**
- `intervention/image-laravel` `^1.5 || ^4.0` (for image previews)

---

Installation
------------

[](#installation)

```
composer require ttbooking/formster
```

The package uses auto-discovery, so the service provider and facades are registered automatically.

If needed, publish the configuration and/or the view templates:

```
# configuration
php artisan vendor:publish --tag=formster-config

# Blade widget templates (to customize markup)
php artisan vendor:publish --tag=formster-views

# the handler generator stub (to customize make:formster-handler)
php artisan vendor:publish --tag=formster-stub
```

---

Quick start
-----------

[](#quick-start)

### 1. Describe the model

[](#1-describe-the-model)

PHPDoc `@property` annotations are enough as a minimum. No `$fillable`, casts, or manual form-field declarations are required — the annotated type determines the widget.

```
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use TTBooking\Formster\Entities\Aura;

/**
 * @property string $text Some text
 * @property int $number Some number
 * @property bool $flag Some flag
 */
#[Aura]
class Frankenstein extends Model
{
    protected $table = 'frankenstein';
}
```

> The `#[Aura]` attribute enables the lenient access-policy mode for the model: until you write policies, every property is visible and editable. Without the attribute (and without policies) the regular `Gate` denies access and the form comes out empty. See [Access control](#access-control-policies) for details.

### 2. Render a form or a table

[](#2-render-a-form-or-a-table)

```
{{-- Editable form (PUT method, "Save" button) --}}

{{-- Read-only table --}}

```

### 3. Handle the submission

[](#3-handle-the-submission)

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

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 back();
})->name('update');
```

`ActionHandler::update()` first validates the request (the rules are gathered from each property's handler and metadata, and error messages address fields by their localized descriptions), then walks over the model's writable properties (read-only ones are skipped), applies the appropriate handler to each field, respects access policies, and returns the modified object — all that's left is to call `->save()`.

---

How it works
------------

[](#how-it-works)

The full "model → form → submission" cycle consists of three stages.

```
                ┌─────────────────────┐
   object  ───► │   PropertyParser    │ ──► Aura { properties: AuraProperty[] }
                │ (aura,phpstan,refl.)│
                └─────────────────────┘
                           │
                           ▼  for each property
                ┌─────────────────────┐
                │   HandlerFactory    │ ──► PropertyHandler (by property type)
                └─────────────────────┘
                           │
            ┌──────────────┴───────────────┐
            ▼                              ▼
   component() → Blade widget      handle($obj, $request) → write value
   (rendering the form)            (processing the submission)

```

1. **Parsing.** `PropertyParser::parse($object)` inspects the object and returns an aggregate **`Aura`** — a class description with a list of **`AuraProperty`** entries (name, type, readability/writability, default value, validation rules, access policies). Before use, `finalize()` filters the property list against the `include`/`exclude` lists of `#[Aura]` and turns the aggregate into the immutable **`FinalAura`** / **`FinalAuraProperty`** counterparts — this is what handlers and widgets receive.
2. **Handler selection.** For each property, `HandlerFactory::for($property)` picks the first `PropertyHandler` whose static `satisfies()` method matches the property type.
3. **Rendering and processing.** When rendering the form the handler's `component()` names the Blade widget. On submission the request is first validated against the rules from `validationRules()`, then `handle()` casts the value from the `Request` and writes it into the object.

### Key entities

[](#key-entities)

EntityPurpose**`Aura`**The class "aura": short (`summary`) and full (`description`) descriptions, a collection of properties (indexed by name), default policies (`viewPolicy`, `updatePolicy`), and the `include`/`exclude` property filters. Also serves as the class-level `#[Aura]` attribute.**`AuraProperty`**A single property description: `readable`, `writable`, `type`, `variableName`, `description`, `hasDefaultValue`/`defaultValue`, `validationRules`, `viewPolicy`, `updatePolicy`. Also serves as the `#[AuraProperty]` property attribute.**`FinalAura`** / **`FinalAuraProperty`**The finalized (immutable, fully populated) counterparts produced by `Aura::finalize()` once all parsers have been merged. Handlers and Blade components work with these.**`AuraType`**The type system: `AuraNamedType` (named/generic type), `AuraUnionType` (`A|B`), `AuraIntersectionType` (`A&B`). Provides `contains()` and a `nullable` flag.---

Describing model properties
---------------------------

[](#describing-model-properties)

Formster supports **three ways** to declare properties, and they can be combined (see [Parsers](#property-parsers)).

### Way 1. PHPDoc annotations (recommended)

[](#way-1-phpdoc-annotations-recommended)

```
/**
 * @property string $name              User name
 * @property int $age
 * @property string|null $bio          May be null
 * @property \App\Enums\Status $status
 * @property-read int $id              Read-only
 * @property-write string $password    Write-only
 */
class User extends Model {}
```

- `@property` — the property is both readable and writable;
- `@property-read` — read-only (shown in the form but not editable);
- `@property-write` — write-only.

The text after the type and name becomes the field description.

### Way 2. Native PHP types (reflection)

[](#way-2-native-php-types-reflection)

```
class Dto
{
    public string $name;
    public int $age = 18;            // the default value is picked up automatically
    public readonly string $id;      // readonly → read-only
    public ?Color $color = null;
}
```

### Way 3. PHP attributes

[](#way-3-php-attributes)

For full control over the metadata you can attach the `#[Aura]` and `#[AuraProperty]` attributes directly:

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

#[Aura(summary: 'Profile', description: 'User data')]
class Profile
{
    #[AuraProperty(
        type: new AuraNamedType('string'),
        description: 'Nickname',
        validationRules: ['...', 'min:3'],
    )]
    public string $nickname;
}
```

Every `#[AuraProperty]` parameter is optional — the attribute only complements or overrides what the other parsers have found. Properties can also be described at the class level via the `properties` parameter of `#[Aura]` (keyed by property name):

```
#[Aura(properties: [
    'text' => new AuraProperty(validationRules: ['...', 'min:3']),
])]
class Frankenstein extends Model {}
```

The `#[Aura]` attribute can also decide which properties make it into the form: `include` is a whitelist (omit it and everything passes), `exclude` is a blacklist:

```
#[Aura(exclude: ['password', 'remember_token'])]
class User extends Model {}
```

The filter is applied at the finalization stage, so it also strips properties discovered by the other parsers — PHPDoc or reflection. When auras are merged (aggregation, inheritance), both lists are combined; `exclude` always beats `include`.

Attributes are picked up from parent classes as well: the `aura` parser walks the whole inheritance chain and merges the metadata into a single aura — shared descriptions and filters can live on a base class. On conflicts the descendant wins: metadata declared closer to the class itself overrides the ancestors'.

---

Property parsers
----------------

[](#property-parsers)

Parsers are responsible for extracting metadata. The active parsers and their order are set by the `formster.property_parser` option (default `aura,phpstan,reflection`).

DriverData source`aura`The `#[Aura]` / `#[AuraProperty]` PHP attributes — from the class itself and all of its ancestors`reflection`Native typed `public` properties`phpdoc`The class PHPDoc block via `phpdocumentor/reflection-docblock``phpstan`PHPDoc via `phpstan/phpdoc-parser` — supports generics, const expressions, and **recursive nested-class parsing**`aggregate`Composite: combines several parsers(internal) `caching`A decorator that caches the result of any parser### Aggregation

[](#aggregation)

If several drivers are listed comma-separated in `property_parser`, the `aggregate` driver is used automatically. It runs the object through each parser in turn and **merges** the results via `Aura::merge()`.

> **Order matters:** parsers listed later have priority. Same-named properties are merged field by field (`AuraProperty::merge()`): each piece of metadata (type, description, policy, …) is taken from the later parser when set there, and kept from the earlier one otherwise. Validation rules are merged using the `'...'` notation (see [Validation](#validation)).

### Caching

[](#caching)

Parsing results are cached automatically (the `CachingParser` decorator). The store and TTL are configured via `formster.property_cache`. The cache key is `formster:properties:{driver}:{class}`.

---

Property handlers
-----------------

[](#property-handlers)

A handler (`PropertyHandler`) ties a property type to a widget, validation rules, and to the write logic. The contract:

```
interface PropertyHandler
{
    public static function satisfies(FinalAuraProperty $property): bool; // does the type match?
    public function component(): string;                                 // Blade widget
    public function validationRules(): string|array;                     // validation rules for the field
    public function handle(object $object, Request $request): void;      // write the value
}
```

`HandlerFactory::for($property)` iterates over the handlers from the `formster.property_handlers` config and returns the first one whose `satisfies()` returned `true`. If none match, `FallbackHandler` is used.

> Handlers receive the finalized `FinalAuraProperty`. The rules returned by `validationRules()` are applied automatically when the submission is processed — see [Validation](#validation).

---

Supported types and widgets
---------------------------

[](#supported-types-and-widgets)

Property typeHandlerWidget (Blade)HTML fieldDefault validation rules`bool``BooleanHandler``form.checkbox````sometimes|in:on``int``IntegerHandler``form.number````required|integer``float``FloatHandler``form.decimal````required|numeric``string``StringHandler``form.text````required|string``BackedEnum``EnumHandler``form.radio` / `form.select`radio buttons or a dropdown`required` + `Rule::enum(...)``DateTimeInterface``DateTimeHandler``form.datetime````required|date``DateTimeZone``DateTimeZoneHandler``form.timezone``` with time zones`required|timezone``Color``ColorHandler``form.color````required|hex_color``File` / `list``FileHandler``form.file````required|file``Image``ImageHandler``form.image``` + preview`required|image:allow_svg`*anything else*`FallbackHandler``form.disclaimer`an "unsupported type" message—**Enum.** `EnumHandler` renders radio buttons (`radio`) when the number of cases does not exceed the `buttonLimit` threshold (default **2**), and a dropdown (`select`) otherwise.

Enum case descriptions are localized (see [Localization](#localization)); if no translation is found, the case's PHPDoc comment or its "humanized" name is used.

---

Validation
----------

[](#validation)

Before writing anything, `ActionHandler::update()` validates the request with the standard `$request->validate()`. The rules are collected automatically:

1. Every handler contributes default rules for its type via `validationRules()` (see the table above).
2. Per-property rules are declared with the `validationRules` parameter of the `#[AuraProperty]` attribute — as a string (`'required|min:3'`), an array, or a closure returning a rule list (closures in attributes require PHP ≥ 8.5).
3. Rules declared on a property **replace** the handler defaults. To **extend** the defaults instead, include the `'...'` element — it is substituted with the handler's rules (the lists are flattened and de-duplicated):

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

#[Aura(properties: [
    // resulting rules: required|string|min:3
    'text' => new AuraProperty(validationRules: ['...', 'min:3']),
])]
class Frankenstein extends Model {}
```

On failure the usual Laravel mechanics kick in (redirect back with `$errors`). The widgets render the message below the field in a `formster-validation-failed` block, and error messages address the field by its localized description (see [Localization](#localization)).

---

Pseudotypes and casts
---------------------

[](#pseudotypes-and-casts)

Beyond scalar types, Formster provides four **pseudotypes** in the `TTBooking\Formster\Types` namespace. They implement `Castable`, so declaring them in the model's `casts()` is enough — Eloquent picks the right cast, and Formster derives the matching widget.

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

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

### Color

[](#color)

A HEX color in `#RRGGBB` format. Rendered as ``, and as a colored swatch in view mode.

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

The constructor validates the format (`/^#[0-9a-fA-F]{6}$/`) and throws `InvalidArgumentException` on error.

### DateTimeZone

[](#datetimezone)

Extends the native `\DateTimeZone`, rendered as a `` grouped by region. The group can be controlled via the pseudotype's parameters in the annotation:

```
/**
 * All zones, grouped by region (default):
 * @property DateTimeZone $tz
 *
 * US-only zones (two-letter ISO country code):
 * @property DateTimeZone $tz_us
 */
```

### File

[](#file)

File upload via `Storage`. In edit mode it's an ``, in view mode it's a download/open link.

Pseudotype parameters are set via a generic annotation: `File`.

```
/**
 * PDF documents, "attachment" disposition (download), "documents" disk:
 * @property File $contract
 *
 * Multiple files (the field gets the multiple attribute):
 * @property list $attachments
 */
```

- `TAccept` — a MIME-type filter for the `accept` attribute (default `*/*`);
- `TDisposition` — `attachment` (download) or `inline` (open in the browser);
- `TDisk` — the filesystem disk (default from config).

> A `list` field renders a multi-file input and the form gets the proper `enctype`, but server-side processing of multiple uploaded files is not implemented yet — `FileHandler` currently skips array uploads.

**Stored file names.** By default `hashName()` is used. The logic can be overridden globally, e.g. in a service provider's `boot()`:

```
use Illuminate\Http\UploadedFile;
use TTBooking\Formster\Entities\FinalAuraProperty;
use TTBooking\Formster\Types\File;

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

// restore the default behavior:
File::generateStorableNamesNormally();
```

When a new file is uploaded, the old one is deleted automatically (unless it is "static" or equals the default value). Static files are those whose name starts with `/` — they live on a separate `static_disk` and are never deleted.

### Image

[](#image)

Inherits from `File`, but accepts `image/*` by default, opens `inline`, and **shows a preview**.

The preview is generated via **Intervention Image**: the image is scaled down to `formster.preview.width × height`. SVGs and files smaller than the `scale_down_threshold` are served as-is. Both major versions of Intervention Image are supported.

---

Blade components
----------------

[](#blade-components)

All components are available under the `formster::` namespace.

### Structural components

[](#structural-components)

ComponentPurposeMain parameters``A full `` (POST + `@method('PUT')`, "Save" button)`:object`, `action`, `:show-defaults```A table of properties (view or edit)`:object`, `action`, `:editable`, `:show-defaults```A table row for a single property`:property```An input widget for a property (picks the component via a handler)`:property`, `:object`**Examples:**

```
{{-- A ready-made form with a "Save" button --}}

{{-- Table only, without the defaults column --}}

{{-- A custom button instead of the default one (via a slot) --}}

        Update profile

```

> If any of the properties is a file or an image — including fields declared as `list` — the form automatically gets `enctype="multipart/form-data"`.

### Widget components (anonymous)

[](#widget-components-anonymous)

Each widget can also be called directly: `form.text`, `form.number`, `form.decimal`, `form.checkbox`, `form.radio`, `form.select`, `form.datetime`, `form.color`, `form.timezone`, `form.file`, `form.image`, `form.disclaimer`.

```

```

Widgets use `@aware` to inherit context from the parent table (`object`, `editable`; the file widgets also inherit `action`) and show either an editable field or a read-only view. When validation fails, the error message is rendered below the field in a `` block.

To change the markup, publish the templates (`vendor:publish --tag=formster-views`) and edit the files in `resources/views/vendor/formster`.

---

Access control (policies)
-------------------------

[](#access-control-policies)

The visibility and editability of every property are checked through **Laravel Gate**. Both `Aura` and `AuraProperty` carry policies:

- `viewPolicy` (default `view`) — whether the property may be **shown**;
- `updatePolicy` (default `update`) — whether the property may be **edited**.

Where and how the checks run:

- **table row visibility** — the class-level and property-level `viewPolicy` combined, arguments `[$object, $propertyName]`;
- **row editability** and **submission processing** — the class-level and property-level `updatePolicy` combined, same arguments;
- **the "Save" button** — the class-level `updatePolicy` only, with just the object as the argument (no property name).

The policy method receives the model and the property name:

```
class OrderPolicy
{
    // $property — the property name, e.g. 'email'
    public function view(User $user, Order $order, ?string $property = null): bool
    {
        return $property !== 'secret_field';
    }

    public function update(User $user, Order $order, ?string $property = null): bool
    {
        return $user->isAdmin();
    }
}
```

### Lenient mode — the default

[](#lenient-mode--the-default)

By default Formster runs in a lenient mode: access is granted automatically when

- **no** policy is defined for the object, **or**
- the policy **has no method** for the ability being checked.

This lets you use Formster without writing any policies, adding restrictions gradually — only where they are needed.

It is implemented via the `TTBooking\Formster\Support\LenientPolicy` `Gate::before()` callback, registered in the service provider. The callback only steps in for objects marked with the `#[Aura]` attribute, and only when the policy/method is missing: in that case it returns `true` (full access). In all other cases it returns `null` and hands control to the standard `Gate::check()`. Objects without `#[Aura]` are left untouched.

> **Important:** the lenient mode only applies to classes marked with the `#[Aura]` attribute — on the class itself or on any of its ancestors. A model described by PHPDoc annotations alone (without the attribute) is **not** covered by the lenient mode: if no policy is defined for it, the regular `Gate::check()` denies access and its properties are not shown. So either mark your models with `#[Aura]`, or write policies.

### Enforcing mode

[](#enforcing-mode)

If you want the regular Laravel Gate behavior (missing policy/method → access **denied**), enable the enforcing mode:

```
FORMSTER_ENFORCE_POLICIES=true
```

or in `config/formster.php`:

```
'enforce_policies' => true,
```

When `enforce_policies = true`, the `LenientPolicy` callback is not registered, and the outcome of every check is fully determined by your policies via `Gate::check()`.

---

Localization
------------

[](#localization)

Interface labels and property descriptions are translatable. The package ships with English and Russian strings; they can be published and extended.

### Interface labels

[](#interface-labels)

The `lang/vendor/formster/{locale}/form.php` file:

KeyEN`description`Parameter`value`Value`default`Default`na`N/A`null`NULL`on` / `off`✔️ / ❌`open` / `download` / `uploaded`open / download / uploaded`save`Save### Property and enum case descriptions

[](#property-and-enum-case-descriptions)

A field description is looked up by translation keys (application strings take priority over the package ones):

```
formster.{model|object}.{alias}.{property_name_in_snake_case}

```

For enum cases:

```
formster.enum.{alias}.{enum_case_in_snake_case}

```

For example, for the `App\Models\User` model and the `firstName` property:

```
// lang/{locale}/formster.php
return [
    'model' => [
        'user' => [
            'first_name' => 'First name',
            '_summary'     => 'User profile',       // table heading
            '_description' => 'Basic data',         // table description
        ],
    ],
];
```

If no translation is found, the PHPDoc annotation text is used; if that is absent too, the description is generated from the property name (`Str::headline`).

---

Aliases
-------

[](#aliases)

An `alias` is the key under which a model/enum appears in localization strings. By default it is derived from the class name (dropping the `App\Models\` / `App\Enums\` namespace and converting the rest to `snake_case`). You can pin your own alias with the `#[Alias]` attribute:

```
use TTBooking\Formster\Attributes\Alias;

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

---

Configuration
-------------

[](#configuration)

After publishing (`vendor:publish --tag=formster-config`) the `config/formster.php` file is available:

```
return [

    // The property parser(s). Several — comma-separated (enables aggregation).
    'property_parser' => env('FORMSTER_PROPERTY_PARSER', 'aura,phpstan,reflection'),

    // Parsing-result cache.
    'property_cache' => [
        'store' => env('FORMSTER_PROPERTY_CACHE_STORE'),      // cache store (default — the standard one)
        'ttl'   => (int) env('FORMSTER_PROPERTY_CACHE_TTL') ?: null, // TTL (null — forever)
    ],

    // Active property handlers (order = satisfies() check priority).
    '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,
    ],

    // Policy enforcement:
    // false — lenient mode (grant access when policy/method is missing),
    // true — the regular Laravel Gate behavior.
    'enforce_policies' => (bool) env('FORMSTER_ENFORCE_POLICIES', false),

    // File pseudotype settings.
    'file' => [
        'disk'                => env('FORMSTER_DISK'),                         // disk for uploads
        'static_disk'         => env('FORMSTER_STATIC_DISK', env('FORMSTER_DISK')), // disk for static files
        'content_disposition' => env('FORMSTER_CONTENT_DISPOSITION', 'attachment'),
        'show_uploaded_name'  => (bool) env('FORMSTER_SHOW_FILENAME', true),   // show the file name in the link
    ],

    // Preview settings for the Image pseudotype.
    '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), // bytes
    ],

];
```

### Environment variables

[](#environment-variables)

VariablePurposeDefault`FORMSTER_PROPERTY_PARSER`Property parser(s)`aura,phpstan,reflection``FORMSTER_ENFORCE_POLICIES`Policy enforcement mode`false``FORMSTER_PROPERTY_CACHE_STORE`Cache storestandard`FORMSTER_PROPERTY_CACHE_TTL`Cache TTL (sec)forever`FORMSTER_DISK`Disk for uploadsdefault disk`FORMSTER_STATIC_DISK`Disk for static files`FORMSTER_DISK``FORMSTER_CONTENT_DISPOSITION`File disposition`attachment``FORMSTER_SHOW_FILENAME`Show the file name`true``FORMSTER_PREVIEW_WIDTH` / `_HEIGHT`Preview size`100` / `100``FORMSTER_PREVIEW_SCALE_DOWN_THRESHOLD`Preview scale-down threshold`10240`---

Writing your own handler
------------------------

[](#writing-your-own-handler)

To add support for a new type, scaffold a handler with the command:

```
php artisan make:formster-handler MoneyHandler --type=Money
```

- `--type` (`-t`) — the handled type or class;
- `--force` (`-f`) — overwrite an existing class.

The command is interactive: with no arguments it asks for a name and lets you pick a type from the `app/Formster/Types` directory. The class is created in the `App\Formster\Handlers` namespace.

The generated handler:

```
namespace App\Formster\Handlers;

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

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

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

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

    public function validationRules(): string|array
    {
        return $this->property->mergeValidationRules();
    }

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

`mergeValidationRules($defaults)` merges the rules declared on the property with the handler's default rules using the `'...'` notation (see [Validation](#validation)).

Register the handler in `config/formster.php` (in the `property_handlers` array, before `FallbackHandler`) and create the `form.money` Blade widget.

The generator stub can be published (`vendor:publish --tag=formster-stub`) and customized — the command picks up `stubs/formster-handler.stub` from the application root.

---

Cleaning up orphaned files
--------------------------

[](#cleaning-up-orphaned-files)

To have uploaded files deleted when a model is deleted, attach the `OrphanedFileCollector` observer:

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

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

When the model is deleted, the observer removes all attached files (`File`/`Image`) except static ones (name starting with `/`). On a **soft** delete (SoftDeletes without a force delete) the files are kept.

---

Facades and public API
----------------------

[](#facades-and-public-api)

FacadeClassPurpose`PropertyParser``PropertyParserManager``parse($objectOrClass): Aura` — parse an object/class into metadata`PropertyHandler``HandlerFactory``for(FinalAuraProperty $property): PropertyHandler` — pick a handler`ActionHandler``ActionHandler``update(Request $request, object $object): object` — apply request data to the 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;
}
```

---

Testing and code quality
------------------------

[](#testing-and-code-quality)

The package uses **Pest**, **PHPStan (larastan, level max)**, and **Laravel Pint**. Available composer scripts:

```
composer test      # run the tests (Pest)
composer analyse   # static analysis (PHPStan)
composer lint      # code-style check (Pint --test)
composer serve     # run the demo app (workbench)
```

CI runs a matrix of PHP 8.2–8.5 × Laravel 12.17 / 13.0 (prefer-lowest and prefer-stable); the PHP 8.2 × Laravel 13 combination is excluded.

---

License
-------

[](#license)

Formster is released under the **MIT** license. See the [LICENSE.md](LICENSE.md) file for details.

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance97

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 95.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~12 days

Recently: every ~1 days

Total

28

Last Release

22d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/40a68a3f1473968f24ec70a09cd3a534f657a48e7d65ce187f8af3481c416ce0?d=identicon)[daniser](/maintainers/daniser)

---

Top Contributors

[![daniser](https://avatars.githubusercontent.com/u/5169543?v=4)](https://github.com/daniser "daniser (68 commits)")[![EgorGruzdev](https://avatars.githubusercontent.com/u/930669?v=4)](https://github.com/EgorGruzdev "EgorGruzdev (3 commits)")

---

Tags

laravelformster

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/ttbooking-formster/health.svg)

```
[![Health](https://phpackages.com/badges/ttbooking-formster/health.svg)](https://phpackages.com/packages/ttbooking-formster)
```

###  Alternatives

[nasirkhan/laravel-starter

A CMS like modular Laravel starter project.

1.4k2.7k](/packages/nasirkhan-laravel-starter)[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17930.8k](/packages/markwalet-nova-modal-response)[ronasit/laravel-helpers

Provided helpers function and some helper class.

2087.0k31](/packages/ronasit-laravel-helpers)[team-nifty-gmbh/tall-datatables

Server-side rendered datatables for Laravel and Livewire

1422.0k5](/packages/team-nifty-gmbh-tall-datatables)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.8k](/packages/tomshaw-electricgrid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
