laravolt/semantic-form
Laravel package for building Semantic UI forms with a fluent API, model binding, validation integration, and ready-made input components. It simplifies creating consistent, reusable forms for CRUD apps and admin panels.
Install and wire up (10 minutes).
composer require laravolt/semantic-form
php artisan vendor:publish --provider="Laravolt\SemanticForm\ServiceProvider"
Package auto-discovery registers the service provider and a SemanticForm facade alias. Publish the config and the views on day one — published views are your only real override surface and they are what you diff after every upstream bump.
The package emits markup only; it does not ship CSS/JS. Add Semantic UI (or Fomantic) to your layout before you judge anything visually:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2/dist/semantic.min.js"></script>
Where to look first, in order:
config/semantic-form.php — template/view paths, error display behaviour, global defaults.resources/views/ inside the package (before publishing) — the actual field templates; this is where markup, error classes, and wrappers live.src/ — the builder class and the field classes. The fluent chain returns field objects, so every method you call lives on one of those; read it rather than guessing from the README.src/Macro* or the trait exposing macro() — the extension point for custom field types.First use case. Pick one internal admin edit screen over a model you already have, and replace hand-written Blade with:
{!! SemanticForm::open()->route('users.update', $user)->method('PUT') !!}
{!! SemanticForm::model($user) !!}
{!! SemanticForm::text('name')->label('Full name')->required() !!}
{!! SemanticForm::email('email')->label('Email') !!}
{!! SemanticForm::select('role')->options($roles)->label('Role') !!}
{!! SemanticForm::textarea('notes')->label('Notes') !!}
{!! SemanticForm::submit('Save')->class('primary') !!}
{!! SemanticForm::close() !!}
That single screen tells you everything that matters: whether model binding covers your field types, whether old-input and error repopulation behave the way you want after a failed validation, and whether your layout's Semantic UI version agrees with the generated class names. Verify exact method signatures against src/ for the version you pin — the README lags the code.
One form template for create and edit. The package's core value is that model binding is optional-by-null. Pass a fresh model on create and the populated one on edit; old input and validation errors take precedence over both, so the same Blade partial serves both routes:
public function edit(User $user) { return view('admin.users.form', ['user' => $user]); }
public function create() { return view('admin.users.form', ['user' => new User]); }
Order matters. Open the form, then bind the model, then render fields. Binding after fields have been instantiated is the single most common "my values are empty" cause.
Wrap it, immediately. Do not let SemanticForm:: calls scatter across 40 Blade files. Put a thin layer in between — either Blade components or a small PHP form class per screen:
{{-- resources/views/components/form/text.blade.php --}}
{!! SemanticForm::text($name)->label($label ?? Str::headline($name))->value($value ?? null) !!}
The wrapper is what converts a vendor lock into a refactor, and it is where you encode team conventions (label casing, required markers, help text placement) once instead of per form.
Selects and relations: prepare options in the controller. Keep queries out of Blade. Pluck in the controller, pass an id→label array, and pass a default when editing so binding has something to match:
$roles = Role::pluck('name', 'id');
{!! SemanticForm::select('role_id')->options($roles)->placeholder('Choose a role') !!}
Enums/booleans: map them to a small array and reuse the same select/radio helper. Centralising the mapping in the wrapper prevents each developer inventing their own wording.
Macros for recurring composite fields. When you find yourself writing the same three-field address block five times, register a macro in a service provider's boot() and return a field object or a rendered segment. This is the sanctioned extension point and keeps call sites readable. Register macros in a provider that boots before views render, and keep the closure free of request state you might not have in queue/CLI contexts.
Validation errors are the payoff — lean on them. Let Laravel's $errors bag drive field-level error rendering (the package reads it per field name) rather than hand-writing @error blocks. If you use custom FormRequest attribute names, they flow through for free.
Grid layout: compose forms out of Semantic UI's fields/field grid by rendering field objects inside your own wrapper markup where the builder doesn't give you the grouping you want. Grouping is where you will drop to raw HTML soonest — that is expected and fine.
Testing: assert on rendered output of the form partial ($this->get(...)->assertSee('name="email"')), and add one test that a bound model's value appears. These catch silent markup changes on upgrade better than anything else.
{!! !!} everywhere. Form builders return raw HTML. Never pass user-controlled data into label(), placeholder(), help(), or value() without escaping — the builder will not do it for you at every call site.withInput()/$request->flash() (Laravel does this automatically for validate() failures). If you redirect manually without flashing, the form silently reverts to the model value and looks like a binding bug.false rather than leaving the old value. For checkbox groups, pass array-style names and confirm the builder's old-input handling for name[] matches — verify against src/, this is a classic mismatch area.items[0][price]) are the weak spot. Binding and value lookup via dot notation is unreliable for repeated rows. Render per-row values explicitly with ->value($row->price) and keep the model binding for the flat fields only.$request->validateWithBag()) if you do this, or avoid it.->method('PUT')/PATCH must be set on open(); the builder emits the _method hidden input. Combine with @csrf — confirm the builder emits the CSRF field in your config; if not, add it explicitly.composer update silently wipes your work. After each bump, git diff the published views — 2.14.3 shipped with a changelog that is only a diff link, so the notes tell you nothing.(string) SemanticForm::text('name')) inside tinker or a route closure. It is faster than bisecting a Blade template and shows you exactly which template and error class are in play.How can I help you explore Laravel packages today?