Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW: Add method to check if a class is ready for db queries. #10276

Open
wants to merge 1 commit into
base: 4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/ORM/DB.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace SilverStripe\ORM;

use BadMethodCallException;
use InvalidArgumentException;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
Expand Down Expand Up @@ -60,8 +59,6 @@ class DB
*/
protected static $configs = [];



/**
* The last SQL query run.
* @var string
Expand Down
93 changes: 93 additions & 0 deletions src/ORM/DataObjectSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!is_subclass_of($class, DataObject::class)) {
// Bail if there's no active database connection yet
if (!DB::connection_attempted() || !DB::is_active()) {
return false;
}
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
*
Expand Down
52 changes: 19 additions & 33 deletions src/Security/Security.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
}

/**
Expand Down