diff --git a/src/ORM/DB.php b/src/ORM/DB.php index 1b45c1c81c8..d247f0dcecc 100644 --- a/src/ORM/DB.php +++ b/src/ORM/DB.php @@ -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; @@ -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. @@ -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; + } } diff --git a/src/Security/Security.php b/src/Security/Security.php index 1e2d330af1c..37e6b2149a2 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -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 */ @@ -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; } @@ -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); + } } /** @@ -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); + } } /**