Forms & validation

Forms in Tiger are declarative — you describe the fields, and the framework handles the rest: markup-free rendering, CSRF, and validators that run both at submit and live on blur, from a single declaration.

Declare fields, not markup

Extend Tiger_Form (never Zend_Form directly) and describe the schema in elements():

class Billing_Form_Invoice extends Tiger_Form
{
    protected function elements(): array
    {
        return [
            ['text', 'amount', [
                'required'   => true,
                'filters'    => ['StringTrim'],
                'validators' => [['Float'], ['GreaterThan', false, ['min' => 0]]],
                'attribs'    => ['class' => 'form-control', 'placeholder' => $this->_t('billing.invoice.amount')],
            ]],
            ['text', 'due_date', [
                'required'   => true,
                'validators' => [['Date', false, ['format' => 'yyyy-MM-dd']]],
                'attribs'    => ['class' => 'form-control'],
            ]],
        ];
    }
}

The base strips decorators to ViewHelper-only — so the view owns all layout, not the form object. (Tiger apps are client/server: forms are submitted over AJAX to a service, so the form's job is fields + validation, not HTML structure.)

Validate: same rules, submit and blur

At submit, the service validates the whole form:

public function create(array $params): void
{
    $form = new Billing_Form_Invoice();
    if (!$form->isValid($params)) { $this->_formErrors($form); return; }   // → inline field errors
    // … validated $form->getValues(), then a transaction …
}

Here's the free part: the validators you declared once also run on blur, before submit. Add these attributes to the <form> and Tiger wires it up:

<form id="invoice-form" data-tiger-validate data-module="billing" data-form="Billing_Form_Invoice"
      onsubmit="return false;" novalidate>
    <?= $this->form->getElement('_csrf') ?>
    <!-- fields … -->
</form>

On blur, tiger.validate.js posts {form, field, value} to the public Tiger_Service_Validate, which runs that element's real validators and returns the first error — the field goes .is-invalid with its message inline while it's still easy to fix. You declare the validator once; it fires in both places. No hand-rolled per-field AJAX checks.

Batteries included

Tiger_Form ships with everything Zend validates, plus Tiger extras you'll reach for constantly:

  • DB-uniquenessZend_Validate_Db_NoRecordExists (e.g. "email already taken").
  • Password policyTiger_Validate_Password (length, history, complexity per config).
  • reCAPTCHA — a drop-in ['recaptcha', 'recaptcha', []] element that self-attaches its server-side validator (v2 + v3, config-driven, keyless in dev).
  • Localized country pickerTiger_I18n_Country, biased so common countries float to the top.
  • Address autocomplete — fills city/state/postal/country via the Location service.

The reference form

Don't start from scratch — copy the gold standard. modules/signup/forms/Signup.php and its view exercise every field type, DB-uniqueness, the password policy + strength meter, address autocomplete, and the country picker. When you build a new form, that's the one to crib from.

The rhythm

Every mutation in Tiger is the same shape — validate the form, then wrap the writes in a transaction:

if (!$form->isValid($params)) { $this->_formErrors($form); return; }
$id = $this->_transaction(function ($db) use ($params) { /* inserts/updates; throw to roll back */ });
$this->_success(['id' => $id], 'billing.invoice.created');

Validate → transaction → standard envelope. Learn it once; it's every service. (See Webservices for the response contract.)