Skip to content

Commit

Permalink
DOC Rename validator classes
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Dec 3, 2024
1 parent 63c767d commit 6404e10
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 53 deletions.
8 changes: 4 additions & 4 deletions en/02_Developer_Guides/03_Forms/00_Introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use PageController;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyFormPageController extends PageController
Expand All @@ -63,7 +63,7 @@ class MyFormPageController extends PageController
FormAction::create('doSayHello')->setTitle('Say hello')
);

$required = RequiredFields::create('Name');
$required = RequiredFieldsValidator::create('Name');

$form = Form::create($this, 'HelloForm', $fields, $actions, $required);

Expand Down Expand Up @@ -414,7 +414,7 @@ namespace App\PageType;

use PageController;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;

class MyFormPageController extends PageController
{
Expand All @@ -424,7 +424,7 @@ class MyFormPageController extends PageController
{
// ...

$validator = RequiredFields::create([
$validator = RequiredFieldsValidator::create([
'Name',
'Email',
]);
Expand Down
94 changes: 59 additions & 35 deletions en/02_Developer_Guides/03_Forms/01_Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use PageController;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyFormPageController extends PageController
Expand All @@ -46,7 +46,7 @@ class MyFormPageController extends PageController
);

// the fields 'Name' and 'Email' are required.
$required = RequiredFields::create([
$required = RequiredFieldsValidator::create([
'Name', 'Email',
]);

Expand All @@ -71,71 +71,95 @@ In this example we will be required to input a value for `Name` and a valid emai

> [!NOTE]
> Each individual [FormField](api:SilverStripe\Forms\FormField) instance is responsible for validating the submitted content through the
> [FormField::validate()](api:SilverStripe\Forms\FormField::validate()) method. By default, this just checks the value exists. Fields like `EmailField` override
> `validate` to check for a specific format.
> [FormField::validate()](api:SilverStripe\Forms\FormField::validate()) method. For example [`EmailField::validate()`](api:SilverStripe\Forms\EmailField::validate()) checks the field value is a valid email.
## Extensions

Extensions applied to `FormField`, or subclasses, can hook into the validation logic and adjust the results by utilising
the `updateValidationResult` method. For example, an extension applied to `EmailField` could look like this:
the `updateValidate` method. For example, an extension applied to `EmailField` could look like this:

```php
namespace App\Extension;

use SilverStripe\Core\Extension;
use SilverStripe\Forms\Validator;
use SilverStripe\Core\Validation\ValidationResult;

class FormFieldValidationExtension extends Extension
{
protected function updateValidationResult(bool &$result, Validator $validator)
protected function updateValidate(ValidationResult $result): void
{
if (str_ends_with($this->owner->Value(), '@example.com')) {
$validator->validationError($this->owner->Name(), 'Please provide a valid email address');
$result = false;
$result->addFieldError(
$this->getOwner()->Name(),
'Please provide a valid email address which does not end with @example.com'
);
}
}
}
```

> [!WARNING]
> This extension hook will not work without the ampersand (`&`) in the `&$result` argument. This is because the return
> value of the function is ignored, so the validation result has to be updated by changing the value of the `$result`
> variable. This is known as [passing by reference](https://www.php.net/manual/en/language.references.pass.php).
## Validation in `FormField` subclasses

Subclasses of `FormField` can define their own version of `validate` to provide custom validation rules such as the
above example with the `Email` validation. The `validate` method on `FormField` takes a single argument of the current
`Validator` instance.
Subclasses of `FormField` can define their own version of `validate()` to provide custom validation rules such as the
above example with the `Email` validation.

```php
namespace App\Form\Field;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Forms\NumericField;

class CustomNumberField extends NumericField
{
// ...

public function validate($validator)
public function validate(): ValidationResult
{
if ((int) $this->Value() === 10) {
$validator->validationError($this->Name(), 'This value cannot be 10');
return $this->extendValidationResult(false, $validator);
}

return $this->extendValidationResult(true, $validator);
$this->beforeExtending('updateValidate', function (ValidationResult $result) {
if ((int) $this->Value() === 20) {
$result->addFieldError($this->Name(), 'This value cannot be 20');
}
});
return parent::validate();
}
}
```

The `validate` method should compute a boolean (`true` if the value passes validation and `false` if Silverstripe CMS
should trigger a validation error on the page) and pass this to the `extendValidationResult` method to allow extensions
to hook into the validation logic. In addition, in the event of failed validation, a useful error message must be set
on the given validator.
The `validate()` method returns a [`ValidationResult`](api:SilverStripe\Core\Validation\ValidationResult) object with any errors added to it, and should contain a useful error message. The `validate()` method should follow the example above and utilise the `$this->beforeExtending('updateValidate', ...)` method and be followed by `return parent::validate();` in order for code in the parent class to be executed in the correct order.

## `FieldValidator` classes used for `FormField` validation

Many of the built-in `FormField` classes use standardised [`FieldValidator`](api:SilverStripe\Core\Validation\FieldValidation\FieldValidator) to perform some or all of their validation. For example, the `EmailField` class uses the both the [`StringFieldValidator`](api:SilverStripe\Core\Validation\FieldValidation\StringFieldValidator) and the [`EmailFieldValidator`](api:SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator) for validation.

These are configured via the [`FormField.field_validators`](api:SilverStripe\Forms\FormField->field_validators) configuration, which you can configure on your own `FormField` subclasses.

```php
namespace App\Form\Field;

use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
use SilverStripe\Core\Validation\FieldValidation\OptionFieldValidator;
use SilverStripe\Forms\EmailField;

class CustomEmailField extends EmailField
{
// ...

private static array $field_validators = [
// Disable the EmailFieldValidator defined in the parent EmailField by setting it to null
EmailFieldValidator::class => null,
// Add an OptionFieldValidator to validate the field value is in a list of allowable values
OptionFieldValidator::class => ['getAllowableEmails'],
];

> [!WARNING]
> You can also override the entire `Form` validation by subclassing `Form` and defining a `validate` method on the form.
public function getAllowableEmails(): array
{
return [
'[email protected]',
'[email protected]',
];
}
}
```

## Form action validation

Expand Down Expand Up @@ -213,7 +237,7 @@ class MyFormPageController extends PageController

public function doSubmitForm($data, $form)
{
// At this point, RequiredFields->isValid() will have been called already,
// At this point, RequiredFieldsValidator->isValid() will have been called already,
// so we can assume that the values exist. Say we want to make sure that email hasn't already been used.

$check = Member::get()->filter('Email', $data['Email'])->first();
Expand Down Expand Up @@ -243,7 +267,7 @@ The Silverstripe framework comes with the following built-in validators:
given form by putting them inside a `CompositeValidator`. The `CompositeValidator` doesn't have perform any validation by itself.
- [`FieldsValidator`](api:SilverStripe\Forms\FieldsValidator)
Simply calls [`validate()`](api:SilverStripe\Forms\FormField::validate()) on all data fields in the form, to ensure fields have valid values.
- [`RequiredFields`](api:SilverStripe\Forms\RequiredFields)
- [`RequiredFieldsValidator`](api:SilverStripe\Forms\RequiredFieldsValidator)
Validates that fields you declare as "required" have a value.

There are additional validators available in community modules, and you can implement your own validators by subclassing the abstract `Validator` class.
Expand Down Expand Up @@ -361,7 +385,7 @@ namespace App\PageType;
use Page;
use SilverStripe\Forms\CompositeValidator;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyPage extends Page
Expand All @@ -384,7 +408,7 @@ class MyPage extends Page
public function getCMSCompositeValidator(): CompositeValidator
{
$validator = parent::getCMSCompositeValidator();
$validator->addValidator(RequiredFields::create([
$validator->addValidator(RequiredFieldsValidator::create([
'MyRequiredField',
]));
return $validator;
Expand All @@ -398,5 +422,5 @@ class MyPage extends Page
## API documentation

- [RequiredFields](api:SilverStripe\Forms\RequiredFields)
- [RequiredFieldsValidator](api:SilverStripe\Forms\RequiredFieldsValidator)
- [Validator](api:SilverStripe\Forms\Validator)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;

class SearchPageController extends PageController
{
Expand Down Expand Up @@ -57,7 +57,7 @@ class SearchPageController extends PageController
FormAction::create('doSearchForm', 'Search')
);

$required = RequiredFields::create([
$required = RequiredFieldsValidator::create([
'Type',
]);

Expand Down Expand Up @@ -92,7 +92,7 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HeaderField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\OptionsetField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;

class SearchForm extends Form
{
Expand Down Expand Up @@ -127,7 +127,7 @@ class SearchForm extends Form
FormAction::create('doSearchForm', 'Search')
);

$required = RequiredFields::create([
$required = RequiredFieldsValidator::create([
'Type',
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ The final thing we do is return a 'thank you for your feedback' message to the u

All forms have some basic validation built in – email fields will only let the user enter email addresses, number fields will only accept numbers, and so on. Sometimes you need more complicated validation, so you can define your own validation by extending the Validator class.

The framework comes with a predefined validator called [RequiredFields](api:SilverStripe\Forms\RequiredFields), which performs the common task of making sure particular fields are filled out. Below is the code to add validation to a contact form:
The framework comes with a predefined validator called [`RequiredFieldsValidator`](api:SilverStripe\Forms\RequiredFieldsValidator), which performs the common task of making sure particular fields are filled out. Below is the code to add validation to a contact form:

```php
namespace App\PageType;

use PageController;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;

class ContactPageController extends PageController
{
Expand All @@ -167,10 +167,10 @@ class ContactPageController extends PageController
public function getForm()
{
// ...
$validator = RequiredFields::create('Name', 'Message');
$validator = RequiredFieldsValidator::create('Name', 'Message');
return Form::create($this, 'Form', $fields, $actions, $validator);
}
}
```

We've created a RequiredFields object, passing the name of the fields we want to be required. The validator we have created is then passed as the fifth argument of the form constructor. If we now try to submit the form without filling out the required fields, JavaScript validation will kick in, and the user will be presented with a message about the missing fields. If the user has JavaScript disabled, PHP validation will kick in when the form is submitted, and the user will be redirected back to the Form with messages about their missing fields.
We've created a RequiredFieldsValidator object, passing the name of the fields we want to be required. The validator we have created is then passed as the fifth argument of the form constructor. If we now try to submit the form without filling out the required fields, JavaScript validation will kick in, and the user will be presented with a message about the missing fields. If the user has JavaScript disabled, PHP validation will kick in when the form is submitted, and the user will be redirected back to the Form with messages about their missing fields.
12 changes: 6 additions & 6 deletions en/02_Developer_Guides/03_Forms/How_Tos/06_Handle_Nested_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyController extends Controller
Expand Down Expand Up @@ -99,7 +99,7 @@ class MyController extends Controller
FieldList::create([
FormAction::create('doSubmitForm', 'Submit'),
]),
RequiredFields::create([
RequiredFieldsValidator::create([
'Name',
'Teams',
'ID',
Expand Down Expand Up @@ -149,7 +149,7 @@ use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyController extends Controller
Expand Down Expand Up @@ -177,7 +177,7 @@ class MyController extends Controller
FieldList::create([
FormAction::create('doSubmitForm', 'Submit'),
]),
RequiredFields::create([
RequiredFieldsValidator::create([
'Name',
'HometownTeam.Name',
'ID',
Expand Down Expand Up @@ -232,7 +232,7 @@ use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\Forms\RequiredFieldsValidator;
use SilverStripe\Forms\TextField;

class MyController extends Controller
Expand Down Expand Up @@ -262,7 +262,7 @@ class MyController extends Controller
FieldList::create([
FormAction::create('doSubmitForm', 'Submit'),
]),
RequiredFields::create([
RequiredFieldsValidator::create([
'Name',
'MyTeams[]',
'ID',
Expand Down
12 changes: 12 additions & 0 deletions en/08_Changelogs/6.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ title: 6.0.0 (unreleased)
- [API changes](#api-changes)
- [Many renamed classes](#renamed-classes)
- [GraphQL removed from the CMS](#graphql-removed)
- [`FormField` classes now use `FieldValidator` for validation](#formfield-validation)
- [Most extension hook methods are now protected](#hooks-protected)
- [Changes to some extension hook names](#hooks-renamed)
- [Strict typing for `Factory` implementations](#factory-strict-typing)
Expand Down Expand Up @@ -906,6 +907,17 @@ PHP code such as resolvers that were in [`silverstripe/asset-admin`](http://gith

If your project does not have any custom GraphQL, after upgrading you may still have the old `.graphql-generated` and `public/_graphql` folders in your project. You can safely remove these folders.

### `FormField` classes now use `FieldValidator` for validation {#formfield-validation}

Many of [`FormField`](api:SilverStripe\Forms\FormField) subclasses in the `SilverStripe\Forms` namespace now use `FieldValidator` classes for validation, which are also used for `DBField` validation. This has meant that much of the old `validate()` logic on `FormField` subclasses has been removed as it was duplicated in the `FieldValidator` classes. Some custom in `validate()` methods not found in `FieldValidator` classes methods has been retained.

As part of this change, the [`FormField::validate()`](api:SilverStripe\Forms\FormField::validate()) now returns a `ValidationResult` object where it used to return a boolean. The `$validator` parameter has also been removed. If you have implemented a custom `validate()` method in a `FormField` subclass, you will need to update it to return a `ValidationResult` object instead and remove the `$validator` parameter.

The `extendValidationResult()` method and the `updateValidationResult` extension hook on `FormField` have both been removed and replaced with an `updateValidate` hook instead, which has a single `ValidationResult $result` parameter. This matches the `updateValidate` extension hook on `DataObject`.

> [!IMPORTANT]
> As part of this change method signature of `FormField::validate()` changed so that it no longer accepts a parameter, and not returns a `ValidationResult` object instead of a boolean.

### Most extension hook methods are now protected {#hooks-protected}

Core implementations of most extension hooks such as `updateCMSFields()` now have protected visibility. Formerly they had public visibility which meant they could be called directly which was not how they were intended to be used. Extension hook implementations are still able to be declared public in project code, though it is recommended that all extension hook methods are declared protected in project code to follow best practice.
Expand Down

0 comments on commit 6404e10

Please sign in to comment.