Mapping classes to forms

If you've already read the chapter about DataModel and configuration system, you'll have noticed that these entities can directly generate forms and it's a feature widely used in Jet that can save a lot of work. I have good news. This form generation is not just a matter of DataModel and configuration, but any class can actually be mapped to a form.

In the chapter on capturing, validating, and passing data, a hypothetical registration form and its connection to a class was shown. With the caveat that in practice it can be done differently. So now let's see the exact same example, but using form definitions, again:

use Jet\Form;
use 
Jet\Form_Field;
use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Definition_Interface;

class 
MyUser implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Username',
        
is_requiredtrue,
        
error_messages: [
            
Form_Field::ERROR_CODE_EMPTY => 'Please enter username'
        
]
    )]
    protected 
string $username '';
    
    protected ?
Form $reg_form null;
    
    public function 
getUsername(): string
    
{
        return 
$this->username;
    }
    
    public function 
setUsernamestring $username ): void
    
{
        
$this->username $username;
    }
    
    public function 
getRegForm() : Form
    
{
        if(!
$this->reg_form) {
            
$this->reg_form $this->createForm('reg_form');
        }
        
        return 
$this->reg_form;
    }
}

That settles everything. Form definition, value capture, validation and also passing the captured and valid values to the class instance. A lot less typing and work, right? The difference is even more striking when we need more and more properties and form fields to go with them. In this case, name, email, password and so on. We just need to add class properties and form field definitions to them (and of course getters and setters, a good modern IDE will generate them automatically and it's a habit that pays off).

The essentials of the class mapped to the form

In order for a class to be automatically mapped to a form (i.e., to define and automatically create and link to a form), it must do two simple things:

This gives the class primarily the createForm method.

If you're creating a DataModel, or configuration definition, you're done. The ability to map forms is automatic with such classes.

Form field definition - attributes and their parameters

I'm sure you haven't overlooked the common use of attributes in Jet, specifically Form_Definition. I won't explain the use of attributes here and let's just look at the list of parameters that a definition must (or can) have.

Parameter Type Required Meaning of
type string yes Basic parameter that determines the type of the generated form field. This is a string and it is convenient to use Form_Field::TYPE_* constants, or your constants for your own form field types.
This does not use the class name of the field type, but the type identifier. The factory is used to create instances of the corresponding classes.
is_required bool no Indicates whether or not the form field will be marked as mandatory.
label bool no Form field label.
help_text string no Form field help text.
help_text asociované pole no Form field help data.
error_messages associated field conditionally yes Texts of error messages to the error codes of the form field.
This parameter is mandatory if the field needs such messages. For example, if the field is mandatory, then an error message for the Form_Field::ERROR_CODE_EMPTY code will definitely be required.
default_value_getter_name string no Name of the method that returns the value that will be used as the default for this field when creating the form field.

The current value of the object property to which the form field belongs is usually taken as the default value. And normally it is not necessary to define a method. However, it may happen that the property value is not suitable as the default value of the form field. For example, it may be an object field or other complex data. In this case, you must define a method that passes usable data to the form field.
setter_name string no This parameter can be used to specify the class method that will be called by the form field setter.

Jet tries to find the name of the appropriate setter itself (just follow the principle of how the name is created everywhere in Jet and in the sample application) and if it does not find the appropriate method, it passes the value directly to the object property. However, if you want to use a specific method, you can use this parameter to determine its name.
creator callable no Sometimes there may be a situation when the definition is not enough to create a correctly set form field and it is necessary to set the field by some more complex logic. In this case, it is possible to define a method call that takes a pre-generated field as a parameter and expects the final form of the field (i.e. the instance of the form field that will be taken as final) as the return value.

Other parameters depend on the specific form field type.

Sub-form / Sub-forms

The form mapping system has another interesting feature and that is the nesting of forms into forms. Sound crazy? Let's demonstrate it again on something from practice and again on my favorite e-commerce topic.

You are creating an e-shop and nowadays one language is probably not enough anymore. So you need everything to be localizable. For example, product descriptions. Let's show a basic product definition with localizable data (for simplicity, without the DataModel definition)

First, let's make a master product class:

namespace JetApplication;

use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Interface;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Field;
use 
Jet\Locale;
use 
JetApplication\Application_Web;

class 
Product implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    protected 
int $id;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Internal code:'
    
)]
    protected 
string $internal_code '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'EAN:'
    
)]
    protected 
string $EAN '';
    
    
/**
     * @var Product_Localized[]
     */
    
#[Form_Definition(is_sub_forms:true)]
    protected array 
$localized = [];
    
    public function 
__construct()
    {
        foreach( 
Application_Web::getBase()->getLocales() as $locale ) {
            
$this->localized[(string)$locale] = new Product_Localized();
            
$this->localized[(string)$locale]->setLocale$locale );
        }
    }
    
    public function 
getInternalCode(): string
    
{
        return 
$this->internal_code;
    }
    
    public function 
setInternalCodestring $internal_code ): void
    
{
        
$this->internal_code $internal_code;
    }
    
    public function 
getEAN(): string
    
{
        return 
$this->EAN;
    }

    public function 
setEANstring $EAN ): void
    
{
        
$this->EAN $EAN;
    }
    
    public function 
getLocalizedLocale $locale ) : Product_Localized
    
{
        return 
$this->localized[(string)$locale];
    }
}

It already contains some basic common data and especially the $localized property, which is crucial for us now. This is an array of objects of class Product_Localized and the property has the attribute #[Form_Definition(is_sub_forms:true)]

Also, please note that the constructor instantiates this property according to a list of locations bases.

Now let's look at the torso of the Product_Localized class, which represents everything about the product that will be bound to a specific localization:

namespace JetApplication;

use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Interface;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Field;
use 
Jet\Locale;

class 
Product_Localized implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    protected 
int $product_id;
    
    protected 
Locale $locale;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Name:'
    
)]
    protected 
string $name '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_WYSIWYG,
        
label'Description:'
    
)]
    protected 
string $description '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_FLOAT,
        
label'Price:'
    
)]
    protected 
float $price 0.0;
    
    public function 
getLocale(): Locale
    
{
        return 
$this->locale;
    }
    
    public function 
setLocaleLocale $locale ): void
    
{
        
$this->locale $locale;
    }    
}

There is nothing special here at first sight. A fairly common class connected to the form. However, the elements of this form become elements of the product editing form.

So try to create a product object and create a form (for example to add a product):

$product = new Product();
$add_form $product->createForm('add_product');

Still quite common practice (except that for practical reasons it is good to keep form instances as singletons in objects - see sample application).

The form can be routinely intercepted:

if($add_form->catch()) {
    
$product->save();
}

But in the view we can have this:

<?=$add_form->field('internal_code')?>
<?=$add_form
->field('EAN')?>

<?php foreach( Application_Web::getBase()->getLocales() as $locale ): ?>
    <?=UI::locale($locale)?>
    <?=$add_form->field('/localized/'.$locale.'/name');?>
    <?=$add_form->field('/localized/'.$locale.'/description');?>
    <?=$add_form->field('/localized/'.$locale.'/price');?>
<?php 
endforeach;?>

And now the control question, comrades: what happens if we add another localization to that base? Well, nothing will be delayed, but we'll add another localization to the products as well and it will automatically affect the form.

The only condition is to say that we want to include the form elements from the objects that are in the array using the attribute:
#[Form_Definition(is_sub_forms:true)].

If it was not an array of objects, but a simple instance of another object, the same procedure is followed, there is only a small difference in the attribute:
#[Form_Definition(is_sub_form:true)]

Previous chapter
Translation of the form
Next chapter
Jet\Form_Definition_Interface