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.