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 #11408

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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"symfony/dom-crawler": "^7.0",
"symfony/filesystem": "^7.0",
"symfony/http-foundation": "^7.0",
"symfony/intl": "^7.0",
"symfony/mailer": "^7.0",
"symfony/mime": "^7.0",
"symfony/translation": "^7.0",
Expand Down
4 changes: 1 addition & 3 deletions src/Forms/CompositeField.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,8 @@ public function getChildren()
* Returns the name (ID) for the element.
* If the CompositeField doesn't have a name, but we still want the ID/name to be set.
* This code generates the ID from the nested children.
*
* @return String $name
*/
public function getName()
public function getName(): string
{
if ($this->name) {
return $this->name;
Expand Down
28 changes: 5 additions & 23 deletions src/Forms/EmailField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,24 @@

namespace SilverStripe\Forms;

use SilverStripe\Core\Validation\ConstraintValidator;
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
use SilverStripe\Validation\EmailValidator;

/**
* Text input field with validation for correct email format according to the relevant RFC.
*/
class EmailField extends TextField
{
private static array $field_validators = [
EmailValidator::class,
];

protected $inputType = 'email';

public function Type()
{
return 'email text';
}

/**
* Validates for RFC compliant email addresses.
*
* @param Validator $validator
*/
public function validate($validator)
{
$this->value = trim($this->value ?? '');

$message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
$result = ConstraintValidator::validate(
$this->value,
new EmailConstraint(message: $message, mode: EmailConstraint::VALIDATION_MODE_STRICT),
$this->getName()
);
$validator->getResult()->combineAnd($result);
$isValid = $result->isValid();

return $this->extendValidationResult($isValid, $validator);
}

public function getSchemaValidation()
{
$rules = parent::getSchemaValidation();
Expand Down
2 changes: 1 addition & 1 deletion src/Forms/FieldGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function __construct($titleOrField = null, $otherFields = null)
* In some cases the FieldGroup doesn't have a title, but we still want
* the ID / name to be set. This code, generates the ID from the nested children
*/
public function getName()
public function getName(): string
{
if ($this->name) {
return $this->name;
Expand Down
30 changes: 22 additions & 8 deletions src/Forms/FormField.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use SilverStripe\View\AttributesHTML;
use SilverStripe\View\SSViewer;
use SilverStripe\Model\ModelData;
use SilverStripe\Validation\FieldValidatorsTrait;

/**
* Represents a field in a form.
Expand Down Expand Up @@ -44,6 +45,7 @@ class FormField extends RequestHandler
{
use AttributesHTML;
use FormMessage;
use FieldValidatorsTrait;

/** @see $schemaDataType */
const SCHEMA_DATA_TYPE_STRING = 'String';
Expand Down Expand Up @@ -424,12 +426,10 @@ public function getTemplateHelper()

/**
* Returns the field name.
*
* @return string
*/
public function getName()
public function getName(): string
{
return $this->name;
return $this->name ?? '';
}

/**
Expand All @@ -443,12 +443,20 @@ public function getInputType()
}

/**
* Returns the field value.
* Alias of getValue()
*
* @see FormField::setSubmittedValue()
* @return mixed
*/
public function Value()
{
return $this->getValue();
}

/**
* Returns the field value.
*/
public function getValue(): mixed
{
return $this->value;
}
Expand Down Expand Up @@ -1231,15 +1239,21 @@ 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;
$result = $this->validate();
if (!$result->isValid()) {
$isValid = false;
$validator->getResult()->combineAnd($result);
}
return $this->extendValidationResult($isValid, $validator);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Forms/SelectionGroup_Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function setTitle($title)
return $this;
}

function getValue()
function getValue(): mixed
{
return $this->value;
}
Expand Down
31 changes: 6 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\StringValidator;

/**
* Text input field.
*/
Expand All @@ -14,6 +16,10 @@ class TextField extends FormField implements TippableFieldInterface

protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;

private static array $field_validators = [
StringValidator::class => [null, 'getMaxLength'],
];

/**
* @var Tip|null A tip to render beside the input
*/
Expand Down Expand Up @@ -117,31 +123,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
60 changes: 20 additions & 40 deletions src/ORM/FieldType/DBDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
use SilverStripe\Model\ModelData;
use SilverStripe\Validation\DateValidator;

/**
* Represents a date field.
Expand All @@ -33,6 +34,7 @@ class DBDate extends DBField
{
/**
* Standard ISO format string for date in CLDR standard format
* This is equivalent to php date format "Y-m-d" e.g. 2024-08-31
*/
public const ISO_DATE = 'y-MM-dd';

Expand All @@ -42,13 +44,14 @@ class DBDate extends DBField
*/
public const ISO_LOCALE = 'en_US';

private static array $field_validators = [
DateValidator::class,
];

public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
{
$value = $this->parseDate($value);
if ($value === false) {
throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
);
if ($value !== null) {
$value = $this->parseDate($value);
}
$this->value = $value;
return $this;
Expand All @@ -58,32 +61,25 @@ public function setValue(mixed $value, null|array|ModelData $record = null, bool
* Parse timestamp or iso8601-ish date into standard iso8601 format
*
* @param mixed $value
* @return string|null|false Formatted date, null if empty but valid, or false if invalid
* @return mixed Formatted date, or the original value if it couldn't be parsed
*/
protected function parseDate(mixed $value): string|null|false
{
// Skip empty values
if (empty($value) && !is_numeric($value)) {
return null;
}

// Determine value to parse
if (is_array($value)) {
$source = $value; // parse array
} elseif (is_numeric($value)) {
$source = $value; // parse timestamp
} else {
// Convert US date -> iso, fix y2k, etc
$value = $this->fixInputDate($value);
if (is_null($value)) {
return null;
}
$source = strtotime($value ?? ''); // convert string to timestamp
$fixedValue = $this->fixInputDate($value);
// convert string to timestamp
$source = strtotime($fixedValue ?? '');
}
if ($value === false) {
return false;
if (!$source) {
// Unable to parse date, keep as is so that the validator can catch it later
return $value;
}

// Format as iso8601
$formatter = $this->getInternalFormatter();
return $formatter->format($source);
Expand Down Expand Up @@ -560,20 +556,12 @@ public function scaffoldFormField(?string $title = null, array $params = []): ?F
*/
protected function fixInputDate($value)
{
// split
[$year, $month, $day, $time] = $this->explodeDateString($value);

if ((int)$year === 0 && (int)$month === 0 && (int)$day === 0) {
return null;
}
// Validate date
if (!checkdate($month ?? 0, $day ?? 0, $year ?? 0)) {
throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
);
if (!checkdate((int) $month, (int) $day, (int) $year)) {
// Keep invalid dates as they are so that the validator can catch them later
return $value;
}

// Convert to y-m-d
// Convert to Y-m-d
return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time);
}

Expand All @@ -591,11 +579,8 @@ protected function explodeDateString($value)
$value ?? '',
$matches
)) {
throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
);
return [0, 0, 0, ''];
}

$parts = [
$matches['first'],
$matches['second'],
Expand All @@ -605,11 +590,6 @@ protected function explodeDateString($value)
if ($parts[0] < 1000 && $parts[2] > 1000) {
$parts = array_reverse($parts ?? []);
}
if ($parts[0] < 1000 && (int)$parts[0] !== 0) {
throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
);
}
$parts[] = $matches['time'];
return $parts;
}
Expand Down
Loading
Loading