mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Compare commits
4 Commits
aca6068d76
...
26166512d0
Author | SHA1 | Date | |
---|---|---|---|
|
26166512d0 | ||
|
f83f56eba1 | ||
|
a213fe2a08 | ||
|
f5ef850085 |
@ -20,6 +20,8 @@ SilverStripe\Core\Injector\Injector:
|
|||||||
class: SilverStripe\ORM\FieldType\DBDecimal
|
class: SilverStripe\ORM\FieldType\DBDecimal
|
||||||
Double:
|
Double:
|
||||||
class: SilverStripe\ORM\FieldType\DBDouble
|
class: SilverStripe\ORM\FieldType\DBDouble
|
||||||
|
Email:
|
||||||
|
class: SilverStripe\ORM\FieldType\DBEmail
|
||||||
Enum:
|
Enum:
|
||||||
class: SilverStripe\ORM\FieldType\DBEnum
|
class: SilverStripe\ORM\FieldType\DBEnum
|
||||||
Float:
|
Float:
|
||||||
@ -36,6 +38,8 @@ SilverStripe\Core\Injector\Injector:
|
|||||||
class: SilverStripe\ORM\FieldType\DBHTMLVarchar
|
class: SilverStripe\ORM\FieldType\DBHTMLVarchar
|
||||||
Int:
|
Int:
|
||||||
class: SilverStripe\ORM\FieldType\DBInt
|
class: SilverStripe\ORM\FieldType\DBInt
|
||||||
|
IP:
|
||||||
|
class: SilverStripe\ORM\FieldType\DBIp
|
||||||
BigInt:
|
BigInt:
|
||||||
class: SilverStripe\ORM\FieldType\DBBigInt
|
class: SilverStripe\ORM\FieldType\DBBigInt
|
||||||
Locale:
|
Locale:
|
||||||
@ -58,6 +62,8 @@ SilverStripe\Core\Injector\Injector:
|
|||||||
class: SilverStripe\ORM\FieldType\DBText
|
class: SilverStripe\ORM\FieldType\DBText
|
||||||
Time:
|
Time:
|
||||||
class: SilverStripe\ORM\FieldType\DBTime
|
class: SilverStripe\ORM\FieldType\DBTime
|
||||||
|
URL:
|
||||||
|
class: SilverStripe\ORM\FieldType\DBUrl
|
||||||
Varchar:
|
Varchar:
|
||||||
class: SilverStripe\ORM\FieldType\DBVarchar
|
class: SilverStripe\ORM\FieldType\DBVarchar
|
||||||
Year:
|
Year:
|
||||||
|
4
bin/sake
4
bin/sake
@ -2,6 +2,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use SilverStripe\Cli\Sake;
|
use SilverStripe\Cli\Sake;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
|
||||||
// Ensure that people can't access this from a web-server
|
// Ensure that people can't access this from a web-server
|
||||||
if (!in_array(PHP_SAPI, ['cli', 'cgi', 'cgi-fcgi'])) {
|
if (!in_array(PHP_SAPI, ['cli', 'cgi', 'cgi-fcgi'])) {
|
||||||
@ -11,5 +12,8 @@ if (!in_array(PHP_SAPI, ['cli', 'cgi', 'cgi-fcgi'])) {
|
|||||||
|
|
||||||
require_once __DIR__ . '/../src/includes/autoload.php';
|
require_once __DIR__ . '/../src/includes/autoload.php';
|
||||||
|
|
||||||
|
// CLI scripts must only use the primary database connection and not replicas
|
||||||
|
DB::setMustUsePrimary();
|
||||||
|
|
||||||
$sake = new Sake();
|
$sake = new Sake();
|
||||||
$sake->run();
|
$sake->run();
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
"symfony/dom-crawler": "^7.0",
|
"symfony/dom-crawler": "^7.0",
|
||||||
"symfony/filesystem": "^7.0",
|
"symfony/filesystem": "^7.0",
|
||||||
"symfony/http-foundation": "^7.0",
|
"symfony/http-foundation": "^7.0",
|
||||||
|
"symfony/intl": "^7.0",
|
||||||
"symfony/mailer": "^7.0",
|
"symfony/mailer": "^7.0",
|
||||||
"symfony/mime": "^7.0",
|
"symfony/mime": "^7.0",
|
||||||
"symfony/translation": "^7.0",
|
"symfony/translation": "^7.0",
|
||||||
|
@ -17,6 +17,7 @@ use SilverStripe\Versioned\Versioned;
|
|||||||
use SilverStripe\View\Requirements;
|
use SilverStripe\View\Requirements;
|
||||||
use SilverStripe\View\Requirements_Backend;
|
use SilverStripe\View\Requirements_Backend;
|
||||||
use SilverStripe\View\TemplateGlobalProvider;
|
use SilverStripe\View\TemplateGlobalProvider;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Director is responsible for processing URLs, and providing environment information.
|
* Director is responsible for processing URLs, and providing environment information.
|
||||||
@ -84,6 +85,14 @@ class Director implements TemplateGlobalProvider
|
|||||||
*/
|
*/
|
||||||
private static $default_base_url = '`SS_BASE_URL`';
|
private static $default_base_url = '`SS_BASE_URL`';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of routing rule patterns that must only use the primary database and not a replica
|
||||||
|
*/
|
||||||
|
private static array $rule_patterns_must_use_primary_db = [
|
||||||
|
'dev',
|
||||||
|
'Security',
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -296,6 +305,18 @@ class Director implements TemplateGlobalProvider
|
|||||||
{
|
{
|
||||||
Injector::inst()->registerService($request, HTTPRequest::class);
|
Injector::inst()->registerService($request, HTTPRequest::class);
|
||||||
|
|
||||||
|
// Check if primary database must be used based on request rules
|
||||||
|
// Note this check must happend before the rules are processed as
|
||||||
|
// $shiftOnSuccess param is passed as true in `$request->match($pattern, true)` later on in
|
||||||
|
// this method, which modifies `$this->dirParts`, thus affecting `$request->match($rule)` directly below
|
||||||
|
$primaryDbOnlyRules = Director::config()->uninherited('rule_patterns_must_use_primary_db');
|
||||||
|
foreach ($primaryDbOnlyRules as $rule) {
|
||||||
|
if ($request->match($rule)) {
|
||||||
|
DB::setMustUsePrimary();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rules = Director::config()->uninherited('rules');
|
$rules = Director::config()->uninherited('rules');
|
||||||
|
|
||||||
$this->extend('updateRules', $rules);
|
$this->extend('updateRules', $rules);
|
||||||
|
@ -85,7 +85,7 @@ class ClassInfo implements Flushable
|
|||||||
public static function hasTable($tableName)
|
public static function hasTable($tableName)
|
||||||
{
|
{
|
||||||
$cache = ClassInfo::getCache();
|
$cache = ClassInfo::getCache();
|
||||||
$configData = serialize(DB::getConfig());
|
$configData = serialize(DB::getConfig(DB::CONN_PRIMARY));
|
||||||
$cacheKey = 'tableList_' . md5($configData);
|
$cacheKey = 'tableList_' . md5($configData);
|
||||||
$tableList = $cache->get($cacheKey) ?? [];
|
$tableList = $cache->get($cacheKey) ?? [];
|
||||||
if (empty($tableList) && DB::is_active()) {
|
if (empty($tableList) && DB::is_active()) {
|
||||||
|
@ -7,12 +7,14 @@ use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
|
|||||||
use SilverStripe\ORM\Connect\NullDatabase;
|
use SilverStripe\ORM\Connect\NullDatabase;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple Kernel container
|
* Simple Kernel container
|
||||||
*/
|
*/
|
||||||
class CoreKernel extends BaseKernel
|
class CoreKernel extends BaseKernel
|
||||||
{
|
{
|
||||||
|
|
||||||
protected bool $bootDatabase = true;
|
protected bool $bootDatabase = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,7 +40,7 @@ class CoreKernel extends BaseKernel
|
|||||||
$this->flush = $flush;
|
$this->flush = $flush;
|
||||||
|
|
||||||
if (!$this->bootDatabase) {
|
if (!$this->bootDatabase) {
|
||||||
DB::set_conn(new NullDatabase());
|
DB::set_conn(new NullDatabase(), DB::CONN_PRIMARY);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->bootPHP();
|
$this->bootPHP();
|
||||||
@ -73,7 +75,7 @@ class CoreKernel extends BaseKernel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load default database configuration from the $database and $databaseConfig globals
|
* Load database configuration from the $database and $databaseConfig globals
|
||||||
*/
|
*/
|
||||||
protected function bootDatabaseGlobals()
|
protected function bootDatabaseGlobals()
|
||||||
{
|
{
|
||||||
@ -84,41 +86,62 @@ class CoreKernel extends BaseKernel
|
|||||||
global $databaseConfig;
|
global $databaseConfig;
|
||||||
global $database;
|
global $database;
|
||||||
|
|
||||||
// Case 1: $databaseConfig global exists. Merge $database in as needed
|
// Ensure global database config has prefix and suffix applied
|
||||||
if (!empty($databaseConfig)) {
|
if (!empty($databaseConfig) && !empty($database)) {
|
||||||
if (!empty($database)) {
|
$databaseConfig['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
||||||
$databaseConfig['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only set it if its valid, otherwise ignore $databaseConfig entirely
|
|
||||||
if (!empty($databaseConfig['database'])) {
|
|
||||||
DB::setConfig($databaseConfig);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: $database merged into existing config
|
// Set config for primary and any replicas
|
||||||
if (!empty($database)) {
|
for ($i = 0; $i <= DB::MAX_REPLICAS; $i++) {
|
||||||
$existing = DB::getConfig();
|
if ($i === 0) {
|
||||||
$existing['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
$name = DB::CONN_PRIMARY;
|
||||||
|
} else {
|
||||||
|
$name = DB::getReplicaConfigKey($i);
|
||||||
|
if (!DB::hasConfig($name)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DB::setConfig($existing);
|
// Case 1: $databaseConfig global exists
|
||||||
|
// Only set it if its valid, otherwise ignore $databaseConfig entirely
|
||||||
|
if (!empty($databaseConfig) && !empty($databaseConfig['database'])) {
|
||||||
|
DB::setConfig($databaseConfig, $name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: $databaseConfig global does not exist
|
||||||
|
// Merge $database global into existing config
|
||||||
|
if (!empty($database)) {
|
||||||
|
$dbConfig = DB::getConfig($name);
|
||||||
|
$dbConfig['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
||||||
|
DB::setConfig($dbConfig, $name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load default database configuration from environment variable
|
* Load database configuration from environment variables
|
||||||
*/
|
*/
|
||||||
protected function bootDatabaseEnvVars()
|
protected function bootDatabaseEnvVars()
|
||||||
{
|
{
|
||||||
if (!$this->bootDatabase) {
|
if (!$this->bootDatabase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Set default database config
|
// Set primary database config
|
||||||
$databaseConfig = $this->getDatabaseConfig();
|
$databaseConfig = $this->getDatabaseConfig();
|
||||||
$databaseConfig['database'] = $this->getDatabaseName();
|
$databaseConfig['database'] = $this->getDatabaseName();
|
||||||
DB::setConfig($databaseConfig);
|
DB::setConfig($databaseConfig, DB::CONN_PRIMARY);
|
||||||
|
|
||||||
|
// Set database replicas config
|
||||||
|
for ($i = 1; $i <= DB::MAX_REPLICAS; $i++) {
|
||||||
|
$envKey = $this->getReplicaEnvKey('SS_DATABASE_SERVER', $i);
|
||||||
|
if (!Environment::hasEnv($envKey)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$replicaDatabaseConfig = $this->getDatabaseReplicaConfig($i);
|
||||||
|
$configKey = DB::getReplicaConfigKey($i);
|
||||||
|
DB::setConfig($replicaDatabaseConfig, $configKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,12 +150,72 @@ class CoreKernel extends BaseKernel
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function getDatabaseConfig()
|
protected function getDatabaseConfig()
|
||||||
|
{
|
||||||
|
return $this->getSingleDataBaseConfig(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDatabaseReplicaConfig(int $replica)
|
||||||
|
{
|
||||||
|
if ($replica <= 0) {
|
||||||
|
throw new InvalidArgumentException('Replica number must be greater than 0');
|
||||||
|
}
|
||||||
|
return $this->getSingleDataBaseConfig($replica);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a database key to a replica key
|
||||||
|
* e.g. SS_DATABASE_SERVER -> SS_DATABASE_SERVER_REPLICA_01
|
||||||
|
*
|
||||||
|
* @param string $key - The key to look up in the environment
|
||||||
|
* @param int $replica - Replica number
|
||||||
|
*/
|
||||||
|
private function getReplicaEnvKey(string $key, int $replica): string
|
||||||
|
{
|
||||||
|
if ($replica <= 0) {
|
||||||
|
throw new InvalidArgumentException('Replica number must be greater than 0');
|
||||||
|
}
|
||||||
|
// Do not allow replicas to define keys that could lead to unexpected behaviour if
|
||||||
|
// they do not match the primary database configuration
|
||||||
|
if (in_array($key, ['SS_DATABASE_CLASS', 'SS_DATABASE_NAME', 'SS_DATABASE_CHOOSE_NAME'])) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
// Left pad replica number with a zeros to match the length of the maximum replica number
|
||||||
|
$len = strlen((string) DB::MAX_REPLICAS);
|
||||||
|
return $key . '_REPLICA_' . str_pad($replica, $len, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a single database configuration variable from the environment
|
||||||
|
* For replica databases, it will first attempt to find replica-specific configuration
|
||||||
|
* before falling back to the primary configuration.
|
||||||
|
*
|
||||||
|
* Replicate specific configuration has `_REPLICA_01` appended to the key
|
||||||
|
* where 01 is the replica number.
|
||||||
|
*
|
||||||
|
* @param string $key - The key to look up in the environment
|
||||||
|
* @param int $replica - Replica number. Passing 0 will return the primary database configuration
|
||||||
|
*/
|
||||||
|
private function getDatabaseConfigVariable(string $key, int $replica): string
|
||||||
|
{
|
||||||
|
if ($replica > 0) {
|
||||||
|
$key = $this->getReplicaEnvKey($key, $replica);
|
||||||
|
}
|
||||||
|
if (Environment::hasEnv($key)) {
|
||||||
|
return Environment::getEnv($key);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $replica - Replica number. Passing 0 will return the primary database configuration
|
||||||
|
*/
|
||||||
|
private function getSingleDataBaseConfig(int $replica): array
|
||||||
{
|
{
|
||||||
$databaseConfig = [
|
$databaseConfig = [
|
||||||
"type" => Environment::getEnv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
|
"type" => $this->getDatabaseConfigVariable('SS_DATABASE_CLASS', $replica) ?: 'MySQLDatabase',
|
||||||
"server" => Environment::getEnv('SS_DATABASE_SERVER') ?: 'localhost',
|
"server" => $this->getDatabaseConfigVariable('SS_DATABASE_SERVER', $replica) ?: 'localhost',
|
||||||
"username" => Environment::getEnv('SS_DATABASE_USERNAME') ?: null,
|
"username" => $this->getDatabaseConfigVariable('SS_DATABASE_USERNAME', $replica) ?: null,
|
||||||
"password" => Environment::getEnv('SS_DATABASE_PASSWORD') ?: null,
|
"password" => $this->getDatabaseConfigVariable('SS_DATABASE_PASSWORD', $replica) ?: null,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Only add SSL keys in the array if there is an actual value associated with them
|
// Only add SSL keys in the array if there is an actual value associated with them
|
||||||
@ -143,7 +226,7 @@ class CoreKernel extends BaseKernel
|
|||||||
'ssl_cipher' => 'SS_DATABASE_SSL_CIPHER',
|
'ssl_cipher' => 'SS_DATABASE_SSL_CIPHER',
|
||||||
];
|
];
|
||||||
foreach ($sslConf as $key => $envVar) {
|
foreach ($sslConf as $key => $envVar) {
|
||||||
$envValue = Environment::getEnv($envVar);
|
$envValue = $this->getDatabaseConfigVariable($envVar, $replica);
|
||||||
if ($envValue) {
|
if ($envValue) {
|
||||||
$databaseConfig[$key] = $envValue;
|
$databaseConfig[$key] = $envValue;
|
||||||
}
|
}
|
||||||
@ -159,25 +242,25 @@ class CoreKernel extends BaseKernel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the port if called for
|
// Set the port if called for
|
||||||
$dbPort = Environment::getEnv('SS_DATABASE_PORT');
|
$dbPort = $this->getDatabaseConfigVariable('SS_DATABASE_PORT', $replica);
|
||||||
if ($dbPort) {
|
if ($dbPort) {
|
||||||
$databaseConfig['port'] = $dbPort;
|
$databaseConfig['port'] = $dbPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the timezone if called for
|
// Set the timezone if called for
|
||||||
$dbTZ = Environment::getEnv('SS_DATABASE_TIMEZONE');
|
$dbTZ = $this->getDatabaseConfigVariable('SS_DATABASE_TIMEZONE', $replica);
|
||||||
if ($dbTZ) {
|
if ($dbTZ) {
|
||||||
$databaseConfig['timezone'] = $dbTZ;
|
$databaseConfig['timezone'] = $dbTZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For schema enabled drivers:
|
// For schema enabled drivers:
|
||||||
$dbSchema = Environment::getEnv('SS_DATABASE_SCHEMA');
|
$dbSchema = $this->getDatabaseConfigVariable('SS_DATABASE_SCHEMA', $replica);
|
||||||
if ($dbSchema) {
|
if ($dbSchema) {
|
||||||
$databaseConfig["schema"] = $dbSchema;
|
$databaseConfig["schema"] = $dbSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For SQlite3 memory databases (mainly for testing purposes)
|
// For SQlite3 memory databases (mainly for testing purposes)
|
||||||
$dbMemory = Environment::getEnv('SS_DATABASE_MEMORY');
|
$dbMemory = $this->getDatabaseConfigVariable('SS_DATABASE_MEMORY', $replica);
|
||||||
if ($dbMemory) {
|
if ($dbMemory) {
|
||||||
$databaseConfig["memory"] = $dbMemory;
|
$databaseConfig["memory"] = $dbMemory;
|
||||||
}
|
}
|
||||||
@ -205,6 +288,7 @@ class CoreKernel extends BaseKernel
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get name of database
|
* Get name of database
|
||||||
|
* Note that any replicas must have the same database name as the primary database
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
@ -35,9 +35,9 @@ class ConstraintValidator
|
|||||||
/** @var ConstraintViolationInterface $violation */
|
/** @var ConstraintViolationInterface $violation */
|
||||||
foreach ($violations as $violation) {
|
foreach ($violations as $violation) {
|
||||||
if ($fieldName) {
|
if ($fieldName) {
|
||||||
$result->addFieldError($fieldName, $violation->getMessage());
|
$result->addFieldError($fieldName, $violation->getMessage(), value: $value);
|
||||||
} else {
|
} else {
|
||||||
$result->addError($violation->getMessage());
|
$result->addError($violation->getMessage(), value: $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for validators that use Symfony constraints
|
||||||
|
*/
|
||||||
|
abstract class AbstractSymfonyFieldValidator extends StringFieldValidator
|
||||||
|
{
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = parent::validateValue();
|
||||||
|
if (!$result->isValid()) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$constraintClass = $this->getConstraintClass();
|
||||||
|
$args = [
|
||||||
|
...$this->getContraintNamedArgs(),
|
||||||
|
'message' => $this->getMessage(),
|
||||||
|
];
|
||||||
|
$constraint = new $constraintClass(...$args);
|
||||||
|
$validationResult = ConstraintValidator::validate($this->value, $constraint, $this->name);
|
||||||
|
return $result->combineAnd($validationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The symfony constraint class to use
|
||||||
|
*/
|
||||||
|
abstract protected function getConstraintClass(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The named args to pass to the constraint
|
||||||
|
* Defined named args as assoc array keys
|
||||||
|
*/
|
||||||
|
protected function getContraintNamedArgs(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message to use when the value is invalid
|
||||||
|
*/
|
||||||
|
abstract protected function getMessage(): string;
|
||||||
|
}
|
36
src/Core/Validation/FieldValidation/BigIntFieldValidator.php
Normal file
36
src/Core/Validation/FieldValidation/BigIntFieldValidator.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
|
|
||||||
|
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 if defined as an int
|
||||||
|
*/
|
||||||
|
private const MIN_64_BIT_INT = '-9223372036854775808';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum value for a signed 64-bit integer.
|
||||||
|
*/
|
||||||
|
private const MAX_64_BIT_INT = '9223372036854775807';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
mixed $value,
|
||||||
|
?int $minValue = null,
|
||||||
|
?int $maxValue = null
|
||||||
|
) {
|
||||||
|
if (is_null($minValue)) {
|
||||||
|
// Casting the string const to an int will properly return an int on 64-bit systems
|
||||||
|
$minValue = (int) BigIntFieldValidator::MIN_64_BIT_INT;
|
||||||
|
}
|
||||||
|
if (is_null($maxValue)) {
|
||||||
|
$maxValue = (int) BigIntFieldValidator::MAX_64_BIT_INT;
|
||||||
|
}
|
||||||
|
parent::__construct($name, $value, $minValue, $maxValue);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates value is boolean stored as an integer i.e. 1 or 0
|
||||||
|
* true and false are not valid values
|
||||||
|
*/
|
||||||
|
class BooleanIntFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
if ($this->value !== 1 && $this->value !== 0) {
|
||||||
|
$message = _t(__CLASS__ . '.INVALID', 'Invalid value');
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
class CompositeFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
40
src/Core/Validation/FieldValidation/DateFieldValidator.php
Normal file
40
src/Core/Validation/FieldValidation/DateFieldValidator.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?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
|
||||||
|
* Emtpy values are allowed
|
||||||
|
*/
|
||||||
|
class DateFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
// Allow empty values
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?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
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
parent::__construct($name, $value);
|
||||||
|
$this->wholeSize = $wholeSize;
|
||||||
|
$this->decimalSize = $decimalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = parent::validateValue();
|
||||||
|
if (!$result->isValid()) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
// 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 rounted to 10000000.00 which exceeds the 9 digits
|
||||||
|
|
||||||
|
// Convert to absolute value - any the minus sign is not counted
|
||||||
|
$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: '');
|
||||||
|
// 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',
|
||||||
|
'Digit count cannot be greater than than {wholeSize}',
|
||||||
|
['wholeSize' => $this->wholeSize]
|
||||||
|
);
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
19
src/Core/Validation/FieldValidation/EmailFieldValidator.php
Normal file
19
src/Core/Validation/FieldValidation/EmailFieldValidator.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\AbstractSymfonyFieldValidator;
|
||||||
|
|
||||||
|
class EmailFieldValidator extends AbstractSymfonyFieldValidator
|
||||||
|
{
|
||||||
|
protected function getConstraintClass(): string
|
||||||
|
{
|
||||||
|
return Constraints\Email::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMessage(): string
|
||||||
|
{
|
||||||
|
return _t(__CLASS__ . '.INVALID', 'Invalid email address');
|
||||||
|
}
|
||||||
|
}
|
27
src/Core/Validation/FieldValidation/EnumFieldValidator.php
Normal file
27
src/Core/Validation/FieldValidation/EnumFieldValidator.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
|
||||||
|
|
||||||
|
class EnumFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
protected array $allowedValues;
|
||||||
|
|
||||||
|
public function __construct(string $name, mixed $value, array $allowedValues)
|
||||||
|
{
|
||||||
|
parent::__construct($name, $value);
|
||||||
|
$this->allowedValues = $allowedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
if (!in_array($this->value, $this->allowedValues, true)) {
|
||||||
|
$message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value');
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationInterface;
|
||||||
|
|
||||||
|
interface FieldValidationInterface extends ValidationInterface
|
||||||
|
{
|
||||||
|
public function getName(): string;
|
||||||
|
|
||||||
|
public function getValueForValidation(): mixed;
|
||||||
|
}
|
39
src/Core/Validation/FieldValidation/FieldValidator.php
Normal file
39
src/Core/Validation/FieldValidation/FieldValidator.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\ValidationInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that can be used as a validator for FormFields and DBFields
|
||||||
|
*/
|
||||||
|
abstract class FieldValidator implements ValidationInterface
|
||||||
|
{
|
||||||
|
protected string $name;
|
||||||
|
protected mixed $value;
|
||||||
|
|
||||||
|
public function __construct(string $name, mixed $value)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the value
|
||||||
|
*/
|
||||||
|
public function validate(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
$validationResult = $this->validateValue($result);
|
||||||
|
if (!$validationResult->isValid()) {
|
||||||
|
$result->combineAnd($validationResult);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inner validatation method that that is implemented by subclasses
|
||||||
|
*/
|
||||||
|
abstract protected function validateValue(): ValidationResult;
|
||||||
|
}
|
118
src/Core/Validation/FieldValidation/FieldValidatorsTrait.php
Normal file
118
src/Core/Validation/FieldValidation/FieldValidatorsTrait.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use SilverStripe\Core\Injector\Injector;
|
||||||
|
use SilverStripe\Core\Config\Configurable;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Forms\FormField;
|
||||||
|
|
||||||
|
trait FieldValidatorsTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* FieldValidators configuration for the field, which is either a FormField or DBField
|
||||||
|
*
|
||||||
|
* Each item in the array can be one of the following
|
||||||
|
* a) MyFieldValidator::class,
|
||||||
|
* b) MyFieldValidator::class => [null, 'getMyArg'],
|
||||||
|
* c) MyFieldValidator::class => null,
|
||||||
|
*
|
||||||
|
* a) Will create a FieldValidator and pass the name and value of the field as args to the constructor
|
||||||
|
* b) Will create a FieldValidator and pass the name, value, make a pass additional args, calling each
|
||||||
|
* non-null value on the field e.g. it will skip the first arg and call $field->getMyArg() for the second arg
|
||||||
|
* c) Will disable a previously set FieldValidator. This is useful to disable a FieldValidator that was set
|
||||||
|
* on a parent class
|
||||||
|
*
|
||||||
|
* You may only have a single instance of a FieldValidator class per field
|
||||||
|
*/
|
||||||
|
private static array $field_validators = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate this field
|
||||||
|
*/
|
||||||
|
public function validate(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
$fieldValidators = $this->getFieldValidators();
|
||||||
|
foreach ($fieldValidators as $fieldValidator) {
|
||||||
|
$validationResult = $fieldValidator->validate();
|
||||||
|
if (!$validationResult->isValid()) {
|
||||||
|
$result->combineAnd($validationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FieldValidators based on `field_validators` configuration
|
||||||
|
*/
|
||||||
|
private function getFieldValidators(): array
|
||||||
|
{
|
||||||
|
$fieldValidators = [];
|
||||||
|
// Used to disable a validator that was previously set with an int index
|
||||||
|
$disabledClasses = [];
|
||||||
|
$interface = FieldValidationInterface::class;
|
||||||
|
// temporary check, will make FormField implement FieldValidationInterface in a future PR
|
||||||
|
$tmp = FormField::class;
|
||||||
|
if (!is_a($this, $interface) && !is_a($this, $tmp)) {
|
||||||
|
$class = get_class($this);
|
||||||
|
throw new RuntimeException("Class $class does not implement interface $interface");
|
||||||
|
}
|
||||||
|
/** @var FieldValidationInterface|Configurable $this */
|
||||||
|
$name = $this->getName();
|
||||||
|
$value = $this->getValueForValidation();
|
||||||
|
// Field name is required for FieldValidators when called ValidationResult::addFieldMessage()
|
||||||
|
if ($name === '') {
|
||||||
|
throw new RuntimeException('Field name is blank');
|
||||||
|
}
|
||||||
|
$classes = [];
|
||||||
|
$config = $this->config()->get('field_validators');
|
||||||
|
foreach ($config as $indexOrClass => $classOrArgCallsOrDisable) {
|
||||||
|
$class = '';
|
||||||
|
$argCalls = [];
|
||||||
|
$disable = false;
|
||||||
|
if (is_int($indexOrClass)) {
|
||||||
|
$class = $classOrArgCallsOrDisable;
|
||||||
|
} else {
|
||||||
|
$class = $indexOrClass;
|
||||||
|
$argCalls = $classOrArgCallsOrDisable;
|
||||||
|
$disable = $classOrArgCallsOrDisable === null;
|
||||||
|
}
|
||||||
|
if ($disable) {
|
||||||
|
$disabledClasses[$class] = true;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (isset($disabledClasses[$class])) {
|
||||||
|
unset($disabledClasses[$class]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_a($class, FieldValidator::class, true)) {
|
||||||
|
throw new RuntimeException("Class $class is not a FieldValidator");
|
||||||
|
}
|
||||||
|
if (!is_array($argCalls)) {
|
||||||
|
throw new RuntimeException("argCalls for FieldValidator $class is not an array");
|
||||||
|
}
|
||||||
|
$classes[$class] = $argCalls;
|
||||||
|
}
|
||||||
|
foreach (array_keys($disabledClasses) as $class) {
|
||||||
|
unset($classes[$class]);
|
||||||
|
}
|
||||||
|
foreach ($classes as $class => $argCalls) {
|
||||||
|
$args = [$name, $value];
|
||||||
|
foreach ($argCalls as $i => $argCall) {
|
||||||
|
if (!is_string($argCall) && !is_null($argCall)) {
|
||||||
|
throw new RuntimeException("argCall $i for FieldValidator $class is not a string or null");
|
||||||
|
}
|
||||||
|
if ($argCall) {
|
||||||
|
$args[] = call_user_func([$this, $argCall]);
|
||||||
|
} else {
|
||||||
|
$args[] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fieldValidators[$class] = Injector::inst()->createWithArgs($class, $args);
|
||||||
|
}
|
||||||
|
return array_values($fieldValidators);
|
||||||
|
}
|
||||||
|
}
|
46
src/Core/Validation/FieldValidation/IntFieldValidator.php
Normal file
46
src/Core/Validation/FieldValidation/IntFieldValidator.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
|
||||||
|
|
||||||
|
class IntFieldValidator extends NumericFieldValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The minimum value for a signed 32-bit integer.
|
||||||
|
* Defined as string instead of int because be cast to a float
|
||||||
|
* on 32-bit systems if defined as an int
|
||||||
|
*/
|
||||||
|
private const MIN_32_BIT_INT = '-2147483648';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum value for a signed 32-bit integer.
|
||||||
|
*/
|
||||||
|
private const MAX_32_BIT_INT = '2147483647';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
mixed $value,
|
||||||
|
?int $minValue = null,
|
||||||
|
?int $maxValue = null
|
||||||
|
) {
|
||||||
|
if (is_null($minValue)) {
|
||||||
|
$minValue = (int) IntFieldValidator::MIN_32_BIT_INT;
|
||||||
|
}
|
||||||
|
if (is_null($maxValue)) {
|
||||||
|
$maxValue = (int) IntFieldValidator::MAX_32_BIT_INT;
|
||||||
|
}
|
||||||
|
parent::__construct($name, $value, $minValue, $maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = parent::validateValue();
|
||||||
|
if (!is_int($this->value)) {
|
||||||
|
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be an integer');
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
30
src/Core/Validation/FieldValidation/IpFieldValidator.php
Normal file
30
src/Core/Validation/FieldValidation/IpFieldValidator.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\AbstractSymfonyFieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for IP addresses. Accepts both IPv4 and IPv6.
|
||||||
|
*/
|
||||||
|
class IpFieldValidator extends AbstractSymfonyFieldValidator
|
||||||
|
{
|
||||||
|
protected function getConstraintClass(): string
|
||||||
|
{
|
||||||
|
return Constraints\Ip::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getContraintNamedArgs(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Allow both IPv4 and IPv6
|
||||||
|
'version' => Constraints\Ip::ALL,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMessage(): string
|
||||||
|
{
|
||||||
|
return _t(__CLASS__ . '.INVALID', 'Invalid IP address');
|
||||||
|
}
|
||||||
|
}
|
22
src/Core/Validation/FieldValidation/LocaleFieldValidator.php
Normal file
22
src/Core/Validation/FieldValidation/LocaleFieldValidator.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\AbstractSymfonyFieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a value is a valid locale, e.g. de, de_DE)
|
||||||
|
*/
|
||||||
|
class LocaleFieldValidator extends AbstractSymfonyFieldValidator
|
||||||
|
{
|
||||||
|
protected function getConstraintClass(): string
|
||||||
|
{
|
||||||
|
return Constraints\Locale::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMessage(): string
|
||||||
|
{
|
||||||
|
return _t(__CLASS__ . '.INVALID', 'Invalid locale');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
|
|
||||||
|
class MultiEnumFieldValidator extends EnumFieldValidator
|
||||||
|
{
|
||||||
|
public function __construct(string $name, mixed $value, array $allowedValues)
|
||||||
|
{
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new InvalidArgumentException('Value must be an array');
|
||||||
|
}
|
||||||
|
parent::__construct($name, $value, $allowedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
foreach ($this->value as $value) {
|
||||||
|
if (!in_array($value, $this->allowedValues, true)) {
|
||||||
|
$message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value');
|
||||||
|
$result->addFieldError($this->name, $message, value: $value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
|
||||||
|
|
||||||
|
class NumericFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Minimum size of the number
|
||||||
|
*/
|
||||||
|
private ?int $minValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum size of the number
|
||||||
|
*/
|
||||||
|
private ?int $maxValue;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
mixed $value,
|
||||||
|
?int $minValue = null,
|
||||||
|
?int $maxValue = null
|
||||||
|
) {
|
||||||
|
$this->minValue = $minValue;
|
||||||
|
$this->maxValue = $maxValue;
|
||||||
|
parent::__construct($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
if (!is_numeric($this->value) || is_string($this->value)) {
|
||||||
|
// Must be a numeric value, though not as a numeric string
|
||||||
|
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be numeric');
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
return $result;
|
||||||
|
} elseif (isset($this->minValue) && $this->value < $this->minValue) {
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.TOOSMALL',
|
||||||
|
'Value cannot be less than {minValue}',
|
||||||
|
['minValue' => $this->minValue]
|
||||||
|
);
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
} elseif (isset($this->maxValue) && $this->value > $this->maxValue) {
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.TOOLARGE',
|
||||||
|
'Value cannot be greater than {maxValue}',
|
||||||
|
['maxValue' => $this->maxValue]
|
||||||
|
);
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
69
src/Core/Validation/FieldValidation/StringFieldValidator.php
Normal file
69
src/Core/Validation/FieldValidation/StringFieldValidator.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a value is a string and optionally checks its multi-byte length.
|
||||||
|
*/
|
||||||
|
class StringFieldValidator extends FieldValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The minimum length of the string
|
||||||
|
*/
|
||||||
|
private ?int $minLength;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum length of the string
|
||||||
|
*/
|
||||||
|
private ?int $maxLength;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
mixed $value,
|
||||||
|
?int $minLength = null,
|
||||||
|
?int $maxLength = null
|
||||||
|
) {
|
||||||
|
parent::__construct($name, $value);
|
||||||
|
if ($minLength && $minLength < 0) {
|
||||||
|
throw new InvalidArgumentException('minLength must be greater than or equal to 0');
|
||||||
|
}
|
||||||
|
$this->minLength = $minLength;
|
||||||
|
$this->maxLength = $maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateValue(): ValidationResult
|
||||||
|
{
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
if (!is_string($this->value)) {
|
||||||
|
$message = _t(__CLASS__ . '.WRONGTYPE', 'Must be a string');
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
// Blank strings are valid, even if there's a minLength requirement
|
||||||
|
if ($this->value === '') {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
$len = mb_strlen($this->value);
|
||||||
|
if (!is_null($this->minLength) && $len < $this->minLength) {
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.TOOSHORT',
|
||||||
|
'Must have at least {minLength} characters',
|
||||||
|
['minLength' => $this->minLength]
|
||||||
|
);
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
if (!is_null($this->maxLength) && $len > $this->maxLength) {
|
||||||
|
$message = _t(
|
||||||
|
__CLASS__ . '.TOOLONG',
|
||||||
|
'Can not have more than {maxLength} characters',
|
||||||
|
['maxLength' => $this->maxLength]
|
||||||
|
);
|
||||||
|
$result->addFieldError($this->name, $message, value: $this->value);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
23
src/Core/Validation/FieldValidation/TimeFieldValidator.php
Normal file
23
src/Core/Validation/FieldValidation/TimeFieldValidator.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a value is a valid time, which means that it follows the equivalent formats:
|
||||||
|
* - PHP date format H:i:s
|
||||||
|
* - ISO format 'HH:mm:ss' i.e. DBTime::ISO_TIME
|
||||||
|
*/
|
||||||
|
class TimeFieldValidator extends DateFieldValidator
|
||||||
|
{
|
||||||
|
protected function getFormat(): string
|
||||||
|
{
|
||||||
|
return 'H:i:s';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMessage(): string
|
||||||
|
{
|
||||||
|
return _t(__CLASS__ . '.INVALID', 'Invalid time');
|
||||||
|
}
|
||||||
|
}
|
19
src/Core/Validation/FieldValidation/UrlFieldValidator.php
Normal file
19
src/Core/Validation/FieldValidation/UrlFieldValidator.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\AbstractSymfonyFieldValidator;
|
||||||
|
|
||||||
|
class UrlFieldValidator extends AbstractSymfonyFieldValidator
|
||||||
|
{
|
||||||
|
protected function getConstraintClass(): string
|
||||||
|
{
|
||||||
|
return Constraints\Url::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMessage(): string
|
||||||
|
{
|
||||||
|
return _t(__CLASS__ . '.INVALID', 'Invalid URL');
|
||||||
|
}
|
||||||
|
}
|
10
src/Core/Validation/ValidationInterface.php
Normal file
10
src/Core/Validation/ValidationInterface.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Validation;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\ValidationResult;
|
||||||
|
|
||||||
|
interface ValidationInterface
|
||||||
|
{
|
||||||
|
public function validate(): ValidationResult;
|
||||||
|
}
|
@ -46,6 +46,11 @@ class ValidationResult
|
|||||||
*/
|
*/
|
||||||
const CAST_TEXT = 'text';
|
const CAST_TEXT = 'text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value of $value parameter
|
||||||
|
*/
|
||||||
|
private const VALUE_UNSET = '_VALUE_UNSET_';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the result valid or not.
|
* Is the result valid or not.
|
||||||
* Note that there can be non-error messages in the list.
|
* Note that there can be non-error messages in the list.
|
||||||
@ -71,11 +76,17 @@ class ValidationResult
|
|||||||
* This can be usedful for ensuring no duplicate messages
|
* This can be usedful for ensuring no duplicate messages
|
||||||
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
||||||
* Bool values will be treated as plain text flag.
|
* Bool values will be treated as plain text flag.
|
||||||
|
* @param mixed $value The value that failed validation
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function addError($message, $messageType = ValidationResult::TYPE_ERROR, $code = null, $cast = ValidationResult::CAST_TEXT)
|
public function addError(
|
||||||
{
|
$message,
|
||||||
return $this->addFieldError(null, $message, $messageType, $code, $cast);
|
$messageType = ValidationResult::TYPE_ERROR,
|
||||||
|
$code = null,
|
||||||
|
$cast = ValidationResult::CAST_TEXT,
|
||||||
|
$value = ValidationResult::VALUE_UNSET,
|
||||||
|
) {
|
||||||
|
return $this->addFieldError(null, $message, $messageType, $code, $cast, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,6 +100,7 @@ class ValidationResult
|
|||||||
* This can be usedful for ensuring no duplicate messages
|
* This can be usedful for ensuring no duplicate messages
|
||||||
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
||||||
* Bool values will be treated as plain text flag.
|
* Bool values will be treated as plain text flag.
|
||||||
|
* @param mixed $value The value that failed validation
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function addFieldError(
|
public function addFieldError(
|
||||||
@ -96,10 +108,11 @@ class ValidationResult
|
|||||||
$message,
|
$message,
|
||||||
$messageType = ValidationResult::TYPE_ERROR,
|
$messageType = ValidationResult::TYPE_ERROR,
|
||||||
$code = null,
|
$code = null,
|
||||||
$cast = ValidationResult::CAST_TEXT
|
$cast = ValidationResult::CAST_TEXT,
|
||||||
|
$value = ValidationResult::VALUE_UNSET,
|
||||||
) {
|
) {
|
||||||
$this->isValid = false;
|
$this->isValid = false;
|
||||||
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast);
|
return $this->addFieldMessage($fieldName, $message, $messageType, $code, $cast, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,11 +125,17 @@ class ValidationResult
|
|||||||
* This can be usedful for ensuring no duplicate messages
|
* This can be usedful for ensuring no duplicate messages
|
||||||
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
||||||
* Bool values will be treated as plain text flag.
|
* Bool values will be treated as plain text flag.
|
||||||
|
* @param mixed $value The value that failed validation
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function addMessage($message, $messageType = ValidationResult::TYPE_ERROR, $code = null, $cast = ValidationResult::CAST_TEXT)
|
public function addMessage(
|
||||||
{
|
$message,
|
||||||
return $this->addFieldMessage(null, $message, $messageType, $code, $cast);
|
$messageType = ValidationResult::TYPE_ERROR,
|
||||||
|
$code = null,
|
||||||
|
$cast = ValidationResult::CAST_TEXT,
|
||||||
|
$value = ValidationResult::VALUE_UNSET,
|
||||||
|
) {
|
||||||
|
return $this->addFieldMessage(null, $message, $messageType, $code, $cast, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -130,6 +149,7 @@ class ValidationResult
|
|||||||
* This can be usedful for ensuring no duplicate messages
|
* This can be usedful for ensuring no duplicate messages
|
||||||
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
* @param string|bool $cast Cast type; One of the CAST_ constant definitions.
|
||||||
* Bool values will be treated as plain text flag.
|
* Bool values will be treated as plain text flag.
|
||||||
|
* @param mixed $value The value that failed validation
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function addFieldMessage(
|
public function addFieldMessage(
|
||||||
@ -137,7 +157,8 @@ class ValidationResult
|
|||||||
$message,
|
$message,
|
||||||
$messageType = ValidationResult::TYPE_ERROR,
|
$messageType = ValidationResult::TYPE_ERROR,
|
||||||
$code = null,
|
$code = null,
|
||||||
$cast = ValidationResult::CAST_TEXT
|
$cast = ValidationResult::CAST_TEXT,
|
||||||
|
$value = ValidationResult::VALUE_UNSET,
|
||||||
) {
|
) {
|
||||||
if ($code && is_numeric($code)) {
|
if ($code && is_numeric($code)) {
|
||||||
throw new InvalidArgumentException("Don't use a numeric code '$code'. Use a string.");
|
throw new InvalidArgumentException("Don't use a numeric code '$code'. Use a string.");
|
||||||
@ -151,7 +172,9 @@ class ValidationResult
|
|||||||
'messageType' => $messageType,
|
'messageType' => $messageType,
|
||||||
'messageCast' => $cast,
|
'messageCast' => $cast,
|
||||||
];
|
];
|
||||||
|
if ($value !== ValidationResult::VALUE_UNSET) {
|
||||||
|
$metadata['value'] = $value;
|
||||||
|
}
|
||||||
if ($code) {
|
if ($code) {
|
||||||
$this->messages[$code] = $metadata;
|
$this->messages[$code] = $metadata;
|
||||||
} else {
|
} else {
|
||||||
|
@ -48,7 +48,7 @@ use ReflectionClass;
|
|||||||
use SilverStripe\Dev\Exceptions\ExpectedErrorException;
|
use SilverStripe\Dev\Exceptions\ExpectedErrorException;
|
||||||
use SilverStripe\Dev\Exceptions\ExpectedNoticeException;
|
use SilverStripe\Dev\Exceptions\ExpectedNoticeException;
|
||||||
use SilverStripe\Dev\Exceptions\ExpectedWarningException;
|
use SilverStripe\Dev\Exceptions\ExpectedWarningException;
|
||||||
use SilverStripe\Dev\Exceptions\UnexpectedErrorException;
|
use SilverStripe\ORM\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test case class for the Silverstripe framework.
|
* Test case class for the Silverstripe framework.
|
||||||
@ -434,6 +434,9 @@ abstract class SapphireTest extends TestCase implements TestOnly
|
|||||||
*/
|
*/
|
||||||
public static function setUpBeforeClass(): void
|
public static function setUpBeforeClass(): void
|
||||||
{
|
{
|
||||||
|
// Disallow the use of DB replicas in tests
|
||||||
|
DB::setMustUsePrimary();
|
||||||
|
|
||||||
// Start tests
|
// Start tests
|
||||||
static::start();
|
static::start();
|
||||||
|
|
||||||
|
@ -119,10 +119,8 @@ class CompositeField extends FormField
|
|||||||
* Returns the name (ID) for the element.
|
* 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.
|
* 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.
|
* This code generates the ID from the nested children.
|
||||||
*
|
|
||||||
* @return String $name
|
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName(): string
|
||||||
{
|
{
|
||||||
if ($this->name) {
|
if ($this->name) {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
|
@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Forms;
|
namespace SilverStripe\Forms;
|
||||||
|
|
||||||
use SilverStripe\Core\Validation\ConstraintValidator;
|
use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
|
||||||
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text input field with validation for correct email format according to the relevant RFC.
|
* Text input field with validation for correct email format according to the relevant RFC.
|
||||||
*/
|
*/
|
||||||
class EmailField extends TextField
|
class EmailField extends TextField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
EmailFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
protected $inputType = 'email';
|
protected $inputType = 'email';
|
||||||
|
|
||||||
public function Type()
|
public function Type()
|
||||||
@ -17,27 +20,6 @@ class EmailField extends TextField
|
|||||||
return 'email text';
|
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()
|
public function getSchemaValidation()
|
||||||
{
|
{
|
||||||
$rules = parent::getSchemaValidation();
|
$rules = parent::getSchemaValidation();
|
||||||
|
@ -106,7 +106,7 @@ class FieldGroup extends CompositeField
|
|||||||
* In some cases the FieldGroup doesn't have a title, but we still want
|
* 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
|
* 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) {
|
if ($this->name) {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
|
@ -15,6 +15,7 @@ use SilverStripe\Core\Validation\ValidationResult;
|
|||||||
use SilverStripe\View\AttributesHTML;
|
use SilverStripe\View\AttributesHTML;
|
||||||
use SilverStripe\View\SSViewer;
|
use SilverStripe\View\SSViewer;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidatorsTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a field in a form.
|
* Represents a field in a form.
|
||||||
@ -44,6 +45,7 @@ class FormField extends RequestHandler
|
|||||||
{
|
{
|
||||||
use AttributesHTML;
|
use AttributesHTML;
|
||||||
use FormMessage;
|
use FormMessage;
|
||||||
|
use FieldValidatorsTrait;
|
||||||
|
|
||||||
/** @see $schemaDataType */
|
/** @see $schemaDataType */
|
||||||
const SCHEMA_DATA_TYPE_STRING = 'String';
|
const SCHEMA_DATA_TYPE_STRING = 'String';
|
||||||
@ -424,12 +426,10 @@ class FormField extends RequestHandler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the field name.
|
* Returns the field name.
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public function getName()
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return $this->name;
|
return $this->name ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -443,16 +443,32 @@ class FormField extends RequestHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the field value.
|
* Alias of getValue()
|
||||||
*
|
*
|
||||||
* @see FormField::setSubmittedValue()
|
* @see FormField::setSubmittedValue()
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function Value()
|
public function Value()
|
||||||
|
{
|
||||||
|
return $this->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the field value.
|
||||||
|
*/
|
||||||
|
public function getValue(): mixed
|
||||||
{
|
{
|
||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of this field for field validation
|
||||||
|
*/
|
||||||
|
public function getValueForValidation(): mixed
|
||||||
|
{
|
||||||
|
return $this->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to save this form field into the given record.
|
* Method to save this form field into the given record.
|
||||||
*
|
*
|
||||||
@ -1231,15 +1247,28 @@ class FormField extends RequestHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract method each {@link FormField} subclass must implement, determines whether the field
|
* Subclasses can define an existing FieldValidatorClass to validate the FormField value
|
||||||
* is valid or not based on the value.
|
* They may also override this method to provide custom validation logic
|
||||||
*
|
*
|
||||||
* @param Validator $validator
|
* @param Validator $validator
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function validate($validator)
|
public function validate($validator)
|
||||||
{
|
{
|
||||||
return $this->extendValidationResult(true, $validator);
|
$isValid = true;
|
||||||
|
$result = ValidationResult::create();
|
||||||
|
$fieldValidators = $this->getFieldValidators();
|
||||||
|
foreach ($fieldValidators as $fieldValidator) {
|
||||||
|
$validationResult = $fieldValidator->validate();
|
||||||
|
if (!$validationResult->isValid()) {
|
||||||
|
$result->combineAnd($validationResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$result->isValid()) {
|
||||||
|
$isValid = false;
|
||||||
|
$validator->getResult()->combineAnd($result);
|
||||||
|
}
|
||||||
|
return $this->extendValidationResult($isValid, $validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +43,7 @@ class SelectionGroup_Item extends CompositeField
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValue()
|
function getValue(): mixed
|
||||||
{
|
{
|
||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace SilverStripe\Forms;
|
namespace SilverStripe\Forms;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text input field.
|
* Text input field.
|
||||||
*/
|
*/
|
||||||
@ -14,6 +16,10 @@ class TextField extends FormField implements TippableFieldInterface
|
|||||||
|
|
||||||
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;
|
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT;
|
||||||
|
|
||||||
|
private static array $field_validators = [
|
||||||
|
StringFieldValidator::class => [null, 'getMaxLength'],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Tip|null A tip to render beside the input
|
* @var Tip|null A tip to render beside the input
|
||||||
*/
|
*/
|
||||||
@ -43,6 +49,14 @@ class TextField extends FormField implements TippableFieldInterface
|
|||||||
parent::__construct($name, $title, $value);
|
parent::__construct($name, $title, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setValue($value, $data = null)
|
||||||
|
{
|
||||||
|
parent::setValue($value, $data = null);
|
||||||
|
if (is_null($this->value)) {
|
||||||
|
$this->value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $maxLength
|
* @param int $maxLength
|
||||||
* @return $this
|
* @return $this
|
||||||
@ -117,31 +131,6 @@ class TextField extends FormField implements TippableFieldInterface
|
|||||||
return $data;
|
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()
|
public function getSchemaValidation()
|
||||||
{
|
{
|
||||||
$rules = parent::getSchemaValidation();
|
$rules = parent::getSchemaValidation();
|
||||||
|
@ -43,7 +43,7 @@ class TempDatabase
|
|||||||
*
|
*
|
||||||
* @param string $name DB Connection name to use
|
* @param string $name DB Connection name to use
|
||||||
*/
|
*/
|
||||||
public function __construct($name = 'default')
|
public function __construct($name = DB::CONN_PRIMARY)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
}
|
}
|
||||||
|
233
src/ORM/DB.php
233
src/ORM/DB.php
@ -3,6 +3,7 @@
|
|||||||
namespace SilverStripe\ORM;
|
namespace SilverStripe\ORM;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use RunTimeException;
|
||||||
use SilverStripe\Control\Director;
|
use SilverStripe\Control\Director;
|
||||||
use SilverStripe\Control\HTTPRequest;
|
use SilverStripe\Control\HTTPRequest;
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
@ -21,6 +22,22 @@ use SilverStripe\ORM\Queries\SQLExpression;
|
|||||||
*/
|
*/
|
||||||
class DB
|
class DB
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* A dynamic connection that will use either a replica connection (if one is
|
||||||
|
* available and not forced to use the 'primary' connection), or the 'primary' connection
|
||||||
|
*/
|
||||||
|
public const CONN_DYNAMIC = 'dynamic';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 'primary' connection name, which is the main database connection and is used for all write
|
||||||
|
* operations and for read operations when the 'dynamic' connection is forced to use the 'primary' connection
|
||||||
|
*/
|
||||||
|
public const CONN_PRIMARY = 'primary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of replicas databases that can be configured
|
||||||
|
*/
|
||||||
|
public const MAX_REPLICAS = 99;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This constant was added in SilverStripe 2.4 to indicate that SQL-queries
|
* This constant was added in SilverStripe 2.4 to indicate that SQL-queries
|
||||||
@ -58,19 +75,47 @@ class DB
|
|||||||
*/
|
*/
|
||||||
protected static $configs = [];
|
protected static $configs = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last SQL query run.
|
* The last SQL query run.
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
public static $lastQuery;
|
public static $lastQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the last connection used. This is only used for unit-testing purposes.
|
||||||
|
* @interal
|
||||||
|
*/
|
||||||
|
private static string $lastConnectionName = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal flag to keep track of when db connection was attempted.
|
* Internal flag to keep track of when db connection was attempted.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
private static $connection_attempted = false;
|
private static $connection_attempted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only use the primary database connection for the rest of the current request
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static bool $mustUsePrimary = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by DB::withPrimary() to count the number of times it has been called
|
||||||
|
* Uses an int instead of a bool to allow for nested calls
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static int $withPrimaryCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key of the replica config to use for this request
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private static string $replicaConfigKey = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the global database connection.
|
* Set the global database connection.
|
||||||
* Pass an object that's a subclass of SS_Database. This object will be used when {@link DB::query()}
|
* Pass an object that's a subclass of SS_Database. This object will be used when {@link DB::query()}
|
||||||
@ -78,11 +123,11 @@ class DB
|
|||||||
*
|
*
|
||||||
* @param Database $connection The connection object to set as the connection.
|
* @param Database $connection The connection object to set as the connection.
|
||||||
* @param string $name The name to give to this connection. If you omit this argument, the connection
|
* @param string $name The name to give to this connection. If you omit this argument, the connection
|
||||||
* will be the default one used by the ORM. However, you can store other named connections to
|
* will be the primary one used by the ORM. However, you can store other named connections to
|
||||||
* be accessed through DB::get_conn($name). This is useful when you have an application that
|
* be accessed through DB::get_conn($name). This is useful when you have an application that
|
||||||
* needs to connect to more than one database.
|
* needs to connect to more than one database.
|
||||||
*/
|
*/
|
||||||
public static function set_conn(Database $connection, $name = 'default')
|
public static function set_conn(Database $connection, $name)
|
||||||
{
|
{
|
||||||
DB::$connections[$name] = $connection;
|
DB::$connections[$name] = $connection;
|
||||||
}
|
}
|
||||||
@ -92,11 +137,17 @@ class DB
|
|||||||
*
|
*
|
||||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||||
* the default connection is returned.
|
* the default connection is returned.
|
||||||
* @return Database
|
* @return Database|null
|
||||||
*/
|
*/
|
||||||
public static function get_conn($name = 'default')
|
public static function get_conn($name = DB::CONN_DYNAMIC)
|
||||||
{
|
{
|
||||||
|
// Allow default to connect to replica if configured
|
||||||
|
if ($name === DB::CONN_DYNAMIC) {
|
||||||
|
$name = DB::getDynamicConnectionName();
|
||||||
|
}
|
||||||
|
|
||||||
if (isset(DB::$connections[$name])) {
|
if (isset(DB::$connections[$name])) {
|
||||||
|
DB::$lastConnectionName = $name;
|
||||||
return DB::$connections[$name];
|
return DB::$connections[$name];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,14 +160,50 @@ class DB
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the primary database connection will be used if the database is used right now
|
||||||
|
*/
|
||||||
|
public static function willUsePrimary(): bool
|
||||||
|
{
|
||||||
|
return DB::$mustUsePrimary || DB::$withPrimaryCount > 0 || !DB::hasReplicaConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to use the primary database connection for rest of the current request
|
||||||
|
* meaning that replia connections will no longer be used
|
||||||
|
*
|
||||||
|
* This intentioally does not have a parameter to set this back to false, as this it to prevent
|
||||||
|
* accidentally attempting writing to a replica, or reading from an out of date replica
|
||||||
|
* after a write
|
||||||
|
*/
|
||||||
|
public static function setMustUsePrimary(): void
|
||||||
|
{
|
||||||
|
DB::$mustUsePrimary = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only use the primary database connection when calling $callback
|
||||||
|
* Use this when doing non-mutable queries on the primary database where querying
|
||||||
|
* an out of sync replica could cause issues
|
||||||
|
* There's no need to use this with mutable queries, or after calling a mutable query
|
||||||
|
* as the primary database connection will be automatically used
|
||||||
|
*/
|
||||||
|
public static function withPrimary(callable $callback): mixed
|
||||||
|
{
|
||||||
|
DB::$withPrimaryCount++;
|
||||||
|
$result = $callback();
|
||||||
|
DB::$withPrimaryCount--;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the schema manager for the current database
|
* Retrieves the schema manager for the current database
|
||||||
*
|
*
|
||||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||||
* the default connection is returned.
|
* If omitted, a dynamic connection is returned.
|
||||||
* @return DBSchemaManager
|
* @return DBSchemaManager|null
|
||||||
*/
|
*/
|
||||||
public static function get_schema($name = 'default')
|
public static function get_schema($name = DB::CONN_DYNAMIC)
|
||||||
{
|
{
|
||||||
$connection = DB::get_conn($name);
|
$connection = DB::get_conn($name);
|
||||||
if ($connection) {
|
if ($connection) {
|
||||||
@ -130,11 +217,11 @@ class DB
|
|||||||
*
|
*
|
||||||
* @param SQLExpression $expression The expression object to build from
|
* @param SQLExpression $expression The expression object to build from
|
||||||
* @param array $parameters Out parameter for the resulting query parameters
|
* @param array $parameters Out parameter for the resulting query parameters
|
||||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||||
* the default connection is returned.
|
* If omitted, a dynamic connection is returned.
|
||||||
* @return string The resulting SQL as a string
|
* @return string|null The resulting SQL as a string
|
||||||
*/
|
*/
|
||||||
public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default')
|
public static function build_sql(SQLExpression $expression, &$parameters, $name = DB::CONN_DYNAMIC)
|
||||||
{
|
{
|
||||||
$connection = DB::get_conn($name);
|
$connection = DB::get_conn($name);
|
||||||
if ($connection) {
|
if ($connection) {
|
||||||
@ -148,11 +235,11 @@ class DB
|
|||||||
/**
|
/**
|
||||||
* Retrieves the connector object for the current database
|
* Retrieves the connector object for the current database
|
||||||
*
|
*
|
||||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||||
* the default connection is returned.
|
* If omitted, a dynamic connection is returned.
|
||||||
* @return DBConnector
|
* @return DBConnector|null
|
||||||
*/
|
*/
|
||||||
public static function get_connector($name = 'default')
|
public static function get_connector($name = DB::CONN_DYNAMIC)
|
||||||
{
|
{
|
||||||
$connection = DB::get_conn($name);
|
$connection = DB::get_conn($name);
|
||||||
if ($connection) {
|
if ($connection) {
|
||||||
@ -268,8 +355,13 @@ class DB
|
|||||||
* @param string $label identifier for the connection
|
* @param string $label identifier for the connection
|
||||||
* @return Database
|
* @return Database
|
||||||
*/
|
*/
|
||||||
public static function connect($databaseConfig, $label = 'default')
|
public static function connect($databaseConfig, $label = DB::CONN_DYNAMIC)
|
||||||
{
|
{
|
||||||
|
// Allow default to connect to replica if configured
|
||||||
|
if ($label === DB::CONN_DYNAMIC) {
|
||||||
|
$label = DB::getDynamicConnectionName();
|
||||||
|
}
|
||||||
|
|
||||||
// This is used by the "testsession" module to test up a test session using an alternative name
|
// This is used by the "testsession" module to test up a test session using an alternative name
|
||||||
if ($name = DB::get_alternative_database_name()) {
|
if ($name = DB::get_alternative_database_name()) {
|
||||||
$databaseConfig['database'] = $name;
|
$databaseConfig['database'] = $name;
|
||||||
@ -288,6 +380,7 @@ class DB
|
|||||||
$conn = Injector::inst()->create($dbClass);
|
$conn = Injector::inst()->create($dbClass);
|
||||||
DB::set_conn($conn, $label);
|
DB::set_conn($conn, $label);
|
||||||
$conn->connect($databaseConfig);
|
$conn->connect($databaseConfig);
|
||||||
|
DB::$lastConnectionName = $label;
|
||||||
|
|
||||||
return $conn;
|
return $conn;
|
||||||
}
|
}
|
||||||
@ -298,7 +391,7 @@ class DB
|
|||||||
* @param array $databaseConfig
|
* @param array $databaseConfig
|
||||||
* @param string $name
|
* @param string $name
|
||||||
*/
|
*/
|
||||||
public static function setConfig($databaseConfig, $name = 'default')
|
public static function setConfig($databaseConfig, $name = DB::CONN_PRIMARY)
|
||||||
{
|
{
|
||||||
static::$configs[$name] = $databaseConfig;
|
static::$configs[$name] = $databaseConfig;
|
||||||
}
|
}
|
||||||
@ -309,13 +402,42 @@ class DB
|
|||||||
* @param string $name
|
* @param string $name
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public static function getConfig($name = 'default')
|
public static function getConfig($name = DB::CONN_PRIMARY)
|
||||||
{
|
{
|
||||||
if (isset(static::$configs[$name])) {
|
if (static::hasConfig($name)) {
|
||||||
return static::$configs[$name];
|
return static::$configs[$name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a named connection config exists
|
||||||
|
*/
|
||||||
|
public static function hasConfig($name = DB::CONN_PRIMARY): bool
|
||||||
|
{
|
||||||
|
return array_key_exists($name, static::$configs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a replica database configuration key
|
||||||
|
* e.g. replica_01
|
||||||
|
*/
|
||||||
|
public static function getReplicaConfigKey(int $replica): string
|
||||||
|
{
|
||||||
|
$len = strlen((string) DB::MAX_REPLICAS);
|
||||||
|
return 'replica_' . str_pad($replica, $len, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are any replica configurations
|
||||||
|
*/
|
||||||
|
public static function hasReplicaConfig(): bool
|
||||||
|
{
|
||||||
|
$configKeys = array_keys(static::$configs);
|
||||||
|
return !empty(array_filter($configKeys, function (string $key) {
|
||||||
|
return (bool) preg_match('#^replica_[0-9]+$#', $key);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a database connection has been attempted.
|
* Returns true if a database connection has been attempted.
|
||||||
* In particular, it lets the caller know if we're still so early in the execution pipeline that
|
* In particular, it lets the caller know if we're still so early in the execution pipeline that
|
||||||
@ -335,8 +457,8 @@ class DB
|
|||||||
public static function query($sql, $errorLevel = E_USER_ERROR)
|
public static function query($sql, $errorLevel = E_USER_ERROR)
|
||||||
{
|
{
|
||||||
DB::$lastQuery = $sql;
|
DB::$lastQuery = $sql;
|
||||||
|
$name = DB::getDynamicConnectionName($sql);
|
||||||
return DB::get_conn()->query($sql, $errorLevel);
|
return DB::get_conn($name)->query($sql, $errorLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -427,8 +549,8 @@ class DB
|
|||||||
public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR)
|
public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR)
|
||||||
{
|
{
|
||||||
DB::$lastQuery = $sql;
|
DB::$lastQuery = $sql;
|
||||||
|
$name = DB::getDynamicConnectionName($sql);
|
||||||
return DB::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
|
return DB::get_conn($name)->preparedQuery($sql, $parameters, $errorLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -680,4 +802,63 @@ class DB
|
|||||||
{
|
{
|
||||||
DB::get_schema()->alterationMessage($message, $type);
|
DB::get_schema()->alterationMessage($message, $type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the database connection to use for the given SQL query
|
||||||
|
* The 'dynamic' connection can be either the primary or a replica connection if configured
|
||||||
|
*/
|
||||||
|
private static function getDynamicConnectionName(string $sql = ''): string
|
||||||
|
{
|
||||||
|
if (DB::willUsePrimary()) {
|
||||||
|
return DB::CONN_PRIMARY;
|
||||||
|
}
|
||||||
|
if (DB::isMutableSql($sql)) {
|
||||||
|
DB::$mustUsePrimary = true;
|
||||||
|
return DB::CONN_PRIMARY;
|
||||||
|
}
|
||||||
|
if (DB::$replicaConfigKey) {
|
||||||
|
return DB::$replicaConfigKey;
|
||||||
|
}
|
||||||
|
$name = DB::getRandomReplicaConfigKey();
|
||||||
|
DB::$replicaConfigKey = $name;
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given SQL query is mutable
|
||||||
|
*/
|
||||||
|
private static function isMutableSql(string $sql): bool
|
||||||
|
{
|
||||||
|
$dbClass = DB::getConfig(DB::CONN_PRIMARY)['type'];
|
||||||
|
// This must use getServiceSpec() and not Injector::get/create() followed by
|
||||||
|
// getConnector() as this can remove the dbConn from a different connection
|
||||||
|
// under edge case conditions
|
||||||
|
$dbSpec = Injector::inst()->getServiceSpec($dbClass);
|
||||||
|
$connectorService = $dbSpec['properties']['connector'];
|
||||||
|
$connector = Injector::inst()->convertServiceProperty($connectorService);
|
||||||
|
return $connector->isQueryMutable($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a random replica database configuration key from the available replica configurations
|
||||||
|
* The replica choosen will be used for the rest of the request, unless the primary connection
|
||||||
|
* is forced
|
||||||
|
*/
|
||||||
|
private static function getRandomReplicaConfigKey(): string
|
||||||
|
{
|
||||||
|
$replicaNumbers = [];
|
||||||
|
for ($i = 1; $i <= DB::MAX_REPLICAS; $i++) {
|
||||||
|
$replicaKey = DB::getReplicaConfigKey($i);
|
||||||
|
if (!DB::hasConfig($replicaKey)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$replicaNumbers[] = $i;
|
||||||
|
}
|
||||||
|
if (count($replicaNumbers) === 0) {
|
||||||
|
throw new RunTimeException('No replica configurations found');
|
||||||
|
}
|
||||||
|
// Choose a random replica
|
||||||
|
$index = rand(0, count($replicaNumbers) - 1);
|
||||||
|
return DB::getReplicaConfigKey($replicaNumbers[$index]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ class DataList extends ModelData implements SS_List, Filterable, Sortable, Limit
|
|||||||
*/
|
*/
|
||||||
public function sql(&$parameters = [])
|
public function sql(&$parameters = [])
|
||||||
{
|
{
|
||||||
return $this->dataQuery->query()->sql($parameters);
|
return $this->dataQuery->sql($parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1040,7 +1040,7 @@ class DataList extends ModelData implements SS_List, Filterable, Sortable, Limit
|
|||||||
|
|
||||||
private function executeQuery(): Query
|
private function executeQuery(): Query
|
||||||
{
|
{
|
||||||
$query = $this->dataQuery->query()->execute();
|
$query = $this->dataQuery->execute();
|
||||||
$this->fetchEagerLoadRelations($query);
|
$this->fetchEagerLoadRelations($query);
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,14 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
|
|||||||
*/
|
*/
|
||||||
private static $default_classname = null;
|
private static $default_classname = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this DataObject class must only use the primary database and not a read-only replica
|
||||||
|
* Note that this will be only be enforced when using DataQuery::execute() or
|
||||||
|
* another method that uses calls DataQuery::execute() internally e.g. DataObject::get()
|
||||||
|
* This will not be enforced when using low-level ORM functionality to query data e.g. SQLSelect or DB::query()
|
||||||
|
*/
|
||||||
|
private static bool $must_use_primary_db = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data stored in this objects database record. An array indexed by fieldname.
|
* Data stored in this objects database record. An array indexed by fieldname.
|
||||||
*
|
*
|
||||||
@ -1230,6 +1238,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
|
|||||||
public function validate()
|
public function validate()
|
||||||
{
|
{
|
||||||
$result = ValidationResult::create();
|
$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);
|
$this->extend('updateValidate', $result);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@ -3268,6 +3285,9 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
|
|||||||
/** @var DBField $obj */
|
/** @var DBField $obj */
|
||||||
$table = $schema->tableName($class);
|
$table = $schema->tableName($class);
|
||||||
$obj = Injector::inst()->create($spec, $fieldName);
|
$obj = Injector::inst()->create($spec, $fieldName);
|
||||||
|
if (is_null($value)) {
|
||||||
|
$value = $obj->getDefaultValue();
|
||||||
|
}
|
||||||
$obj->setTable($table);
|
$obj->setTable($table);
|
||||||
$obj->setValue($value, $this, false);
|
$obj->setValue($value, $this, false);
|
||||||
return $obj;
|
return $obj;
|
||||||
|
@ -10,6 +10,7 @@ use SilverStripe\ORM\Connect\Query;
|
|||||||
use SilverStripe\ORM\Queries\SQLConditionGroup;
|
use SilverStripe\ORM\Queries\SQLConditionGroup;
|
||||||
use SilverStripe\ORM\Queries\SQLSelect;
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\Core\Config\Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object representing a query of data from the DataObject's supporting database.
|
* An object representing a query of data from the DataObject's supporting database.
|
||||||
@ -449,7 +450,9 @@ class DataQuery
|
|||||||
*/
|
*/
|
||||||
public function execute()
|
public function execute()
|
||||||
{
|
{
|
||||||
return $this->getFinalisedQuery()->execute();
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $this->getFinalisedQuery()->execute()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -472,7 +475,9 @@ class DataQuery
|
|||||||
public function count()
|
public function count()
|
||||||
{
|
{
|
||||||
$quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
|
$quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
|
||||||
return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}");
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -501,7 +506,9 @@ class DataQuery
|
|||||||
|
|
||||||
// Wrap the whole thing in an "EXISTS"
|
// Wrap the whole thing in an "EXISTS"
|
||||||
$sql = 'SELECT CASE WHEN EXISTS(' . $statement->sql($params) . ') THEN 1 ELSE 0 END';
|
$sql = 'SELECT CASE WHEN EXISTS(' . $statement->sql($params) . ') THEN 1 ELSE 0 END';
|
||||||
$result = DB::prepared_query($sql, $params);
|
$result = $this->withCorrectDatabase(
|
||||||
|
fn() => DB::prepared_query($sql, $params)
|
||||||
|
);
|
||||||
$row = $result->record();
|
$row = $result->record();
|
||||||
$result = reset($row);
|
$result = reset($row);
|
||||||
|
|
||||||
@ -582,7 +589,9 @@ class DataQuery
|
|||||||
*/
|
*/
|
||||||
public function aggregate($expression)
|
public function aggregate($expression)
|
||||||
{
|
{
|
||||||
return $this->getFinalisedQuery()->aggregate($expression)->execute()->value();
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $this->getFinalisedQuery()->aggregate($expression)->execute()->value()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -593,7 +602,9 @@ class DataQuery
|
|||||||
*/
|
*/
|
||||||
public function firstRow()
|
public function firstRow()
|
||||||
{
|
{
|
||||||
return $this->getFinalisedQuery()->firstRow();
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $this->getFinalisedQuery()->firstRow()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -604,7 +615,9 @@ class DataQuery
|
|||||||
*/
|
*/
|
||||||
public function lastRow()
|
public function lastRow()
|
||||||
{
|
{
|
||||||
return $this->getFinalisedQuery()->lastRow();
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $this->getFinalisedQuery()->lastRow()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1344,7 +1357,9 @@ class DataQuery
|
|||||||
$query->selectField($fieldExpression, $field);
|
$query->selectField($fieldExpression, $field);
|
||||||
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
|
$this->ensureSelectContainsOrderbyColumns($query, $originalSelect);
|
||||||
|
|
||||||
return $query->execute()->column($field);
|
return $this->withCorrectDatabase(
|
||||||
|
fn() => $query->execute()->column($field)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1495,4 +1510,16 @@ class DataQuery
|
|||||||
|
|
||||||
return $updated;
|
return $updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls a callback on either the primary database or a replica, with respect to the configured
|
||||||
|
* value of `must_use_primary_db` on the current dataClass
|
||||||
|
*/
|
||||||
|
private function withCorrectDatabase(callable $callback): mixed
|
||||||
|
{
|
||||||
|
if (Config::inst()->get($this->dataClass(), 'must_use_primary_db')) {
|
||||||
|
return DB::withPrimary($callback);
|
||||||
|
}
|
||||||
|
return $callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,24 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BigIntFieldValidator;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a signed 8 byte integer field. Do note PHP running as 32-bit might not work with Bigint properly, as it
|
* Represents a signed 8 byte integer field. Do note PHP running as 32-bit might not work with Bigint properly, as it
|
||||||
* would convert the value to a float when queried from the database since the value is a 64-bit one.
|
* would convert the value to a float when queried from the database since the value is a 64-bit one.
|
||||||
*
|
*
|
||||||
* @package framework
|
* BigInt is always signed i.e. can be negative
|
||||||
* @subpackage model
|
* Their range is -9223372036854775808 to 9223372036854775807
|
||||||
* @see Int
|
|
||||||
*/
|
*/
|
||||||
class DBBigInt extends DBInt
|
class DBBigInt extends DBInt
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
// Remove parent validator and add BigIntValidator instead
|
||||||
|
IntFieldValidator::class => null,
|
||||||
|
BigIntFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function requireField(): void
|
public function requireField(): void
|
||||||
{
|
{
|
||||||
@ -24,7 +30,6 @@ class DBBigInt extends DBInt
|
|||||||
'default' => $this->defaultVal,
|
'default' => $this->defaultVal,
|
||||||
'arrayValue' => $this->arrayValue
|
'arrayValue' => $this->arrayValue
|
||||||
];
|
];
|
||||||
|
|
||||||
$values = ['type' => 'bigint', 'parts' => $parts];
|
$values = ['type' => 'bigint', 'parts' => $parts];
|
||||||
DB::require_field($this->tableName, $this->name, $values);
|
DB::require_field($this->tableName, $this->name, $values);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||||
use SilverStripe\Forms\CheckboxField;
|
use SilverStripe\Forms\CheckboxField;
|
||||||
use SilverStripe\Forms\DropdownField;
|
use SilverStripe\Forms\DropdownField;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
@ -9,13 +10,18 @@ use SilverStripe\ORM\DB;
|
|||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a boolean field.
|
* Represents a boolean field
|
||||||
|
* Values are stored as a tinyint i.e. 1 or 0 and NOT as true or false
|
||||||
*/
|
*/
|
||||||
class DBBoolean extends DBField
|
class DBBoolean extends DBField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
BooleanIntFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(?string $name = null, bool|int $defaultVal = 0)
|
public function __construct(?string $name = null, bool|int $defaultVal = 0)
|
||||||
{
|
{
|
||||||
$this->defaultVal = ($defaultVal) ? 1 : 0;
|
$this->setDefaultValue($defaultVal ? 1 : 0);
|
||||||
|
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
}
|
}
|
||||||
@ -34,6 +40,13 @@ class DBBoolean extends DBField
|
|||||||
DB::require_field($this->tableName, $this->name, $values);
|
DB::require_field($this->tableName, $this->name, $values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
parent::setValue($value);
|
||||||
|
$this->value = $this->convertBooleanLikeValueToTinyInt($value);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function Nice(): string
|
public function Nice(): string
|
||||||
{
|
{
|
||||||
return ($this->value) ? _t(__CLASS__ . '.YESANSWER', 'Yes') : _t(__CLASS__ . '.NOANSWER', 'No');
|
return ($this->value) ? _t(__CLASS__ . '.YESANSWER', 'Yes') : _t(__CLASS__ . '.NOANSWER', 'No');
|
||||||
@ -77,12 +90,19 @@ class DBBoolean extends DBField
|
|||||||
->setEmptyString($anyText);
|
->setEmptyString($anyText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nullValue(): ?int
|
public function nullValue(): int
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepValueForDB(mixed $value): array|int|null
|
public function prepValueForDB(mixed $value): array|int|null
|
||||||
|
{
|
||||||
|
$ret = $this->convertBooleanLikeValueToTinyInt($value);
|
||||||
|
// Ensure a tiny int is returned no matter what e.g. value is an
|
||||||
|
return $ret ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertBooleanLikeValueToTinyInt(mixed $value): mixed
|
||||||
{
|
{
|
||||||
if (is_bool($value)) {
|
if (is_bool($value)) {
|
||||||
return $value ? 1 : 0;
|
return $value ? 1 : 0;
|
||||||
@ -94,12 +114,16 @@ class DBBoolean extends DBField
|
|||||||
switch (strtolower($value ?? '')) {
|
switch (strtolower($value ?? '')) {
|
||||||
case 'false':
|
case 'false':
|
||||||
case 'f':
|
case 'f':
|
||||||
|
case '0':
|
||||||
return 0;
|
return 0;
|
||||||
case 'true':
|
case 'true':
|
||||||
case 't':
|
case 't':
|
||||||
|
case '1':
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $value ? 1 : 0;
|
// Note that something like "lorem" will NOT be converted to 1
|
||||||
|
// instead it will throw a ValidationException in BooleanIntFieldValidator
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
use SilverStripe\ORM\FieldType\DBVarchar;
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An alternative to DBClassName that stores the class name as a varchar instead of an enum
|
* An alternative to DBClassName that stores the class name as a varchar instead of an enum
|
||||||
@ -24,4 +25,8 @@ use SilverStripe\ORM\FieldType\DBVarchar;
|
|||||||
class DBClassNameVarchar extends DBVarchar
|
class DBClassNameVarchar extends DBVarchar
|
||||||
{
|
{
|
||||||
use DBClassNameTrait;
|
use DBClassNameTrait;
|
||||||
|
|
||||||
|
private static array $field_validators = [
|
||||||
|
EnumFieldValidator::class => ['getEnum'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ use SilverStripe\ORM\DataObject;
|
|||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use SilverStripe\ORM\Queries\SQLSelect;
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field.
|
* Extend this class when designing a {@link DBField} that doesn't have a 1-1 mapping with a database field.
|
||||||
@ -25,6 +26,10 @@ use SilverStripe\Model\ModelData;
|
|||||||
*/
|
*/
|
||||||
abstract class DBComposite extends DBField
|
abstract class DBComposite extends DBField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
CompositeFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to {@link DataObject::$db},
|
* Similar to {@link DataObject::$db},
|
||||||
* holds an array of composite field names.
|
* holds an array of composite field names.
|
||||||
@ -190,6 +195,15 @@ abstract class DBComposite extends DBField
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getValueForValidation(): mixed
|
||||||
|
{
|
||||||
|
$fields = [];
|
||||||
|
foreach (array_keys($this->compositeDatabaseFields()) as $fieldName) {
|
||||||
|
$fields[] = $this->dbObject($fieldName);
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind this field to the model, and set the underlying table to that of the owner
|
* Bind this field to the model, and set the underlying table to that of the owner
|
||||||
*/
|
*/
|
||||||
|
@ -12,6 +12,7 @@ use SilverStripe\ORM\DB;
|
|||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a date field.
|
* Represents a date field.
|
||||||
@ -33,6 +34,7 @@ class DBDate extends DBField
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Standard ISO format string for date in CLDR standard format
|
* 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';
|
public const ISO_DATE = 'y-MM-dd';
|
||||||
|
|
||||||
@ -42,13 +44,14 @@ class DBDate extends DBField
|
|||||||
*/
|
*/
|
||||||
public const ISO_LOCALE = 'en_US';
|
public const ISO_LOCALE = 'en_US';
|
||||||
|
|
||||||
|
private static array $field_validators = [
|
||||||
|
DateFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
{
|
{
|
||||||
$value = $this->parseDate($value);
|
if ($value !== null) {
|
||||||
if ($value === false) {
|
$value = $this->parseDate($value);
|
||||||
throw new InvalidArgumentException(
|
|
||||||
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
return $this;
|
return $this;
|
||||||
@ -58,15 +61,10 @@ class DBDate extends DBField
|
|||||||
* Parse timestamp or iso8601-ish date into standard iso8601 format
|
* Parse timestamp or iso8601-ish date into standard iso8601 format
|
||||||
*
|
*
|
||||||
* @param mixed $value
|
* @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
|
protected function parseDate(mixed $value): string|null|false
|
||||||
{
|
{
|
||||||
// Skip empty values
|
|
||||||
if (empty($value) && !is_numeric($value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine value to parse
|
// Determine value to parse
|
||||||
if (is_array($value)) {
|
if (is_array($value)) {
|
||||||
$source = $value; // parse array
|
$source = $value; // parse array
|
||||||
@ -74,19 +72,18 @@ class DBDate extends DBField
|
|||||||
$source = $value; // parse timestamp
|
$source = $value; // parse timestamp
|
||||||
} else {
|
} else {
|
||||||
// Convert US date -> iso, fix y2k, etc
|
// Convert US date -> iso, fix y2k, etc
|
||||||
$value = $this->fixInputDate($value);
|
$fixedValue = $this->fixInputDate($value);
|
||||||
if (is_null($value)) {
|
// convert string to timestamp
|
||||||
return null;
|
$source = strtotime($fixedValue ?? '');
|
||||||
}
|
|
||||||
$source = strtotime($value ?? ''); // convert string to timestamp
|
|
||||||
}
|
}
|
||||||
if ($value === false) {
|
if (!$source) {
|
||||||
return false;
|
// Unable to parse date, keep as is so that the validator can catch it later
|
||||||
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format as iso8601
|
// Format as iso8601
|
||||||
$formatter = $this->getInternalFormatter();
|
$formatter = $this->getInternalFormatter();
|
||||||
return $formatter->format($source);
|
$ret = $formatter->format($source);
|
||||||
|
return $ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -560,20 +557,12 @@ class DBDate extends DBField
|
|||||||
*/
|
*/
|
||||||
protected function fixInputDate($value)
|
protected function fixInputDate($value)
|
||||||
{
|
{
|
||||||
// split
|
|
||||||
[$year, $month, $day, $time] = $this->explodeDateString($value);
|
[$year, $month, $day, $time] = $this->explodeDateString($value);
|
||||||
|
if (!checkdate((int) $month, (int) $day, (int) $year)) {
|
||||||
if ((int)$year === 0 && (int)$month === 0 && (int)$day === 0) {
|
// Keep invalid dates as they are so that the validator can catch them later
|
||||||
return null;
|
return $value;
|
||||||
}
|
}
|
||||||
// Validate date
|
// Convert to Y-m-d
|
||||||
if (!checkdate($month ?? 0, $day ?? 0, $year ?? 0)) {
|
|
||||||
throw new InvalidArgumentException(
|
|
||||||
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to y-m-d
|
|
||||||
return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time);
|
return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,11 +580,8 @@ class DBDate extends DBField
|
|||||||
$value ?? '',
|
$value ?? '',
|
||||||
$matches
|
$matches
|
||||||
)) {
|
)) {
|
||||||
throw new InvalidArgumentException(
|
return [0, 0, 0, ''];
|
||||||
"Invalid date: '$value'. Use " . DBDate::ISO_DATE . " to prevent this error."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = [
|
$parts = [
|
||||||
$matches['first'],
|
$matches['first'],
|
||||||
$matches['second'],
|
$matches['second'],
|
||||||
@ -605,11 +591,6 @@ class DBDate extends DBField
|
|||||||
if ($parts[0] < 1000 && $parts[2] > 1000) {
|
if ($parts[0] < 1000 && $parts[2] > 1000) {
|
||||||
$parts = array_reverse($parts ?? []);
|
$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'];
|
$parts[] = $matches['time'];
|
||||||
return $parts;
|
return $parts;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ use SilverStripe\Security\Member;
|
|||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\View\TemplateGlobalProvider;
|
use SilverStripe\View\TemplateGlobalProvider;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DatetimeFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a date-time field.
|
* Represents a date-time field.
|
||||||
@ -39,6 +41,7 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
|
|||||||
/**
|
/**
|
||||||
* Standard ISO format string for date and time in CLDR standard format,
|
* Standard ISO format string for date and time in CLDR standard format,
|
||||||
* with a whitespace separating date and time (common database representation, e.g. in MySQL).
|
* with a whitespace separating date and time (common database representation, e.g. in MySQL).
|
||||||
|
* This is equivalent to php date format "Y-m-d H:i:s" e.g. 2024-08-31 09:30:00
|
||||||
*/
|
*/
|
||||||
public const ISO_DATETIME = 'y-MM-dd HH:mm:ss';
|
public const ISO_DATETIME = 'y-MM-dd HH:mm:ss';
|
||||||
|
|
||||||
@ -48,10 +51,16 @@ class DBDatetime extends DBDate implements TemplateGlobalProvider
|
|||||||
*/
|
*/
|
||||||
public const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss';
|
public const ISO_DATETIME_NORMALISED = 'y-MM-dd\'T\'HH:mm:ss';
|
||||||
|
|
||||||
|
private static array $field_validators = [
|
||||||
|
DatetimeFieldValidator::class,
|
||||||
|
// disable parent validator
|
||||||
|
DateFieldValidator::class => null,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag idicating if this field is considered immutable
|
* Flag idicating if this field is considered immutable
|
||||||
* when this is enabled setting the value of this field will return a new field instance
|
* when this is enabled setting the value of this field will return a new field instance
|
||||||
* instead updatin the old one
|
* instead updating the old one
|
||||||
*/
|
*/
|
||||||
protected bool $immutable = false;
|
protected bool $immutable = false;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DecimalFieldValidator;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\Forms\NumericField;
|
use SilverStripe\Forms\NumericField;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
@ -12,6 +13,10 @@ use SilverStripe\Model\ModelData;
|
|||||||
*/
|
*/
|
||||||
class DBDecimal extends DBField
|
class DBDecimal extends DBField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
DecimalFieldValidator::class => ['getWholeSize', 'getDecimalSize'],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whole number size
|
* Whole number size
|
||||||
*/
|
*/
|
||||||
@ -35,7 +40,7 @@ class DBDecimal extends DBField
|
|||||||
$this->wholeSize = is_int($wholeSize) ? $wholeSize : 9;
|
$this->wholeSize = is_int($wholeSize) ? $wholeSize : 9;
|
||||||
$this->decimalSize = is_int($decimalSize) ? $decimalSize : 2;
|
$this->decimalSize = is_int($decimalSize) ? $decimalSize : 2;
|
||||||
|
|
||||||
$this->defaultValue = number_format((float) $defaultValue, $this->decimalSize);
|
$this->setDefaultValue(round($defaultValue, $this->decimalSize));
|
||||||
|
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
}
|
}
|
||||||
@ -50,6 +55,16 @@ class DBDecimal extends DBField
|
|||||||
return floor($this->value ?? 0.0);
|
return floor($this->value ?? 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getWholeSize(): int
|
||||||
|
{
|
||||||
|
return $this->wholeSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDecimalSize(): int
|
||||||
|
{
|
||||||
|
return $this->decimalSize;
|
||||||
|
}
|
||||||
|
|
||||||
public function requireField(): void
|
public function requireField(): void
|
||||||
{
|
{
|
||||||
$parts = [
|
$parts = [
|
||||||
@ -91,7 +106,7 @@ class DBDecimal extends DBField
|
|||||||
->setScale($this->decimalSize);
|
->setScale($this->decimalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nullValue(): ?int
|
public function nullValue(): int
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
29
src/ORM/FieldType/DBEmail.php
Normal file
29
src/ORM/FieldType/DBEmail.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Forms\EmailField;
|
||||||
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
|
||||||
|
use SilverStripe\Forms\FormField;
|
||||||
|
use SilverStripe\Forms\NullableField;
|
||||||
|
|
||||||
|
class DBEmail extends DBVarchar
|
||||||
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
EmailFieldValidator::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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,14 @@
|
|||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
use SilverStripe\Forms\DropdownField;
|
use SilverStripe\Forms\DropdownField;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\Forms\SelectField;
|
use SilverStripe\Forms\SelectField;
|
||||||
use SilverStripe\Core\ArrayLib;
|
use SilverStripe\Core\ArrayLib;
|
||||||
use SilverStripe\ORM\Connect\MySQLDatabase;
|
use SilverStripe\ORM\Connect\MySQLDatabase;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\Model\ModelData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Enum represents an enumeration of a set of strings.
|
* Class Enum represents an enumeration of a set of strings.
|
||||||
@ -17,6 +19,10 @@ use SilverStripe\ORM\DB;
|
|||||||
*/
|
*/
|
||||||
class DBEnum extends DBString
|
class DBEnum extends DBString
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
EnumFieldValidator::class => ['getEnum'],
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of enum values
|
* List of enum values
|
||||||
*/
|
*/
|
||||||
@ -73,14 +79,14 @@ class DBEnum extends DBString
|
|||||||
|
|
||||||
// If there's a default, then use this
|
// If there's a default, then use this
|
||||||
if ($default && !is_int($default)) {
|
if ($default && !is_int($default)) {
|
||||||
if (in_array($default, $enum ?? [])) {
|
if (in_array($default, $enum)) {
|
||||||
$this->setDefault($default);
|
$this->setDefault($default);
|
||||||
} else {
|
} else {
|
||||||
throw new \InvalidArgumentException(
|
throw new \InvalidArgumentException(
|
||||||
"Enum::__construct() The default value '$default' does not match any item in the enumeration"
|
"Enum::__construct() The default value '$default' does not match any item in the enumeration"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} elseif (is_int($default) && $default < count($enum ?? [])) {
|
} elseif (is_int($default) && $default < count($enum)) {
|
||||||
// Set to specified index if given
|
// Set to specified index if given
|
||||||
$this->setDefault($enum[$default]);
|
$this->setDefault($enum[$default]);
|
||||||
} else {
|
} else {
|
||||||
@ -242,4 +248,13 @@ class DBEnum extends DBString
|
|||||||
$this->setDefaultValue($default);
|
$this->setDefaultValue($default);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
parent::setValue($value, $record, $markChanged);
|
||||||
|
if (empty($this->value)) {
|
||||||
|
$this->value = $this->getDefault();
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ use SilverStripe\Forms\TextField;
|
|||||||
use SilverStripe\ORM\Filters\SearchFilter;
|
use SilverStripe\ORM\Filters\SearchFilter;
|
||||||
use SilverStripe\ORM\Queries\SQLSelect;
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidatorsTrait;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\FieldValidationInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single field in the database.
|
* Single field in the database.
|
||||||
@ -41,8 +43,9 @@ use SilverStripe\Model\ModelData;
|
|||||||
* }
|
* }
|
||||||
* </code>
|
* </code>
|
||||||
*/
|
*/
|
||||||
abstract class DBField extends ModelData implements DBIndexable
|
abstract class DBField extends ModelData implements DBIndexable, FieldValidationInterface
|
||||||
{
|
{
|
||||||
|
use FieldValidatorsTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raw value of this field
|
* Raw value of this field
|
||||||
@ -99,12 +102,14 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
'ProcessedRAW' => 'HTMLFragment',
|
'ProcessedRAW' => 'HTMLFragment',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static array $field_validators = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default value in the database.
|
* Default value in the database.
|
||||||
* Might be overridden on DataObject-level, but still useful for setting defaults on
|
* Might be overridden on DataObject-level, but still useful for setting defaults on
|
||||||
* already existing records after a db-build.
|
* already existing records after a db-build.
|
||||||
*/
|
*/
|
||||||
protected mixed $defaultVal = null;
|
private mixed $defaultValue = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false]
|
* Provide the DBField name and an array of options, e.g. ['index' => true], or ['nullifyEmpty' => false]
|
||||||
@ -121,6 +126,8 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
}
|
}
|
||||||
$this->setOptions($options);
|
$this->setOptions($options);
|
||||||
}
|
}
|
||||||
|
// Setting value needs to happen below the call to setOptions() in case the default value is set there
|
||||||
|
$this->value = $this->getDefaultValue();
|
||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@ -161,7 +168,7 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
*
|
*
|
||||||
* If you try an alter the name a warning will be thrown.
|
* If you try an alter the name a warning will be thrown.
|
||||||
*/
|
*/
|
||||||
public function setName(?string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
if ($this->name && $this->name !== $name) {
|
if ($this->name && $this->name !== $name) {
|
||||||
user_error("DBField::setName() shouldn't be called once a DBField already has a name."
|
user_error("DBField::setName() shouldn't be called once a DBField already has a name."
|
||||||
@ -189,6 +196,18 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of this field for field validation
|
||||||
|
*/
|
||||||
|
public function getValueForValidation(): mixed
|
||||||
|
{
|
||||||
|
$value = $this->getValue();
|
||||||
|
if (is_null($value)) {
|
||||||
|
return $this->nullValue();
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of this field in various formats.
|
* Set the value of this field in various formats.
|
||||||
* Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()}
|
* Used by {@link DataObject->getField()}, {@link DataObject->setCastedField()}
|
||||||
@ -214,7 +233,7 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
*/
|
*/
|
||||||
public function getDefaultValue(): mixed
|
public function getDefaultValue(): mixed
|
||||||
{
|
{
|
||||||
return $this->defaultVal;
|
return $this->defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,7 +241,7 @@ abstract class DBField extends ModelData implements DBIndexable
|
|||||||
*/
|
*/
|
||||||
public function setDefaultValue(mixed $defaultValue): static
|
public function setDefaultValue(mixed $defaultValue): static
|
||||||
{
|
{
|
||||||
$this->defaultVal = $defaultValue;
|
$this->defaultValue = $defaultValue;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class DBFloat extends DBField
|
|||||||
{
|
{
|
||||||
public function __construct(?string $name = null, float|int $defaultVal = 0)
|
public function __construct(?string $name = null, float|int $defaultVal = 0)
|
||||||
{
|
{
|
||||||
$this->defaultVal = is_float($defaultVal) ? $defaultVal : (float) 0;
|
$this->setDefaultValue(is_float($defaultVal) ? $defaultVal : (float) 0);
|
||||||
|
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ class DBFloat extends DBField
|
|||||||
return $field;
|
return $field;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nullValue(): ?int
|
public function nullValue(): int
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,11 @@ class DBForeignKey extends DBInt
|
|||||||
if ($record instanceof DataObject) {
|
if ($record instanceof DataObject) {
|
||||||
$this->object = $record;
|
$this->object = $record;
|
||||||
}
|
}
|
||||||
|
// Convert blank string to 0, this is sometimes required when calling DataObject::setCastedValue()
|
||||||
|
// after a form submission where the value is a blank string when no value is selected
|
||||||
|
if ($value === '') {
|
||||||
|
$value = 0;
|
||||||
|
}
|
||||||
return parent::setValue($value, $record, $markChanged);
|
return parent::setValue($value, $record, $markChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,46 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\Forms\NumericField;
|
use SilverStripe\Forms\NumericField;
|
||||||
use SilverStripe\Model\List\ArrayList;
|
use SilverStripe\Model\List\ArrayList;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use SilverStripe\Model\List\SS_List;
|
use SilverStripe\Model\List\SS_List;
|
||||||
use SilverStripe\Model\ArrayData;
|
use SilverStripe\Model\ArrayData;
|
||||||
|
use SilverStripe\Model\ModelData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a signed 32 bit integer field.
|
* Represents a signed 32 bit integer field
|
||||||
|
*
|
||||||
|
* Ints are always signed i.e. they can be negative
|
||||||
|
* Their range is -2147483648 to 2147483647
|
||||||
*/
|
*/
|
||||||
class DBInt extends DBField
|
class DBInt extends DBField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
IntFieldValidator::class
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(?string $name = null, int $defaultVal = 0)
|
public function __construct(?string $name = null, int $defaultVal = 0)
|
||||||
{
|
{
|
||||||
$this->defaultVal = is_int($defaultVal) ? $defaultVal : 0;
|
$this->setDefaultValue($defaultVal);
|
||||||
|
|
||||||
parent::__construct($name);
|
parent::__construct($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getField($fieldName): mixed
|
||||||
* Ensure int values are always returned.
|
|
||||||
* This is for mis-configured databases that return strings.
|
|
||||||
*/
|
|
||||||
public function getValue(): ?int
|
|
||||||
{
|
{
|
||||||
return (int) $this->value;
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
parent::setValue($value, $record, $markChanged);
|
||||||
|
// Cast int like strings as ints
|
||||||
|
if (is_string($this->value) && preg_match('/^-?\d+$/', $this->value)) {
|
||||||
|
$this->value = (int) $value;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,7 +85,7 @@ class DBInt extends DBField
|
|||||||
return NumericField::create($this->name, $title);
|
return NumericField::create($this->name, $title);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function nullValue(): ?int
|
public function nullValue(): int
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
13
src/ORM/FieldType/DBIp.php
Normal file
13
src/ORM/FieldType/DBIp.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IpFieldValidator;
|
||||||
|
|
||||||
|
class DBIp extends DBVarchar
|
||||||
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
IpFieldValidator::class,
|
||||||
|
];
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\LocaleFieldValidator;
|
||||||
use SilverStripe\i18n\i18n;
|
use SilverStripe\i18n\i18n;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,6 +10,10 @@ use SilverStripe\i18n\i18n;
|
|||||||
*/
|
*/
|
||||||
class DBLocale extends DBVarchar
|
class DBLocale extends DBVarchar
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
LocaleFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct(?string $name = null, int $size = 16)
|
public function __construct(?string $name = null, int $size = 16)
|
||||||
{
|
{
|
||||||
parent::__construct($name, $size);
|
parent::__construct($name, $size);
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
use SilverStripe\Core\Config\Config;
|
use SilverStripe\Core\Config\Config;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\MultiEnumFieldValidator;
|
||||||
use SilverStripe\Forms\CheckboxSetField;
|
use SilverStripe\Forms\CheckboxSetField;
|
||||||
use SilverStripe\Forms\MultiSelectField;
|
use SilverStripe\Forms\MultiSelectField;
|
||||||
use SilverStripe\ORM\Connect\MySQLDatabase;
|
use SilverStripe\ORM\Connect\MySQLDatabase;
|
||||||
@ -13,6 +15,13 @@ use SilverStripe\ORM\DB;
|
|||||||
*/
|
*/
|
||||||
class DBMultiEnum extends DBEnum
|
class DBMultiEnum extends DBEnum
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
// disable parent field validator
|
||||||
|
EnumFieldValidator::class => null,
|
||||||
|
// enable multi enum field validator
|
||||||
|
MultiEnumFieldValidator::class => ['getEnum'],
|
||||||
|
];
|
||||||
|
|
||||||
public function __construct($name = null, $enum = null, $default = null)
|
public function __construct($name = null, $enum = null, $default = null)
|
||||||
{
|
{
|
||||||
// MultiEnum needs to take care of its own defaults
|
// MultiEnum needs to take care of its own defaults
|
||||||
@ -34,6 +43,15 @@ class DBMultiEnum extends DBEnum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getValueForValidation(): array
|
||||||
|
{
|
||||||
|
$value = parent::getValueForValidation();
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
return explode(',', (string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
public function requireField(): void
|
public function requireField(): void
|
||||||
{
|
{
|
||||||
$charset = Config::inst()->get(MySQLDatabase::class, 'charset');
|
$charset = Config::inst()->get(MySQLDatabase::class, 'charset');
|
||||||
|
@ -5,12 +5,18 @@ namespace SilverStripe\ORM\FieldType;
|
|||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A special ForeignKey class that handles relations with arbitrary class types
|
* A special ForeignKey class that handles relations with arbitrary class types
|
||||||
*/
|
*/
|
||||||
class DBPolymorphicForeignKey extends DBComposite
|
class DBPolymorphicForeignKey extends DBComposite
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
// Disable parent field validator
|
||||||
|
CompositeFieldValidator::class => null,
|
||||||
|
];
|
||||||
|
|
||||||
private static bool $index = true;
|
private static bool $index = true;
|
||||||
|
|
||||||
private static array $composite_db = [
|
private static array $composite_db = [
|
||||||
|
@ -2,11 +2,18 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\FieldType;
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract base class for the string field types (i.e. Varchar and Text)
|
* An abstract base class for the string field types (i.e. Varchar and Text)
|
||||||
*/
|
*/
|
||||||
abstract class DBString extends DBField
|
abstract class DBString extends DBField
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
private static array $casting = [
|
private static array $casting = [
|
||||||
'LimitCharacters' => 'Text',
|
'LimitCharacters' => 'Text',
|
||||||
'LimitCharactersToClosestWord' => 'Text',
|
'LimitCharactersToClosestWord' => 'Text',
|
||||||
@ -17,13 +24,14 @@ abstract class DBString extends DBField
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the default value for "nullify empty"
|
* Set the default value for "nullify empty" and 'default'
|
||||||
*
|
*
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
public function __construct($name = null, $options = [])
|
public function __construct($name = null, $options = [])
|
||||||
{
|
{
|
||||||
$this->options['nullifyEmpty'] = true;
|
$this->options['nullifyEmpty'] = true;
|
||||||
|
$this->options['default'] = '';
|
||||||
parent::__construct($name, $options);
|
parent::__construct($name, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +90,16 @@ abstract class DBString extends DBField
|
|||||||
return $value || (is_string($value) && strlen($value ?? ''));
|
return $value || (is_string($value) && strlen($value ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
$this->value = '';
|
||||||
|
} else {
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function prepValueForDB(mixed $value): array|string|null
|
public function prepValueForDB(mixed $value): array|string|null
|
||||||
{
|
{
|
||||||
// Cast non-empty value
|
// Cast non-empty value
|
||||||
|
@ -11,6 +11,7 @@ use SilverStripe\ORM\DB;
|
|||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
use SilverStripe\Security\Security;
|
use SilverStripe\Security\Security;
|
||||||
use SilverStripe\Model\ModelData;
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\TimeFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a column in the database with the type 'Time'.
|
* Represents a column in the database with the type 'Time'.
|
||||||
@ -26,17 +27,17 @@ class DBTime extends DBField
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Standard ISO format string for time in CLDR standard format
|
* Standard ISO format string for time in CLDR standard format
|
||||||
|
* This is equivalent to php date format "H:i:s" e.g. 09:30:00
|
||||||
*/
|
*/
|
||||||
public const ISO_TIME = 'HH:mm:ss';
|
public const ISO_TIME = 'HH:mm:ss';
|
||||||
|
|
||||||
|
private static array $field_validators = [
|
||||||
|
TimeFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
{
|
{
|
||||||
$value = $this->parseTime($value);
|
$value = $this->parseTime($value);
|
||||||
if ($value === false) {
|
|
||||||
throw new InvalidArgumentException(
|
|
||||||
'Invalid date passed. Use ' . $this->getISOFormat() . ' to prevent this error.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
22
src/ORM/FieldType/DBUrl.php
Normal file
22
src/ORM/FieldType/DBUrl.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\FieldType;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\UrlFieldValidator;
|
||||||
|
use SilverStripe\Forms\FormField;
|
||||||
|
use SilverStripe\Forms\UrlField;
|
||||||
|
|
||||||
|
class DBUrl extends DBVarchar
|
||||||
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
UrlFieldValidator::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function scaffoldFormField(?string $title = null, array $params = []): ?FormField
|
||||||
|
{
|
||||||
|
$field = UrlField::create($this->name, $title);
|
||||||
|
$field->setMaxLength($this->getSize());
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ use SilverStripe\Forms\NullableField;
|
|||||||
use SilverStripe\Forms\TextField;
|
use SilverStripe\Forms\TextField;
|
||||||
use SilverStripe\ORM\Connect\MySQLDatabase;
|
use SilverStripe\ORM\Connect\MySQLDatabase;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text
|
* Class Varchar represents a variable-length string of up to 255 characters, designed to store raw text
|
||||||
@ -18,6 +19,10 @@ use SilverStripe\ORM\DB;
|
|||||||
*/
|
*/
|
||||||
class DBVarchar extends DBString
|
class DBVarchar extends DBString
|
||||||
{
|
{
|
||||||
|
private static array $field_validators = [
|
||||||
|
StringFieldValidator::class => [null, 'getSize'],
|
||||||
|
];
|
||||||
|
|
||||||
private static array $casting = [
|
private static array $casting = [
|
||||||
'Initial' => 'Text',
|
'Initial' => 'Text',
|
||||||
'URL' => 'Text',
|
'URL' => 'Text',
|
||||||
|
@ -5,12 +5,23 @@ namespace SilverStripe\ORM\FieldType;
|
|||||||
use SilverStripe\Forms\DropdownField;
|
use SilverStripe\Forms\DropdownField;
|
||||||
use SilverStripe\Forms\FormField;
|
use SilverStripe\Forms\FormField;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\Model\ModelData;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single year field.
|
* Represents a single year field
|
||||||
*/
|
*/
|
||||||
class DBYear extends DBField
|
class DBYear extends DBField
|
||||||
{
|
{
|
||||||
|
// MySQL year datatype supports years between 1901 and 2155
|
||||||
|
// https://dev.mysql.com/doc/refman/8.0/en/year.html
|
||||||
|
private const MIN_YEAR = 1901;
|
||||||
|
private const MAX_YEAR = 2155;
|
||||||
|
|
||||||
|
private static $field_validators = [
|
||||||
|
IntFieldValidator::class => ['getMinYear', 'getMaxYear'],
|
||||||
|
];
|
||||||
|
|
||||||
public function requireField(): void
|
public function requireField(): void
|
||||||
{
|
{
|
||||||
$parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue];
|
$parts = ['datatype' => 'year', 'precision' => 4, 'arrayValue' => $this->arrayValue];
|
||||||
@ -25,11 +36,56 @@ class DBYear extends DBField
|
|||||||
return $selectBox;
|
return $selectBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||||
|
{
|
||||||
|
parent::setValue($value, $record, $markChanged);
|
||||||
|
// 0 is used to represent a null value, which will be stored as 0000 in MySQL
|
||||||
|
if ($this->value === '0000') {
|
||||||
|
$this->value = 0;
|
||||||
|
}
|
||||||
|
// shorthand for 2000 in MySQL
|
||||||
|
if ($this->value === '00') {
|
||||||
|
$this->value = 2000;
|
||||||
|
}
|
||||||
|
// convert string int to int
|
||||||
|
// string int and int are both valid in MySQL, though only use int internally
|
||||||
|
if (is_string($this->value) && preg_match('#^\d+$#', (string) $this->value)) {
|
||||||
|
$this->value = (int) $this->value;
|
||||||
|
}
|
||||||
|
if (!is_int($this->value)) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
// shorthand for 2001-2069 in MySQL
|
||||||
|
if ($this->value >= 1 && $this->value <= 69) {
|
||||||
|
$this->value = 2000 + $this->value;
|
||||||
|
}
|
||||||
|
// shorthand for 1970-1999 in MySQL
|
||||||
|
if ($this->value >= 70 && $this->value <= 99) {
|
||||||
|
$this->value = 1900 + $this->value;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nullValue(): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMinYear(): int
|
||||||
|
{
|
||||||
|
return DBYear::MIN_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaxYear(): int
|
||||||
|
{
|
||||||
|
return DBYear::MAX_YEAR;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of default options that can
|
* Returns a list of default options that can
|
||||||
* be used to populate a select box, or compare against
|
* be used to populate a select box, or compare against
|
||||||
* input values. Starts by default at the current year,
|
* input values. Starts by default at the current year,
|
||||||
* and counts back to 1900.
|
* and counts back to 1901.
|
||||||
*
|
*
|
||||||
* @param int|null $start starting date to count down from
|
* @param int|null $start starting date to count down from
|
||||||
* @param int|null $end end date to count down to
|
* @param int|null $end end date to count down to
|
||||||
@ -37,10 +93,10 @@ class DBYear extends DBField
|
|||||||
private function getDefaultOptions(?int $start = null, ?int $end = null): array
|
private function getDefaultOptions(?int $start = null, ?int $end = null): array
|
||||||
{
|
{
|
||||||
if (!$start) {
|
if (!$start) {
|
||||||
$start = (int)date('Y');
|
$start = (int) date('Y');
|
||||||
}
|
}
|
||||||
if (!$end) {
|
if (!$end) {
|
||||||
$end = 1900;
|
$end = DBYear::MIN_YEAR;
|
||||||
}
|
}
|
||||||
$years = [];
|
$years = [];
|
||||||
for ($i = $start; $i >= $end; $i--) {
|
for ($i = $start; $i >= $end; $i--) {
|
||||||
|
@ -88,6 +88,8 @@ class Group extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "Group";
|
private static $table_name = "Group";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
private static $indexes = [
|
private static $indexes = [
|
||||||
'Title' => true,
|
'Title' => true,
|
||||||
'Code' => true,
|
'Code' => true,
|
||||||
|
@ -50,6 +50,8 @@ class LoginAttempt extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "LoginAttempt";
|
private static $table_name = "LoginAttempt";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $includerelations Indicate if the labels returned include relation fields
|
* @param bool $includerelations Indicate if the labels returned include relation fields
|
||||||
* @return array
|
* @return array
|
||||||
|
@ -105,6 +105,8 @@ class Member extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "Member";
|
private static $table_name = "Member";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
private static $default_sort = '"Surname", "FirstName"';
|
private static $default_sort = '"Surname", "FirstName"';
|
||||||
|
|
||||||
private static $indexes = [
|
private static $indexes = [
|
||||||
|
@ -27,6 +27,8 @@ class MemberPassword extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "MemberPassword";
|
private static $table_name = "MemberPassword";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a password change from the given member.
|
* Log a password change from the given member.
|
||||||
* Call MemberPassword::log($this) from within Member whenever the password is changed.
|
* Call MemberPassword::log($this) from within Member whenever the password is changed.
|
||||||
|
@ -46,6 +46,8 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
|
|
||||||
private static $table_name = "Permission";
|
private static $table_name = "Permission";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the value to use for the "Type" field if a permission should be
|
* This is the value to use for the "Type" field if a permission should be
|
||||||
* granted.
|
* granted.
|
||||||
@ -233,15 +235,15 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Raw SQL for efficiency
|
// Raw SQL for efficiency
|
||||||
$permission = DB::prepared_query(
|
$permission = DB::withPrimary(fn() => DB::prepared_query(
|
||||||
"SELECT \"ID\"
|
"SELECT \"ID\"
|
||||||
FROM \"Permission\"
|
FROM \"Permission\"
|
||||||
WHERE (
|
WHERE (
|
||||||
\"Code\" IN ($codeClause $adminClause)
|
\"Code\" IN ($codeClause $adminClause)
|
||||||
AND \"Type\" = ?
|
AND \"Type\" = ?
|
||||||
AND \"GroupID\" IN ($groupClause)
|
AND \"GroupID\" IN ($groupClause)
|
||||||
$argClause
|
$argClause
|
||||||
)",
|
)",
|
||||||
array_merge(
|
array_merge(
|
||||||
$codeParams,
|
$codeParams,
|
||||||
$adminParams,
|
$adminParams,
|
||||||
@ -249,7 +251,7 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
$groupParams,
|
$groupParams,
|
||||||
$argParams
|
$argParams
|
||||||
)
|
)
|
||||||
)->value();
|
)->value());
|
||||||
|
|
||||||
if ($permission) {
|
if ($permission) {
|
||||||
return $permission;
|
return $permission;
|
||||||
@ -257,15 +259,15 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
|
|
||||||
// Strict checking disabled?
|
// Strict checking disabled?
|
||||||
if (!static::config()->strict_checking || !$strict) {
|
if (!static::config()->strict_checking || !$strict) {
|
||||||
$hasPermission = DB::prepared_query(
|
$hasPermission = DB::withPrimary(fn() => DB::prepared_query(
|
||||||
"SELECT COUNT(*)
|
"SELECT COUNT(*)
|
||||||
FROM \"Permission\"
|
FROM \"Permission\"
|
||||||
WHERE (
|
WHERE (
|
||||||
\"Code\" IN ($codeClause) AND
|
\"Code\" IN ($codeClause) AND
|
||||||
\"Type\" = ?
|
\"Type\" = ?
|
||||||
)",
|
)",
|
||||||
array_merge($codeParams, [Permission::GRANT_PERMISSION])
|
array_merge($codeParams, [Permission::GRANT_PERMISSION])
|
||||||
)->value();
|
)->value());
|
||||||
|
|
||||||
if (!$hasPermission) {
|
if (!$hasPermission) {
|
||||||
return false;
|
return false;
|
||||||
@ -288,25 +290,29 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
if ($groupList) {
|
if ($groupList) {
|
||||||
$groupCSV = implode(", ", $groupList);
|
$groupCSV = implode(", ", $groupList);
|
||||||
|
|
||||||
$allowed = array_unique(DB::query("
|
$allowed = array_unique(
|
||||||
SELECT \"Code\"
|
DB::withPrimary(fn() => DB::query("
|
||||||
FROM \"Permission\"
|
SELECT \"Code\"
|
||||||
WHERE \"Type\" = " . Permission::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
FROM \"Permission\"
|
||||||
|
WHERE \"Type\" = " . Permission::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||||
|
|
||||||
UNION
|
UNION
|
||||||
|
|
||||||
SELECT \"Code\"
|
SELECT \"Code\"
|
||||||
FROM \"PermissionRoleCode\" PRC
|
FROM \"PermissionRoleCode\" PRC
|
||||||
INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
|
INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
|
||||||
INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
|
INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
|
||||||
WHERE \"GroupID\" IN ($groupCSV)
|
WHERE \"GroupID\" IN ($groupCSV)
|
||||||
")->column() ?? []);
|
"))->column() ?? []
|
||||||
|
);
|
||||||
|
|
||||||
$denied = array_unique(DB::query("
|
$denied = array_unique(
|
||||||
SELECT \"Code\"
|
DB::withPrimary(fn() => DB::query("
|
||||||
FROM \"Permission\"
|
SELECT \"Code\"
|
||||||
WHERE \"Type\" = " . Permission::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
FROM \"Permission\"
|
||||||
")->column() ?? []);
|
WHERE \"Type\" = " . Permission::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||||
|
"))->column() ?? []
|
||||||
|
);
|
||||||
|
|
||||||
return array_diff($allowed ?? [], $denied);
|
return array_diff($allowed ?? [], $denied);
|
||||||
}
|
}
|
||||||
@ -584,7 +590,9 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
|||||||
$flatCodeArray[] = $code;
|
$flatCodeArray[] = $code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$otherPerms = DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column();
|
$otherPerms = DB::withPrimary(
|
||||||
|
fn() => DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column()
|
||||||
|
);
|
||||||
|
|
||||||
if ($otherPerms) {
|
if ($otherPerms) {
|
||||||
foreach ($otherPerms as $otherPerm) {
|
foreach ($otherPerms as $otherPerm) {
|
||||||
|
@ -40,6 +40,8 @@ class PermissionRole extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "PermissionRole";
|
private static $table_name = "PermissionRole";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
private static $default_sort = '"Title"';
|
private static $default_sort = '"Title"';
|
||||||
|
|
||||||
private static $singular_name = 'Role';
|
private static $singular_name = 'Role';
|
||||||
|
@ -23,6 +23,8 @@ class PermissionRoleCode extends DataObject
|
|||||||
];
|
];
|
||||||
|
|
||||||
private static $table_name = "PermissionRoleCode";
|
private static $table_name = "PermissionRoleCode";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
private static $indexes = [
|
private static $indexes = [
|
||||||
"Code" => true,
|
"Code" => true,
|
||||||
|
@ -44,6 +44,8 @@ class RememberLoginHash extends DataObject
|
|||||||
|
|
||||||
private static $table_name = "RememberLoginHash";
|
private static $table_name = "RememberLoginHash";
|
||||||
|
|
||||||
|
private static bool $must_use_primary_db = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if logging out on one device also clears existing login tokens
|
* Determines if logging out on one device also clears existing login tokens
|
||||||
* on all other devices owned by the member.
|
* on all other devices owned by the member.
|
||||||
|
@ -437,6 +437,12 @@ class Security extends Controller implements TemplateGlobalProvider
|
|||||||
*/
|
*/
|
||||||
public static function setCurrentUser($currentUser = null)
|
public static function setCurrentUser($currentUser = null)
|
||||||
{
|
{
|
||||||
|
// Always use the primary database and not a replica if a CMS user is logged in
|
||||||
|
// This is to ensure that when viewing content on the frontend it is always
|
||||||
|
// up to date i.e. not from an unsynced replica
|
||||||
|
if ($currentUser && Permission::checkMember($currentUser, 'CMS_ACCESS')) {
|
||||||
|
DB::setMustUsePrimary();
|
||||||
|
}
|
||||||
Security::$currentUser = $currentUser;
|
Security::$currentUser = $currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,10 @@ use SilverStripe\Core\Injector\Injector;
|
|||||||
use SilverStripe\Core\Injector\InjectorLoader;
|
use SilverStripe\Core\Injector\InjectorLoader;
|
||||||
use SilverStripe\Core\Kernel;
|
use SilverStripe\Core\Kernel;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Core\Environment;
|
||||||
|
use ReflectionClass;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
use ReflectionObject;
|
||||||
|
|
||||||
class KernelTest extends SapphireTest
|
class KernelTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -81,4 +85,32 @@ class KernelTest extends SapphireTest
|
|||||||
|
|
||||||
$kernel->getConfigLoader()->getManifest();
|
$kernel->getConfigLoader()->getManifest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testReplicaDatabaseVarsLoaded()
|
||||||
|
{
|
||||||
|
// Set environment variables for a fake replica database
|
||||||
|
Environment::setEnv('SS_DATABASE_SERVER_REPLICA_01', 'the-moon');
|
||||||
|
Environment::setEnv('SS_DATABASE_USERNAME_REPLICA_01', 'alien');
|
||||||
|
Environment::setEnv('SS_DATABASE_PASSWORD_REPLICA_01', 'hi_people');
|
||||||
|
// Get the CoreKernel
|
||||||
|
/** @var Kernel $kernel */
|
||||||
|
$kernel = Injector::inst()->get(Kernel::class);
|
||||||
|
/** @var CoreKernel $coreKernel */
|
||||||
|
$coreKernel = $kernel->nest();
|
||||||
|
$this->assertTrue(is_a($coreKernel, CoreKernel::class));
|
||||||
|
// Boot the database environment variables
|
||||||
|
$reflector = new ReflectionObject($coreKernel);
|
||||||
|
$method = $reflector->getMethod('bootDatabaseEnvVars');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
$method->invoke($coreKernel);
|
||||||
|
// Assert DB config was updated
|
||||||
|
$default = DB::getConfig(DB::CONN_PRIMARY);
|
||||||
|
$configs = (new ReflectionClass(DB::class))->getStaticPropertyValue('configs');
|
||||||
|
$this->assertSame([
|
||||||
|
'type' => $default['type'],
|
||||||
|
'server' => 'the-moon',
|
||||||
|
'username' => 'alien',
|
||||||
|
'password' => 'hi_people',
|
||||||
|
], $configs['replica_01']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BigIntFieldValidator;
|
||||||
|
|
||||||
|
class BigIntFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative-int' => [
|
||||||
|
'value' => -123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max-int' => [
|
||||||
|
'value' => 9223372036854775807,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-min-int' => [
|
||||||
|
'value' => '-9223372036854775808',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
// Note: cannot test out of range values as they casting them to int
|
||||||
|
// will change the value to PHP_INT_MIN/PHP_INT_MAX
|
||||||
|
'invalid-string-int' => [
|
||||||
|
'value' => '123',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-float' => [
|
||||||
|
'value' => 123.45,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => [123],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
// On 64-bit systems, -9223372036854775808 will end up as a float
|
||||||
|
// however it works correctly when cast to an int
|
||||||
|
if ($value === '-9223372036854775808') {
|
||||||
|
$value = (int) $value;
|
||||||
|
}
|
||||||
|
$validator = new BigIntFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||||
|
|
||||||
|
class BooleanIntFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-int-1' => [
|
||||||
|
'value' => 1,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-int-0' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invvalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-1' => [
|
||||||
|
'value' => '1',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-0' => [
|
||||||
|
'value' => '0',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-true' => [
|
||||||
|
'value' => 'true',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-false' => [
|
||||||
|
'value' => 'false',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string' => [
|
||||||
|
'value' => 'abc',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => [],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new BooleanIntFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use stdClass;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator;
|
||||||
|
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||||
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
|
||||||
|
class CompositeFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'valueBoolean' => true,
|
||||||
|
'valueString' => 'fish',
|
||||||
|
'valueIsNull' => false,
|
||||||
|
'exception' => null,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'exception-not-iterable' => [
|
||||||
|
'valueBoolean' => true,
|
||||||
|
'valueString' => 'not-iterable',
|
||||||
|
'valueIsNull' => false,
|
||||||
|
'exception' => InvalidArgumentException::class,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'exception-not-field-validator' => [
|
||||||
|
'valueBoolean' => true,
|
||||||
|
'valueString' => 'no-field-validation',
|
||||||
|
'valueIsNull' => false,
|
||||||
|
'exception' => InvalidArgumentException::class,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'exception-do-not-skip-null' => [
|
||||||
|
'valueBoolean' => true,
|
||||||
|
'valueString' => 'fish',
|
||||||
|
'valueIsNull' => true,
|
||||||
|
'exception' => InvalidArgumentException::class,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-bool-field' => [
|
||||||
|
'valueBoolean' => 'dog',
|
||||||
|
'valueString' => 'fish',
|
||||||
|
'valueIsNull' => false,
|
||||||
|
'exception' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-field' => [
|
||||||
|
'valueBoolean' => true,
|
||||||
|
'valueString' => 456.789,
|
||||||
|
'valueIsNull' => false,
|
||||||
|
'exception' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(
|
||||||
|
mixed $valueBoolean,
|
||||||
|
mixed $valueString,
|
||||||
|
bool $valueIsNull,
|
||||||
|
?string $exception,
|
||||||
|
bool $expected
|
||||||
|
): void {
|
||||||
|
if ($exception) {
|
||||||
|
$this->expectException($exception);
|
||||||
|
}
|
||||||
|
if ($valueIsNull) {
|
||||||
|
$iterable = null;
|
||||||
|
} else {
|
||||||
|
$booleanField = new DBBoolean('BooleanField');
|
||||||
|
$booleanField->setValue($valueBoolean);
|
||||||
|
if ($exception && $valueString === 'no-field-validation') {
|
||||||
|
$stringField = new stdClass();
|
||||||
|
} else {
|
||||||
|
$stringField = new DBVarchar('StringField');
|
||||||
|
$stringField->setValue($valueString);
|
||||||
|
}
|
||||||
|
if ($exception && $valueString === 'not-iterable') {
|
||||||
|
$iterable = 'banana';
|
||||||
|
} else {
|
||||||
|
$iterable = [$booleanField, $stringField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$validator = new CompositeFieldValidator('MyField', $iterable);
|
||||||
|
$result = $validator->validate();
|
||||||
|
if (!$exception) {
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
|
||||||
|
|
||||||
|
class DateFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => '2020-09-15',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => '2020-02-30',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-wrong-format' => [
|
||||||
|
'value' => '15-09-2020',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-date-time' => [
|
||||||
|
'value' => '2020-09-15 13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-time' => [
|
||||||
|
'value' => '13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new DateFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DatetimeFieldValidator;
|
||||||
|
|
||||||
|
class DatetimeFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => '2020-09-15 13:34:56',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-date' => [
|
||||||
|
'value' => '2020-02-30 13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-time' => [
|
||||||
|
'value' => '2020-02-15 13:99:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-wrong-format' => [
|
||||||
|
'value' => '15-09-2020 13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-date-only' => [
|
||||||
|
'value' => '2020-09-15',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-time-only' => [
|
||||||
|
'value' => '13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new DatetimeFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DecimalFieldValidator;
|
||||||
|
|
||||||
|
class DecimalFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => 123.45,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative' => [
|
||||||
|
'value' => -123.45,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-rounded-dp' => [
|
||||||
|
'value' => 123.456,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-rounded-up' => [
|
||||||
|
'value' => 123.999,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative-int' => [
|
||||||
|
'value' => -123,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max' => [
|
||||||
|
'value' => 999.99,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max-negative' => [
|
||||||
|
'value' => -999.99,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-rounded-to-6-digts' => [
|
||||||
|
'value' => 999.999,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-too-long' => [
|
||||||
|
'value' => 1234.56,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-too-long-3dp' => [
|
||||||
|
'value' => 123.456,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 3,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-too-long-1dp' => [
|
||||||
|
'value' => 123.4,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 3,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-too-long-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 3,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string' => [
|
||||||
|
'value' => '123.45',
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => [123.45],
|
||||||
|
'wholeSize' => 5,
|
||||||
|
'decimalSize' => 2,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, int $wholeSize, int $decimalSize, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new DecimalFieldValidator('MyField', $value, $wholeSize, $decimalSize);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
|
||||||
|
|
||||||
|
class EmailFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
// Using symfony/validator for implementation so only smoke testing
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => 'test@example.com',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new EmailFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
|
|
||||||
|
class EnumFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-string' => [
|
||||||
|
'value' => 'cat',
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'allowedValues' => [123, 456],
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-none' => [
|
||||||
|
'value' => '',
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-strict' => [
|
||||||
|
'value' => '123',
|
||||||
|
'allowedValues' => [123, 456],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, array $allowedValues, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new EnumFieldValidator('MyField', $value, $allowedValues);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
|
|
||||||
|
class IntFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative-int' => [
|
||||||
|
'value' => -123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max-int' => [
|
||||||
|
'value' => 2147483647,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-min-int' => [
|
||||||
|
'value' => -2147483648,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-out-of-bounds' => [
|
||||||
|
'value' => 2147483648,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-out-of-negative-bounds' => [
|
||||||
|
'value' => -2147483649,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-string-int' => [
|
||||||
|
'value' => '123',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-float' => [
|
||||||
|
'value' => 123.45,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => [123],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new IntFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IpFieldValidator;
|
||||||
|
|
||||||
|
class IpFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
// Using symfony/validator for implementation so only smoke testing
|
||||||
|
return [
|
||||||
|
'valid-ipv4' => [
|
||||||
|
'value' => '127.0.0.1',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-ipv6' => [
|
||||||
|
'value' => '0:0:0:0:0:0:0:1',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-ipv6-short' => [
|
||||||
|
'value' => '::1',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => '12345',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new IpFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\LocaleFieldValidator;
|
||||||
|
|
||||||
|
class LocaleFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
// Using symfony/validator for implementation so only smoke testing
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => 'de_DE',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-dash' => [
|
||||||
|
'value' => 'de-DE',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-short' => [
|
||||||
|
'value' => 'de',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => 'zz_ZZ',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-dash' => [
|
||||||
|
'value' => 'zz-ZZ',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-short' => [
|
||||||
|
'value' => 'zz',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-dashes' => [
|
||||||
|
'value' => '-----',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-donut' => [
|
||||||
|
'value' => 'donut',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new LocaleFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\MultiEnumFieldValidator;
|
||||||
|
|
||||||
|
class MultiEnumFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-string' => [
|
||||||
|
'value' => ['cat'],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-multi-string' => [
|
||||||
|
'value' => ['cat', 'dog'],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-none' => [
|
||||||
|
'value' => [],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => [123],
|
||||||
|
'allowedValues' => [123, 456],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'exception-not-array' => [
|
||||||
|
'value' => 'cat,dog',
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => ['fish'],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => [null],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-multi' => [
|
||||||
|
'value' => ['dog', 'fish'],
|
||||||
|
'allowedValues' => ['cat', 'dog'],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-strict' => [
|
||||||
|
'value' => ['123'],
|
||||||
|
'allowedValues' => [123, 456],
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, array $allowedValues, bool $exception, bool $expected): void
|
||||||
|
{
|
||||||
|
if ($exception) {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
}
|
||||||
|
$validator = new MultiEnumFieldValidator('MyField', $value, $allowedValues);
|
||||||
|
$result = $validator->validate();
|
||||||
|
if (!$exception) {
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
|
||||||
|
|
||||||
|
class NumericFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative-int' => [
|
||||||
|
'value' => -123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-float' => [
|
||||||
|
'value' => 123.45,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-negative-float' => [
|
||||||
|
'value' => -123.45,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max-int' => [
|
||||||
|
'value' => PHP_INT_MAX,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-min-int' => [
|
||||||
|
'value' => PHP_INT_MIN,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max-float' => [
|
||||||
|
'value' => PHP_FLOAT_MAX,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-min-float' => [
|
||||||
|
'value' => PHP_FLOAT_MIN,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-string' => [
|
||||||
|
'value' => '123',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => [123],
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new NumericFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
|
||||||
|
class StringFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid-no-limit' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-blank' => [
|
||||||
|
'value' => '',
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-blank-when-min' => [
|
||||||
|
'value' => '',
|
||||||
|
'minLength' => 5,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-max' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => 0,
|
||||||
|
'maxLength' => 4,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-less-than-max-null-min' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => 4,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-less-than-max-unicode' => [
|
||||||
|
'value' => '☕☕☕☕',
|
||||||
|
'minLength' => 0,
|
||||||
|
'maxLength' => 4,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'exception-negative-min' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => -1,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => true,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-below-min' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => 5,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-below-min-unicode' => [
|
||||||
|
'value' => '☕☕☕☕',
|
||||||
|
'minLength' => 5,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-above-min' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'minLength' => 0,
|
||||||
|
'maxLength' => 3,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-above-min-unicode' => [
|
||||||
|
'value' => '☕☕☕☕',
|
||||||
|
'minLength' => 0,
|
||||||
|
'maxLength' => 3,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-float' => [
|
||||||
|
'value' => 123.56,
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-true' => [
|
||||||
|
'value' => true,
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-false' => [
|
||||||
|
'value' => false,
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-array' => [
|
||||||
|
'value' => ['fish'],
|
||||||
|
'minLength' => null,
|
||||||
|
'maxLength' => null,
|
||||||
|
'exception' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, ?int $minLength, ?int $maxLength, bool $exception, bool $expected): void
|
||||||
|
{
|
||||||
|
if ($exception) {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
}
|
||||||
|
$validator = new StringFieldValidator('MyField', $value, $minLength, $maxLength);
|
||||||
|
$result = $validator->validate();
|
||||||
|
if (!$exception) {
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\TimeFieldValidator;
|
||||||
|
|
||||||
|
class TimeFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => '13:34:56',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => '13:99:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-wrong-format' => [
|
||||||
|
'value' => '13-34-56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-date-time' => [
|
||||||
|
'value' => '2020-09-15 13:34:56',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-date' => [
|
||||||
|
'value' => '2020-09-15',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new TimeFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\UrlFieldValidator;
|
||||||
|
|
||||||
|
class UrlFieldValidatorTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
// Using symfony/validator for implementation so only smoke testing
|
||||||
|
return [
|
||||||
|
'valid-https' => [
|
||||||
|
'value' => 'https://www.example.com',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'valid-http' => [
|
||||||
|
'value' => 'https://www.example.com',
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid-ftp' => [
|
||||||
|
'value' => 'ftp://www.example.com',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-no-scheme' => [
|
||||||
|
'value' => 'www.example.com',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'invalid-null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$validator = new UrlFieldValidator('MyField', $value);
|
||||||
|
$result = $validator->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ use SilverStripe\Dev\SapphireTest;
|
|||||||
use SilverStripe\Forms\TextField;
|
use SilverStripe\Forms\TextField;
|
||||||
use SilverStripe\Forms\RequiredFields;
|
use SilverStripe\Forms\RequiredFields;
|
||||||
use SilverStripe\Forms\Tip;
|
use SilverStripe\Forms\Tip;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
class TextFieldTest extends SapphireTest
|
class TextFieldTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -45,4 +46,42 @@ class TextFieldTest extends SapphireTest
|
|||||||
$textField->setTip(new Tip('TestTip'));
|
$textField->setTip(new Tip('TestTip'));
|
||||||
$this->assertArrayHasKey('tip', $textField->getSchemaDataDefaults());
|
$this->assertArrayHasKey('tip', $textField->getSchemaDataDefaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'string' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'expected' => 'fish',
|
||||||
|
],
|
||||||
|
'string-blank' => [
|
||||||
|
'value' => '',
|
||||||
|
'expected' => '',
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => '',
|
||||||
|
],
|
||||||
|
'zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, mixed $expected): void
|
||||||
|
{
|
||||||
|
$field = new TextField('TestField');
|
||||||
|
$field->setValue($value);
|
||||||
|
$this->assertSame($expected, $field->getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,9 @@ use SilverStripe\ORM\FieldType\DBMoney;
|
|||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\ORM\FieldType\DBVarchar;
|
||||||
|
use SilverStripe\ORM\FieldType\DBDecimal;
|
||||||
|
|
||||||
class DBCompositeTest extends SapphireTest
|
class DBCompositeTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -140,4 +143,12 @@ class DBCompositeTest extends SapphireTest
|
|||||||
// $this->assertSame($moneyField, $obj->dbObject('DoubleMoney'));
|
// $this->assertSame($moneyField, $obj->dbObject('DoubleMoney'));
|
||||||
// $this->assertEquals(20, $obj->dbObject('DoubleMoney')->getAmount());
|
// $this->assertEquals(20, $obj->dbObject('DoubleMoney')->getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetValueForValidation(): void
|
||||||
|
{
|
||||||
|
$obj = DBCompositeTest\DBDoubleMoney::create();
|
||||||
|
$expected = [DBVarchar::class, DBDecimal::class];
|
||||||
|
$actual = array_map('get_class', $obj->getValueForValidation());
|
||||||
|
$this->assertSame($expected, $actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,4 +141,38 @@ class DBEnumTest extends SapphireTest
|
|||||||
$colourField->getEnumObsolete()
|
$colourField->getEnumObsolete()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'string' => [
|
||||||
|
'value' => 'green',
|
||||||
|
'expected' => 'green',
|
||||||
|
],
|
||||||
|
'string-not-in-set' => [
|
||||||
|
'value' => 'purple',
|
||||||
|
'expected' => 'purple',
|
||||||
|
],
|
||||||
|
'int' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => 123,
|
||||||
|
],
|
||||||
|
'empty-string' => [
|
||||||
|
'value' => '',
|
||||||
|
'expected' => 'blue',
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => 'blue',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, mixed $expected): void
|
||||||
|
{
|
||||||
|
$field = new DBEnum('TestField', ['red', 'green', 'blue'], 'blue');
|
||||||
|
$field->setValue($value);
|
||||||
|
$this->assertSame($expected, $field->getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace SilverStripe\ORM\Tests;
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use SilverStripe\Assets\Image;
|
use SilverStripe\Assets\Image;
|
||||||
use SilverStripe\ORM\FieldType\DBBigInt;
|
use SilverStripe\ORM\FieldType\DBBigInt;
|
||||||
use SilverStripe\ORM\FieldType\DBBoolean;
|
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||||
@ -30,6 +31,33 @@ use SilverStripe\Dev\SapphireTest;
|
|||||||
use SilverStripe\ORM\FieldType\DBField;
|
use SilverStripe\ORM\FieldType\DBField;
|
||||||
use SilverStripe\ORM\FieldType\DBYear;
|
use SilverStripe\ORM\FieldType\DBYear;
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\Core\ClassInfo;
|
||||||
|
use ReflectionClass;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\BigIntFieldValidator;
|
||||||
|
use SilverStripe\ORM\FieldType\DBClassName;
|
||||||
|
use ReflectionMethod;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\CompositeFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DateFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DecimalFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\EnumFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\IpFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\LocaleFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\MultiEnumFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\TimeFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\UrlFieldValidator;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\YearFieldValidator;
|
||||||
|
use SilverStripe\ORM\FieldType\DBUrl;
|
||||||
|
use SilverStripe\ORM\FieldType\DBPolymorphicRelationAwareForeignKey;
|
||||||
|
use SilverStripe\ORM\FieldType\DBIp;
|
||||||
|
use SilverStripe\ORM\FieldType\DBEmail;
|
||||||
|
use SilverStripe\Core\Validation\FieldValidation\DatetimeFieldValidator;
|
||||||
|
use SilverStripe\Assets\Storage\DBFile;
|
||||||
|
use SilverStripe\ORM\FieldType\DBClassNameVarchar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for DBField objects.
|
* Tests for DBField objects.
|
||||||
@ -392,4 +420,154 @@ class DBFieldTest extends SapphireTest
|
|||||||
|
|
||||||
$this->assertEquals('new value', $obj->getField('MyTestField'));
|
$this->assertEquals('new value', $obj->getField('MyTestField'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDefaultValues(): void
|
||||||
|
{
|
||||||
|
$expectedBaseDefault = null;
|
||||||
|
$expectedDefaults = [
|
||||||
|
DBBoolean::class => 0,
|
||||||
|
DBDecimal::class => 0.0,
|
||||||
|
DBInt::class => 0,
|
||||||
|
DBFloat::class => 0.0,
|
||||||
|
];
|
||||||
|
$classes = ClassInfo::subclassesFor(DBField::class);
|
||||||
|
foreach ($classes as $class) {
|
||||||
|
if (is_a($class, TestOnly::class, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$reflector = new ReflectionClass($class);
|
||||||
|
if ($reflector->isAbstract()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$expected = $expectedBaseDefault;
|
||||||
|
foreach ($expectedDefaults as $baseClass => $default) {
|
||||||
|
if ($class === $baseClass || is_subclass_of($class, $baseClass)) {
|
||||||
|
$expected = $default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$field = new $class('TestField');
|
||||||
|
$this->assertSame($expected, $field->getValue(), $class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFieldValidatorConfig(): void
|
||||||
|
{
|
||||||
|
$expectedFieldValidators = [
|
||||||
|
DBBigInt::class => [
|
||||||
|
BigIntFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBBoolean::class => [
|
||||||
|
BooleanIntFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBClassName::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
EnumFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBClassNameVarchar::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
EnumFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBCurrency::class => [
|
||||||
|
DecimalFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBDate::class => [
|
||||||
|
DateFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBDatetime::class => [
|
||||||
|
DatetimeFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBDecimal::class => [
|
||||||
|
DecimalFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBDouble::class => [],
|
||||||
|
DBEmail::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
EmailFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBEnum::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
EnumFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBFloat::class => [],
|
||||||
|
DBForeignKey::class => [
|
||||||
|
IntFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBHTMLText::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBHTMLVarchar::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBInt::class => [
|
||||||
|
IntFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBIp::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
IpFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBLocale::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
LocaleFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBMoney::class => [
|
||||||
|
CompositeFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBMultiEnum::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
MultiEnumFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBPercentage::class => [
|
||||||
|
DecimalFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBPolymorphicForeignKey::class => [],
|
||||||
|
DBPolymorphicRelationAwareForeignKey::class => [],
|
||||||
|
DBPrimaryKey::class => [
|
||||||
|
IntFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBText::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBTime::class => [
|
||||||
|
TimeFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBUrl::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
UrlFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBVarchar::class => [
|
||||||
|
StringFieldValidator::class,
|
||||||
|
],
|
||||||
|
DBYear::class => [
|
||||||
|
YearFieldValidator::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$count = 0;
|
||||||
|
$classes = ClassInfo::subclassesFor(DBField::class);
|
||||||
|
foreach ($classes as $class) {
|
||||||
|
if (is_a($class, TestOnly::class, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_starts_with($class, 'SilverStripe\ORM\FieldType')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$reflector = new ReflectionClass($class);
|
||||||
|
if ($reflector->isAbstract()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!array_key_exists($class, $expectedFieldValidators)) {
|
||||||
|
throw new Exception("No field validator config found for $class");
|
||||||
|
}
|
||||||
|
$expected = $expectedFieldValidators[$class];
|
||||||
|
$method = new ReflectionMethod($class, 'getFieldValidators');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
$obj = new $class('MyField');
|
||||||
|
$actual = array_map('get_class', $method->invoke($obj));
|
||||||
|
$this->assertSame($expected, $actual, $class);
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
// Assert that we have tested all classes e.g. namespace wasn't changed, no new classes were added
|
||||||
|
// that haven't been tested
|
||||||
|
$this->assertSame(29, $count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
44
tests/php/ORM/DBForiegnKeyTest.php
Normal file
44
tests/php/ORM/DBForiegnKeyTest.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use SilverStripe\ORM\FieldType\DBForeignKey;
|
||||||
|
|
||||||
|
class DBForiegnKeyTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'int' => [
|
||||||
|
'value' => 2,
|
||||||
|
'expected' => 2,
|
||||||
|
],
|
||||||
|
'string' => [
|
||||||
|
'value' => '2',
|
||||||
|
'expected' => '2',
|
||||||
|
],
|
||||||
|
'zero' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'blank-string' => [
|
||||||
|
'value' => '',
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, mixed $expected): void
|
||||||
|
{
|
||||||
|
$field = new DBForeignKey('TestField');
|
||||||
|
$field->setValue($value);
|
||||||
|
$this->assertSame($expected, $field->getValue());
|
||||||
|
}
|
||||||
|
}
|
@ -4,15 +4,78 @@ namespace SilverStripe\ORM\Tests;
|
|||||||
|
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\ORM\FieldType\DBInt;
|
use SilverStripe\ORM\FieldType\DBInt;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
class DBIntTest extends SapphireTest
|
class DBIntTest extends SapphireTest
|
||||||
{
|
{
|
||||||
public function testGetValueCastToInt()
|
public function testDefaultValue(): void
|
||||||
{
|
{
|
||||||
$field = DBInt::create('MyField');
|
$field = new DBInt('MyField');
|
||||||
$field->setValue(3);
|
$this->assertSame(0, $field->getValue());
|
||||||
$this->assertSame(3, $field->getValue());
|
}
|
||||||
$field->setValue('3');
|
|
||||||
$this->assertSame(3, $field->getValue());
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'int' => [
|
||||||
|
'value' => 3,
|
||||||
|
'expected' => 3,
|
||||||
|
],
|
||||||
|
'string-int' => [
|
||||||
|
'value' => '3',
|
||||||
|
'expected' => 3,
|
||||||
|
],
|
||||||
|
'negative-int' => [
|
||||||
|
'value' => -3,
|
||||||
|
'expected' => -3,
|
||||||
|
],
|
||||||
|
'negative-string-int' => [
|
||||||
|
'value' => '-3',
|
||||||
|
'expected' => -3,
|
||||||
|
],
|
||||||
|
'string' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'expected' => 'fish',
|
||||||
|
],
|
||||||
|
'array' => [
|
||||||
|
'value' => [],
|
||||||
|
'expected' => [],
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, mixed $expected): void
|
||||||
|
{
|
||||||
|
$field = new DBInt('MyField');
|
||||||
|
$field->setValue($value);
|
||||||
|
$this->assertSame($expected, $field->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideValidate(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'valid' => [
|
||||||
|
'value' => 123,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'invalid' => [
|
||||||
|
'value' => 'abc',
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideValidate')]
|
||||||
|
public function testValidate(mixed $value, bool $expected): void
|
||||||
|
{
|
||||||
|
$field = new DBInt('MyField');
|
||||||
|
$field->setValue($value);
|
||||||
|
$result = $field->validate();
|
||||||
|
$this->assertSame($expected, $result->isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
44
tests/php/ORM/DBMultiEnumTest.php
Normal file
44
tests/php/ORM/DBMultiEnumTest.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\ORM\FieldType\DBMultiEnum;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
|
class DBMultiEnumTest extends SapphireTest
|
||||||
|
{
|
||||||
|
public static function provideGetValueForValidation(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'array' => [
|
||||||
|
'value' => ['Red', 'Green'],
|
||||||
|
'expected' => ['Red', 'Green'],
|
||||||
|
],
|
||||||
|
'string' => [
|
||||||
|
'value' => 'Red,Green',
|
||||||
|
'expected' => ['Red', 'Green'],
|
||||||
|
],
|
||||||
|
'string-non-existant-value' => [
|
||||||
|
'value' => 'Red,Green,Purple',
|
||||||
|
'expected' => ['Red', 'Green', 'Purple'],
|
||||||
|
],
|
||||||
|
'empty-string' => [
|
||||||
|
'value' => '',
|
||||||
|
'expected' => [''],
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => [''],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideGetValueForValidation')]
|
||||||
|
public function testGetValueForValidation(mixed $value, array $expected): void
|
||||||
|
{
|
||||||
|
$obj = new DBMultiEnum('TestField', ['Red', 'Green', 'Blue']);
|
||||||
|
$obj->setValue($value);
|
||||||
|
$this->assertSame($expected, $obj->getValueForValidation());
|
||||||
|
}
|
||||||
|
}
|
260
tests/php/ORM/DBReplicaTest.php
Normal file
260
tests/php/ORM/DBReplicaTest.php
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use SilverStripe\Dev\FunctionalTest;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\Control\Director;
|
||||||
|
use SilverStripe\Security\Security;
|
||||||
|
use SilverStripe\Core\Config\Config;
|
||||||
|
use SilverStripe\ORM\Tests\DBReplicaTest\TestController;
|
||||||
|
use SilverStripe\ORM\Tests\DBReplicaTest\TestObject;
|
||||||
|
use SilverStripe\Security\Group;
|
||||||
|
use SilverStripe\Security\Member;
|
||||||
|
use SilverStripe\ORM\DataQuery;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
|
class DBReplicaTest extends FunctionalTest
|
||||||
|
{
|
||||||
|
protected static $extra_dataobjects = [
|
||||||
|
TestObject::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static $fixture_file = 'DBReplicaTest.yml';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->setupConfigsAndConnections(true);
|
||||||
|
// Set DB:$mustUsePrimary to true to allow using replicas
|
||||||
|
// This is disabled by default in SapphireTest::setUpBeforeClass()
|
||||||
|
// Also reset mustUsePrimary after using mutable sql to create yml fixtures
|
||||||
|
// and also because by default an ADMIN user is logged in when using fixtures in SapphireTest::setUp()
|
||||||
|
// and also prevent tests from affecting subsequent tests
|
||||||
|
(new ReflectionClass(DB::class))->setStaticPropertyValue('mustUsePrimary', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$this->setupConfigsAndConnections(false);
|
||||||
|
// Reset DB:$mustUsePrimary to true which is the default set by SapphireTest::setUpBeforeClass()
|
||||||
|
(new ReflectionClass(DB::class))->setStaticPropertyValue('mustUsePrimary', true);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsesReplica(): void
|
||||||
|
{
|
||||||
|
// Assert uses replica by default
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame('replica_01', $this->getLastConnectionName());
|
||||||
|
// Assert uses primary when using withPrimary()
|
||||||
|
DB::withPrimary(fn() => TestObject::get()->count());
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
// Assert that withPrimary() was only temporary
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame('replica_01', $this->getLastConnectionName());
|
||||||
|
// Assert DB::setMustUsePrimary() forces primary from now on
|
||||||
|
DB::setMustUsePrimary();
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMutableSql(): void
|
||||||
|
{
|
||||||
|
// Assert that using mutable sql in an ORM method with a dataclass uses primary
|
||||||
|
TestObject::create(['Title' => 'testing'])->write();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
// Assert that now all subsequent queries use primary
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMutableSqlDbQuery(): void
|
||||||
|
{
|
||||||
|
// Assert that using mutable sql in DB::query() uses primary
|
||||||
|
DB::query('INSERT INTO "DBReplicaTest_TestObject" ("Title") VALUES (\'testing\')');
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
// Assert that now all subsequent queries use primary
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMutableSqlDbPreparedQuery(): void
|
||||||
|
{
|
||||||
|
// Assert that using mutable sql in DB::prepared_query() uses primary
|
||||||
|
DB::prepared_query('INSERT INTO "DBReplicaTest_TestObject" ("Title") VALUES (?)', ['testing']);
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
// Assert that now all subsequent queries use primary
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetCurrentUser')]
|
||||||
|
public function testSetCurrentUser(string $firstName, string $expected): void
|
||||||
|
{
|
||||||
|
$member = Member::get()->find('FirstName', $firstName);
|
||||||
|
Security::setCurrentUser($member);
|
||||||
|
TestObject::get()->count();
|
||||||
|
$this->assertSame($expected, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDataObjectMustUsePrimaryDb(): void
|
||||||
|
{
|
||||||
|
// Assert that DataList::getIterator() respect DataObject.must_use_primary_db
|
||||||
|
foreach (TestObject::get() as $object) {
|
||||||
|
$object->Title = 'test2';
|
||||||
|
}
|
||||||
|
$this->assertSame('replica_01', $this->getLastConnectionName());
|
||||||
|
foreach (Group::get() as $group) {
|
||||||
|
$group->Title = 'test2';
|
||||||
|
}
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName());
|
||||||
|
// Assert that DataQuery methods without params respect DataObject.must_use_primary_db
|
||||||
|
$methods = [
|
||||||
|
'count',
|
||||||
|
'exists',
|
||||||
|
'firstRow',
|
||||||
|
'lastRow'
|
||||||
|
];
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
(new DataQuery(TestObject::class))->$method();
|
||||||
|
$this->assertSame('replica_01', $this->getLastConnectionName(), "method is $method");
|
||||||
|
(new DataQuery(Group::class))->$method();
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName(), "method is $method");
|
||||||
|
}
|
||||||
|
// Assert that DataQuery methods with a param respect DataObject.must_use_primary_db
|
||||||
|
$methods = [
|
||||||
|
'max',
|
||||||
|
'min',
|
||||||
|
'avg',
|
||||||
|
'sum',
|
||||||
|
'column',
|
||||||
|
];
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
(new DataQuery(TestObject::class))->$method('ID');
|
||||||
|
$this->assertSame('replica_01', $this->getLastConnectionName(), "method is $method");
|
||||||
|
(new DataQuery(Group::class))->$method('ID');
|
||||||
|
$this->assertSame(DB::CONN_PRIMARY, $this->getLastConnectionName(), "method is $method");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideSetCurrentUser(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'non_cms_user' => [
|
||||||
|
'firstName' => 'random',
|
||||||
|
'expected' => 'replica_01'
|
||||||
|
],
|
||||||
|
'cms_user' => [
|
||||||
|
'firstName' => 'cmsuser',
|
||||||
|
'expected' => DB::CONN_PRIMARY
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideRoutes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'normal_route' => [
|
||||||
|
'path' => 'test',
|
||||||
|
'expected' => 'replica_01'
|
||||||
|
],
|
||||||
|
'security_route' => [
|
||||||
|
'path' => 'Security/login',
|
||||||
|
'expected' => DB::CONN_PRIMARY
|
||||||
|
],
|
||||||
|
'dev_route' => [
|
||||||
|
'path' => 'dev/tasks',
|
||||||
|
'expected' => DB::CONN_PRIMARY
|
||||||
|
],
|
||||||
|
'dev_in_path_but_not_dev_route' => [
|
||||||
|
'path' => 'test/dev',
|
||||||
|
'expected' => 'replica_01'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideRoutes')]
|
||||||
|
public function testRoutes(string $path, string $expected): void
|
||||||
|
{
|
||||||
|
// Create a custom rule to test our controller that should default to using a replica
|
||||||
|
$rules = Config::inst()->get(Director::class, 'rules');
|
||||||
|
$rules['test'] = TestController::class;
|
||||||
|
// Ensure that routes staring with '$' are at the bottom of the assoc array index and don't override
|
||||||
|
// our new 'test' route
|
||||||
|
uksort($rules, fn($a, $b) => str_starts_with($a, '$') ? 1 : (str_starts_with($b, '$') ? -1 : 0));
|
||||||
|
$this->get($path);
|
||||||
|
$this->assertSame($expected, $this->getLastConnectionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function provideHasReplicaConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no_replica' => [
|
||||||
|
'includeReplica' => false,
|
||||||
|
'expected' => false
|
||||||
|
],
|
||||||
|
'with_replica' => [
|
||||||
|
'includeReplica' => true,
|
||||||
|
'expected' => true
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideHasReplicaConfig')]
|
||||||
|
public function testHasReplicaConfig(bool $includeReplica, bool $expected): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(DB::hasReplicaConfig());
|
||||||
|
$primaryConfig = DB::getConfig(DB::CONN_PRIMARY);
|
||||||
|
$config = [DB::CONN_PRIMARY => $primaryConfig];
|
||||||
|
if ($includeReplica) {
|
||||||
|
$config['replica_01'] = $primaryConfig;
|
||||||
|
}
|
||||||
|
(new ReflectionClass(DB::class))->setStaticPropertyValue('configs', $config);
|
||||||
|
$this->assertSame($expected, DB::hasReplicaConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasConfig(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse(DB::hasConfig('lorem'));
|
||||||
|
DB::setConfig(['type' => 'lorem'], 'lorem');
|
||||||
|
$this->assertTrue(DB::hasConfig('lorem'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetReplicaConfigKey(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('replica_03', DB::getReplicaConfigKey(3));
|
||||||
|
$this->assertSame('replica_58', DB::getReplicaConfigKey(58));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using reflection, set DB::configs and DB::connections with a fake a replica connection
|
||||||
|
* that points to the same connection as the primary connection.
|
||||||
|
*/
|
||||||
|
private function setupConfigsAndConnections($includeReplica = true): void
|
||||||
|
{
|
||||||
|
$reflector = new ReflectionClass(DB::class);
|
||||||
|
$primaryConfig = DB::getConfig(DB::CONN_PRIMARY);
|
||||||
|
$configs = [DB::CONN_PRIMARY => $primaryConfig];
|
||||||
|
if ($includeReplica) {
|
||||||
|
$configs['replica_01'] = $primaryConfig;
|
||||||
|
}
|
||||||
|
$reflector->setStaticPropertyValue('configs', $configs);
|
||||||
|
// Create connections
|
||||||
|
$primaryConnection = DB::get_conn(DB::CONN_PRIMARY);
|
||||||
|
$connections = [DB::CONN_PRIMARY => $primaryConnection];
|
||||||
|
if ($includeReplica) {
|
||||||
|
$connections['replica_01'] = $primaryConnection;
|
||||||
|
}
|
||||||
|
$reflector->setStaticPropertyValue('connections', $connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last connection name used by the DB class. This shows if a replica was used.
|
||||||
|
*/
|
||||||
|
private function getLastConnectionName(): string
|
||||||
|
{
|
||||||
|
return (new ReflectionClass(DB::class))->getStaticPropertyValue('lastConnectionName');
|
||||||
|
}
|
||||||
|
}
|
18
tests/php/ORM/DBReplicaTest.yml
Normal file
18
tests/php/ORM/DBReplicaTest.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
SilverStripe\ORM\Tests\DBReplicaTest\TestObject:
|
||||||
|
test:
|
||||||
|
Title: 'test'
|
||||||
|
SilverStripe\Security\Permission:
|
||||||
|
test:
|
||||||
|
Code: 'CMS_ACCESS_Something'
|
||||||
|
SilverStripe\Security\Group:
|
||||||
|
test:
|
||||||
|
Title: 'test'
|
||||||
|
Permissions:
|
||||||
|
- =>SilverStripe\Security\Permission.test
|
||||||
|
SilverStripe\Security\Member:
|
||||||
|
cmsuser:
|
||||||
|
FirstName: 'CmsUser'
|
||||||
|
Groups:
|
||||||
|
- =>SilverStripe\Security\Group.test
|
||||||
|
random:
|
||||||
|
FirstName: 'Random'
|
18
tests/php/ORM/DBReplicaTest/TestController.php
Normal file
18
tests/php/ORM/DBReplicaTest/TestController.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DBReplicaTest;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
class TestController extends Controller implements TestOnly
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Make a call to the database
|
||||||
|
TestObject::get()->count();
|
||||||
|
$response = $this->getResponse();
|
||||||
|
$response->setBody('DB_REPLICA_TEST_CONTROLLER');
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
16
tests/php/ORM/DBReplicaTest/TestObject.php
Normal file
16
tests/php/ORM/DBReplicaTest/TestObject.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\DBReplicaTest;
|
||||||
|
|
||||||
|
use SilverStripe\Control\Controller;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class TestObject extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $table_name = 'DBReplicaTest_TestObject';
|
||||||
|
}
|
@ -7,6 +7,7 @@ use SilverStripe\ORM\FieldType\DBField;
|
|||||||
use SilverStripe\ORM\FieldType\DBString;
|
use SilverStripe\ORM\FieldType\DBString;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
use SilverStripe\ORM\Tests\DBStringTest\MyStringField;
|
use SilverStripe\ORM\Tests\DBStringTest\MyStringField;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
class DBStringTest extends SapphireTest
|
class DBStringTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -68,4 +69,30 @@ class DBStringTest extends SapphireTest
|
|||||||
$this->assertFalse(DBField::create_field(MyStringField::class, 0)->exists());
|
$this->assertFalse(DBField::create_field(MyStringField::class, 0)->exists());
|
||||||
$this->assertFalse(DBField::create_field(MyStringField::class, 0.0)->exists());
|
$this->assertFalse(DBField::create_field(MyStringField::class, 0.0)->exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'string' => [
|
||||||
|
'value' => 'fish',
|
||||||
|
'expected' => 'fish',
|
||||||
|
],
|
||||||
|
'blank-string' => [
|
||||||
|
'value' => '',
|
||||||
|
'expected' => '',
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, string $expected): void
|
||||||
|
{
|
||||||
|
$obj = new MyStringField('TestField');
|
||||||
|
$obj->setValue($value);
|
||||||
|
$this->assertSame($expected, $obj->getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ namespace SilverStripe\ORM\Tests;
|
|||||||
use SilverStripe\Forms\DropdownField;
|
use SilverStripe\Forms\DropdownField;
|
||||||
use SilverStripe\ORM\FieldType\DBYear;
|
use SilverStripe\ORM\FieldType\DBYear;
|
||||||
use SilverStripe\Dev\SapphireTest;
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
|
||||||
class DBYearTest extends SapphireTest
|
class DBYearTest extends SapphireTest
|
||||||
{
|
{
|
||||||
@ -18,15 +19,15 @@ class DBYearTest extends SapphireTest
|
|||||||
$field = $year->scaffoldFormField("YearTest");
|
$field = $year->scaffoldFormField("YearTest");
|
||||||
$this->assertEquals(DropdownField::class, get_class($field));
|
$this->assertEquals(DropdownField::class, get_class($field));
|
||||||
|
|
||||||
//This should be a list of years from the current one, counting down to 1900
|
//This should be a list of years from the current one, counting down to 1901
|
||||||
$source = $field->getSource();
|
$source = $field->getSource();
|
||||||
|
|
||||||
$lastValue = end($source);
|
$lastValue = end($source);
|
||||||
$lastKey = key($source ?? []);
|
$lastKey = key($source ?? []);
|
||||||
|
|
||||||
//Keys and values should be the same - and the last one should be 1900
|
//Keys and values should be the same - and the last one should be 1901
|
||||||
$this->assertEquals(1900, $lastValue);
|
$this->assertEquals(1901, $lastValue);
|
||||||
$this->assertEquals(1900, $lastKey);
|
$this->assertEquals(1901, $lastKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testScaffoldFormFieldLast()
|
public function testScaffoldFormFieldLast()
|
||||||
@ -43,4 +44,98 @@ class DBYearTest extends SapphireTest
|
|||||||
$this->assertEquals($currentYear, $firstValue);
|
$this->assertEquals($currentYear, $firstValue);
|
||||||
$this->assertEquals($currentYear, $firstKey);
|
$this->assertEquals($currentYear, $firstKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function provideSetValue(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'4-int' => [
|
||||||
|
'value' => 2024,
|
||||||
|
'expected' => 2024,
|
||||||
|
],
|
||||||
|
'2-int' => [
|
||||||
|
'value' => 24,
|
||||||
|
'expected' => 2024,
|
||||||
|
],
|
||||||
|
'0-int' => [
|
||||||
|
'value' => 0,
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'4-string' => [
|
||||||
|
'value' => '2024',
|
||||||
|
'expected' => 2024,
|
||||||
|
],
|
||||||
|
'2-string' => [
|
||||||
|
'value' => '24',
|
||||||
|
'expected' => 2024,
|
||||||
|
],
|
||||||
|
'0-string' => [
|
||||||
|
'value' => '0',
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'00-string' => [
|
||||||
|
'value' => '00',
|
||||||
|
'expected' => 2000,
|
||||||
|
],
|
||||||
|
'0000-string' => [
|
||||||
|
'value' => '0000',
|
||||||
|
'expected' => 0,
|
||||||
|
],
|
||||||
|
'4-int-low' => [
|
||||||
|
'value' => 1900,
|
||||||
|
'expected' => 1900,
|
||||||
|
],
|
||||||
|
'4-int-low' => [
|
||||||
|
'value' => 2156,
|
||||||
|
'expected' => 2156,
|
||||||
|
],
|
||||||
|
'4-string-low' => [
|
||||||
|
'value' => '1900',
|
||||||
|
'expected' => 1900,
|
||||||
|
],
|
||||||
|
'4-string-low' => [
|
||||||
|
'value' => '2156',
|
||||||
|
'expected' => 2156,
|
||||||
|
],
|
||||||
|
'int-negative' => [
|
||||||
|
'value' => -2024,
|
||||||
|
'expected' => -2024,
|
||||||
|
],
|
||||||
|
'string-negative' => [
|
||||||
|
'value' => '-2024',
|
||||||
|
'expected' => '-2024',
|
||||||
|
],
|
||||||
|
'float' => [
|
||||||
|
'value' => 2024.0,
|
||||||
|
'expected' => 2024.0,
|
||||||
|
],
|
||||||
|
'string-float' => [
|
||||||
|
'value' => '2024.0',
|
||||||
|
'expected' => '2024.0',
|
||||||
|
],
|
||||||
|
'null' => [
|
||||||
|
'value' => null,
|
||||||
|
'expected' => null,
|
||||||
|
],
|
||||||
|
'true' => [
|
||||||
|
'value' => true,
|
||||||
|
'expected' => true,
|
||||||
|
],
|
||||||
|
'false' => [
|
||||||
|
'value' => false,
|
||||||
|
'expected' => false,
|
||||||
|
],
|
||||||
|
'array' => [
|
||||||
|
'value' => [],
|
||||||
|
'expected' => [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('provideSetValue')]
|
||||||
|
public function testSetValue(mixed $value, mixed $expected): void
|
||||||
|
{
|
||||||
|
$field = new DBYear('MyField');
|
||||||
|
$result = $field->setValue($value);
|
||||||
|
$this->assertSame($expected, $field->getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class MySQLiConnectorTest extends SapphireTest implements TestOnly
|
|||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$config = DB::getConfig();
|
$config = DB::getConfig(DB::CONN_PRIMARY);
|
||||||
|
|
||||||
if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
|
if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
|
||||||
$this->markTestSkipped("The test only relevant for MySQL - but $config[type] is in use");
|
$this->markTestSkipped("The test only relevant for MySQL - but $config[type] is in use");
|
||||||
|
Loading…
Reference in New Issue
Block a user