Capture, validation and transfer of data

Generally speaking, form processing can be divided into three steps:

  • Input Capturing
    Capturing input data. Most commonly from POST or GET, but as we will see later, this is not a strict requirement. Any data from any source can be "captured".
  • Validation
    Captured data must naturally be verified according to defined rules and/or using various validators. Without this, data cannot be passed further.
  • Data Passing
    We will certainly want to perform actions with data that we have captured and verified as valid. This is essentially the final step of working with a form and a standalone topic in itself.

Attention! Everything stated here applies, but technically speaking, form elements use an InputCatcher for input capturing and a Validator for validation. The form system is de facto a facade over these subsystems that complements them with form field rendering functionality.

Input Capturing

The catchInput method is used to capture input data, returning true/false depending on whether the form was submitted or not. if( $form->catchInput() ) {
    
//Form has been sent
    //... ... ...
}

As discussed in the chapter on definitions, every form has a name that is sent in a hidden form field. Based on this, the system can identify that this specific form was submitted, even if there are multiple forms on the page.

However, as we also mentioned, this is not the only way to capture data. For example, if we need to capture and validate data within a REST API via a form, we can hardly expect the API user to send a "magic value". Therefore, form capturing can be forced even without the presence of the special field:

$form->catchInput( force_catch: true );

Thus, the form is captured even without a field containing the form name. However, it still captures data from POST or GET (depending on how it is defined).

Furthermore, we noted that a form does not necessarily have to capture data from POST or GET, but can receive arbitrary data from anywhere. This can be handled as follows:

$form->catchInput(
    
input_data: $my_data,
    
force_catch: true
);

The data must take the form of either an array or an instance of Jet\Data_Array.

Validation

To clarify validation, we need to briefly touch upon form definitions. A range of field types feature integrated basic validation. For example, numeric fields Form_Field_Int and Form_Field_Float provide options to restrict the range of entered numbers, the email field Form_Field_Email validates the address automatically, the generic input field Form_Field_Input allows specifying a regular expression for format checking, and so on. See form field types.

However, this integrated validation may often not be enough. Imagine building a registration form and needing to verify whether a user with the given email is already registered, or whether the entered password is sufficiently strong. There are numerous scenarios. For this reason, Jet provides the capability to define validators for fields.

Before diving into validators, however, we must look at defining error codes. We will start there.

Error Codes

When reviewing form field types, you will notice that the vast majority of fields have default error codes. These codes represent validation error states depending on the field type. Jet will even require that an error message be defined for the relevant error code; otherwise, the form will throw an exception. What does this mean? Let's demonstrate in practice: use Jet\Form;
use 
Jet\Form_Field_Input;

$username_field = new Form_Field_Input('username', 'Username:');
$username_field->setIsRequired( true );


$reg_form = new Form('registration_form', [
    
$username_field
]);
We have a registration form with a username field. This field is marked as required ($username_field->setIsRequired( true )), but no error message is defined at all. Therefore, the form is invalid and Jet will throw an exception when attempting to use it. For everything to function properly, an error message must be defined for the correct error code: use Jet\Form;
use 
Jet\Form_Field_Input;

$username_field = new Form_Field_Input('username', 'Username:');
$username_field->setIsRequired( true );
$username_field->setErrorMessages([
    
Form_Field_Input::ERROR_CODE_EMPTY => 'Please enter your username'
]);

$reg_form = new Form('registration_form', [
    
$username_field
]);
And now it is correct. The field is now automatically validated as required, an error message can be displayed to the user (or an error response generated in a REST API). Jet helps you build consistent applications.

Note: Please notice that the error message during definition is not passed through the translator manually. Forms are connected to the translator automatically – see the standalone chapter.

Naturally, default error codes do not constitute an exhaustive list of error codes a form can operate with. Additional error codes can (and in fact must) be defined according to your needs, for example when creating a custom validator.

Validators

We will seamlessly continue and enhance our initial registration form code. We already have a check verifying whether the user entered a value at all. That will now be checked automatically. But how do we verify whether the username is already taken by someone else? For that, we need a custom validator: use Jet\Form;
use 
Jet\Form_Field_Input;

$username_field = new Form_Field_Input('username', 'Username:');
$username_field->setIsRequired( true );
$username_field->setErrorMessages([
    
Form_Field_Input::ERROR_CODE_EMPTY => 'Please enter your username',
    
'already_exists' => 'Sorry, but username %username% is already reserved'
]);

$username_field->setValidator( function( Form_Field_Input $field ) : bool {
    
$username = $field->getValue();
    
    if(
Auth_Visitor_User::usernameExists($username)) {
        
$field->setError(
            
code: 'already_exists', 
            
data: ['username'=>$username] 
        );
        
        return 
false;
    }
    
    return 
true;
} );

$reg_form = new Form('registration_form', [
    
$username_field
]);

A validator is nothing more than a callback that receives the instance of the field it is validating as its sole parameter. Its task is to return true if no issue was encountered, and in case of an error, set the appropriate error code and return false.

You surely noticed that along with the error code, error data can also be passed, which can then be dynamically populated into the corresponding placeholders in the error message (which will naturally be translated).

Form Validation Result

We demonstrated how to attach validation to individual fields. Now let's look at how to work with validation at the form level. Calling validation is simple: if(
    
$form->catchInput() &&
    
$form->validate()
) {
    
//Form has been sent and values are valid
    //... ... ...
}

That was the basic standard scenario where the form is normally captured, validated, and if everything is correct, execution proceeds. Any error messages will be displayed automatically by the form.

But what if we simply need the list of errors when validation fails? We do it like this: if( $form->catchInput() ) {
    if(
$form->validate()) {
        
//Form has been sent and values are valid
        //... ... ...
    
} else {
        
$errors = $form->getValidationErrors();
        
        
var_dump($errors);
        
//... ... ...
        
        
if(!$username_field->isValid()) {
            
var_dump(
                
$username_field->getLastErrorCode(), 
                
$username_field->getLastErrorMessage()
            );
        }
    }
}
Thus, errors can be further manipulated – either all at once or individually per field.

Influencing Validation

Consider this scenario: We have an e-shop checkout form. It contains a "Company Purchase" checkbox. If checked, corporate details must be validated. Otherwise, these details are optional. How do we achieve this? By influencing the form after input capturing, prior to validation: if( $form->catchInput() ) {
    if(
$form->field('is_company_order')->getValue()) {
        
$form->field('company_name')->setIsRequired( true );
        
$form->field('company_id')->setIsRequired( true );
        
$form->field('company_vat_id')->setIsRequired( true );
    }
    
    if(
$form->validate()) {
        
//Form has been sent and values are valid
        //... ... ...
    
}
}

Data Passing

Our form has been captured, validated, and we can now operate on the captured data.

Simple Value Retrieval

The first basic option (though rarely used by myself in practice) is retrieving all form values as an array: if(
    
$form->catchInput() &&
    
$form->validate()
) {
    
var_dump( $form->getValues() );
}

Value Catchers

Much more frequently, I use value catchers. In that case, using the form looks like this: if(
    
$form->catchInput() &&
    
$form->validate()
) {
    
$form->catchFieldValues();
    
//Form has been sent, is valid ....
}
Or even more commonly (as seen in the sample application) like this: if( $form->catch() ) {
    
//Form has been sent, is valid ....
}

And that's all! Data magically reaches where I need it, end of story. Well ... I'm joking. Yes, this is how forms are commonly used, and automatically generated forms mapped to classes (most frequently from DataModel and the configuration system) are already pre-configured for this style of usage.

However, we will certainly explain why and how it actually works. Few forms exist in isolation. Most forms relate to a specific object. For example, a registration form relates to a user – an instance of a class representing a user. Or an e-shop order form relates to an order object, an item description editing form relates to an item object, and this form in which I am currently typing text relates to a documentation article object, and so forth.

And what do we need to happen after the form is captured and confirmed valid? We need to "transfer" the data from the form into the object to which the form relates. So when I save this text, it flows into the property of the class representing the article, and that class is subsequently persisted (it is naturally a DataModel).

As mentioned, if you leverage Jet's built-in capabilities, you only need to manage this manually in special scenarios. Otherwise, this mapping happens automatically. But it is good – or rather essential – to understand how it operates. Let's imagine a scenario where we implement user registration, but the user is not persisted to a standard database (so you don't use DataModel), but sent to an external service via an API instead.

What was stated at the beginning still holds true. The user is an object – an instance of a class. However, within this class, we will set up everything manually, so to speak: use Jet\Form;
use 
Jet\Form_Field_Input;

class 
MyUser {
    protected 
string $username = '';
    
    protected ?
Form $reg_form = null;
    
    public function 
getUsername(): string
    
{
        return 
$this->username;
    }
    
    public function 
setUsername( string $username ): void
    
{
        
$this->username = $username;
    }

    public function 
getRegForm() : Form
    
{
        if(!
$this->reg_form) {
            
$username_field = new Form_Field_Input(
                
name: 'username', 
                
label: 'Username'
            
);
            
$username_field->setDefaultValue( $this->username );
            
$username_field->setIsRequired( true );
            
$username_field->setErrorMessages([
                
//... ... ..
            
]);
            
$username_field->setValidator(function( Form_Field_Input $field ) : bool {
                
//... ... ...
            
});

            
//************************************
            //************************************
            
$username_field->setFieldValueCatcher(function( $value ) {
                
$this->setUsername($value);
            });
            
//************************************
            //************************************
            
            
$this->reg_form = new Form('reg_form', [
                
$username_field
            
]);
        }
        
        return 
$this->reg_form;
    }
    
    public function 
save() : void
    
{
        
//TODO: ... ...
    
}
    
}

The class together with its form is ready. Now we simply use it in a controller: $new_user = new MyUser();

$form = $new_user->getRegForm();
if(
$form->catch()) {
    
$new_user->save();
}

Attention! I might disappoint you now, but what we just demonstrated is actually redundant. In practice, such scenarios are handled differently using mapping classes to forms. However, knowing this underlying principle is essential and in certain edge cases necessary to apply. So it's not so redundant after all :-) ;-).

Where Next?

Now you know how to define, capture, validate, and use a form. What's next? It's time to look at how to render forms.

Previous chapter
Jet\Form_Field_Select_Option
Next chapter
Displaying forms