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(
type: Form_Field::TYPE_INPUT,
label: 'Username',
is_required: true,
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 setUsername( string $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:
- Implement the Jet\Form_Definition_Interface interface
- Use trait Jet\Form_Definition_Trait - which implements the above interface.
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(
type: Form_Field::TYPE_INPUT,
label: 'Internal code:'
)]
protected string $internal_code = '';
#[Form_Definition(
type: Form_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 setInternalCode( string $internal_code ): void
{
$this->internal_code = $internal_code;
}
public function getEAN(): string
{
return $this->EAN;
}
public function setEAN( string $EAN ): void
{
$this->EAN = $EAN;
}
public function getLocalized( Locale $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(
type: Form_Field::TYPE_INPUT,
label: 'Name:'
)]
protected string $name = '';
#[Form_Definition(
type: Form_Field::TYPE_WYSIWYG,
label: 'Description:'
)]
protected string $description = '';
#[Form_Definition(
type: Form_Field::TYPE_FLOAT,
label: 'Price:'
)]
protected float $price = 0.0;
public function getLocale(): Locale
{
return $this->locale;
}
public function setLocale( Locale $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)]