Skip to content

Commit

Permalink
NEW: Add method to DB to check if a class is ready for db queries.
Browse files Browse the repository at this point in the history
This effectively makes the functionality from
Security::database_is_ready() reusable for any arbitrary DataObject
subclass.
  • Loading branch information
GuySartorelli committed Feb 17, 2022
1 parent 6922549 commit 9ce74ed
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 58 deletions.
99 changes: 98 additions & 1 deletion src/ORM/DB.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use InvalidArgumentException;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Environment;
Expand Down Expand Up @@ -60,7 +61,14 @@ class DB
*/
protected static $configs = [];


/**
* Array of classes that have been confirmed ready for database queries.
* When the database has once been verified as ready, it will not do the
* checks again.
*
* @var boolean[]
*/
private static $database_is_ready = [];

/**
* The last SQL query run.
Expand Down Expand Up @@ -696,4 +704,93 @@ public static function alteration_message($message, $type = "")
{
self::get_schema()->alterationMessage($message, $type);
}

/**
* Check if all tables and field columns for a class exist in the database.
*
* @param string $class
* @return boolean
*/
public static function database_is_ready(string $class): bool
{
if (!is_subclass_of($class, DataObject::class)) {
throw new InvalidArgumentException("$class is not a subclass of " . DataObject::class);
}

// Check if all tables and fields required for the class exist in the database.
$requiredClasses = ClassInfo::dataClassesFor($class);
$schema = DataObject::getSchema();
foreach ($requiredClasses as $required) {
// Skip test classes, as not all test classes are scaffolded at once
if (is_a($required, TestOnly::class, true)) {
continue;
}

// Don't check again if we already know the db is ready for this class.
if (!empty(self::$database_is_ready[$class])) {
continue;
}

// if any of the tables aren't created in the database
$table = $schema->tableName($required);
if (!ClassInfo::hasTable($table)) {
return false;
}

// HACK: DataExtensions aren't applied until a class is instantiated for
// the first time, so create an instance here.
singleton($required);

// if any of the tables don't have all fields mapped as table columns
$dbFields = DB::field_list($table);
if (!$dbFields) {
return false;
}

$objFields = $schema->databaseFields($required, false);
$missingFields = array_diff_key($objFields, $dbFields);

if ($missingFields) {
return false;
}

// Add each ready class to the cached array.
self::$database_is_ready[$required] = true;
}

return true;
}

/**
* Resets the database_is_ready cache.
*
* @param string|null $class The specific class to be cleared.
* If not passed, the cache for all classes is cleared.
* @param bool $clearFullHeirarchy Whether to clear the full class hierarchy or only the given class.
*/
public static function clear_database_is_ready(?string $class = null, bool $clearFullHierarchy = true)
{
if ($class) {
$clearClasses = [$class];
if ($clearFullHierarchy) {
$clearClasses = ClassInfo::dataClassesFor($class);
}
foreach ($clearClasses as $clear) {
unset(self::$database_is_ready[$clear]);
}
} else {
self::$database_is_ready = [];
}
}

/**
* For the database_is_ready call to return a certain value for the given class - used for testing
*
* @param string $class The class to be forced as ready/not ready.
* @param boolean $isReady The value to force.
*/
public static function force_database_is_ready(string $class, bool $isReady)
{
self::$database_is_ready[$class] = $isReady;
}
}
80 changes: 23 additions & 57 deletions src/Security/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,20 +173,6 @@ class Security extends Controller implements TemplateGlobalProvider
*/
private static $login_recording = false;

/**
* @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
* will always return FALSE. Used for unit testing.
*/
protected static $force_database_is_ready;

/**
* When the database has once been verified as ready, it will not do the
* checks again.
*
* @var bool
*/
protected static $database_is_ready = false;

/**
* @var Authenticator[] available authenticators
*/
Expand Down Expand Up @@ -1229,49 +1215,16 @@ public static function encrypt_password($password, $salt = null, $algorithm = nu
*/
public static function database_is_ready()
{
// Used for unit tests
if (self::$force_database_is_ready !== null) {
return self::$force_database_is_ready;
}

if (self::$database_is_ready) {
return self::$database_is_ready;
}

$requiredClasses = ClassInfo::dataClassesFor(Member::class);
$requiredClasses[] = Group::class;
$requiredClasses[] = Permission::class;
$schema = DataObject::getSchema();
foreach ($requiredClasses as $class) {
// Skip test classes, as not all test classes are scaffolded at once
if (is_a($class, TestOnly::class, true)) {
continue;
}

// if any of the tables aren't created in the database
$table = $schema->tableName($class);
if (!ClassInfo::hasTable($table)) {
return false;
}

// HACK: DataExtensions aren't applied until a class is instantiated for
// the first time, so create an instance here.
singleton($class);

// if any of the tables don't have all fields mapped as table columns
$dbFields = DB::field_list($table);
if (!$dbFields) {
return false;
}

$objFields = $schema->databaseFields($class, false);
$missingFields = array_diff_key($objFields, $dbFields);

if ($missingFields) {
$toCheck = [
Member::class,
Group::class,
Permission::class,
];
foreach ($toCheck as $class) {
if (!DB::database_is_ready($class)) {
return false;
}
}
self::$database_is_ready = true;

return true;
}
Expand All @@ -1281,8 +1234,14 @@ public static function database_is_ready()
*/
public static function clear_database_is_ready()
{
self::$database_is_ready = null;
self::$force_database_is_ready = null;
$toClear = [
Member::class,
Group::class,
Permission::class,
];
foreach ($toClear as $class) {
DB::clear_database_is_ready($class);
}
}

/**
Expand All @@ -1292,7 +1251,14 @@ public static function clear_database_is_ready()
*/
public static function force_database_is_ready($isReady)
{
self::$force_database_is_ready = $isReady;
$toForce = [
Member::class,
Group::class,
Permission::class,
];
foreach ($toForce as $class) {
DB::force_database_is_ready($class, $isReady);
}
}

/**
Expand Down

0 comments on commit 9ce74ed

Please sign in to comment.