Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Forms Laravel Package

nette/forms

Nette Forms is a PHP library for building, rendering, and validating HTML forms with minimal code. It offers CSRF protection, file uploads, flexible controls, and a rich set of validation rules through a clean, secure, and extensible API.

View on GitHub
Deep Wiki
Context7

Getting Started

nette/forms is not a Laravel package — it is a standalone, framework-agnostic form builder from the Nette ecosystem. There is no service provider, no facade, no Blade directives, and no config/forms.php. You install it, instantiate a Form per request, and render it yourself.

composer require nette/forms

It will pull in nette/utils, nette/http, nette/component-model (and nette/php-generator if you use Blueprint). PHP 8.1+ is required on the 3.2 line, which any current Laravel app satisfies.

Where to look first

  1. vendor/nette/forms/examples/ — the fastest orientation; full working forms, both plain-PHP and Latte.
  2. Nette\Forms\Form and Nette\Forms\Container — the fluent builder API (addText, addEmail, addSelect, addUpload, addSubmit, addProtection).
  3. Nette\Forms\Validator — the catalogue of built-in rules (EMAIL, MIN_LENGTH, RANGE, PATTERN, EQUAL, …).
  4. src/assets/netteForms.js — the client-side validation engine that consumes the data-nette-rules attribute the renderer emits.

First use case — a contact form in a controller

use Nette\Forms\Form;

$form = new Form;                  // or Form::initialize() if you need standalone request/session wiring
$form->setHtmlAttribute('class', 'space-y-4');

$form->addText('name', 'Name')
    ->setRequired('Please enter your name')
    ->addRule($form::MAX_LENGTH, 'Too long', 100);

$form->addEmail('email', 'E-mail')
    ->setRequired('Please enter your e-mail');

$form->addTextArea('message', 'Message')
    ->setRequired('Please enter a message')
    ->addRule($form::MIN_LENGTH, 'At least %d characters', 10);

$form->addSubmit('send', 'Send');

$form->onSuccess[] = function (Form $form, array $values) {
    // persist however you like — Mail::to(...)->send(...), Contact::create($values)
};

if ($form->isSuccess()) {
    return redirect()->route('contact')->with('status', 'Thanks!');
}

return view('contact', ['form' => $form]);

In Blade, Nette\Forms\Form implements HtmlStringable, so {!! $form !!} renders the whole thing with labels, controls, and error messages already wired up. That is the entire "integration".

Implementation Patterns

Build forms in dedicated factory classes, not controllers. The natural Laravel analogue to a Nette presenter is an invokable action plus a ContactFormFactory (or app/Forms/ directory) that returns a configured Form. Controllers stay thin:

public function __invoke(ContactFormFactory $factory)
{
    $form = $factory->create();
    if ($form->isSuccess()) { /* ... */ }
    return view('contact', compact('form'));
}

Hydrate straight into a DTO or Eloquent model. Container::getValues() accepts a class name or an object instance and maps values onto properties (and, since 3.1.5, onto PHP 8 constructor parameters). Combined with enum support in 3.2.4, this removes a lot of hand-written assignment:

$dto = $form->getValues(ContactData::class);   // typed properties populated

Use $form->getUntrustedValues() when you need raw data before validation, and prefer getValues() inside onSuccess so you never see a partially valid payload.

Validation rules live on the control, and that is a feature. setRequired(), addRule(), addCondition(), and addFilter() co-locate the constraint with the field, plus the label substitution (%label%, %d) in messages. For conditional fields, addCondition() + toggle() is the idiomatic pattern and it stays in sync with the JS engine via Rules::getToggleStates():

$form->addCheckbox('company', 'I am ordering as a company')
    ->addCondition($form::EQUAL, true)
    ->toggle('companyId');   // hidden/shown client-side AND enforced server-side

$form->addText('companyId', 'VAT ID')
    ->addConditionOn($form['company'], $form::EQUAL, true)
    ->setRequired('VAT ID is required for company orders');

Custom validators: prefer addRule with a closure over extending Validator.

$form->addText('code', 'Coupon')
    ->addRule(fn($control) => Coupon::where('code', $control->getValue())->exists(), 'Invalid coupon');

Reusable ones belong on the prototype: Form::addPrototype('vat', ...) or Validator::addMethod(). Note that rules exported to the browser must be static/simple — a closure rule validates server-side only, which is fine but means you lose the client-side check for that field.

File uploads map cleanly onto Laravel's filesystem: addUpload('avatar')->addRule($form::MAX_FILE_SIZE, ..., 2 * 1024 * 1024) and then $form['avatar']->getValue() returns a Nette\Http\FileUpload; call ->move($path) or stream it into Storage::put(). The control auto-emits MAX_FILE_SIZE and checks upload_max_filesize.

Multi-step / nested forms use addContainer() and validation scopes (SubmitButton::setValidationScope()), which is genuinely nicer than hand-rolled Laravel partial validation for wizard flows. Container::addDynamic() handles repeatable row groups with a prototype.

Client-side validation is opt-in and asset-based: copy vendor/nette/forms/src/assets/netteForms.js into resources/js, import it in app.js (Vite), and make sure the rendered controls carry data-nette-rules (the default renderer does). Without the script everything still validates server-side — it just degrades to a full page round trip.

Rendering: for anything beyond a quick internal form, you will end up either writing Latte templates (the package's first-class path, with {input}, {label}, {inputError} macros) or rendering field-by-field in Blade:

<div>
    {!! $form['email']->getLabel() !!}
    {!! $form['email']->getControl()->addAttributes(['class' => 'input']) !!}
    @foreach ($form['email']->getErrors() as $error)
        <p class="text-red-600">{{ $error }}</p>
    @endforeach
</div>

Nette\Forms\Blueprint (3.1.15, replacing LatteRenderer/DataClassGenerator/{formPrint}) generates renderer code and the matching data class from a live form — genuinely useful for bootstrapping a template quickly.

Gotchas and Tips

There is no bridge to Laravel's error bag, old(), or @error. Errors live on controls ($form['email']->getErrors()), not in $errors. If your Blade partials are already written against @error('email'), you either re-render via the Nette renderer or write a small adapter that copies $form->getErrors() into the session error bag before redirecting. This is the single biggest day-to-day friction point.

Skip addProtection() in Laravel. Nette's CSRF protection is tied to Nette\Http\Session and Request::isSameSite(). In a Laravel app you already have VerifyCsrfToken and @csrf; using both means two tokens, two failure modes, and a confusing 403. Put @csrf in the <form> and leave addProtection() out.

Form::getValues() warns when the form is not valid (since 3.1.0) and returns only controls in the current validation scope. Call it inside onSuccess, or guard with isSuccess() — do not call it from a Blade template to prefill.

Values come back as strings/arrays, not typed scalars. addInteger() validates but still yields a string. Cast explicitly, use addFilter(), or go through DTO hydration / enum conversion. The same applies to SelectBox prompts (3.2.2 hides/disables the prompt <option> when required, and 3.2.6 fixed prompt selection) and to disabled options, which are processed at getValue() time rather than when HTTP data is loaded (3.2.5).

Request coupling is the testing trap. The form reads submitted data from an HTTP request — by default one built from PHP globals. Laravel's test client constructs its own request object, so $this->post(...) in a feature test may leave the form seeing an empty submission. Mitigations: populate $_POST/$_FILES in the test, or push validation logic into your own service and test that directly. Do not attempt to reuse a single Form instance across requests (or across Octane workers) — build it per request.

Rendering quirks worth knowing before you fight the renderer: Button is omitted from values (3.2.4), BaseControl::getOption($default) is deprecated (3.2.0), getValues(true) is deprecated in favour of passing 'array', and DefaultFormRenderer respects class/id options on the control part when the pair part is absent. Group addGroup() and ControlGroup::setOption() are how you get Bootstrap/Tailwind structure out of the default renderer without touching HTML.

Translations go through Form::setTranslator(), and Validator::formatMessage() translates the label too — if you're on Laravel's __() helper, wrap it: $form->setTranslator(new Nette\Localization\TranslatorAdapter(...)) or a one-method anon class delegating to __().

Latte is optional but not free. nette/forms works without Latte (the default renderer is plain PHP), but the docs, examples, and macro niceties all assume it. If you pull in Latte alongside Blade you now maintain two templating languages. Decide up front: default renderer + Blade, or Latte for form-heavy screens only.

Extensions points if you need to go deeper: subclass Nette\Forms\Controls\BaseControl for a custom control, Nette\Forms\Controls\SelectBox / ChoiceControl for option behaviour, and DefaultFormRenderer (renderPair, renderControl, renderLabel) for markup. Register custom validators on the prototype so they are available across the whole app.

Debugging: dump $form->getErrors() for form-level failures and $form['field']->getErrors() per control; inspect the rendered data-nette-rules attribute to confirm what the browser will enforce versus what only runs server-side. If a rule silently doesn't fire client-side, it's almost always a non-static validator or a control rendered outside the default renderer.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log