NEW Validate DBFields

This commit is contained in:
Steve Boyd 2024-09-26 11:01:41 +12:00
parent c523022cb9
commit e10f6f421a
11 changed files with 228 additions and 63 deletions

View File

@ -20,6 +20,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBDecimal
Double:
class: SilverStripe\ORM\FieldType\DBDouble
Email:
class: SilverStripe\ORM\FieldType\DBEmail
Enum:
class: SilverStripe\ORM\FieldType\DBEnum
Float:

View File

@ -2,11 +2,18 @@
namespace SilverStripe\Forms;
use SilverStripe\Validation\EmailValidator;
/**
* Text input field with validation for correct email format according to RFC 2822.
* Text input field with validation for correct email format
*/
class EmailField extends TextField
{
private static array $field_validators = [
[
'class' => EmailValidator::class,
],
];
protected $inputType = 'email';
/**
@ -17,39 +24,6 @@ class EmailField extends TextField
return 'email text';
}
/**
* Validates for RFC 2822 compliant email addresses.
*
* @see http://www.regular-expressions.info/email.html
* @see http://www.ietf.org/rfc/rfc2822.txt
*
* @param Validator $validator
*
* @return string
*/
public function validate($validator)
{
$result = true;
$this->value = trim($this->value ?? '');
$pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$';
// Escape delimiter characters.
$safePattern = str_replace('/', '\\/', $pattern ?? '');
if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) {
$validator->validationError(
$this->name,
_t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'),
'validation'
);
$result = false;
}
return $this->extendValidationResult($result, $validator);
}
public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();

View File

@ -15,6 +15,8 @@ use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\View\AttributesHTML;
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Validation\FieldValidator;
/**
* Represents a field in a form.
@ -275,6 +277,8 @@ class FormField extends RequestHandler
'Description' => 'HTMLFragment',
];
private static array $field_validators = [];
/**
* Structured schema state representing the FormField's current data and validation.
* Used to render the FormField as a ReactJS Component on the front-end.
@ -1231,15 +1235,25 @@ class FormField extends RequestHandler
}
/**
* Abstract method each {@link FormField} subclass must implement, determines whether the field
* is valid or not based on the value.
* Subclasses can define an existing FieldValidatorClass to validate the FormField value
* They may also override this method to provide custom validation logic
*
* @param Validator $validator
* @return bool
*/
public function validate($validator)
{
return $this->extendValidationResult(true, $validator);
$isValid = true;
$name = strip_tags($this->Title() ? $this->Title() : $this->getName());
$fieldValidators = FieldValidator::createFieldValidatorsForField($this, $name, $this->value);
foreach ($fieldValidators as $fieldValidator) {
$validationResult = $fieldValidator->validate();
if (!$validationResult->isValid()) {
$validator->getResult()->combineAnd($validationResult);
$isValid = false;
}
}
return $this->extendValidationResult($isValid, $validator);
}
/**

View File

@ -2,6 +2,8 @@
namespace SilverStripe\Forms;
use SilverStripe\Validation\StringLengthValidator;
/**
* Text input field.
*/
@ -14,6 +16,13 @@ class TextField extends FormField implements TippableFieldInterface
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;
private static array $field_validators = [
[
'class' => StringLengthValidator::class,
'argCalls' => [null, 'getMaxLength'],
]
];
/**
* @var Tip|null A tip to render beside the input
*/
@ -117,31 +126,6 @@ class TextField extends FormField implements TippableFieldInterface
return $data;
}
/**
* Validate this field
*
* @param Validator $validator
* @return bool
*/
public function validate($validator)
{
$result = true;
if (!is_null($this->maxLength) && mb_strlen($this->value ?? '') > $this->maxLength) {
$name = strip_tags($this->Title() ? $this->Title() : $this->getName());
$validator->validationError(
$this->name,
_t(
'SilverStripe\\Forms\\TextField.VALIDATEMAXLENGTH',
'The value for {name} must not exceed {maxLength} characters in length',
['name' => $name, 'maxLength' => $this->maxLength]
),
"validation"
);
$result = false;
}
return $this->extendValidationResult($result, $validator);
}
public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();

View File

@ -1230,6 +1230,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
public function validate()
{
$result = ValidationResult::create();
// Call DBField::validate() on every DBField
$specs = static::getSchema()->fieldSpecs(static::class);
foreach (array_keys($specs) as $fieldName) {
$dbField = $this->dbObject($fieldName);
$validationResult = $dbField->validate();
if (!$validationResult->isValid()) {
$result->combineAnd($validationResult);
}
}
$this->extend('updateValidate', $result);
return $result;
}

View File

@ -0,0 +1,31 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Forms\EmailField;
use SilverStripe\ORM\FieldType\DBVarchar;
use SilverStripe\Validation\EmailValidator;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\NullableField;
class DBEmail extends DBVarchar
{
private static array $field_validators = [
[
'class' => EmailValidator::class,
],
];
public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
{
// Set field with appropriate size
$field = EmailField::create($this->name, $title);
$field->setMaxLength($this->getSize());
// Allow the user to select if it's null instead of automatically assuming empty string is
if (!$this->getNullifyEmpty()) {
return NullableField::create($field);
}
return $field;
}
}

View File

@ -10,6 +10,8 @@ use SilverStripe\Forms\TextField;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\Model\ModelData;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Validation\FieldValidator;
/**
* Single field in the database.
@ -43,7 +45,6 @@ use SilverStripe\Model\ModelData;
*/
abstract class DBField extends ModelData implements DBIndexable
{
/**
* Raw value of this field
*/
@ -99,6 +100,8 @@ abstract class DBField extends ModelData implements DBIndexable
'ProcessedRAW' => 'HTMLFragment',
];
private static array $field_validators = [];
/**
* Default value in the database.
* Might be overridden on DataObject-level, but still useful for setting defaults on
@ -468,6 +471,22 @@ abstract class DBField extends ModelData implements DBIndexable
}
}
/**
* Validate this field. Called during DataObject::validate().
*/
public function validate(): ValidationResult
{
$result = ValidationResult::create();
$fieldValidators = FieldValidator::createFieldValidatorsForField($this, $this->getName(), $this->getValue());
foreach ($fieldValidators as $fieldValidator) {
$validationResult = $fieldValidator->validate();
if (!$validationResult->isValid()) {
$result->combineAnd($validationResult);
}
}
return $result;
}
/**
* Returns a FormField instance used as a default
* for form scaffolding.

View File

@ -8,6 +8,7 @@ use SilverStripe\Forms\NullableField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\Connect\MySQLDatabase;
use SilverStripe\ORM\DB;
use SilverStripe\Validation\StringLengthValidator;
/**
* Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text
@ -18,6 +19,13 @@ use SilverStripe\ORM\DB;
*/
class DBVarchar extends DBString
{
private static array $field_validators = [
[
'class' => StringLengthValidator::class,
'argCalls' => [null, 'getSize'],
]
];
private static array $casting = [
'Initial' => 'Text',
'URL' => 'Text',

View File

@ -0,0 +1,24 @@
<?php
namespace SilverStripe\Validation;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Validation\FieldValidator;
use SilverStripe\Core\Validation\ConstraintValidator;
use Symfony\Component\Validator\Constraints;
use SilverStripe\Forms\FormField;
use SilverStripe\ORM\FieldType\DBField;
class EmailValidator extends FieldValidator
{
protected function validateValue(ValidationResult $result): ValidationResult
{
$message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
$validationResult = ConstraintValidator::validate(
$this->value,
new Constraints\Email(message: $message),
$this->name
);
return $result->combineAnd($validationResult);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\Validation;
use RuntimeException;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Forms\FormField;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\Core\Injector\Injector;
/**
* Abstract class that can be used as a validator for FormFields and DBFields
*/
abstract class FieldValidator
{
protected string $name;
protected mixed $value;
public function __construct(string $name, mixed $value)
{
$this->name = $name;
$this->value = $value;
}
public function validate(): ValidationResult
{
$result = ValidationResult::create();
$result = $this->validateValue($result);
return $result;
}
abstract protected function validateValue(ValidationResult $result): ValidationResult;
public static function createFieldValidatorsForField(
FormField|DBField $field,
string $name,
mixed $value
): array {
$fieldValidators = [];
$config = $field->config()->get('field_validators');
foreach ($config as $spec) {
$class = $spec['class'];
$argCalls = $spec['argCalls'] ?? null;
if (!is_a($class, FieldValidator::class, true)) {
throw new RuntimeException("Class $class is not a FieldValidator");
}
$args = [$name, $value];
if (!is_null($argCalls)) {
if (!is_array($argCalls)) {
throw new RuntimeException("argCalls for $class is not an array");
}
foreach ($argCalls as $i => $argCall) {
if (!is_string($argCall) && !is_null($argCall)) {
throw new RuntimeException("argCall $i for $class is not a string or null");
}
if ($argCall) {
$args[] = call_user_func([$field, $argCall]);
} else {
$args[] = null;
}
}
}
$fieldValidators[] = Injector::inst()->createWithArgs($class, $args);
}
return $fieldValidators;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace SilverStripe\Validation;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Validation\FieldValidator;
class StringLengthValidator extends FieldValidator
{
private ?int $minLength;
private ?int $maxLength;
public function __construct(string $name, mixed $value, ?int $minLength = null, ?int $maxLength = null)
{
parent::__construct($name, $value);
$this->minLength = $minLength;
$this->maxLength = $maxLength;
}
protected function validateValue(ValidationResult $result): ValidationResult
{
if (!is_null($this->maxLength) && mb_strlen($this->value ?? '') > $this->maxLength) {
$message = _t(
'SilverStripe\\Forms\\TextField.VALIDATEMAXLENGTH',
'The value for {name} must not exceed {maxLength} characters in length',
['name' => $this->name, 'maxLength' => $this->maxLength]
);
$result->addFieldError($this->name, $message);
}
// TODO: minlength check
return $result;
}
}