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

Html Quickform2 Laravel Package

pear/html_quickform2

HTML_QuickForm2 is a PHP library for creating, validating, and rendering HTML forms, with server-side and client-side validation, multipage wizards, and pluggable elements and renderers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and locate

composer require pear/html_quickform2:^2.5

Nothing registers itself. There is no service provider, facade, config file, Blade directive or publish command — this is a framework-agnostic PEAR library that happens to be Composer-installable. Since 2.1 it is pure-autoload, so no require_once is needed (and you should not add one; mixing the PEAR-installed copy with the Composer copy in the same process is a support nightmare).

Where to look, in order:

  1. vendor/pear/html_quickform2/docs/examples/ — the only real usage reference; the PEAR manual pages linked from the README are ancient but still accurate for the core API.
  2. vendor/pear/html_quickform2/src/HTML/QuickForm2/ — Element/ (what type strings map to what class), Rule/ (what validators ship), Renderer/ (output and plugins).
  3. vendor/pear/html_quickform2/Release/v2.5.0.md — read this before blaming the library for a deprecation. 2.5.0 is a compatibility-only release (PHP 8.5/8.6 clean, PHPUnit-Polyfills bump); there is no new API surface in recent history, so don't hunt for features.

First use case

A single-page "contact" form posted back to the same URI, built in the controller, echoed in Blade.

// app/Http/Controllers/ContactController.php
use Illuminate\Http\Request;

public function contact(Request $request)
{
    $form = new HTML_QuickForm2('contact');
    $form->setAttribute('action', route('contact'))
         ->setAttribute('method', 'post');

    // Defaults come from the request so a failed submit repopulates itself
    $form->addDataSource(new HTML_QuickForm2_DataSource_Array($request->all()));

    $fieldset = $form->addElement('fieldset')->setLabel('Contact us');
    $name  = $fieldset->addElement('text', 'name', ['size' => 50])->setLabel('Your name');
    $email = $fieldset->addElement('text', 'email', ['size' => 50])->setLabel('Email');
    $message = $fieldset->addElement('textarea', 'message', ['rows' => 6])->setLabel('Message');

    $fieldset->addElement('submit', 'send', ['value' => 'Send']);

    $name->addFilter('trim');
    $name->addRule('required', 'Please enter your name');
    $email->addFilter('trim');
    $email->addRule('required', 'Please enter your email')
          ->addRule('email', 'That does not look like an email address');
    $message->addRule('required', 'Please write something');

    if ($form->validate()) {                 // true only for a POST of this form
        $values = $form->exportValues();     // ['name' => ..., 'email' => ...]
        // persist, then redirect (PRG)
    }

    return view('contact', ['form' => $form]);
}
{{-- resources/views/contact.blade.php --}}
{!! $form !!}

Two things make this work on day one:

  • $form->validate() calls isSubmitted() internally, which checks an auto-generated hidden _qf__<formname> field. On a plain GET it returns false without you writing any method check — that's why the same route can serve GET and POST. Use Route::match(['get','post'], ...) or set action explicitly.
  • echo $form / {!! $form !!} invokes the default renderer via __toString(). No view composer, no partial, no escaping surprises — but also no Tailwind/Bootstrap classes, so expect to style it yourself.

Implementation Patterns

Put each form in its own plain class

The single biggest day-to-day win is treating a form as a value object in app/Forms/, not as something the container knows about.

// app/Forms/AbstractForm.php
abstract class AbstractForm
{
    protected HTML_QuickForm2 $form;

    public function __construct(protected Request $request)
    {
        $this->form = new HTML_QuickForm2(static::class);
        $this->form->addDataSource(new HTML_QuickForm2_DataSource_Array($request->all()));
        $this->build();
    }

    abstract protected function build(): void;

    public function submitted(): bool  { return $this->form->validate(); }
    public function values(): array    { return $this->form->exportValues(); }
    public function render(): HtmlString { return new HtmlString((string) $this->form); }
}

Then a controller is three lines and the form is unit-testable without HTTP:

$form = new ContactForm($request);
if ($request->isMethod('post') && $form->submitted()) {
    Contact::create($form->values());
    return redirect()->route('contact')->with('status', 'Thanks!');
}
return view('contact', ['form' => $form]);

Never register a form instance as a singleton. Forms are stateful (values, errors, submit state). Under Octane a cached instance leaks values between requests. Build a fresh object per request.

Data sources are the whole binding layer

There is no old() integration — HTML_QuickForm2_DataSource_Array fed from $request->all() is the old-input mechanism, because QuickForm2 uses the data source both as defaults and as the submitted values. Consequences:

  • Field names in the request must match element names exactly, including group syntax (address[street], tags[]).
  • JSON bodies work via $request->all(), but a JSON payload that only contains a nested data key will silently not match anything and validate() will fail on required.
  • If you need to keep query-string values out of the round-trip, feed $request->only([...]) or $request->except(['page', 'sort']) instead.
  • setConstant($value) is for values the user must not be able to override (id, CSRF token, computed hashes) — the data source and filters never touch constants. setValue() is a default and will be overridden by input.

CSRF

QuickForm2 renders its own <form> tag, so @csrf inside a Blade template won't land inside it. Add the token as a hidden element with a constant value:

$form->addElement('hidden', '_token')->setConstant(csrf_token());

Laravel's VerifyCsrfToken middleware then validates it exactly as it would a @csrf field. (The auto _qf__<formname> hidden field is separate and is what drives isSubmitted().)

Element types you'll actually reach for

Need Call
Single-line text addElement('text', 'name', ['size' => 40])
Password addElement('password', 'pw')
Select / radio / checkbox lists addElement('select', 'city', null, $options) — the 4th $data argument carries options (verify in Element/Select.php if you're on an old 2.x)
Static copy / headings addElement('static', 'intro')->setContent('<p>…</p>')
Container addElement('fieldset')->setLabel('Legend text')
Group (nested values) addElement('group', 'address'), then ->addElement(...) on it
Hidden marker addElement('hidden', 'step')->setValue('2')

HTML5 input types (email, number, date, tel) are explicitly not shipped — the README says "planned". Either pass the attribute on a text element and check what the renderer emits, or subclass HTML_QuickForm2_Element_Input_Text and override the rendered type. This is the most common reason people bounce off the package in 2025: you get HTML4 markup plus whatever attributes you bolt on, and mobile keyboards are driven by type.

Rules and filters

  • addRule($type, $message, $options) lives on the element, not the form. (The $form->addRule('username', 'Required', 'required') shape is HTML_QuickForm 1 API — a frequent migration bug.)
  • Shipped rule types: required, notempty, email, regex, length/minlength/maxlength, compare, callback, plus the logical combinators mentioned in the README. compare is the password-confirmation rule: $confirm->addRule('compare', 'Passwords differ', ['operand' => 'password']).
  • callback is your escape hatch into Laravel-land, and it is where you should do anything non-trivial rather than fight regex:
$vat->addRule('callback', 'Unknown VAT number', [
    'callback' => fn ($value) => VatService::exists($value),
]);
  • Filters (trim, htmlspecialchars, custom callables) run when the value is read, so addFilter('trim') before addRule('required') behaves the way you want. Keep filter-then-rule ordering as a habit and you'll never need to reason about it.
  • exportValues() is a plain array. Never Model::create($form->exportValues()). Whitelist explicitly or run the exported array through Validator::make().

Rendering: stop fighting the default renderer

The default output is deliberately plain markup with generic element / label classes. Two sane paths:

  1. Wrap in Blade. {!! $form !!} inside a styled card, then target the emitted classes in CSS. Fine for one or two forms.
  2. Subclass the renderer. HTML_QuickForm2_Renderer::factory('default') gives you a renderer with a plugin system and options; for Tailwind or Bootstrap you'll get further, faster, by extending HTML_QuickForm2_Renderer_Default and overriding renderElement() / renderForm() than by registering plugins:
$renderer = HTML_QuickForm2_Renderer::factory('default');
$renderer->setOption('group_errors', true);   // collects errors rather than per-element
echo $form->render($renderer);

Check the "Form output customization" manual page and Renderer/Default.php for the exact option names in your installed version — they are not discoverable from the README.

Client-side validation

The self-contained JS lives at vendor/pear/html_quickform2/js/ (there is no vendor:publish). Copy quickform.js into public/js/ (or import it in resources/js/app.js), load it on the page, and let the renderer emit the setup script for elements that have rules. Verify with view-source that (a) the script tag is present, (b) it appears after quickform.js. Under Livewire, Turbo or any partial-page navigation the setup script may only run once — re-initialise after navigation, or just rely on server-side validation there.

Multipage forms

Core QuickForm2 only makes multipage "possible"; the HTML_QuickForm2_Controller class came from a sibling PEAR package. In a Laravel app you are better off with session-backed steps, Livewire, or one form per step plus a session array — the controller port is not worth the archaeology.

Gotchas and Tips

No namespaces, no stubs, unhappy static analysis. Every class is a global HTML_QuickForm2_* with underscore naming. PHPStan/Larastan won't find them unless you add the vendor path to scanFiles/paths, and you'll get class.notFound noise on return types like HTML_QuickForm2_Element. Add the path once and move on; there are no official stubs.

addElement()'s argument order. addElement($type, $name, $attributes, $data). Attributes (third) are HTML attributes plus library keys the element consumes; $data (fourth) is element-specific payload (options for select/radio/checkbox). Passing options as the third argument "sometimes works" depending on version — pass them fourth.

Group values are nested. A group named address with member street submits as address[street] and exports as ['address' => ['street' => ...]]. Flat $request->validate(['street' => ...]) rules will not match. Multi-select/checkbox elements export PHP arrays, so $request->all() round-trips fine but your DB column needs json_encode.

Disabled vs readonly vs frozen. A disabled attribute means the browser does not submit the field, so QuickForm2 falls back to the data source — a required rule on it may fail (or quietly pass with a stale value) unpredictably. For "visible but not editable", use readonly, or freeze the element; reserve disabled for values you also setConstant().

Errors are only available after validate(). $form->getElement('email')->getError() returns nothing until validation has run. In tests, drive validate() first, then assert per element. For a quick "why did this fail" in dev, loop $form->getElements() and dump name + error rather than looking for a debug flag.

The _qf__<formname> hidden field is real. It's how isSubmitted() works, and it's why $form->validate() returns false on GET. It also means a functional test posting via HTML_QuickForm2_DataSource_Array must include '_qf__myform' => '1', or the form will appear unsubmitted. If your form name contains characters a browser would mangle, rename it.

Escaping. The renderer escapes labels and messages, but static elements' content and anything you feed through setLabel()/message strings is your responsibility once you start putting HTML or user data in it. Sanitise anything derived from $request.

Deprecations will come from your code, not theirs. 2.5.0 is verified clean on PHP 8.5 and 8.6. If you see Passing null to parameter of type array offset or spl_object_hash() is deprecated, it's your custom element/validator/renderer. Run php -d error_reporting=E_ALL -d display_errors=1 artisan serve once when you first add a custom element and fix it there rather than in production logs.

Don't mix install methods. Composer install has no require_once; the PEAR tarball does. If you inherit a project that require_once 'HTML/QuickForm2.php' and autoloads, you can end up with two class definitions or a version skew where the renderer and element come from different copies. Pick one.

Maintenance risk is the real gotcha. 23 stars, zero dependents, single-maintainer PEAR package, releases that are compatibility-only. For a long-lived Laravel app, weigh it honestly: Blade + FormRequest + a UI kit covers 90% of what this does, with types and IDE support. QuickForm2 earns its place when (a) you have dozens of structurally similar admin forms and want one declarative definition driving validation + rendering, (b) you're porting a legacy QF1/PEAR application, or (c) you genuinely want rule definitions co-located with markup and a renderer pipeline. If it becomes load-bearing, pin ^2.5, vendor the source into packages/, and budget for the fork — there's no ecosystem to fall back on.

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