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

Merged
Merged
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
6 changes: 6 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 All @@ -36,6 +38,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBHTMLVarchar
Int:
class: SilverStripe\ORM\FieldType\DBInt
IP:
class: SilverStripe\ORM\FieldType\DBIp
BigInt:
class: SilverStripe\ORM\FieldType\DBBigInt
Locale:
Expand All @@ -58,6 +62,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBText
Time:
class: SilverStripe\ORM\FieldType\DBTime
URL:
class: SilverStripe\ORM\FieldType\DBUrl
Varchar:
class: SilverStripe\ORM\FieldType\DBVarchar
Year:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"symfony/dom-crawler": "^7.0",
"symfony/filesystem": "^7.0",
"symfony/http-foundation": "^7.0",
"symfony/intl": "^7.0",
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
"symfony/mailer": "^7.0",
"symfony/mime": "^7.0",
"symfony/translation": "^7.0",
Expand Down
65 changes: 65 additions & 0 deletions src/Core/Validation/FieldValidation/BigIntFieldValidator.php
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use RunTimeException;
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
use SilverStripe\Core\Validation\ValidationResult;

/**
* A field validator for 64-bit integers
* Will throw a RunTimeException if used on a 32-bit system
*/
class BigIntFieldValidator extends IntFieldValidator
{
/**
* The minimum value for a signed 64-bit integer.
* Defined as string instead of int otherwise will end up as a float
* on 64-bit systems
*
* When this is cast to an int in IntFieldValidator::__construct()
* it will be properly cast to an int
*/
protected const MIN_INT = '-9223372036854775808';

/**
* The maximum value for a signed 64-bit integer.
*/
protected const MAX_INT = '9223372036854775807';

public function __construct(
string $name,
mixed $value,
?int $minValue = null,
?int $maxValue = null
) {
if (is_null($minValue) || is_null($maxValue)) {
$bits = strlen(decbin(~0));
if ($bits === 32) {
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
throw new RunTimeException('Cannot use BigIntFieldValidator on a 32-bit system');
}
}
$this->minValue = $minValue;
$this->maxValue = $maxValue;
parent::__construct($name, $value, $minValue, $maxValue);
}

protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
// Validate string values that are too large or too small
// Only testing for string values here as that's all bccomp can take as arguments
// int values that are too large or too small will be cast to float
// on 64-bit systems and will fail the validation in IntFieldValidator
if (is_string($this->value)) {
if (!is_null($this->minValue) && bccomp($this->value, static::MIN_INT) === -1) {
$result->addFieldError($this->name, $this->getTooSmallMessage());
}
if (!is_null($this->maxValue) && bccomp($this->value, static::MAX_INT) === 1) {
$result->addFieldError($this->name, $this->getTooLargeMessage());
}
}
$result->combineAnd(parent::validateValue());
return $result;
}
}
22 changes: 22 additions & 0 deletions src/Core/Validation/FieldValidation/BooleanFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;

/**
* Validates that a value is a boolean
*/
class BooleanFieldValidator extends FieldValidator
{
protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
if (!is_bool($this->value)) {
$message = _t(__CLASS__ . '.INVALID', 'Invalid value');
$result->addFieldError($this->name, $message);
}
return $result;
}
}
39 changes: 39 additions & 0 deletions src/Core/Validation/FieldValidation/CompositeFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use InvalidArgumentException;
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;

/**
* A field validator used to validate DBComposite fields
*/
class CompositeFieldValidator extends FieldValidator
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* @param mixed $value - an iterable list of FieldValidators
*/
public function __construct(string $name, mixed $value)
{
parent::__construct($name, $value);
if (!is_iterable($value)) {
throw new InvalidArgumentException('Value must be iterable');
}
foreach ($value as $child) {
if (!is_a($child, FieldValidationInterface::class)) {
throw new InvalidArgumentException('Child is not a' . FieldValidationInterface::class);
}
}
}

protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
foreach ($this->value as $child) {
$result->combineAnd($child->validate());
}
return $result;
}
}
41 changes: 41 additions & 0 deletions src/Core/Validation/FieldValidation/DateFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
use SilverStripe\Core\Validation\ValidationResult;

/**
* Validates that a value is a valid date, which means that it follows the equivalent formats:
* - PHP date format Y-m-d
* - SO format y-MM-dd i.e. DBDate::ISO_DATE
*
* Blank string values are allowed
*/
class DateFieldValidator extends FieldValidator
{
protected function validateValue(): ValidationResult
{
$result = ValidationResult::create();
// Allow empty strings
if ($this->value === '') {
return $result;
}
// Not using symfony/validator because it was allowing d-m-Y format strings
$date = date_parse_from_format($this->getFormat(), $this->value ?? '');
if ($date === false || $date['error_count'] > 0 || $date['warning_count'] > 0) {
$result->addFieldError($this->name, $this->getMessage());
}
return $result;
}

protected function getFormat(): string
{
return 'Y-m-d';
}

protected function getMessage(): string
{
return _t(__CLASS__ . '.INVALID', 'Invalid date');
}
}
25 changes: 25 additions & 0 deletions src/Core/Validation/FieldValidation/DatetimeFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;

/**
* Validates that a value is a valid date/time, which means that it follows the equivalent formats:
* - PHP date format Y-m-d H:i:s
* - ISO format 'y-MM-dd HH:mm:ss' i.e. DBDateTime::ISO_DATETIME
*
* Blank string values are allowed
*/
class DatetimeFieldValidator extends DateFieldValidator
{
protected function getFormat(): string
{
return 'Y-m-d H:i:s';
}

protected function getMessage(): string
{
return _t(__CLASS__ . '.INVALID', 'Invalid date/time');
}
}
78 changes: 78 additions & 0 deletions src/Core/Validation/FieldValidation/DecimalFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;

/**
* Validates that a value is a valid decimal
* This intended for use when validating that a value can be stored in a database as a decimal
*
* Example of how digits are stored in the database
* Decimal(5,2) is allowed a total of 5 digits, and will always round to 2 decimal places
* This means it has a maximum 3 digits before the decimal point
*
* Valid
* 123.99
* 999.99
* -999.99
* 123.999 - will round to 124.00
*
* Not valid
* 1234.9 - 4 digits the before the decimal point
* 999.999 - would be rounded to 1000.00 which exceeds 5 total digits
*/
class DecimalFieldValidator extends NumericFieldValidator
{
/**
* Whole number size e.g. For Decimal(9,2) this would be 9
*/
private int $wholeSize;

/**
* Decimal size e.g. For Decimal(5,2) this would be 2
*/
private int $decimalSize;

public function __construct(
string $name,
mixed $value,
int $wholeSize,
int $decimalSize,
int $minValue = null,
int $maxValue = null,
) {
parent::__construct($name, $value, $minValue, $maxValue);
$this->wholeSize = $wholeSize;
$this->decimalSize = $decimalSize;
}

protected function validateValue(): ValidationResult
{
$result = parent::validateValue();
if (!$result->isValid()) {
return $result;
}
// Convert to absolute value - the minus sign is not relevant for validation
$absValue = abs($this->value);
// Round to the decimal size which is what the database will do
$rounded = round($absValue, $this->decimalSize);
// Get formatted as a string, which will right pad with zeros to the decimal size
$rounded = number_format($rounded, $this->decimalSize, thousands_separator: '');
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
// Count this number of digits - the minus 1 is for the decimal point
$digitCount = strlen((string) $rounded) - 1;
if ($digitCount > $this->wholeSize) {
$message = _t(
__CLASS__ . '.TOOLARGE',
'Cannot have more than {wholeSize} digits which includes {decimalSize} decimal places',
[
'wholeSize' => $this->wholeSize,
'decimalSize' => $this->decimalSize
]
);
$result->addFieldError($this->name, $message);
}
return $result;
}
}
24 changes: 24 additions & 0 deletions src/Core/Validation/FieldValidation/EmailFieldValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorTrait;
use SilverStripe\Core\Validation\FieldValidation\SymfonyFieldValidatorInterface;

/**
* Validates that a value is a valid email address
* Uses Symfony's Email constraint to validate
*/
class EmailFieldValidator extends StringFieldValidator implements SymfonyFieldValidatorInterface
{
use SymfonyFieldValidatorTrait;

public function getConstraint(): Constraint|array
{
$message = _t(__CLASS__ . '.INVALID', 'Invalid email address');
return new Email(message: $message);
}
}
18 changes: 18 additions & 0 deletions src/Core/Validation/FieldValidation/FieldValidationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace SilverStripe\Core\Validation\FieldValidation;

use SilverStripe\Core\Validation\ValidationInterface;

/**
* Interface for fields e.g. a DBField or FormField, that can use FieldValidator's
* Intended for use on classes that have the FieldValidationTrait applied
*/
interface FieldValidationInterface extends ValidationInterface
GuySartorelli marked this conversation as resolved.
Show resolved Hide resolved
{
public function getName(): string;

public function getValue(): mixed;

public function getValueForValidation(): mixed;
}
Loading
Loading