Skip to content
Nibiru docsv0.9.2

Forms

Build forms fluently with Nibiru's static factory.

Stable Reading time ~ 4 min Edit on GitHub

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.

use Nibiru\Factory\Form;

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

There are three flavours of method, by historical naming.

addInputType* — for actual <input> elements

Section titled “addInputType* — for actual <input> elements”
addInputTypeText addInputTypePassword addInputTypeEmail
addInputTypeDate addInputTypeDatetime addInputTypeColor
addInputTypeRadio addInputTypeCheckbox addInputTypeSwitch
addInputTypeSubmit addInputTypeTextarea

addType* — for non-<input> form elements

Section titled “addType* — for non-<input> form elements”
addTypeFileUpload addTypeHidden addTypeImageSubmit
addTypeNumber addTypeRange addTypeReset
addTypeSearch addTypeTelefon addTypeUrl
addTypeButton addTypeLabel

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

Section titled “addSelect / addSelectOption — for <select> + <option>”
addSelect addSelectOption
addOpenDiv addCloseDiv addOpenAny addCloseAny
addOpenSpan addCloseSpan
create / reset the static buffer; call before building a new form
addForm / wrap the buffer in <form>...</form> and return as a string
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:

View::assign(['loginForm' => $html]);
<div class="card-body">
{$loginForm nofilter}
</div>

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

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.

foreach (['standard', 'admin', 'editor'] as $r) {
Form::addInputTypeRadio([
'name' => 'role', 'value' => $r, 'id' => "role-$r",
]);
Form::addTypeLabel(['for' => "role-$r", 'value' => ucfirst($r)]);
}
Form::addTypeFileUpload([
'name' => 'avatar',
'accept' => 'image/png,image/jpeg',
]);

Don’t forget enctype on the form:

Form::addForm([
'method' => 'POST',
'action' => '/profile/upload',
'enctype' => 'multipart/form-data',
]);
Form::addTypeHidden([
'name' => 'csrf',
'value' => bin2hex(random_bytes(16)),
]);

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

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

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.

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:

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_replaces 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.

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

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.

  • 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).

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.