Skip to content
Imants Horsts edited this page Apr 9, 2017 · 3 revisions

typora-copy-images-to: ./images

Agile UI and Agile Data make it simple for developer to use a Form component when editing the data:

$form->setModel(new Project($db));
$form->onSubmit(function($form) {
  $form->model->save();
  return $form->success('saved!');
});

However what happens under the hood is not trivial at all. Agile Toolkit follows a principle of "single responsibility", so there are various objects involved with the simple code above that work together. In most scenarios you wouldn't even need to know what is going on - form will just work for you, but If you need to understand this process, I've written this article to help you dive in.

Part 1 - Data Processing

Our design goals call out for simplicity and extensibility. To explain the problem I'll divide it into two parts. The first will deal with the following code:

$m = new Project($db);

$m['date'] = '2018-01-03';
$m->save();

There are 3 major classes at play here: Model, Field and Persistence. Each of them is responsible for a certain functionality, but none of them is willing to over-commit. Each class has number of sub-classes, e.g. any of the Models can be used, Persistence can be either SQL or Array and there are number of custom field classes.

field-persistence-model-triangle

Responsible Not Responsible
Field Storing field meta-information (label, flags, type); normalizing value; handling relations (e.g. hasOne) Holding value, UI-presentation or anything to do with storage.
Model Binding fields together, hooks, storing / accessing field values or defining custom methods. Specific type handling or anything persistent storage-related.
Persistence Storing standard PHP types in specific database/storage, forming queries, typecasting. Holding model structure, field types or meta-information.

So given the above setup - here is a big question..

Who handles validation?

So it happens that validation is not simple and need to be handled by all 3 of the above. You can also use 3rd party validators, but their job would be just to apply rules on data, so to keep our scenario simple I'll just assume that we perform a very basic validation manually.

As it turns out there are 3 stages of validation.

1. set() validation / normalization

This process is handled by a Field class through Field::normalize(). When field of a specific type and configuration is assigned a certain value, in some cases it needs to be converted. Here are few examples:

$m['date'] = '2018-01-03';
echo $m['date']; // will actually be DateTime object

$m['budget'] = '2,300.102';
echo $m['budget']; // will be float 2300.10

$m->addField('gender', ['enum'=>['M', 'F']]);
$m['gender'] = 'X';  // will cause exception, value not allowed

Normalization is not a "validation" in the way you think about it, but it must make sure that the value you pass to the field can be properly stored.

2. Hook validation

This is a most common type of validation. Suppose you have a business requirement that "name" and "surname" values each must be longer than 2 characters. Consider this code:

$m['name'] = 'John';
// echo $m['surname'] -- responds with null
$m['surname'] = 'Smith';
$m->save();

You could say that on the second line your model does not meet the validation rules, yet by line 4 those rules are now satisfied. So the validation must be called before save take place and not earlier.

This validation is happening in a Domain Model (since it's not relevant where and how the model is saved), so it's done with the help of Model's hook:

$model->addHook('beforeSave', function($model) { 
  if (strlen($model['name'])<3) throw new ValidationException('name');
  if (strlen($model['surname'])<3) throw new ValidationException('surname');
});

It's always advisable to group exceptions together:

throw new ValidationException(['name', 'surname']);

This gives the UI layer the opportunity to display them to user simultaneously and most Validator libraries will be able to produce extended list of errors for you.

Some 3rd party code can add additional hook-based validations, for example to make sure that logged-in user is allowed to change field values.

3. Persistence validation

Now as we are starting to prepare the vendor-specific save, there may be still some problems. For instance if you have defined a type-less field and assigned it a value of an object - SQL persistence wouldn't know how to store a generic object inside SQL, some exceptions can be raised here.

The other type of check is to make sure "mandatory" fields are not null. If you are designing your own Persistence driver you may restrict length/size of certain fields or make sure they contain a valid UTF8 information.

The tricky case of 'mandatory' and 'required' property.

The Agile Data fields have a property "mandatory" which means that this field must carry the value. The other property - 'required' - has a more human-like interpretation. The next table will list values which are NOT permitted by a 'mandatory' / 'required' option.

Think this way:

  • 'mandatory' properties MUST be included on a form.
  • 'required' properties will have red asterisk next to field.
Field 'type' Values if mandatory Values if required
string not null trim(val) != ""
integer not null int(val) != 0
reference (hasOne) not null not null
boolean not null not false
enum not null not null
money not null not null, not zero

Why having separate 'mandatory' and 'required'

Suppose your "Project" model have 3 fields: 'user_id', 'name', and 'amount'. The 'user_id' cannot be set by a user - the application must specify it. Name and amount are supplied by a user through a Form. As per business rules 'amount' CAN be zero, but must not be empty. Name must not be empty string.

$project->hasOne(new User(), ['user_id', 'mandatory' => true]);
$project->addField('name', ['type'=>'string', 'required' => true]);
$project->addField('amount', ['type'=>'money', 'mandatory' => true]);

$project['user_id'] = $logged_user_id;
$form->setModel($project, ['name', 'amount']);
$form->onSubmit(function($form) {
  $form->model->save();
});

Mandatory property on 'user_id' will prevent save() operation if $logged_user_id is null. This won't stop the form from being displayed, however. Required attribute on the "name" field will add an asterisk next to the "Name" label. If user enters spaces inside the field, they will be trimmed (normalization of string type), then 'required' property will alert user if empty string is supplied.

If monetary field is left blank, then it is storing null. By setting 'mandatory' we make sure that some amount is entered, although it can be zero.

It's important to note, that "required" rule is considered during 1-set validation while "mandatory" rule is checked during 3-persistence validation.

User Interface

When it comes to User Interface each component can have a slightly different handling of data validation but any component will follow the rules set out by Agile Data.

The most common component for data entry is Form, so I'll break down all the Form sub-components and their responsibility areas.

Responsible Not Responsible
Form Association with model, handling data, interracting with POST submission, displaying validation errors and executing submission handler. Generating form HTML, decorating fields or calling save().
FormLayout Laying out fields, drawing field labels and buttons. Rendering fields or handling data.
FormField Displaying individual field, icons and related actions / dropdown lists. Associating JavaScript. Some fields will poke through POST data. Holding model structure, field types or meta-information.
UI.Persistence Converting values to user-friendly output and interpreting user-entered values. Field structure or dependencies.

Form State Diagram

Now that all the components are introduced, the following state diagram will illustrate basic process of submitting a form. The diagram does not include any operations inside Agile Data:

form-submission-state-diagram

As you can see from the diagram, most of the work is happening inside Form::loadPOST(). This method will actually catch exceptions that happen during type casting, model->set() or inside your submission handler and will display errors on the form.

Grouping errors together

For usability errors must be grouped together if possible. Practically it's not always possible, for example if your business rule says "start_date < end_date" this precedes by the check that validates date format of both fields. If date format is incorrect, it's not possible to carry out the comparison.

Phase 1 - Automatic Single-field Checks

During loading of the post fields, converting their types and setting those types into $model, each field will be processed individually. If field "start_date" fails to convert into date, the error will be displayed, but the process will continue with next field. This phase will also make sure that "required" and "mandatory" fields have their values specified properly.

At the end of this phase, Form will convert check what field data changed during normalization. List of fields and their 'modified' values will be transmitted to the Form using JS Actions and user will see them inside a form. For instance, entering "tomorrow" inside a date field may be normalized into an actual date that will be sent back into the form UI even if rest of the form failed to validate.

If there is at least one failure during Phase 1, the form will not proceed to Phase 2. This is done to make sure your callback receives clean and sensible form data.

Phase 2 - Your Submit Handler

During this phase callback specified through onSubmit() is executed and it will either finish normally or will be interrupted by exception. Form will treat "ValidationException" by showing validation errors on the form. ValidationException supports ability to specify multiple field errors.

If "Exception" is raised inside submission Handler, then Form will not catch it and will let App take care of that. The Exception will most likely mean that there are some problems with data storage. User does not need to know the details about that (not on the form anyways).

If everything was successful

If phase 1 was completed without errors and phase 2 was also successful, this does not mean there are no errors on the form. onSubmit handler may still return form->error() actions which will display errors.

Form does not really distinguish between different JS actions and will simply execute them inside the browser.

Conclusion

Forms in Agile Data are one of the most advanced and full-featured implementation, which was designed to be extensible, flexible and simple to use. If you look to use some advanced features, e.g. create your own FormField, you must follow the standard implementation pattern to make sure your code is compatible with other extensions and use-cases.