---
title: Forms
description: Build forms fluently with Nibiru's static factory.
---

Forms in Nibiru are built **fluently** by calling static methods on `\Nibiru\Factory\Form`. Each call appends an HTML fragment to an internal static buffer; a final `Form::addForm()` wraps the buffer in a `<form>` element and returns the rendered HTML string.

## Import the factory

```php
use Nibiru\Factory\Form;
```

That's the only `use` you need. Every input type is a static method on this class.

## The full method catalogue

There are three flavours of method, by historical naming.

### `addInputType*` — for actual `<input>` elements

```
addInputTypeText        addInputTypePassword    addInputTypeEmail
addInputTypeDate        addInputTypeDatetime    addInputTypeColor
addInputTypeRadio       addInputTypeCheckbox    addInputTypeSwitch
addInputTypeSubmit      addInputTypeTextarea
```

### `addType*` — for non-`<input>` form elements

```
addTypeFileUpload       addTypeHidden           addTypeImageSubmit
addTypeNumber           addTypeRange            addTypeReset
addTypeSearch           addTypeTelefon          addTypeUrl
addTypeButton           addTypeLabel
```

### `addSelect` / `addSelectOption` — for `<select>` + `<option>`

```
addSelect               addSelectOption
```

### Layout helpers

```
addOpenDiv     addCloseDiv     addOpenAny     addCloseAny
addOpenSpan    addCloseSpan
```

### Lifecycle

```
create     // reset the static buffer; call before building a new form
addForm    // wrap the buffer in <form>...</form> and return as a string
```

:::caution[The naming inconsistency is intentional, sort of]
`addInputTypePassword` vs `addTypePassword`, `addInputTypeFileupload` vs `addTypeFileUpload` — the prefix matches whether the element is an `<input type="…">` (Input prefix) or some other tag (Type prefix). It's awkward but stable; the CLI's `./nibiru -c` scaffolds use the same pattern, so muscle memory works.
:::

## Building a form

```php
use Nibiru\Factory\Form;

Form::create();   // reset the static buffer

Form::addOpenDiv(['class' => 'form-group']);
Form::addTypeLabel(['for' => 'login', 'value' => 'Username']);
Form::addInputTypeText([
    'name'        => 'login',
    'id'          => 'login',
    'class'       => 'form-control',
    'required'    => 'required',
    'placeholder' => 'Type your name…',
]);
Form::addCloseDiv();

Form::addOpenDiv(['class' => 'form-group']);
Form::addTypeLabel(['for' => 'password', 'value' => 'Password']);
Form::addInputTypePassword([
    'name'     => 'password',
    'id'       => 'password',
    'class'    => 'form-control',
    'required' => 'required',
]);
Form::addCloseDiv();

Form::addInputTypeSubmit(['value' => 'Sign in', 'class' => 'btn btn-primary']);

$html = Form::addForm([
    'method' => 'POST',
    'action' => '/login',
    'name'   => 'loginForm',
]);
```

Pass `$html` to your view:

```php
View::assign(['loginForm' => $html]);
```

```smarty
<div class="card-body">
  {$loginForm nofilter}
</div>
```

(`nofilter` because `Form::addForm()` already returns rendered HTML — Smarty otherwise escapes it.)

## Recipes

### Select with options

```php
Form::addSelect(['name' => 'country', 'class' => 'form-control', 'id' => 'country']);
Form::addSelectOption(['value' => 'at', 'label' => 'Austria']);
Form::addSelectOption(['value' => 'lu', 'label' => 'Luxembourg']);
Form::addSelectOption(['value' => 'us', 'label' => 'United States']);
```

`addSelect` opens the `<select>` and queues option-collection state; each `addSelectOption` appends to it; the wrapping `</select>` is emitted automatically when the next non-option call happens.

### Radio group

```php
foreach (['standard', 'admin', 'editor'] as $r) {
    Form::addInputTypeRadio([
        'name' => 'role', 'value' => $r, 'id' => "role-$r",
    ]);
    Form::addTypeLabel(['for' => "role-$r", 'value' => ucfirst($r)]);
}
```

### File upload

```php
Form::addTypeFileUpload([
    'name'   => 'avatar',
    'accept' => 'image/png,image/jpeg',
]);
```

Don't forget `enctype` on the form:

```php
Form::addForm([
    'method'  => 'POST',
    'action'  => '/profile/upload',
    'enctype' => 'multipart/form-data',
]);
```

### Hidden CSRF token

```php
Form::addTypeHidden([
    'name'  => 'csrf',
    'value' => bin2hex(random_bytes(16)),
]);
```

(Stash the value in `$_SESSION['csrf']` and verify on POST.)

## Layout helpers

`addOpenDiv` / `addCloseDiv` and `addOpenAny` / `addCloseAny` let you compose Bootstrap-style layouts inside the same fluent stream:

```php
Form::addOpenDiv(['class' => 'row']);
  Form::addOpenDiv(['class' => 'col-md-6']);
    Form::addInputTypeText(['name' => 'first']);
  Form::addCloseDiv();
  Form::addOpenDiv(['class' => 'col-md-6']);
    Form::addInputTypeText(['name' => 'last']);
  Form::addCloseDiv();
Form::addCloseDiv();
```

`addOpenAny([…, 'tag' => 'fieldset'])` opens any other tag; `addCloseAny([…, 'tag' => 'fieldset'])` closes it.

## How it works under the hood

Form rendering is **string-based**, not DOM-based. Each type class lives at `core/c/type<X>.php` and contains an HTML template with placeholders:

```php
// core/c/typetext.php
private function _setElement() {
    $this->_element = '<input type="text" name="NAME" value="VALUE" '
                    . 'placeholder="PLACEHOLDER" maxlength="MAXLENGTH" ID CLASS '
                    . 'REQUIRED DATA>' . "\n";
}
```

The factory passes your `$attributes` array through `FormAttributes::loadAttributeValues()` which `str_replace`s each placeholder with the corresponding value. Empty values get the placeholder erased so attributes don't render with empty strings.

This is why **only known keys work** — `'name'`, `'value'`, `'placeholder'`, `'class'`, `'id'`, `'required'`, `'data'` are recognised; arbitrary keys are dropped silently.

## Pattern from production

The `formsController` on `thorax.nibiru-framework.com` builds its contact form in the constructor and assigns it to a property:

```php
namespace Nibiru;
use Nibiru\Adapter\Controller;
use Nibiru\Factory\Form;

class formsController extends Controller {
    private string $form;

    public function __construct() {
        parent::__construct();
        Form::create();
        Form::addTypeLabel(['value' => 'Full Name', 'for' => 'full-name']);
        Form::addInputTypeText([
            'name'     => 'full-name',
            'id'       => 'full-name',
            'required' => 'required',
            'class'    => 'contacts-input form-control',
        ]);
        // ...more fields...
        $this->form = Form::addForm([
            'name'   => 'newregister',
            'method' => 'post',
            'action' => '/forms/submit',
        ]);
    }

    public function pageAction() {
        View::assign(['form' => $this->form]);
    }
}
```

This keeps the controller's actions tiny — the form is a one-liner to render in the template.

## Common pitfalls

- **Forgetting `Form::create()`.** The buffer is static. Without `create()` you'll concatenate onto whatever was there last (including across requests in long-running PHP processes).
- **Smarty escaping the HTML.** Add `nofilter` (or `|nofilter`) when echoing the rendered string.
- **Custom attributes silently ignored.** Each type accepts a fixed set of placeholder keys. Use the `data` key for `data-*` attributes (which gets expanded), but truly arbitrary attributes are dropped.
- **No automatic XSS escaping.** The form layer is string-based. If you're rendering user input as a default `value`, escape it yourself before passing to the factory.
- **`addInputType…` vs `addType…` confusion.** When in doubt, look at the `core/c/type<X>.php` filename — if the type's HTML element is `<input type="X">` use `addInputType<X>`, otherwise `addType<X>`. `addSelect` is the lone exception (just `addSelect`, no prefix).

## Form validation

Nibiru does not ship server-side validation. Common patterns:

- `Respect/Validation` for declarative checks (already in many production Nibiru apps).
- A module with a `validate()` plugin per form type.
- HTML5 `required`, `pattern`, `min`/`max` for the first line of defence.
