Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Qubus Form

A fluent HTML5 form builder with Bootstrap 4 and Bootstrap 5 rendering.

Requirements

  • PHP 8.4+

Installation

composer require qubus/form

Fluent API

use Qubus\Form\FormBuilder;
use Qubus\Form\FormBuilder\Decorator\Bootstrap;

$form = FormBuilder::create(
    options: ['id' => 'account'],
    attributes: ['action' => '/accounts']
)->addDecorator(new Bootstrap(Bootstrap::VERSION_5));

$form
    ->field('email', 'user[email]', ['required' => true])
        ->attributes(['autocomplete' => 'email'])
        ->classes('email-field')
    ->end()
    ->field('password', 'user[password]', ['required' => true, 'minlength' => 12])
    ->end()
    ->field('select', 'user[role]', [
        'items' => ['member' => 'Member', 'admin' => 'Administrator'],
    ]);

echo $form;

field() returns the new element. Call end() to return to its parent. Existing methods such as setOption() and setAttr() remain available, while option(), options(), attributes(), classes(), and value() provide the concise fluent surface.

Field examples and rendered HTML

The following examples show the builder calls and their rendered output. Generated IDs combine the form ID and field name, including nested field names.

Email field

$form = FormBuilder::create(['id' => 'account']);
$form->field('email', 'user[email]', [
    'description' => 'Email address',
    'required' => true,
])
    ->attributes([
        'autocomplete' => 'email',
        'placeholder' => 'name@example.com',
    ])
    ->value('person@example.com');
<form method="post" id="account">
    <div>
        <label for="account-user-email">Email address *</label>
        <input type="email" placeholder="name@example.com" name="user[email]" value="person@example.com" required id="account-user-email" autocomplete="email">
    </div>
</form>

Textarea

Textarea values are placed between the tags and escaped; they are never rendered as a value attribute.

$form = FormBuilder::create(['id' => 'profile']);
$form->field('textarea', 'bio', [
    'description' => 'Biography',
    'maxlength' => 160,
])->value('Developer & writer');
<form method="post" id="profile">
    <div>
        <label for="profile-bio">Biography</label>
        <textarea maxlength="160" name="bio" id="profile-bio">Developer &amp; writer</textarea>
    </div>
</form>

Select field

The configured value selects the matching item. Submitted values not present in items fail server-side validation.

$form = FormBuilder::create(['id' => 'settings']);
$form->field('select', 'timezone', [
    'description' => 'Time zone',
    'placeholder' => 'Choose a time zone',
    'items' => [
        'America/Los_Angeles' => 'Pacific Time',
        'America/New_York' => 'Eastern Time',
    ],
])->value('America/Los_Angeles');
<form method="post" id="settings">
    <div>
        <label for="settings-timezone">Time zone</label>
        <select name="timezone" id="settings-timezone">
            <option value="" disabled>Choose a time zone</option>
            <option value="America/Los_Angeles" selected>Pacific Time</option>
            <option value="America/New_York">Eastern Time</option>
        </select>
    </div>
</form>

Checkbox

The boolean type creates a checkbox. Its hidden input ensures an unchecked checkbox still submits an empty value.

$form = FormBuilder::create(['id' => 'terms']);
$form->field('boolean', 'terms', [
    'description' => 'I accept the terms',
    'required' => true,
])->value(true);
<form method="post" id="terms">
    <div>
        <label><input type="hidden" value="" name="terms">
        <input type="checkbox" value="1" checked name="terms" required id="terms-terms"> I accept the terms *</label>
    </div>
</form>

Bootstrap 5 field with help text

$form = FormBuilder::create(['id' => 'contact'])
    ->addDecorator(new Bootstrap(Bootstrap::VERSION_5));

$form->field('email', 'email', [
    'description' => 'Email',
    'help' => 'We will never share your email.',
])->attributes(['placeholder' => 'name@example.com']);
<form method="post" id="contact">
    <div class="mb-3">
        <label class="form-label" for="contact-email">Email</label>
        <input type="email" placeholder="name@example.com" name="email" id="contact-email" class="form-control">
        <div class="form-text">
            We will never share your email.
        </div>
    </div>
</form>

Use Bootstrap::VERSION_4 for Bootstrap 4. The same field uses form-group, form-control, and Bootstrap 4 help-text conventions.

Required indicators

Setting required adds both the native HTML attribute and a visible suffix to the label. The default suffix is *:

$form->field('text', 'name', ['required' => true]);
// <label for="form-id-name">Name *</label>

Customize or disable the marker per field:

$form->field('text', 'name', [
    'required' => true,
    'required-suffix' => ' (required)',
]);

$form->field('text', 'reference', [
    'required' => true,
    'required-suffix' => '',
]);

You can also change the default for all fields through FormBuilder::$options['required-suffix']. The suffix is escaped as label text and is not added to placeholders.

Bootstrap 4 and 5

Pass the target major version explicitly:

$form->addDecorator(new Bootstrap(Bootstrap::VERSION_4));
$form->addDecorator(new Bootstrap(Bootstrap::VERSION_5));

// The registered fluent equivalent:
$form->addDecorator('bootstrap', 5);

Bootstrap 4 emits form-group, form-control, and text-muted conventions. Bootstrap 5 emits mb-3, form-select, and current input-group markup without the removed prepend and append wrapper elements.

Submission and CSRF

$form->csrf($_SESSION['csrf_token']);

if ($form->isSubmitted() && $form->isValid()) {
    $values = $form->getValues();
}

CSRF protection is opt-in because token storage belongs to the host application. When enabled, a hidden token field is rendered and isSubmitted() rejects mismatches using a timing-safe comparison. Request arrays can also be injected into isSubmitted() for tests. Nested names such as user[address][city] are hydrated from normal PHP request arrays.

Rendered attribute values, labels, errors, textarea values, choices, and button text are escaped. Passing literal strings to Group::add() is the explicit raw-HTML escape hatch; never pass untrusted input to it.

Password and file values are not repopulated. Select and choice controls reject submitted values that are absent from their configured item list.

Uploads

file uses FileInput; image uses ImageInput and additionally verifies that PHP recognizes the temporary upload as an image. Upload fields support these optional validation rules:

  • max-size: maximum size in bytes, measured from the temporary file rather than client data.
  • mime-types: one MIME type or an array of allowed types, detected from file contents with PHP's Fileinfo extension. Wildcards such as image/* are supported.
  • extensions: one filename extension or an array of allowed extensions. Matching is case-insensitive and leading dots are optional.

When accept is not set explicitly, it is generated from mime-types, or from extensions when no MIME types are configured. The browser attribute is only a file-picker hint; the configured options are also enforced by server-side validation.

FileInput::moveUploadedFile() accepts an explicit path or directory, sanitizes client filenames used with directories, refuses overwrites by default, and does not delete wildcard matches. Applications should still choose a safe storage location and generated destination name, and apply any domain-specific content scanning needed before serving uploaded files.

Upload form example

Upload forms must use multipart/form-data. This Bootstrap 5 example includes a required PDF, an optional image, help text, and a submit button:

use Qubus\Form\FormBuilder;
use Qubus\Form\FormBuilder\Decorator\Bootstrap;

$form = FormBuilder::create(
    options: ['id' => 'documents'],
    attributes: [
        'action' => '/documents/upload',
        'enctype' => 'multipart/form-data',
    ],
)->addDecorator(new Bootstrap(Bootstrap::VERSION_5));

$form
    ->field('file', 'document', [
        'description' => 'Document',
        'required' => true,
        'help' => 'PDF files up to 10 MB.',
        'max-size' => 10 * 1024 * 1024,
        'mime-types' => ['application/pdf'],
        'extensions' => ['pdf'],
    ])
    ->end()
    ->field('image', 'cover', [
        'description' => 'Cover image',
        'help' => 'JPEG, PNG, or WebP.',
        'max-size' => 5 * 1024 * 1024,
        'mime-types' => ['image/jpeg', 'image/png', 'image/webp'],
        'extensions' => ['jpg', 'jpeg', 'png', 'webp'],
    ])
    ->end()
    ->field('submit', 'upload', [
        'description' => 'Upload files',
    ]);

echo $form;

Rendered HTML:

<form action="/documents/upload" enctype="multipart/form-data" method="post" id="documents">
    <div class="mb-3">
        <label class="form-label" for="documents-document">Document *</label>
        <input accept="application/pdf" type="file" name="document" required id="documents-document" class="form-control">
        <div class="form-text">
            PDF files up to 10 MB.
        </div>
    </div>

    <div class="mb-3">
        <label class="form-label" for="documents-cover">Cover image</label>
        <input accept="image/jpeg,image/png,image/webp" type="file" name="cover" id="documents-cover" class="form-control">
        <div class="form-text">
            JPEG, PNG, or WebP.
        </div>
    </div>

    <div class="mb-3">
        <button type="submit" id="documents-upload" class="btn btn-secondary">Upload files</button>
    </div>
</form>

Validate before moving either upload. moveUploadedFile() only accepts a genuine file uploaded through PHP and refuses to replace an existing destination unless its second argument is true:

use Qubus\Form\FormBuilder\FileInput;

if ($form->isSubmitted() && $form->isValid()) {
    $document = $form->get('document');

    if ($document instanceof FileInput && $document->isUploaded()) {
        $document->moveUploadedFile('/srv/private-uploads/generated-document.pdf');
    }
}

About

A PHP form builder for HTML 5 and Bootstrap forms.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages