From 89a8d52fe64174d2e28d6952bd25651e63d98211 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Tue, 3 Aug 2021 15:40:17 +1200 Subject: [PATCH] NEW: Add method to check if a class is ready for db queries. This effectively makes the functionality from Security::database_is_ready() reusable for any arbitrary DataObject subclass. --- src/ORM/DB.php | 3 -- src/ORM/DataObjectSchema.php | 93 ++++++++++++++++++++++++++++++++++++ src/Security/Security.php | 52 ++++++++------------ 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/ORM/DB.php b/src/ORM/DB.php index 1b45c1c81c8..5904f58383d 100644 --- a/src/ORM/DB.php +++ b/src/ORM/DB.php @@ -2,7 +2,6 @@ namespace SilverStripe\ORM; -use BadMethodCallException; use InvalidArgumentException; use SilverStripe\Control\Director; use SilverStripe\Control\HTTPRequest; @@ -60,8 +59,6 @@ class DB */ protected static $configs = []; - - /** * The last SQL query run. * @var string diff --git a/src/ORM/DataObjectSchema.php b/src/ORM/DataObjectSchema.php index 9e85e0dfbed..063ec1670db 100644 --- a/src/ORM/DataObjectSchema.php +++ b/src/ORM/DataObjectSchema.php @@ -68,6 +68,15 @@ class DataObjectSchema */ protected $tableNames = []; + /** + * 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[] + */ + protected $tableReadyClasses = []; + /** * Clear cached table names */ @@ -1114,6 +1123,90 @@ public function getRemoteJoinField($class, $component, $type = 'has_many', &$pol } } + /** + * Check if all tables and field columns for a class exist in the database. + * + * @param string $class + * @return boolean + */ + public function tableIsReadyForClass(string $class): bool + { + if (!is_subclass_of($class, DataObject::class)) { + throw new InvalidArgumentException("$class is not a subclass of " . DataObject::class); + } + + // Don't check again if we already know the db is ready for this class. + // Necessary here before the loop to catch situations where a subclass + // is forced as ready without having to check all the superclasses. + if (!empty($this->tableReadyClasses[$class])) { + return true; + } + + // Check if all tables and fields required for the class exist in the database. + $requiredClasses = ClassInfo::dataClassesFor($class); + 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($this->tableReadyClasses[$class])) { + continue; + } + + // if any of the tables aren't created in the database + $table = $this->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 = $this->databaseFields($required, false); + $missingFields = array_diff_key($objFields, $dbFields); + + if ($missingFields) { + return false; + } + + // Add each ready class to the cached array. + $this->tableReadyClasses[$required] = true; + } + + return true; + } + + /** + * Resets the tableReadyClasses 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 function clearTableReadyForClass(?string $class = null, bool $clearFullHierarchy = true): void + { + if ($class) { + $clearClasses = [$class]; + if ($clearFullHierarchy) { + $clearClasses = ClassInfo::dataClassesFor($class); + } + foreach ($clearClasses as $clear) { + unset($this->tableReadyClasses[$clear]); + } + } else { + $this->tableReadyClasses = []; + } + } + /** * Validate the to or from field on a has_many mapping class * diff --git a/src/Security/Security.php b/src/Security/Security.php index 1e2d330af1c..4e74bb75255 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -13,15 +13,12 @@ use SilverStripe\Control\HTTPResponse_Exception; use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware; use SilverStripe\Control\RequestHandler; -use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Convert; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\Deprecation; -use SilverStripe\Dev\TestOnly; use SilverStripe\Forms\Form; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; -use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\ValidationResult; @@ -180,6 +177,8 @@ class Security extends Controller implements TemplateGlobalProvider protected static $force_database_is_ready; /** + * @deprecated 5.0 use {@link DataObject::getSchema()->tableReadyClasses} instead + * * When the database has once been verified as ready, it will not do the * checks again. * @@ -1238,41 +1237,19 @@ public static function database_is_ready() return self::$database_is_ready; } - $requiredClasses = ClassInfo::dataClassesFor(Member::class); - $requiredClasses[] = Group::class; - $requiredClasses[] = Permission::class; + $toCheck = [ + Member::class, + Group::class, + 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) { + foreach ($toCheck as $class) { + if (!$schema->tableIsReadyForClass($class)) { return false; } } - self::$database_is_ready = true; + self::$database_is_ready = true; return true; } @@ -1283,6 +1260,15 @@ 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, + ]; + $schema = DataObject::getSchema(); + foreach ($toClear as $class) { + $schema->clearTableReadyForClass($class); + } } /**