Feature PR-5.3-dev Pending

User tests: Successful: Unsuccessful:

avatar wilsonge
wilsonge
3 Feb 2024

Summary of Changes

This is a relatively major rework on form & field validation. There are a variety of ideas here for allowing Models to implement exceptions going forwards - that means that Form no longer returns exception objects. In order to facilitate we've introduced the concept of a Constraint (very loosely inspired from the Symfony validation package). These tell the user what checks have been applied and provide helper methods for checking what's failed - along with the error string. The rule system is then modified to take advantage of the new constraints system

A nice side effect of constraints - is that it allows server side validation of form field properties not currently validated - such as the max field length.

In this PR I've migrated the SubformRule to the new RuleInterface (as it's a special case) and also the PasswordRule so that you can see the change. Note that I've left the other rules for now to allow easy testing to prove that old style Form Rules also work.

There are no practical b/c changes right now with the \Joomla\CMS\Form\Form::$setModernValidationResponse (and associated helper method), the intention will be in J6 to change this to enabled by default and in J7 remove the ability to swap back to the current Legacy system

The only b/c change here is the return type change of \Joomla\CMS\Form\Field::validate - but that is unavoidable to migrate to this new structure and is practically unlikely to affect people - I'm only aware of a very small number of users who currently override this method and all the cases I've found so far don't use the parent return value (they add in extra checks and we still support that method returning true & false in case)

Testing Instructions

Check form fields are correctly validated before and after this is merged. There shouldn't be any changes.

Link to documentations

Please select:

  • Documentation link for docs.joomla.org:

  • No documentation changes for docs.joomla.org needed

  • Pull Request link for manual.joomla.org: To come when this is given a clean bill of health by people

  • [] No documentation changes for manual.joomla.org needed

avatar wilsonge wilsonge - open - 3 Feb 2024
avatar wilsonge wilsonge - change - 3 Feb 2024
Status New Pending
avatar joomla-cms-bot joomla-cms-bot - change - 3 Feb 2024
Category Libraries
avatar wilsonge wilsonge - change - 3 Feb 2024
The description was changed
avatar wilsonge wilsonge - edited - 3 Feb 2024
avatar wilsonge wilsonge - change - 3 Feb 2024
Labels Added: PR-5.1-dev
avatar wilsonge wilsonge - change - 3 Feb 2024
The description was changed
avatar wilsonge wilsonge - edited - 3 Feb 2024
avatar wilsonge wilsonge - change - 3 Feb 2024
The description was changed
avatar wilsonge wilsonge - edited - 3 Feb 2024
avatar Fedik
Fedik - comment - 4 Feb 2024

Please help to understand the changes ?
Currently we have:

sequenceDiagram
Controller->>Model: validate()
Model->>Form: process()
Form->>Form: validate()
Form->>Field: validate()
Field->>Rule: test()
Rule->>Field: returns <br>bool or exteprion
Field->>Form: returns <br>true or exception
Form->>Model: returns bool, <br>and keep exceptions in $errors
opt in case of "invalid form"
Model-->>Form: getErrors()
Note over Model, Form: store form errors <br>in $model->setError()
end
Model->>Controller: returns false <br>or data array
opt in case of "invalid form"
Controller-->>Model: getErrors()
Note over Controller,Model: set errors <br>in $app->enqueueMessage()
end

How it is planed to be with new approach in the end? (for people who not familiar with Symfony validation)

avatar wilsonge
wilsonge - comment - 4 Feb 2024

The contents of the interface are the only things in inspired by Symfony. The process in your diagram is the same - just some the return type hints chaining and better support for fields doing their own validation. What website you using to do that diagram (or is it just paint?)

avatar brianteeman
brianteeman - comment - 4 Feb 2024

better support for fields doing their own validation.

that's the bit I like

avatar Fedik
Fedik - comment - 4 Feb 2024

What website you using to do that diagram (or is it just paint?)

It is this https://mermaid.js.org/syntax/sequenceDiagram.html, Harald added it to our Manual repo.
Github support it in MD markup also.

The process in your diagram is the same - just some the return type hints chaining and better support for fields doing their own validation

What currently I do not like, that we put all errors in to $app->enqueueMessage() in the end.
And no way to get an error per field, so it not possible to render validation error within the field.

In one of my component I have custom form class that store errors per field in $form->errors[$group.$fieldName] = $exception;
Later one, while the field rendering I can do kind of $form->getError($fieldName, $group), and render this error within the field.

Would be nice if new approach allows to do it in the core (I mean getting error per field).
(I did not look your code in details for now, maybe missed it.)

avatar brianteeman
brianteeman - comment - 4 Feb 2024

it not possible to render validation error within the field.

Being able to render the error with the field would be a huge accessibility gain

avatar dgrammatiko
dgrammatiko - comment - 5 Feb 2024

@wilsonge I have also a request (is it still xmas? ?). So, client side Native HTML validation has some quirks with the one that stands out in the case of J: server side rendered form elements that HAVE a value the value is interpreted as VALID! I guess you understand what I will ask: empty the value of any invalid form elements in the data object. (Then a thin layer ontop of the native element.checkValidity(), etc can replace the archaic validate.js)

avatar wilsonge
wilsonge - comment - 6 Feb 2024

I'm not touching server side validation in this PR - sorry :D it's already a big enough PR as it is

avatar wilsonge
wilsonge - comment - 6 Feb 2024

Would be nice if new approach allows to do it in the core (I mean getting error per field).
(I did not look your code in details for now, maybe missed it.)

Yes it does (kinda) because you have access to the full constraint object and have the method to see which of them failed validation.

avatar wilsonge
wilsonge - comment - 6 Feb 2024

Hopefully this looks about right:

sequenceDiagram
Controller->>Model: validate()
Model->>Form: process()
Form->>Form: validate()
Form->>Field: validate()
Field->>Rule: test()
Rule->>Field: returns <br> ConstraintInterface[] <br>bool or exception (supported for b/c until J7)
Field->>Form: returns <br>FieldValidationResponse (which contains the constraints above) <br>true or exception (supported for b/c until J7)
Form->>Model: returns<br>FormValidationResponse (which contains all the fields above) <br> bool, <br>and keep exceptions in $errors
opt in case of "invalid form"
Model-->>Form: <br> no longer required - Model now uses the information contained in the FormValidationResponse <br> getErrors()
Note over Model, Form: store form errors <br>in $model->setError()
end
Model->>Controller: <br> to be determined outside the scope of this PR but probably throws some sort of new Exception (can still be the current way where it returns false <br>or data array)
opt in case of "invalid form"
Controller-->>Model: getErrors()
Note over Controller,Model: set errors <br>in $app->enqueueMessage()
end
avatar dgrammatiko
dgrammatiko - comment - 6 Feb 2024

I'm not touching server side validation in this PR

Did you mean client side? Confused

avatar wilsonge
wilsonge - comment - 6 Feb 2024

Yes I mean client side. This is what happens when I'm up at 6 and still haven't had coffee nearly 3 hours later :)

avatar Fedik
Fedik - comment - 6 Feb 2024

you did not tested your code, did you? ?

Hopefully this looks about right

Thanks for the visualisation

Yes it does (kinda) because you have access to the full constraint object and have the method to see which of them failed validation.

hmhm, I see, I do not know how to say it, but it will not really work ? (in current stage)

As you know, Joomla controller process for content editing is:
layout=edit => task=article.save => redirect => layout=edit

And when controller doing redirect then validation result is lost.
To keep track of validation result we then need to have FormValidationResponse serialisable, wich we can pick after the redirect and somehow set it back to the form instance, so the code can check the field validity during FormField::renderField() (not in the scope of this PR, but the direction).

Because you placing FormField instance in to ConstraintInterface, we can forget about serialisation.
Addittionaly to FormValidationResponse::getInvalidFields() it will be much usefull to have something like FormValidationResponse::isFieldValid($name, $group)

UPD, well it probably also can be some new interface/class/implementation, that allows to exctract FormValidationResponse result and serialise.

8597da1 25 Feb 2024 avatar wilsonge PHPCS
avatar wilsonge
wilsonge - comment - 25 Feb 2024

I thought I had - but i was redirecting my virtualhost to the wrong place :D :D which helpfully allowed it to work really well!!

But i'm not super sure what your issue is though - after very minor fixes and code style I've got a test of adding $form->setModernValidationResponse(true); into Joomla\CMS\MVC\Model\FormBehaviorTrait::loadForm after we load the form. Then in FormModel I've added this in as extra:

        if ($return instanceof FormValidationResponse) {
            if ($return->isValid()){
                throw new \LogicException('Should only be received here if this failed!');
            }

            foreach ($return->getInvalidFields() as $invalidField) {
                $invalidFieldObject = $return->getField($invalidField['name'], $invalidField['group']);

                foreach ($invalidFieldObject->getInvalidConstraints() as $invalidConstraint) {
                    $invalidConstraintObject = $invalidFieldObject->getConstraint($invalidConstraint);

                    $this->setError($invalidConstraintObject->getErrorMessage());
                }
            }

            return false;
        }

and basic form validation stuff seems to work to me. I've forced fields to be invalid and it shows correct errors (both editing a select fields value and also forcing the length of a text field).

I mean my long term plan is to throw an exception in the model rather than the code above - but i haven't got round to working on that part yet - and I think this is good enough standalone to justify.

avatar HLeithner
HLeithner - comment - 24 Apr 2024

This pull request has been automatically rebased to 5.2-dev.

avatar HLeithner HLeithner - change - 24 Apr 2024
Title
[5.1] Rework form validation
[5.2] Rework form validation
avatar HLeithner HLeithner - edited - 24 Apr 2024
avatar HLeithner
HLeithner - comment - 2 Sep 2024

This pull request has been automatically rebased to 5.3-dev.

avatar HLeithner HLeithner - change - 2 Sep 2024
Title
[5.2] Rework form validation
[5.3] Rework form validation
avatar HLeithner HLeithner - edited - 2 Sep 2024
avatar richard67 richard67 - change - 15 Sep 2024
Labels Added: Feature PR-5.3-dev
Removed: PR-5.1-dev

Add a Comment

Login with GitHub to post a comment