mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Compare commits
4 Commits
a213fe2a08
...
d30ee1c702
Author | SHA1 | Date | |
---|---|---|---|
|
d30ee1c702 | ||
|
f83f56eba1 | ||
|
f5ef850085 | ||
|
33929e2992 |
4
bin/sake
4
bin/sake
@ -2,6 +2,7 @@
|
||||
<?php
|
||||
|
||||
use SilverStripe\Cli\Sake;
|
||||
use SilverStripe\ORM\DB;
|
||||
|
||||
// Ensure that people can't access this from a web-server
|
||||
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';
|
||||
|
||||
// CLI scripts must only use the primary database connection and not replicas
|
||||
DB::setMustUsePrimary();
|
||||
|
||||
$sake = new Sake();
|
||||
$sake->run();
|
||||
|
@ -17,6 +17,7 @@ use SilverStripe\Versioned\Versioned;
|
||||
use SilverStripe\View\Requirements;
|
||||
use SilverStripe\View\Requirements_Backend;
|
||||
use SilverStripe\View\TemplateGlobalProvider;
|
||||
use SilverStripe\ORM\DB;
|
||||
|
||||
/**
|
||||
* 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`';
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
}
|
||||
@ -296,6 +305,18 @@ class Director implements TemplateGlobalProvider
|
||||
{
|
||||
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');
|
||||
|
||||
$this->extend('updateRules', $rules);
|
||||
|
@ -85,7 +85,7 @@ class ClassInfo implements Flushable
|
||||
public static function hasTable($tableName)
|
||||
{
|
||||
$cache = ClassInfo::getCache();
|
||||
$configData = serialize(DB::getConfig());
|
||||
$configData = serialize(DB::getConfig(DB::CONN_PRIMARY));
|
||||
$cacheKey = 'tableList_' . md5($configData);
|
||||
$tableList = $cache->get($cacheKey) ?? [];
|
||||
if (empty($tableList) && DB::is_active()) {
|
||||
|
@ -7,12 +7,14 @@ use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
|
||||
use SilverStripe\ORM\Connect\NullDatabase;
|
||||
use SilverStripe\ORM\DB;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Simple Kernel container
|
||||
*/
|
||||
class CoreKernel extends BaseKernel
|
||||
{
|
||||
|
||||
protected bool $bootDatabase = true;
|
||||
|
||||
/**
|
||||
@ -38,7 +40,7 @@ class CoreKernel extends BaseKernel
|
||||
$this->flush = $flush;
|
||||
|
||||
if (!$this->bootDatabase) {
|
||||
DB::set_conn(new NullDatabase());
|
||||
DB::set_conn(new NullDatabase(), DB::CONN_PRIMARY);
|
||||
}
|
||||
|
||||
$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()
|
||||
{
|
||||
@ -84,41 +86,62 @@ class CoreKernel extends BaseKernel
|
||||
global $databaseConfig;
|
||||
global $database;
|
||||
|
||||
// Case 1: $databaseConfig global exists. Merge $database in as needed
|
||||
if (!empty($databaseConfig)) {
|
||||
if (!empty($database)) {
|
||||
$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;
|
||||
}
|
||||
// Ensure global database config has prefix and suffix applied
|
||||
if (!empty($databaseConfig) && !empty($database)) {
|
||||
$databaseConfig['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
||||
}
|
||||
|
||||
// Case 2: $database merged into existing config
|
||||
if (!empty($database)) {
|
||||
$existing = DB::getConfig();
|
||||
$existing['database'] = $this->getDatabasePrefix() . $database . $this->getDatabaseSuffix();
|
||||
// Set config for primary and any replicas
|
||||
for ($i = 0; $i <= DB::MAX_REPLICAS; $i++) {
|
||||
if ($i === 0) {
|
||||
$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()
|
||||
{
|
||||
if (!$this->bootDatabase) {
|
||||
return;
|
||||
}
|
||||
// Set default database config
|
||||
// Set primary database config
|
||||
$databaseConfig = $this->getDatabaseConfig();
|
||||
$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
|
||||
*/
|
||||
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 = [
|
||||
"type" => Environment::getEnv('SS_DATABASE_CLASS') ?: 'MySQLDatabase',
|
||||
"server" => Environment::getEnv('SS_DATABASE_SERVER') ?: 'localhost',
|
||||
"username" => Environment::getEnv('SS_DATABASE_USERNAME') ?: null,
|
||||
"password" => Environment::getEnv('SS_DATABASE_PASSWORD') ?: null,
|
||||
"type" => $this->getDatabaseConfigVariable('SS_DATABASE_CLASS', $replica) ?: 'MySQLDatabase',
|
||||
"server" => $this->getDatabaseConfigVariable('SS_DATABASE_SERVER', $replica) ?: 'localhost',
|
||||
"username" => $this->getDatabaseConfigVariable('SS_DATABASE_USERNAME', $replica) ?: 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
|
||||
@ -143,7 +226,7 @@ class CoreKernel extends BaseKernel
|
||||
'ssl_cipher' => 'SS_DATABASE_SSL_CIPHER',
|
||||
];
|
||||
foreach ($sslConf as $key => $envVar) {
|
||||
$envValue = Environment::getEnv($envVar);
|
||||
$envValue = $this->getDatabaseConfigVariable($envVar, $replica);
|
||||
if ($envValue) {
|
||||
$databaseConfig[$key] = $envValue;
|
||||
}
|
||||
@ -159,25 +242,25 @@ class CoreKernel extends BaseKernel
|
||||
}
|
||||
|
||||
// Set the port if called for
|
||||
$dbPort = Environment::getEnv('SS_DATABASE_PORT');
|
||||
$dbPort = $this->getDatabaseConfigVariable('SS_DATABASE_PORT', $replica);
|
||||
if ($dbPort) {
|
||||
$databaseConfig['port'] = $dbPort;
|
||||
}
|
||||
|
||||
// Set the timezone if called for
|
||||
$dbTZ = Environment::getEnv('SS_DATABASE_TIMEZONE');
|
||||
$dbTZ = $this->getDatabaseConfigVariable('SS_DATABASE_TIMEZONE', $replica);
|
||||
if ($dbTZ) {
|
||||
$databaseConfig['timezone'] = $dbTZ;
|
||||
}
|
||||
|
||||
// For schema enabled drivers:
|
||||
$dbSchema = Environment::getEnv('SS_DATABASE_SCHEMA');
|
||||
$dbSchema = $this->getDatabaseConfigVariable('SS_DATABASE_SCHEMA', $replica);
|
||||
if ($dbSchema) {
|
||||
$databaseConfig["schema"] = $dbSchema;
|
||||
}
|
||||
|
||||
// For SQlite3 memory databases (mainly for testing purposes)
|
||||
$dbMemory = Environment::getEnv('SS_DATABASE_MEMORY');
|
||||
$dbMemory = $this->getDatabaseConfigVariable('SS_DATABASE_MEMORY', $replica);
|
||||
if ($dbMemory) {
|
||||
$databaseConfig["memory"] = $dbMemory;
|
||||
}
|
||||
@ -205,6 +288,7 @@ class CoreKernel extends BaseKernel
|
||||
|
||||
/**
|
||||
* Get name of database
|
||||
* Note that any replicas must have the same database name as the primary database
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
@ -9,12 +9,12 @@ 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
|
||||
class BooleanFieldValidator extends FieldValidator
|
||||
{
|
||||
protected function validateValue(): ValidationResult
|
||||
{
|
||||
$result = ValidationResult::create();
|
||||
if ($this->value !== 1 && $this->value !== 0) {
|
||||
if ($this->value !== true && $this->value !== false) {
|
||||
$message = _t(__CLASS__ . '.INVALID', 'Invalid value');
|
||||
$result->addFieldError($this->name, $message, value: $this->value);
|
||||
}
|
@ -18,6 +18,10 @@ class EnumFieldValidator extends FieldValidator
|
||||
protected function validateValue(): ValidationResult
|
||||
{
|
||||
$result = ValidationResult::create();
|
||||
// Allow empty strings
|
||||
if ($this->value === '') {
|
||||
return $result;
|
||||
}
|
||||
if (!in_array($this->value, $this->allowedValues, true)) {
|
||||
$message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value');
|
||||
$result->addFieldError($this->name, $message, value: $this->value);
|
||||
|
42
src/Core/Validation/FieldValidation/YearFieldValidator.php
Normal file
42
src/Core/Validation/FieldValidation/YearFieldValidator.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Validation\FieldValidation;
|
||||
|
||||
use SilverStripe\Core\Validation\ValidationResult;
|
||||
use SilverStripe\Core\Validation\FieldValidation\NumericFieldValidator;
|
||||
|
||||
/**
|
||||
* Validates that a field is an integer year between two dates, or 0 for a null value.
|
||||
*/
|
||||
class YearFieldValidator extends IntFieldValidator
|
||||
{
|
||||
private ?int $minValue;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
mixed $value,
|
||||
?int $minValue = null,
|
||||
?int $maxValue = null
|
||||
) {
|
||||
$this->minValue = $minValue;
|
||||
parent::__construct($name, $value, 0, $maxValue);
|
||||
}
|
||||
|
||||
protected function validateValue(): ValidationResult
|
||||
{
|
||||
$result = parent::validateValue();
|
||||
if ($this->value === 0) {
|
||||
return $result;
|
||||
}
|
||||
if ($this->minValue && $this->value < $this->minValue) {
|
||||
// Uses the same translation key as NumericFieldValidator
|
||||
$message = _t(
|
||||
NumericFieldValidator::class . '.TOOSMALL',
|
||||
'Value cannot be less than {minValue}',
|
||||
['minValue' => $this->minValue]
|
||||
);
|
||||
$result->addFieldError($this->name, $message, value: $this->value);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ use ReflectionClass;
|
||||
use SilverStripe\Dev\Exceptions\ExpectedErrorException;
|
||||
use SilverStripe\Dev\Exceptions\ExpectedNoticeException;
|
||||
use SilverStripe\Dev\Exceptions\ExpectedWarningException;
|
||||
use SilverStripe\Dev\Exceptions\UnexpectedErrorException;
|
||||
use SilverStripe\ORM\DB;
|
||||
|
||||
/**
|
||||
* Test case class for the Silverstripe framework.
|
||||
@ -434,6 +434,9 @@ abstract class SapphireTest extends TestCase implements TestOnly
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
// Disallow the use of DB replicas in tests
|
||||
DB::setMustUsePrimary();
|
||||
|
||||
// Start tests
|
||||
static::start();
|
||||
|
||||
|
@ -2,17 +2,14 @@
|
||||
|
||||
namespace SilverStripe\Forms;
|
||||
|
||||
use SilverStripe\Core\Validation\FieldValidation\EmailFieldValidator;
|
||||
use SilverStripe\Core\Validation\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Constraints\Email as EmailConstraint;
|
||||
|
||||
/**
|
||||
* Text input field with validation for correct email format according to the relevant RFC.
|
||||
*/
|
||||
class EmailField extends TextField
|
||||
{
|
||||
private static array $field_validators = [
|
||||
EmailFieldValidator::class,
|
||||
];
|
||||
|
||||
protected $inputType = 'email';
|
||||
|
||||
public function Type()
|
||||
@ -20,6 +17,27 @@ class EmailField extends TextField
|
||||
return 'email text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates for RFC compliant email addresses.
|
||||
*
|
||||
* @param Validator $validator
|
||||
*/
|
||||
public function validate($validator)
|
||||
{
|
||||
$this->value = trim($this->value ?? '');
|
||||
|
||||
$message = _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address');
|
||||
$result = ConstraintValidator::validate(
|
||||
$this->value,
|
||||
new EmailConstraint(message: $message, mode: EmailConstraint::VALIDATION_MODE_STRICT),
|
||||
$this->getName()
|
||||
);
|
||||
$validator->getResult()->combineAnd($result);
|
||||
$isValid = $result->isValid();
|
||||
|
||||
return $this->extendValidationResult($isValid, $validator);
|
||||
}
|
||||
|
||||
public function getSchemaValidation()
|
||||
{
|
||||
$rules = parent::getSchemaValidation();
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
namespace SilverStripe\Forms;
|
||||
|
||||
use SilverStripe\Core\Validation\FieldValidation\StringFieldValidator;
|
||||
|
||||
/**
|
||||
* Text input field.
|
||||
*/
|
||||
@ -16,10 +14,6 @@ class TextField extends FormField implements TippableFieldInterface
|
||||
|
||||
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
|
||||
*/
|
||||
@ -49,14 +43,6 @@ class TextField extends FormField implements TippableFieldInterface
|
||||
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
|
||||
* @return $this
|
||||
|
@ -53,12 +53,12 @@ class UrlField extends TextField
|
||||
}
|
||||
|
||||
/**
|
||||
* Set which protocols valid URLs are allowed to have
|
||||
* Set which protocols valid URLs are allowed to have.
|
||||
* Passing an empty array will result in using configured defaults.
|
||||
*/
|
||||
public function setAllowedProtocols(array $protocols): static
|
||||
{
|
||||
// Ensure the array isn't associative so we can use 0 index in validate().
|
||||
$this->protocols = array_keys($protocols);
|
||||
$this->protocols = $protocols;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -67,10 +67,12 @@ class UrlField extends TextField
|
||||
*/
|
||||
public function getAllowedProtocols(): array
|
||||
{
|
||||
if (empty($this->protocols)) {
|
||||
return static::config()->get('default_protocols');
|
||||
$protocols = $this->protocols;
|
||||
if (empty($protocols)) {
|
||||
$protocols = static::config()->get('default_protocols');
|
||||
}
|
||||
return $this->protocols;
|
||||
// Ensure the array isn't associative so we can use 0 index in validate().
|
||||
return array_values($protocols);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,7 +43,7 @@ class TempDatabase
|
||||
*
|
||||
* @param string $name DB Connection name to use
|
||||
*/
|
||||
public function __construct($name = 'default')
|
||||
public function __construct($name = DB::CONN_PRIMARY)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
233
src/ORM/DB.php
233
src/ORM/DB.php
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\ORM;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RunTimeException;
|
||||
use SilverStripe\Control\Director;
|
||||
use SilverStripe\Control\HTTPRequest;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
@ -21,6 +22,22 @@ use SilverStripe\ORM\Queries\SQLExpression;
|
||||
*/
|
||||
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
|
||||
@ -58,19 +75,47 @@ class DB
|
||||
*/
|
||||
protected static $configs = [];
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The last SQL query run.
|
||||
* @var string
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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.
|
||||
* 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 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
|
||||
* 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;
|
||||
}
|
||||
@ -92,11 +137,17 @@ class DB
|
||||
*
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* 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])) {
|
||||
DB::$lastConnectionName = $name;
|
||||
return DB::$connections[$name];
|
||||
}
|
||||
|
||||
@ -109,14 +160,50 @@ class DB
|
||||
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
|
||||
*
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* the default connection is returned.
|
||||
* @return DBSchemaManager
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||
* If omitted, a dynamic connection is returned.
|
||||
* @return DBSchemaManager|null
|
||||
*/
|
||||
public static function get_schema($name = 'default')
|
||||
public static function get_schema($name = DB::CONN_DYNAMIC)
|
||||
{
|
||||
$connection = DB::get_conn($name);
|
||||
if ($connection) {
|
||||
@ -130,11 +217,11 @@ class DB
|
||||
*
|
||||
* @param SQLExpression $expression The expression object to build from
|
||||
* @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,
|
||||
* the default connection is returned.
|
||||
* @return string The resulting SQL as a string
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||
* If omitted, a dynamic connection is returned.
|
||||
* @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);
|
||||
if ($connection) {
|
||||
@ -148,11 +235,11 @@ class DB
|
||||
/**
|
||||
* 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,
|
||||
* the default connection is returned.
|
||||
* @return DBConnector
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call.
|
||||
* If omitted, a dynamic connection is returned.
|
||||
* @return DBConnector|null
|
||||
*/
|
||||
public static function get_connector($name = 'default')
|
||||
public static function get_connector($name = DB::CONN_DYNAMIC)
|
||||
{
|
||||
$connection = DB::get_conn($name);
|
||||
if ($connection) {
|
||||
@ -268,8 +355,13 @@ class DB
|
||||
* @param string $label identifier for the connection
|
||||
* @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
|
||||
if ($name = DB::get_alternative_database_name()) {
|
||||
$databaseConfig['database'] = $name;
|
||||
@ -288,6 +380,7 @@ class DB
|
||||
$conn = Injector::inst()->create($dbClass);
|
||||
DB::set_conn($conn, $label);
|
||||
$conn->connect($databaseConfig);
|
||||
DB::$lastConnectionName = $label;
|
||||
|
||||
return $conn;
|
||||
}
|
||||
@ -298,7 +391,7 @@ class DB
|
||||
* @param array $databaseConfig
|
||||
* @param string $name
|
||||
*/
|
||||
public static function setConfig($databaseConfig, $name = 'default')
|
||||
public static function setConfig($databaseConfig, $name = DB::CONN_PRIMARY)
|
||||
{
|
||||
static::$configs[$name] = $databaseConfig;
|
||||
}
|
||||
@ -309,13 +402,42 @@ class DB
|
||||
* @param string $name
|
||||
* @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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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)
|
||||
{
|
||||
DB::$lastQuery = $sql;
|
||||
|
||||
return DB::get_conn()->query($sql, $errorLevel);
|
||||
$name = DB::getDynamicConnectionName($sql);
|
||||
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)
|
||||
{
|
||||
DB::$lastQuery = $sql;
|
||||
|
||||
return DB::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
|
||||
$name = DB::getDynamicConnectionName($sql);
|
||||
return DB::get_conn($name)->preparedQuery($sql, $parameters, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -680,4 +802,63 @@ class DB
|
||||
{
|
||||
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 = [])
|
||||
{
|
||||
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
|
||||
{
|
||||
$query = $this->dataQuery->query()->execute();
|
||||
$query = $this->dataQuery->execute();
|
||||
$this->fetchEagerLoadRelations($query);
|
||||
return $query;
|
||||
}
|
||||
|
@ -145,6 +145,14 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -10,6 +10,7 @@ use SilverStripe\ORM\Connect\Query;
|
||||
use SilverStripe\ORM\Queries\SQLConditionGroup;
|
||||
use SilverStripe\ORM\Queries\SQLSelect;
|
||||
use InvalidArgumentException;
|
||||
use SilverStripe\Core\Config\Config;
|
||||
|
||||
/**
|
||||
* An object representing a query of data from the DataObject's supporting database.
|
||||
@ -449,7 +450,9 @@ class DataQuery
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
return $this->getFinalisedQuery()->execute();
|
||||
return $this->withCorrectDatabase(
|
||||
fn() => $this->getFinalisedQuery()->execute()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -472,7 +475,9 @@ class DataQuery
|
||||
public function count()
|
||||
{
|
||||
$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"
|
||||
$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();
|
||||
$result = reset($row);
|
||||
|
||||
@ -582,7 +589,9 @@ class DataQuery
|
||||
*/
|
||||
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()
|
||||
{
|
||||
return $this->getFinalisedQuery()->firstRow();
|
||||
return $this->withCorrectDatabase(
|
||||
fn() => $this->getFinalisedQuery()->firstRow()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -604,7 +615,9 @@ class DataQuery
|
||||
*/
|
||||
public function lastRow()
|
||||
{
|
||||
return $this->getFinalisedQuery()->lastRow();
|
||||
return $this->withCorrectDatabase(
|
||||
fn() => $this->getFinalisedQuery()->lastRow()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1344,7 +1357,9 @@ class DataQuery
|
||||
$query->selectField($fieldExpression, $field);
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,7 +2,7 @@
|
||||
|
||||
namespace SilverStripe\ORM\FieldType;
|
||||
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanFieldValidator;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use SilverStripe\Forms\DropdownField;
|
||||
use SilverStripe\Forms\FormField;
|
||||
@ -11,17 +11,17 @@ use SilverStripe\Model\ModelData;
|
||||
|
||||
/**
|
||||
* Represents a boolean field
|
||||
* Values are stored as a tinyint i.e. 1 or 0 and NOT as true or false
|
||||
* Values are stored in the database as tinyint i.e. 1 or 0
|
||||
*/
|
||||
class DBBoolean extends DBField
|
||||
{
|
||||
private static array $field_validators = [
|
||||
BooleanIntFieldValidator::class,
|
||||
BooleanFieldValidator::class,
|
||||
];
|
||||
|
||||
public function __construct(?string $name = null, bool|int $defaultVal = 0)
|
||||
public function __construct(?string $name = null, bool $defaultVal = false)
|
||||
{
|
||||
$this->setDefaultValue($defaultVal ? 1 : 0);
|
||||
$this->setDefaultValue($defaultVal);
|
||||
|
||||
parent::__construct($name);
|
||||
}
|
||||
@ -43,7 +43,7 @@ class DBBoolean extends DBField
|
||||
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||
{
|
||||
parent::setValue($value);
|
||||
$this->value = $this->convertBooleanLikeValueToTinyInt($value);
|
||||
$this->value = $this->convertBooleanLikeValue($value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ class DBBoolean extends DBField
|
||||
if ($this->value instanceof DBField) {
|
||||
$this->value->saveInto($dataObject);
|
||||
} else {
|
||||
$dataObject->__set($fieldName, $this->value ? 1 : 0);
|
||||
$dataObject->__set($fieldName, $this->value ? true : false);
|
||||
}
|
||||
} else {
|
||||
$class = static::class;
|
||||
@ -82,8 +82,8 @@ class DBBoolean extends DBField
|
||||
$anyText = _t(__CLASS__ . '.ANY', 'Any');
|
||||
$source = [
|
||||
'' => $anyText,
|
||||
1 => _t(__CLASS__ . '.YESANSWER', 'Yes'),
|
||||
0 => _t(__CLASS__ . '.NOANSWER', 'No')
|
||||
'1' => _t(__CLASS__ . '.YESANSWER', 'Yes'),
|
||||
'0' => _t(__CLASS__ . '.NOANSWER', 'No')
|
||||
];
|
||||
|
||||
return DropdownField::create($this->name, $title, $source)
|
||||
@ -97,33 +97,35 @@ class DBBoolean extends DBField
|
||||
|
||||
public function prepValueForDB(mixed $value): array|int|null
|
||||
{
|
||||
$ret = $this->convertBooleanLikeValueToTinyInt($value);
|
||||
$bool = $this->convertBooleanLikeValue($value);
|
||||
// Ensure a tiny int is returned no matter what e.g. value is an
|
||||
return $ret ? 1 : 0;
|
||||
return $bool ? 1 : 0;
|
||||
}
|
||||
|
||||
private function convertBooleanLikeValueToTinyInt(mixed $value): mixed
|
||||
/**
|
||||
* Convert boolean-like values to boolean
|
||||
* Does not convert non-boolean-like values e.g. array - will be handled by the FieldValidator
|
||||
*/
|
||||
private function convertBooleanLikeValue(mixed $value): mixed
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value ? 1 : 0;
|
||||
}
|
||||
if (empty($value)) {
|
||||
return 0;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
switch (strtolower($value ?? '')) {
|
||||
switch (strtolower($value)) {
|
||||
case 'false':
|
||||
case 'f':
|
||||
case '0':
|
||||
return 0;
|
||||
return false;
|
||||
case 'true':
|
||||
case 't':
|
||||
case '1':
|
||||
return 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Note that something like "lorem" will NOT be converted to 1
|
||||
// instead it will throw a ValidationException in BooleanIntFieldValidator
|
||||
if ($value === 0) {
|
||||
return false;
|
||||
}
|
||||
if ($value === 1) {
|
||||
return true;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class DBDate extends DBField
|
||||
* @param mixed $value
|
||||
* @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): mixed
|
||||
{
|
||||
// Determine value to parse
|
||||
if (is_array($value)) {
|
||||
@ -73,11 +73,16 @@ class DBDate extends DBField
|
||||
} else {
|
||||
// Convert US date -> iso, fix y2k, etc
|
||||
$fixedValue = $this->fixInputDate($value);
|
||||
if ($fixedValue === '') {
|
||||
// Dates with an invalid format will be caught by validator later
|
||||
return $value;
|
||||
}
|
||||
// convert string to timestamp
|
||||
$source = strtotime($fixedValue ?? '');
|
||||
}
|
||||
if (!$source) {
|
||||
if (!$source && $source !== 0 and $source !== '0') {
|
||||
// Unable to parse date, keep as is so that the validator can catch it later
|
||||
// Note that 0 and '0' are valid dates for Jan 1 1970
|
||||
return $value;
|
||||
}
|
||||
// Format as iso8601
|
||||
@ -551,16 +556,12 @@ class DBDate extends DBField
|
||||
|
||||
/**
|
||||
* Fix non-iso dates
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected function fixInputDate($value)
|
||||
protected function fixInputDate(string $value): string
|
||||
{
|
||||
[$year, $month, $day, $time] = $this->explodeDateString($value);
|
||||
if (!checkdate((int) $month, (int) $day, (int) $year)) {
|
||||
// Keep invalid dates as they are so that the validator can catch them later
|
||||
return $value;
|
||||
return '';
|
||||
}
|
||||
// Convert to Y-m-d
|
||||
return sprintf('%d-%02d-%02d%s', $year, $month, $day, $time);
|
||||
|
@ -82,6 +82,16 @@ class DBDecimal extends DBField
|
||||
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, $record, $markChanged);
|
||||
// Cast ints and numeric strings to floats
|
||||
if (is_int($this->value) || (is_string($this->value) && is_numeric($this->value))) {
|
||||
$this->value = (float) $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function saveInto(ModelData $model): void
|
||||
{
|
||||
$fieldName = $this->name;
|
||||
|
@ -176,6 +176,11 @@ class DBEnum extends DBString
|
||||
return $this->enum;
|
||||
}
|
||||
|
||||
public function nullValue(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of enum values, including obsolete values still present in the database
|
||||
*
|
||||
@ -248,13 +253,4 @@ class DBEnum extends DBString
|
||||
$this->setDefaultValue($default);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\ORM\FieldType;
|
||||
|
||||
use SilverStripe\Model\ModelData;
|
||||
use SilverStripe\Core\Validation\FieldValidation\DecimalFieldValidator;
|
||||
|
||||
/**
|
||||
* Represents a decimal field from 0-1 containing a percentage value.
|
||||
@ -17,6 +18,10 @@ use SilverStripe\Model\ModelData;
|
||||
*/
|
||||
class DBPercentage extends DBDecimal
|
||||
{
|
||||
private static array $field_validators = [
|
||||
DecimalFieldValidator::class => ['getWholeSize', 'getDecimalSize', 'getMinValue', 'getMaxValue'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new Decimal field.
|
||||
*/
|
||||
@ -29,6 +34,16 @@ class DBPercentage extends DBDecimal
|
||||
parent::__construct($name, $precision + 1, $precision);
|
||||
}
|
||||
|
||||
public function getMinValue(): float
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public function getMaxValue(): float
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number, expressed as a percentage. For example, “36.30%”
|
||||
*/
|
||||
|
@ -90,14 +90,9 @@ abstract class DBString extends DBField
|
||||
return $value || (is_string($value) && strlen($value ?? ''));
|
||||
}
|
||||
|
||||
public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static
|
||||
public function nullValue(): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
$this->value = '';
|
||||
} else {
|
||||
$this->value = $value;
|
||||
}
|
||||
return $this;
|
||||
return '';
|
||||
}
|
||||
|
||||
public function prepValueForDB(mixed $value): array|string|null
|
||||
|
@ -6,7 +6,7 @@ use SilverStripe\Forms\DropdownField;
|
||||
use SilverStripe\Forms\FormField;
|
||||
use SilverStripe\ORM\DB;
|
||||
use SilverStripe\Model\ModelData;
|
||||
use SilverStripe\Core\Validation\FieldValidation\IntFieldValidator;
|
||||
use SilverStripe\Core\Validation\FieldValidation\YearFieldValidator;
|
||||
|
||||
/**
|
||||
* Represents a single year field
|
||||
@ -15,11 +15,11 @@ 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;
|
||||
public const MIN_YEAR = 1901;
|
||||
public const MAX_YEAR = 2155;
|
||||
|
||||
private static $field_validators = [
|
||||
IntFieldValidator::class => ['getMinYear', 'getMaxYear'],
|
||||
YearFieldValidator::class => ['getMinYear', 'getMaxYear'],
|
||||
];
|
||||
|
||||
public function requireField(): void
|
||||
|
@ -88,6 +88,8 @@ class Group extends DataObject
|
||||
|
||||
private static $table_name = "Group";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
private static $indexes = [
|
||||
'Title' => true,
|
||||
'Code' => true,
|
||||
|
@ -50,6 +50,8 @@ class LoginAttempt extends DataObject
|
||||
|
||||
private static $table_name = "LoginAttempt";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
/**
|
||||
* @param bool $includerelations Indicate if the labels returned include relation fields
|
||||
* @return array
|
||||
|
@ -105,6 +105,8 @@ class Member extends DataObject
|
||||
|
||||
private static $table_name = "Member";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
private static $default_sort = '"Surname", "FirstName"';
|
||||
|
||||
private static $indexes = [
|
||||
|
@ -27,6 +27,8 @@ class MemberPassword extends DataObject
|
||||
|
||||
private static $table_name = "MemberPassword";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
/**
|
||||
* Log a password change from the given member.
|
||||
* 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 bool $must_use_primary_db = true;
|
||||
|
||||
/**
|
||||
* This is the value to use for the "Type" field if a permission should be
|
||||
* granted.
|
||||
@ -233,15 +235,15 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
}
|
||||
|
||||
// Raw SQL for efficiency
|
||||
$permission = DB::prepared_query(
|
||||
$permission = DB::withPrimary(fn() => DB::prepared_query(
|
||||
"SELECT \"ID\"
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" IN ($codeClause $adminClause)
|
||||
AND \"Type\" = ?
|
||||
AND \"GroupID\" IN ($groupClause)
|
||||
$argClause
|
||||
)",
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" IN ($codeClause $adminClause)
|
||||
AND \"Type\" = ?
|
||||
AND \"GroupID\" IN ($groupClause)
|
||||
$argClause
|
||||
)",
|
||||
array_merge(
|
||||
$codeParams,
|
||||
$adminParams,
|
||||
@ -249,7 +251,7 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
$groupParams,
|
||||
$argParams
|
||||
)
|
||||
)->value();
|
||||
)->value());
|
||||
|
||||
if ($permission) {
|
||||
return $permission;
|
||||
@ -257,15 +259,15 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
|
||||
// Strict checking disabled?
|
||||
if (!static::config()->strict_checking || !$strict) {
|
||||
$hasPermission = DB::prepared_query(
|
||||
$hasPermission = DB::withPrimary(fn() => DB::prepared_query(
|
||||
"SELECT COUNT(*)
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" IN ($codeClause) AND
|
||||
\"Type\" = ?
|
||||
)",
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" IN ($codeClause) AND
|
||||
\"Type\" = ?
|
||||
)",
|
||||
array_merge($codeParams, [Permission::GRANT_PERMISSION])
|
||||
)->value();
|
||||
)->value());
|
||||
|
||||
if (!$hasPermission) {
|
||||
return false;
|
||||
@ -288,25 +290,29 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
if ($groupList) {
|
||||
$groupCSV = implode(", ", $groupList);
|
||||
|
||||
$allowed = array_unique(DB::query("
|
||||
SELECT \"Code\"
|
||||
FROM \"Permission\"
|
||||
WHERE \"Type\" = " . Permission::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||
$allowed = array_unique(
|
||||
DB::withPrimary(fn() => DB::query("
|
||||
SELECT \"Code\"
|
||||
FROM \"Permission\"
|
||||
WHERE \"Type\" = " . Permission::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||
|
||||
UNION
|
||||
UNION
|
||||
|
||||
SELECT \"Code\"
|
||||
FROM \"PermissionRoleCode\" PRC
|
||||
INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
|
||||
INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
|
||||
WHERE \"GroupID\" IN ($groupCSV)
|
||||
")->column() ?? []);
|
||||
SELECT \"Code\"
|
||||
FROM \"PermissionRoleCode\" PRC
|
||||
INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
|
||||
INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
|
||||
WHERE \"GroupID\" IN ($groupCSV)
|
||||
"))->column() ?? []
|
||||
);
|
||||
|
||||
$denied = array_unique(DB::query("
|
||||
SELECT \"Code\"
|
||||
FROM \"Permission\"
|
||||
WHERE \"Type\" = " . Permission::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||
")->column() ?? []);
|
||||
$denied = array_unique(
|
||||
DB::withPrimary(fn() => DB::query("
|
||||
SELECT \"Code\"
|
||||
FROM \"Permission\"
|
||||
WHERE \"Type\" = " . Permission::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
|
||||
"))->column() ?? []
|
||||
);
|
||||
|
||||
return array_diff($allowed ?? [], $denied);
|
||||
}
|
||||
@ -584,7 +590,9 @@ class Permission extends DataObject implements TemplateGlobalProvider, Resettabl
|
||||
$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) {
|
||||
foreach ($otherPerms as $otherPerm) {
|
||||
|
@ -40,6 +40,8 @@ class PermissionRole extends DataObject
|
||||
|
||||
private static $table_name = "PermissionRole";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
private static $default_sort = '"Title"';
|
||||
|
||||
private static $singular_name = 'Role';
|
||||
|
@ -23,6 +23,8 @@ class PermissionRoleCode extends DataObject
|
||||
];
|
||||
|
||||
private static $table_name = "PermissionRoleCode";
|
||||
|
||||
private static bool $must_use_primary_db = true;
|
||||
|
||||
private static $indexes = [
|
||||
"Code" => true,
|
||||
|
@ -44,6 +44,8 @@ class RememberLoginHash extends DataObject
|
||||
|
||||
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
|
||||
* on all other devices owned by the member.
|
||||
|
@ -437,6 +437,12 @@ class Security extends Controller implements TemplateGlobalProvider
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,10 @@ use SilverStripe\Core\Injector\Injector;
|
||||
use SilverStripe\Core\Injector\InjectorLoader;
|
||||
use SilverStripe\Core\Kernel;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\Core\Environment;
|
||||
use ReflectionClass;
|
||||
use SilverStripe\ORM\DB;
|
||||
use ReflectionObject;
|
||||
|
||||
class KernelTest extends SapphireTest
|
||||
{
|
||||
@ -81,4 +85,32 @@ class KernelTest extends SapphireTest
|
||||
|
||||
$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']);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanFieldValidator;
|
||||
|
||||
class BooleanIntFieldValidatorTest extends SapphireTest
|
||||
class BooleanFieldValidatorTest extends SapphireTest
|
||||
{
|
||||
public static function provideValidate(): array
|
||||
{
|
||||
@ -65,7 +65,7 @@ class BooleanIntFieldValidatorTest extends SapphireTest
|
||||
#[DataProvider('provideValidate')]
|
||||
public function testValidate(mixed $value, bool $expected): void
|
||||
{
|
||||
$validator = new BooleanIntFieldValidator('MyField', $value);
|
||||
$validator = new BooleanFieldValidator('MyField', $value);
|
||||
$result = $validator->validate();
|
||||
$this->assertSame($expected, $result->isValid());
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\Core\Tests\Validation\FieldValidation;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\Core\Validation\FieldValidation\YearFieldValidator;
|
||||
use SilverStripe\ORM\FieldType\DBYear;
|
||||
|
||||
class YearFieldValidatorTest extends SapphireTest
|
||||
{
|
||||
public static function provideValidate(): array
|
||||
{
|
||||
// YearFieldValidator extends IntFieldValidator so only testing a subset
|
||||
// of possible values here
|
||||
return [
|
||||
'valid-int' => [
|
||||
'value' => 2021,
|
||||
'expected' => true,
|
||||
],
|
||||
'valid-zero' => [
|
||||
'value' => 0,
|
||||
'expected' => true,
|
||||
],
|
||||
'invalid-out-of-range-low' => [
|
||||
'value' => 1850,
|
||||
'expected' => false,
|
||||
],
|
||||
'invalid-out-of-range-high' => [
|
||||
'value' => 3000,
|
||||
'expected' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideValidate')]
|
||||
public function testValidate(mixed $value, bool $expected): void
|
||||
{
|
||||
$validator = new YearFieldValidator('MyField', $value, DBYear::MIN_YEAR, DBYear::MAX_YEAR);
|
||||
$result = $validator->validate();
|
||||
$this->assertSame($expected, $result->isValid());
|
||||
}
|
||||
}
|
@ -89,4 +89,23 @@ class UrlFieldTest extends SapphireTest
|
||||
$expectedCount = $valid ? 0 : 1;
|
||||
$this->assertEquals($expectedCount, count($validator->getErrors()));
|
||||
}
|
||||
|
||||
public function testAllowedProtocols(): void
|
||||
{
|
||||
$field = new UrlField('MyUrl');
|
||||
// Defaults should be http and https
|
||||
$this->assertSame(['https', 'http'], $field->getAllowedProtocols());
|
||||
|
||||
// Defaults change with config, and ignore keys
|
||||
UrlField::config()->set('default_protocols', ['my-key' => 'ftp']);
|
||||
$this->assertSame(['ftp'], $field->getAllowedProtocols());
|
||||
|
||||
// Can set explicit protocols - again keys are ignored
|
||||
$field->setAllowedProtocols(['http', 'key' => 'irc', 'nntp']);
|
||||
$this->assertSame(['http', 'irc', 'nntp'], $field->getAllowedProtocols());
|
||||
|
||||
// Can reset back to config defaults
|
||||
$field->setAllowedProtocols([]);
|
||||
$this->assertSame(['ftp'], $field->getAllowedProtocols());
|
||||
}
|
||||
}
|
||||
|
98
tests/php/ORM/DBBooleanTest.php
Normal file
98
tests/php/ORM/DBBooleanTest.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\ORM\FieldType\DBBoolean;
|
||||
|
||||
class DBBooleanTest extends SapphireTest
|
||||
{
|
||||
public function testDefaultValue(): void
|
||||
{
|
||||
$field = new DBBoolean('MyField');
|
||||
$this->assertSame(false, $field->getValue());
|
||||
}
|
||||
|
||||
public static function provideSetValue(): array
|
||||
{
|
||||
return [
|
||||
'true' => [
|
||||
'value' => true,
|
||||
'expected' => true,
|
||||
],
|
||||
'false' => [
|
||||
'value' => false,
|
||||
'expected' => false,
|
||||
],
|
||||
'1-int' => [
|
||||
'value' => 1,
|
||||
'expected' => true,
|
||||
],
|
||||
'1-string' => [
|
||||
'value' => '1',
|
||||
'expected' => true,
|
||||
],
|
||||
'0-int' => [
|
||||
'value' => 0,
|
||||
'expected' => false,
|
||||
],
|
||||
'0-string' => [
|
||||
'value' => '0',
|
||||
'expected' => false,
|
||||
],
|
||||
't' => [
|
||||
'value' => 't',
|
||||
'expected' => true,
|
||||
],
|
||||
'f' => [
|
||||
'value' => 'f',
|
||||
'expected' => false,
|
||||
],
|
||||
'T' => [
|
||||
'value' => 'T',
|
||||
'expected' => true,
|
||||
],
|
||||
'F' => [
|
||||
'value' => 'F',
|
||||
'expected' => false,
|
||||
],
|
||||
'true-string' => [
|
||||
'value' => 'true',
|
||||
'expected' => true,
|
||||
],
|
||||
'false-string' => [
|
||||
'value' => 'false',
|
||||
'expected' => false,
|
||||
],
|
||||
'2-int' => [
|
||||
'value' => 2,
|
||||
'expected' => 2,
|
||||
],
|
||||
'0.0' => [
|
||||
'value' => 0.0,
|
||||
'expected' => 0.0,
|
||||
],
|
||||
'1.0' => [
|
||||
'value' => 1.0,
|
||||
'expected' => 1.0,
|
||||
],
|
||||
'null' => [
|
||||
'value' => null,
|
||||
'expected' => null,
|
||||
],
|
||||
'array' => [
|
||||
'value' => [],
|
||||
'expected' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideSetValue')]
|
||||
public function testSetValue(mixed $value, mixed $expected): void
|
||||
{
|
||||
$field = new DBBoolean('MyField');
|
||||
$field->setValue($value);
|
||||
$this->assertSame($expected, $field->getValue());
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ use SilverStripe\ORM\FieldType\DBDate;
|
||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||
use SilverStripe\ORM\FieldType\DBField;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\Core\Validation\ValidationException;
|
||||
|
||||
class DBDateTest extends SapphireTest
|
||||
{
|
||||
@ -91,20 +92,6 @@ class DBDateTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testMDYConversion()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage("Invalid date: '3/16/2003'. Use y-MM-dd to prevent this error.");
|
||||
DBField::create_field('Date', '3/16/2003');
|
||||
}
|
||||
|
||||
public function testY2kCorrection()
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage("Invalid date: '03-03-04'. Use y-MM-dd to prevent this error.");
|
||||
DBField::create_field('Date', '03-03-04');
|
||||
}
|
||||
|
||||
public function testInvertedYearCorrection()
|
||||
{
|
||||
// iso8601 expects year first, but support year last
|
||||
@ -194,31 +181,57 @@ class DBDateTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetNullAndZeroValues()
|
||||
public static function provideSetNullAndZeroValues()
|
||||
{
|
||||
$date = DBField::create_field('Date', '');
|
||||
$this->assertNull($date->getValue(), 'Empty string evaluates to NULL');
|
||||
return [
|
||||
'blank-string' => [
|
||||
'value' => '',
|
||||
'expected' => ''
|
||||
],
|
||||
'null' => [
|
||||
'value' => null,
|
||||
'expected' => null
|
||||
],
|
||||
'false' => [
|
||||
'value' => false,
|
||||
'expected' => false
|
||||
],
|
||||
'empty-array' => [
|
||||
'value' => [],
|
||||
'expected' => []
|
||||
],
|
||||
'zero-string' => [
|
||||
'value' => '0',
|
||||
'expected' => '1970-01-01'
|
||||
],
|
||||
'zero-int' => [
|
||||
'value' => 0,
|
||||
'expected' => '1970-01-01'
|
||||
],
|
||||
'zero-datetime' => [
|
||||
'value' => '0000-00-00 00:00:00',
|
||||
'expected' => '0000-00-00 00:00:00'
|
||||
],
|
||||
'zero-date-slashes' => [
|
||||
'value' => '00/00/0000',
|
||||
'expected' => '00/00/0000'
|
||||
],
|
||||
'wrong-format-a' => [
|
||||
'value' => '3/16/2003',
|
||||
'expected' => '3/16/2003',
|
||||
],
|
||||
'wrong-format-b' => [
|
||||
'value' => '03-03-04',
|
||||
'expected' => '2003-03-04',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$date = DBField::create_field('Date', null);
|
||||
$this->assertNull($date->getValue(), 'NULL is set as NULL');
|
||||
|
||||
$date = DBField::create_field('Date', false);
|
||||
$this->assertNull($date->getValue(), 'Boolean FALSE evaluates to NULL');
|
||||
|
||||
$date = DBField::create_field('Date', []);
|
||||
$this->assertNull($date->getValue(), 'Empty array evaluates to NULL');
|
||||
|
||||
$date = DBField::create_field('Date', '0');
|
||||
$this->assertEquals('1970-01-01', $date->getValue(), 'Zero is UNIX epoch date');
|
||||
|
||||
$date = DBField::create_field('Date', 0);
|
||||
$this->assertEquals('1970-01-01', $date->getValue(), 'Zero is UNIX epoch date');
|
||||
|
||||
$date = DBField::create_field('Date', '0000-00-00 00:00:00');
|
||||
$this->assertNull($date->getValue(), '0000-00-00 00:00:00 is set as NULL');
|
||||
|
||||
$date = DBField::create_field('Date', '00/00/0000');
|
||||
$this->assertNull($date->getValue(), '00/00/0000 is set as NULL');
|
||||
#[DataProvider('provideSetNullAndZeroValues')]
|
||||
public function testSetNullAndZeroValues(mixed $value, mixed $expected)
|
||||
{
|
||||
$date = DBField::create_field('Date', $value);
|
||||
$this->assertSame($expected, $date->getValue());
|
||||
}
|
||||
|
||||
public function testDayOfMonth()
|
||||
|
87
tests/php/ORM/DBDecimalTest.php
Normal file
87
tests/php/ORM/DBDecimalTest.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace SilverStripe\ORM\Tests;
|
||||
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\ORM\FieldType\DBInt;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\ORM\FieldType\DBDecimal;
|
||||
|
||||
class DBDecimalTest extends SapphireTest
|
||||
{
|
||||
public function testDefaultValue(): void
|
||||
{
|
||||
$field = new DBDecimal('MyField');
|
||||
$this->assertSame(0.0, $field->getValue());
|
||||
}
|
||||
|
||||
public static function provideSetValue(): array
|
||||
{
|
||||
return [
|
||||
'float' => [
|
||||
'value' => 9.123,
|
||||
'expected' => 9.123,
|
||||
],
|
||||
'negative-float' => [
|
||||
'value' => -9.123,
|
||||
'expected' => -9.123,
|
||||
],
|
||||
'string-float' => [
|
||||
'value' => '9.123',
|
||||
'expected' => 9.123,
|
||||
],
|
||||
'string-negative-float' => [
|
||||
'value' => '-9.123',
|
||||
'expected' => -9.123,
|
||||
],
|
||||
'zero' => [
|
||||
'value' => 0,
|
||||
'expected' => 0.0,
|
||||
],
|
||||
'int' => [
|
||||
'value' => 3,
|
||||
'expected' => 3.0,
|
||||
],
|
||||
'negative-int' => [
|
||||
'value' => -3,
|
||||
'expected' => -3.0,
|
||||
],
|
||||
'string-int' => [
|
||||
'value' => '3',
|
||||
'expected' => 3.0,
|
||||
],
|
||||
'negative-string-int' => [
|
||||
'value' => '-3',
|
||||
'expected' => -3.0,
|
||||
],
|
||||
'string' => [
|
||||
'value' => 'fish',
|
||||
'expected' => 'fish',
|
||||
],
|
||||
'array' => [
|
||||
'value' => [],
|
||||
'expected' => [],
|
||||
],
|
||||
'null' => [
|
||||
'value' => null,
|
||||
'expected' => null,
|
||||
],
|
||||
'true' => [
|
||||
'value' => true,
|
||||
'expected' => true,
|
||||
],
|
||||
'false' => [
|
||||
'value' => false,
|
||||
'expected' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('provideSetValue')]
|
||||
public function testSetValue(mixed $value, mixed $expected): void
|
||||
{
|
||||
$field = new DBDecimal('MyField');
|
||||
$field->setValue($value);
|
||||
$this->assertSame($expected, $field->getValue());
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ class DBEnumTest extends SapphireTest
|
||||
|
||||
$obj2 = new FieldType\DBEnumTestObject();
|
||||
$obj2->Colour = 'Purple';
|
||||
$obj2->write();
|
||||
$obj2->write(skipValidation: true);
|
||||
|
||||
$this->assertEquals(
|
||||
['Purple', 'Red'],
|
||||
|
@ -33,7 +33,7 @@ use SilverStripe\ORM\FieldType\DBYear;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use SilverStripe\Core\ClassInfo;
|
||||
use ReflectionClass;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanIntFieldValidator;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BooleanFieldValidator;
|
||||
use SilverStripe\Dev\TestOnly;
|
||||
use SilverStripe\Core\Validation\FieldValidation\BigIntFieldValidator;
|
||||
use SilverStripe\ORM\FieldType\DBClassName;
|
||||
@ -458,7 +458,7 @@ class DBFieldTest extends SapphireTest
|
||||
BigIntFieldValidator::class,
|
||||
],
|
||||
DBBoolean::class => [
|
||||
BooleanIntFieldValidator::class,
|
||||
BooleanFieldValidator::class,
|
||||
],
|
||||
DBClassName::class => [
|
||||
StringFieldValidator::class,
|
||||
|
@ -33,6 +33,10 @@ class DBIntTest extends SapphireTest
|
||||
'value' => '-3',
|
||||
'expected' => -3,
|
||||
],
|
||||
'float' => [
|
||||
'value' => 3.5,
|
||||
'expected' => 3.5,
|
||||
],
|
||||
'string' => [
|
||||
'value' => 'fish',
|
||||
'expected' => 'fish',
|
||||
|
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';
|
||||
}
|
@ -69,30 +69,4 @@ class DBStringTest extends SapphireTest
|
||||
$this->assertFalse(DBField::create_field(MyStringField::class, 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());
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class MySQLiConnectorTest extends SapphireTest implements TestOnly
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$config = DB::getConfig();
|
||||
$config = DB::getConfig(DB::CONN_PRIMARY);
|
||||
|
||||
if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
|
||||
$this->markTestSkipped("The test only relevant for MySQL - but $config[type] is in use");
|
||||
|
Loading…
Reference in New Issue
Block a user