Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW Validate DBFields #11402

Draft
wants to merge 1 commit into
base: 6
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _config/model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
42 changes: 8 additions & 34 deletions src/Forms/EmailField.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand All @@ -17,39 +24,6 @@ public function Type()
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();
Expand Down
20 changes: 17 additions & 3 deletions src/Forms/FormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1231,15 +1235,25 @@ protected function extendValidationResult(bool $result, Validator $validator): b
}

/**
* 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);
}

/**
Expand Down
34 changes: 9 additions & 25 deletions src/Forms/TextField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace SilverStripe\Forms;

use SilverStripe\Validation\StringLengthValidator;

/**
* Text input field.
*/
Expand All @@ -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
*/
Expand Down Expand Up @@ -117,31 +126,6 @@ public function getSchemaDataDefaults()
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();
Expand Down
9 changes: 9 additions & 0 deletions src/ORM/DataObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,15 @@ public function forceChange()
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;
}
Expand Down
31 changes: 31 additions & 0 deletions src/ORM/FieldType/DBEmail.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
21 changes: 20 additions & 1 deletion src/ORM/FieldType/DBField.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
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.
Expand Down Expand Up @@ -43,7 +45,6 @@
*/
abstract class DBField extends ModelData implements DBIndexable
{

/**
* Raw value of this field
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -468,6 +471,22 @@ public function saveInto(ModelData $model): void
}
}

/**
* 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.
Expand Down
8 changes: 8 additions & 0 deletions src/ORM/FieldType/DBVarchar.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
Expand All @@ -18,6 +19,13 @@
*/
class DBVarchar extends DBString
{
private static array $field_validators = [
[
'class' => StringLengthValidator::class,
'argCalls' => [null, 'getSize'],
]
];

private static array $casting = [
'Initial' => 'Text',
'URL' => 'Text',
Expand Down
24 changes: 24 additions & 0 deletions src/Validation/EmailValidator.php
Original file line number Diff line number Diff line change
@@ -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);
}
}
67 changes: 67 additions & 0 deletions src/Validation/FieldValidator.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading