silverstripe / silverstripe-framework

Silverstripe Framework, the MVC framework that powers Silverstripe CMS
https://www.silverstripe.org
BSD 3-Clause "New" or "Revised" License
722 stars 821 forks source link

Consolidate validation systems #11391

Open emteknetnz opened 3 days ago

emteknetnz commented 3 days ago

Currently there are two main types of validation within the CMS, which is fairly confusing:

Ideally we'd get rid of Form validation entirely and have all validation fire whenever DataObject::write() is called. This would have two major benefits:

One idea that could help greatly here would be to move validation logic from FormFields and on to their DBField counterparts, for instance move EmailField::validate() to a new class DBEmail and put the validation logic there. Projects would then need to change 'Email' => 'Varchar', to 'Email' => 'Email'. All DataObject->write()'s to this field would now have the Email field validated. This would also have an additional benefit where form auto-scaffolding would now automatically give you the correct EmailField instead of the default TextField.

We could mitigate the risk that projects forget to change Varchar to Email by putting the validation logic in a trait and just leaving it on the Email field and keeping Form field validation functional (so things double validate), though update our documentation to only give examples of the new best practice.

We'd also probably want to rename getCMSCompositeValidator() to just getCompositeValidator() and get it to run on DataObject::write(), rather than on Form submission, as RequiredField's is probably the main way that validation is added within projects. We could also just deprecate it and add a new private $static array $required_fields = []; or just $required on DataObject.

Related issues

Acceptance criteria

GuySartorelli commented 3 days ago

Ideally we'd get rid of Form validation entirely

I assume you mean for data objects in the CMS specifically. We would obviously need to keep form validation for custom and front-end forms.

emteknetnz commented 3 days ago

Realistically we'd probably need to retain form validation as just removing it could cause significant upgrade pain. However I think it should be pushed to the background as much as possible and we ourselves ideally would upgrade things we manage to use model validation exclusively, and we encourage others to do the same.

We would obviously need to keep form validation for custom and front-end forms.

I'm not sure that's true, if the idea I raised in the original description of moving validation to DBField is viable, then projects could in theory create custom DBField's to correspond with any custom fields that have custom validation. That's probably annoying for people, though it doesn't mean it's not possible.

GuySartorelli commented 3 days ago

Not all forms are backed by a DataObject. Not all form fields would have a suitable DbField analogue.

I would be very resistant to moving form validation to non-form constructs. We don't want to make it unnecessarily difficult to make front-end forms, nor to create new form field types.

emteknetnz commented 3 days ago

Yes agree, as I said we'd probably need to retain Form validation, though encourage the use of model validation instead.

For an example of how this would look, assuming it all worked as envisioned, linkfields EmailLink would go from this:

class EmailLink extends Link
{
    // ...
    private static array $db = [
        'Email' => 'Varchar(255)',
    ];

    public function getCMSFields(): FieldList
    {
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
            $fields->replaceField('Email', EmailField::create(
                'Email',
                _t(__CLASS__ . '.EMAIL_FIELD', 'Email address'),
            ));
            $fields->removeByName('OpenInNew');
        });
        return parent::getCMSFields();
    }

    public function getCMSCompositeValidator(): CompositeValidator
    {
        $validator = parent::getCMSCompositeValidator();
        $validator->addValidator(RequiredFields::create(['Email']));
        return $validator;
    }  
    // ...

to this

class EmailLink extends Link
{
    // ...
    private static array $db = [
        'Email' => 'Email',
    ];

    private static array $required = [
        'Email',
    ];

    public function getCMSFields(): FieldList
    {
        $this->beforeUpdateCMSFields(function (FieldList $fields) {
            $fields->removeByName('OpenInNew');
        });
        return parent::getCMSFields();
    }   
    // ...
GuySartorelli commented 3 days ago

That's fine, so long as someone can still create a contact form on the front end with an email field, and it gets validated correctly without needing a dbfield or data object connected to it 👍

That doesn't necessarily mean the validation for email address has to live inside the email field, but there needs to be an intuitive way to validate it with minimal additional boilerplate.