mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Refactor dataobject schema management into separate service
API Allow table_name to be configured via Config
This commit is contained in:
parent
19a27d22a3
commit
5e8ae41d47
@ -271,7 +271,7 @@ JSON;
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
|
protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) {
|
||||||
$baseClass = ClassInfo::baseDataClass($changeSetItem->ObjectClass);
|
$baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass);
|
||||||
$baseSingleton = DataObject::singleton($baseClass);
|
$baseSingleton = DataObject::singleton($baseClass);
|
||||||
$thumbnailWidth = (int)$this->config()->thumbnail_width;
|
$thumbnailWidth = (int)$this->config()->thumbnail_width;
|
||||||
$thumbnailHeight = (int)$this->config()->thumbnail_height;
|
$thumbnailHeight = (int)$this->config()->thumbnail_height;
|
||||||
|
@ -268,7 +268,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
// These are needed so that calling Director::test() does not muck with whoever is calling it.
|
// These are needed so that calling Director::test() does not muck with whoever is calling it.
|
||||||
// Really, it's some inappropriate coupling and should be resolved by making less use of statics.
|
// Really, it's some inappropriate coupling and should be resolved by making less use of statics.
|
||||||
$oldStage = Versioned::get_stage();
|
$oldReadingMode = Versioned::get_reading_mode();
|
||||||
$getVars = array();
|
$getVars = array();
|
||||||
|
|
||||||
if (!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
|
if (!$httpMethod) $httpMethod = ($postVars || is_array($postVars)) ? "POST" : "GET";
|
||||||
@ -294,7 +294,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
// Set callback to invoke prior to return
|
// Set callback to invoke prior to return
|
||||||
$onCleanup = function() use(
|
$onCleanup = function() use(
|
||||||
$existingRequestVars, $existingGetVars, $existingPostVars, $existingSessionVars,
|
$existingRequestVars, $existingGetVars, $existingPostVars, $existingSessionVars,
|
||||||
$existingCookies, $existingServer, $existingRequirementsBackend, $oldStage
|
$existingCookies, $existingServer, $existingRequirementsBackend, $oldReadingMode
|
||||||
) {
|
) {
|
||||||
// Restore the super globals
|
// Restore the super globals
|
||||||
$_REQUEST = $existingRequestVars;
|
$_REQUEST = $existingRequestVars;
|
||||||
@ -308,7 +308,7 @@ class Director implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
// These are needed so that calling Director::test() does not muck with whoever is calling it.
|
// These are needed so that calling Director::test() does not muck with whoever is calling it.
|
||||||
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
|
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
|
||||||
Versioned::set_stage($oldStage);
|
Versioned::set_reading_mode($oldReadingMode);
|
||||||
|
|
||||||
Injector::unnest(); // Restore old CookieJar, etc
|
Injector::unnest(); // Restore old CookieJar, etc
|
||||||
Config::unnest();
|
Config::unnest();
|
||||||
|
@ -78,8 +78,9 @@ class ClassInfo {
|
|||||||
* Returns an array of the current class and all its ancestors and children
|
* Returns an array of the current class and all its ancestors and children
|
||||||
* which require a DB table.
|
* which require a DB table.
|
||||||
*
|
*
|
||||||
|
* @todo Move this into {@see DataObjectSchema}
|
||||||
|
*
|
||||||
* @param string|object $class
|
* @param string|object $class
|
||||||
* @todo Move this into data object
|
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function dataClassesFor($class) {
|
public static function dataClassesFor($class) {
|
||||||
@ -104,28 +105,11 @@ class ClassInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the root class (the first to extend from DataObject) for the
|
* @deprecated 4.0..5.0
|
||||||
* passed class.
|
|
||||||
*
|
|
||||||
* @param string|object $class
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public static function baseDataClass($class) {
|
public static function baseDataClass($class) {
|
||||||
if(is_string($class) && !class_exists($class)) return null;
|
Deprecation::notice('5.0', 'Use DataObject::getSchema()->baseDataClass()');
|
||||||
|
return DataObject::getSchema()->baseDataClass($class);
|
||||||
$class = self::class_name($class);
|
|
||||||
|
|
||||||
if (!is_subclass_of($class, 'DataObject')) {
|
|
||||||
throw new InvalidArgumentException("$class is not a subclass of DataObject");
|
|
||||||
}
|
|
||||||
|
|
||||||
while ($next = get_parent_class($class)) {
|
|
||||||
if ($next == 'DataObject') {
|
|
||||||
return $class;
|
|
||||||
}
|
|
||||||
|
|
||||||
$class = $next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -174,8 +158,6 @@ class ClassInfo {
|
|||||||
public static function class_name($nameOrObject) {
|
public static function class_name($nameOrObject) {
|
||||||
if (is_object($nameOrObject)) {
|
if (is_object($nameOrObject)) {
|
||||||
return get_class($nameOrObject);
|
return get_class($nameOrObject);
|
||||||
} elseif (!self::exists($nameOrObject)) {
|
|
||||||
throw new InvalidArgumentException("Class {$nameOrObject} doesn't exist");
|
|
||||||
}
|
}
|
||||||
$reflection = new ReflectionClass($nameOrObject);
|
$reflection = new ReflectionClass($nameOrObject);
|
||||||
return $reflection->getName();
|
return $reflection->getName();
|
||||||
@ -291,53 +273,12 @@ class ClassInfo {
|
|||||||
return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
|
return strtolower(self::$method_from_cache[$lClass][$lMethod]) == $lCompclass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the table name in the class hierarchy which contains a given
|
* @deprecated 4.0..5.0
|
||||||
* field column for a {@link DataObject}. If the field does not exist, this
|
|
||||||
* will return null.
|
|
||||||
*
|
|
||||||
* @param string $candidateClass
|
|
||||||
* @param string $fieldName
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
public static function table_for_object_field($candidateClass, $fieldName) {
|
public static function table_for_object_field($candidateClass, $fieldName) {
|
||||||
if(!$candidateClass
|
Deprecation::notice('5.0', 'Use DataObject::getSchema()->tableForField()');
|
||||||
|| !$fieldName
|
return DataObject::getSchema()->tableForField($candidateClass, $fieldName);
|
||||||
|| !class_exists($candidateClass)
|
|
||||||
|| !is_subclass_of($candidateClass, 'DataObject')
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
//normalise class name
|
|
||||||
$candidateClass = self::class_name($candidateClass);
|
|
||||||
$exists = self::exists($candidateClass);
|
|
||||||
|
|
||||||
// Short circuit for fixed fields
|
|
||||||
$fixed = DataObject::config()->fixed_fields;
|
|
||||||
if($exists && isset($fixed[$fieldName])) {
|
|
||||||
return self::baseDataClass($candidateClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find regular field
|
|
||||||
while($candidateClass && $candidateClass != 'DataObject' && $exists) {
|
|
||||||
if( DataObject::has_own_table($candidateClass)
|
|
||||||
&& DataObject::has_own_table_database_field($candidateClass, $fieldName)
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$candidateClass = get_parent_class($candidateClass);
|
|
||||||
$exists = $candidateClass && self::exists($candidateClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!$candidateClass || !$exists) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $candidateClass;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,19 +176,13 @@ class Convert {
|
|||||||
* table, or column name. Supports encoding of multi identfiers separated by
|
* table, or column name. Supports encoding of multi identfiers separated by
|
||||||
* a delimiter (e.g. ".")
|
* a delimiter (e.g. ".")
|
||||||
*
|
*
|
||||||
* @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title'
|
* @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title' or list of identifiers
|
||||||
|
* to be joined via the separator.
|
||||||
* @param string $separator The string that delimits subsequent identifiers
|
* @param string $separator The string that delimits subsequent identifiers
|
||||||
* @return string|array The escaped identifier. E.g. '"SiteTree"."Title"'
|
* @return string The escaped identifier. E.g. '"SiteTree"."Title"'
|
||||||
*/
|
*/
|
||||||
public static function symbol2sql($identifier, $separator = '.') {
|
public static function symbol2sql($identifier, $separator = '.') {
|
||||||
if(is_array($identifier)) {
|
return DB::get_conn()->escapeIdentifier($identifier, $separator);
|
||||||
foreach($identifier as $k => $v) {
|
|
||||||
$identifier[$k] = self::symbol2sql($v, $separator);
|
|
||||||
}
|
|
||||||
return $identifier;
|
|
||||||
} else {
|
|
||||||
return DB::get_conn()->escapeIdentifier($identifier, $separator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,13 +59,14 @@ class FixtureBlueprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param String $identifier Unique identifier for this fixture type
|
* @param string $identifier Unique identifier for this fixture type
|
||||||
* @param Array $data Map of property names to their values.
|
* @param array $data Map of property names to their values.
|
||||||
* @param Array $fixtures Map of fixture names to an associative array of their in-memory
|
* @param array $fixtures Map of fixture names to an associative array of their in-memory
|
||||||
* identifiers mapped to their database IDs. Used to look up
|
* identifiers mapped to their database IDs. Used to look up
|
||||||
* existing fixtures which might be referenced in the $data attribute
|
* existing fixtures which might be referenced in the $data attribute
|
||||||
* via the => notation.
|
* via the => notation.
|
||||||
* @return DataObject
|
* @return DataObject
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function createObject($identifier, $data = null, $fixtures = null) {
|
public function createObject($identifier, $data = null, $fixtures = null) {
|
||||||
// We have to disable validation while we import the fixtures, as the order in
|
// We have to disable validation while we import the fixtures, as the order in
|
||||||
@ -89,12 +90,13 @@ class FixtureBlueprint {
|
|||||||
|
|
||||||
// The database needs to allow inserting values into the foreign key column (ID in our case)
|
// The database needs to allow inserting values into the foreign key column (ID in our case)
|
||||||
$conn = DB::get_conn();
|
$conn = DB::get_conn();
|
||||||
|
$baseTable = DataObject::getSchema()->baseDataTable($class);
|
||||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
||||||
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true);
|
$conn->allowPrimaryKeyEditing($baseTable, true);
|
||||||
}
|
}
|
||||||
$obj->write(false, true);
|
$obj->write(false, true);
|
||||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
||||||
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), false);
|
$conn->allowPrimaryKeyEditing($baseTable, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +208,8 @@ class FixtureBlueprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Array $defaults
|
* @param array $defaults
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setDefaults($defaults) {
|
public function setDefaults($defaults) {
|
||||||
$this->defaults = $defaults;
|
$this->defaults = $defaults;
|
||||||
@ -214,14 +217,14 @@ class FixtureBlueprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function getDefaults() {
|
public function getDefaults() {
|
||||||
return $this->defaults;
|
return $this->defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return String
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getClass() {
|
public function getClass() {
|
||||||
return $this->class;
|
return $this->class;
|
||||||
@ -230,8 +233,9 @@ class FixtureBlueprint {
|
|||||||
/**
|
/**
|
||||||
* See class documentation.
|
* See class documentation.
|
||||||
*
|
*
|
||||||
* @param String $type
|
* @param string $type
|
||||||
* @param callable $callback
|
* @param callable $callback
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function addCallback($type, $callback) {
|
public function addCallback($type, $callback) {
|
||||||
if(!array_key_exists($type, $this->callbacks)) {
|
if(!array_key_exists($type, $this->callbacks)) {
|
||||||
@ -243,12 +247,15 @@ class FixtureBlueprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param String $type
|
* @param string $type
|
||||||
* @param callable $callback
|
* @param callable $callback
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function removeCallback($type, $callback) {
|
public function removeCallback($type, $callback) {
|
||||||
$pos = array_search($callback, $this->callbacks[$type]);
|
$pos = array_search($callback, $this->callbacks[$type]);
|
||||||
if($pos !== false) unset($this->callbacks[$type][$pos]);
|
if($pos !== false) {
|
||||||
|
unset($this->callbacks[$type][$pos]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -263,7 +270,7 @@ class FixtureBlueprint {
|
|||||||
* Parse a value from a fixture file. If it starts with =>
|
* Parse a value from a fixture file. If it starts with =>
|
||||||
* it will get an ID from the fixture dictionary
|
* it will get an ID from the fixture dictionary
|
||||||
*
|
*
|
||||||
* @param string $fieldVal
|
* @param string $value
|
||||||
* @param array $fixtures See {@link createObject()}
|
* @param array $fixtures See {@link createObject()}
|
||||||
* @param string $class If the value parsed is a class relation, this parameter
|
* @param string $class If the value parsed is a class relation, this parameter
|
||||||
* will be given the value of that class's name
|
* will be given the value of that class's name
|
||||||
@ -293,13 +300,16 @@ class FixtureBlueprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
|
protected function overrideField($obj, $fieldName, $value, $fixtures = null) {
|
||||||
$table = ClassInfo::table_for_object_field(get_class($obj), $fieldName);
|
$class = get_class($obj);
|
||||||
|
$table = DataObject::getSchema()->tableForField($class, $fieldName);
|
||||||
$value = $this->parseValue($value, $fixtures);
|
$value = $this->parseValue($value, $fixtures);
|
||||||
|
|
||||||
DB::manipulate(array(
|
DB::manipulate(array(
|
||||||
$table => array(
|
$table => array(
|
||||||
"command" => "update", "id" => $obj->ID,
|
"command" => "update",
|
||||||
"fields" => array($fieldName => $value)
|
"id" => $obj->ID,
|
||||||
|
"class" => $class,
|
||||||
|
"fields" => array($fieldName => $value),
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
$obj->$fieldName = $value;
|
$obj->$fieldName = $value;
|
||||||
|
@ -489,6 +489,59 @@ offset, if not provided as an argument, will default to 0.
|
|||||||
Note that the `limit` argument order is different from a MySQL LIMIT clause.
|
Note that the `limit` argument order is different from a MySQL LIMIT clause.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Mapping classes to tables with DataObjectSchema
|
||||||
|
|
||||||
|
Note that in most cases, the underlying database table for any DataObject instance will be the same as the class name.
|
||||||
|
However in cases where dealing with namespaced classes, especially when using DB schema which don't support
|
||||||
|
slashes in table names, it is necessary to provide an alternate mapping.
|
||||||
|
|
||||||
|
For instance, the below model will be stored in the table name `BannerImage`
|
||||||
|
|
||||||
|
|
||||||
|
:::php
|
||||||
|
namespace SilverStripe\BannerManager;
|
||||||
|
class BannerImage extends \DataObject {
|
||||||
|
private static $table_name = 'BannerImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Note that any model class which does not explicitly declare a `table_name` config option will have a name
|
||||||
|
automatically generated for them. In the above case, the table name would have been
|
||||||
|
`SilverStripe\BannerManager\BannerImage`
|
||||||
|
|
||||||
|
When creating raw SQL queries that contain table names, it is necessary to ensure your queries have the correct
|
||||||
|
table. This functionality can be provided by the [api:DataObjectSchema] service, which can be accessed via
|
||||||
|
`DataObject::getSchema()`. This service provides the following methods, most of which have a table and class
|
||||||
|
equivalent version.
|
||||||
|
|
||||||
|
Methods which return class names:
|
||||||
|
|
||||||
|
* `tableClass($table)` Finds the class name for a given table. This also handles suffixed tables such as `Table_Live`.
|
||||||
|
* `baseDataClass($class)` Returns the base data class for the given class.
|
||||||
|
* `classForField($class, $field)` Finds the specific class that directly holds the given field
|
||||||
|
|
||||||
|
Methods which return table names:
|
||||||
|
|
||||||
|
* `tableName($class)` Returns the table name for a given class or object.
|
||||||
|
* `baseDataTable($class)` Returns the base data class for the given class.
|
||||||
|
* `tableForField($class, $field)` Finds the specific class that directly holds the given field and returns the table.
|
||||||
|
|
||||||
|
Note that in cases where the class name is required, an instance of the object may be substituted.
|
||||||
|
|
||||||
|
For example, if running a query against a particular model, you will need to ensure you use the correct
|
||||||
|
table and column.
|
||||||
|
|
||||||
|
|
||||||
|
:::php
|
||||||
|
public function countDuplicates($model, $fieldToCheck) {
|
||||||
|
$table = DataObject::getSchema()->tableForField($model, $field);
|
||||||
|
$query = new SQLSelect();
|
||||||
|
$query->setFrom("\"{$table}\"");
|
||||||
|
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
### Raw SQL
|
### Raw SQL
|
||||||
|
|
||||||
Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have
|
Occasionally, the system described above won't let you do exactly what you need to do. In these situations, we have
|
||||||
@ -620,3 +673,4 @@ To retrieve a news article, SilverStripe joins the [api:SiteTree], [api:Page] an
|
|||||||
* [api:DataObject]
|
* [api:DataObject]
|
||||||
* [api:DataList]
|
* [api:DataList]
|
||||||
* [api:DataQuery]
|
* [api:DataQuery]
|
||||||
|
* [api:DataObjectSchema]
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
* Versioned constructor now only allows a single string to declare whether staging is enabled or not. The
|
* Versioned constructor now only allows a single string to declare whether staging is enabled or not. The
|
||||||
number of names of stages are no longer able to be specified. See below for upgrading notes for models
|
number of names of stages are no longer able to be specified. See below for upgrading notes for models
|
||||||
with custom stages.
|
with custom stages.
|
||||||
* `reading_stage` is now `set_stage`
|
* `reading_stage` is now `set_stage` and throws an error if setting an invalid stage.
|
||||||
* `current_stage` is now `get_stage`
|
* `current_stage` is now `get_stage`
|
||||||
* `getVersionedStages` is gone.
|
* `getVersionedStages` is gone.
|
||||||
* `get_live_stage` is removed. Use the `Versioned::LIVE` constant instead.
|
* `get_live_stage` is removed. Use the `Versioned::LIVE` constant instead.
|
||||||
@ -91,8 +91,13 @@
|
|||||||
* `$versionableExtensions` is now `private static` instead of `protected static`
|
* `$versionableExtensions` is now `private static` instead of `protected static`
|
||||||
* `hasStages` is addded to check if an object has a given stage.
|
* `hasStages` is addded to check if an object has a given stage.
|
||||||
* `stageTable` is added to get the table for a given class and stage.
|
* `stageTable` is added to get the table for a given class and stage.
|
||||||
|
* Any extension declared via `versionableExtensions` config on Versioned dataobject must now
|
||||||
|
`VersionableExtension` interface at a minimum. `Translatable` has been removed from default
|
||||||
|
`versionableExtensions`
|
||||||
* `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects.
|
* `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects.
|
||||||
* `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions
|
* `FormAction::setValidationExempt` can be used to turn on or off form validation for individual actions
|
||||||
|
* `DataObject.table_name` config can now be used to customise the database table for any record.
|
||||||
|
* `DataObjectSchema` class added to assist with mapping between classes and tables.
|
||||||
|
|
||||||
### Front-end build tooling for CMS interface
|
### Front-end build tooling for CMS interface
|
||||||
|
|
||||||
@ -163,6 +168,10 @@ admin/font/ => admin/client/dist/font/
|
|||||||
|
|
||||||
* `debugmethods` querystring argument has been removed from debugging.
|
* `debugmethods` querystring argument has been removed from debugging.
|
||||||
|
|
||||||
|
* The following ClassInfo methods are now deprecated:
|
||||||
|
* `ClassInfo::baseDataClass` - Use `DataObject::getSchema()->baseDataClass()` instead.
|
||||||
|
* `ClassInfo::table_for_object_field` - Use `DataObject::getSchema()->tableForField()` instead
|
||||||
|
|
||||||
### ORM
|
### ORM
|
||||||
|
|
||||||
* `DataList::getRelation` is removed, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
|
* `DataList::getRelation` is removed, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
|
||||||
@ -563,6 +572,36 @@ these references should be replaced with `SQLSelect`. Legacy code which generate
|
|||||||
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
|
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
|
||||||
subclass of `SQLSelect`, but the inverse is not true.
|
subclass of `SQLSelect`, but the inverse is not true.
|
||||||
|
|
||||||
|
### Update code that references table names
|
||||||
|
|
||||||
|
A major change in 4.0.0 is that now tables and class names can differ from model to model. In order to
|
||||||
|
fix a table name, to prevent it being changed (for instance, when applying a namespace to a model)
|
||||||
|
the `table_name` config can be applied to any DataObject class.
|
||||||
|
|
||||||
|
|
||||||
|
:::php
|
||||||
|
namespace SilverStripe\BannerManager;
|
||||||
|
class BannerImage extends \DataObject {
|
||||||
|
private static $table_name = 'BannerImage';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
In order to ensure you are using the correct table for any class a new [api:DataObjectSchema] service
|
||||||
|
is available to manage these mappings.
|
||||||
|
|
||||||
|
|
||||||
|
:::php
|
||||||
|
public function countDuplicates($model, $fieldToCheck) {
|
||||||
|
$table = DataObject::getSchema()->tableForField($model, $field);
|
||||||
|
$query = new SQLSelect();
|
||||||
|
$query->setFrom("\"{$table}\"");
|
||||||
|
$query->setWhere(["\"{$table}\".\"{$field}\"" => $model->$fieldToCheck]);
|
||||||
|
return $query->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
See [versioned documentation](/developer_guides/model/data_model_and_orm) for more information.
|
||||||
|
|
||||||
### Update implementations of augmentSQL
|
### Update implementations of augmentSQL
|
||||||
|
|
||||||
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`
|
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`
|
||||||
|
@ -155,7 +155,7 @@ class AssetControlExtension extends \DataExtension
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unauthenticated member to use for checking visibility
|
// Unauthenticated member to use for checking visibility
|
||||||
$baseClass = \ClassInfo::baseDataClass($this->owner);
|
$baseClass = $this->owner->baseClass();
|
||||||
$filter = array("\"{$baseClass}\".\"ID\"" => $this->owner->ID);
|
$filter = array("\"{$baseClass}\".\"ID\"" => $this->owner->ID);
|
||||||
$stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages}
|
$stages = $this->owner->getVersionedStages(); // {@see Versioned::getVersionedStages}
|
||||||
foreach ($stages as $stage) {
|
foreach ($stages as $stage) {
|
||||||
|
@ -511,12 +511,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
|||||||
$relationModelName = $query->applyRelation($relations, $linearOnly);
|
$relationModelName = $query->applyRelation($relations, $linearOnly);
|
||||||
|
|
||||||
// Find the db field the relation belongs to
|
// Find the db field the relation belongs to
|
||||||
$className = ClassInfo::table_for_object_field($relationModelName, $fieldName);
|
$columnName = DataObject::getSchema()->sqlColumnForField($relationModelName, $fieldName);
|
||||||
if(empty($className)) {
|
|
||||||
$className = $relationModelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
$columnName = '"'.$className.'"."'.$fieldName.'"';
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -193,8 +193,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
protected static $_cache_has_own_table = array();
|
protected static $_cache_has_own_table = array();
|
||||||
protected static $_cache_get_one;
|
protected static $_cache_get_one;
|
||||||
protected static $_cache_get_class_ancestry;
|
protected static $_cache_get_class_ancestry;
|
||||||
protected static $_cache_composite_fields = array();
|
|
||||||
protected static $_cache_database_fields = array();
|
|
||||||
protected static $_cache_field_labels = array();
|
protected static $_cache_field_labels = array();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,6 +218,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension'
|
'AssetControl' => '\\SilverStripe\\Filesystem\\AssetControlExtension'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override table name for this class. If ignored will default to FQN of class.
|
||||||
|
* This option is not inheritable, and must be set on each class.
|
||||||
|
* If left blank naming will default to the legacy (3.x) behaviour.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static $table_name = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non-static relationship cache, indexed by component name.
|
* Non-static relationship cache, indexed by component name.
|
||||||
*/
|
*/
|
||||||
@ -230,6 +237,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
*/
|
*/
|
||||||
protected $unsavedRelations;
|
protected $unsavedRelations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get schema object
|
||||||
|
*
|
||||||
|
* @return DataObjectSchema
|
||||||
|
*/
|
||||||
|
public static function getSchema() {
|
||||||
|
return Injector::inst()->get('DataObjectSchema');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the complete map of fields to specification on this object, including fixed_fields.
|
* Return the complete map of fields to specification on this object, including fixed_fields.
|
||||||
* "ID" will be included on every table.
|
* "ID" will be included on every table.
|
||||||
@ -246,74 +262,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
if(empty($class)) {
|
if(empty($class)) {
|
||||||
$class = get_called_class();
|
$class = get_called_class();
|
||||||
}
|
}
|
||||||
|
return static::getSchema()->databaseFields($class);
|
||||||
// Refresh cache
|
|
||||||
self::cache_database_fields($class);
|
|
||||||
|
|
||||||
// Return cached values
|
|
||||||
return self::$_cache_database_fields[$class];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache all database and composite fields for the given class.
|
|
||||||
* Will do nothing if already cached
|
|
||||||
*
|
|
||||||
* @param string $class Class name to cache
|
|
||||||
*/
|
|
||||||
protected static function cache_database_fields($class) {
|
|
||||||
// Skip if already cached
|
|
||||||
if( isset(self::$_cache_database_fields[$class])
|
|
||||||
&& isset(self::$_cache_composite_fields[$class])
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$compositeFields = array();
|
|
||||||
$dbFields = array();
|
|
||||||
|
|
||||||
// Ensure fixed fields appear at the start
|
|
||||||
$fixedFields = self::config()->fixed_fields;
|
|
||||||
if(get_parent_class($class) === 'DataObject') {
|
|
||||||
// Merge fixed with ClassName spec and custom db fields
|
|
||||||
$dbFields = $fixedFields;
|
|
||||||
} else {
|
|
||||||
$dbFields['ID'] = $fixedFields['ID'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each DB value as either a field or composite field
|
|
||||||
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
|
|
||||||
foreach($db as $fieldName => $fieldSpec) {
|
|
||||||
$fieldClass = strtok($fieldSpec, '(');
|
|
||||||
if(singleton($fieldClass) instanceof DBComposite) {
|
|
||||||
$compositeFields[$fieldName] = $fieldSpec;
|
|
||||||
} else {
|
|
||||||
$dbFields[$fieldName] = $fieldSpec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add in all has_ones
|
|
||||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
|
|
||||||
foreach($hasOne as $fieldName => $hasOneClass) {
|
|
||||||
if($hasOneClass === 'DataObject') {
|
|
||||||
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
|
||||||
} else {
|
|
||||||
$dbFields["{$fieldName}ID"] = 'ForeignKey';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge composite fields into DB
|
|
||||||
foreach($compositeFields as $fieldName => $fieldSpec) {
|
|
||||||
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
|
|
||||||
$fieldObj->setTable($class);
|
|
||||||
$nestedFields = $fieldObj->compositeDatabaseFields();
|
|
||||||
foreach($nestedFields as $nestedName => $nestedSpec) {
|
|
||||||
$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return cached results
|
|
||||||
self::$_cache_database_fields[$class] = $dbFields;
|
|
||||||
self::$_cache_composite_fields[$class] = $compositeFields;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,10 +286,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$class = get_called_class();
|
$class = get_called_class();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all fields
|
|
||||||
$fields = self::database_fields($class);
|
|
||||||
|
|
||||||
// Remove fixed fields. This assumes that NO fixed_fields are composite
|
// Remove fixed fields. This assumes that NO fixed_fields are composite
|
||||||
|
$fields = static::getSchema()->databaseFields($class);
|
||||||
$fields = array_diff_key($fields, self::config()->fixed_fields);
|
$fields = array_diff_key($fields, self::config()->fixed_fields);
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
@ -377,24 +324,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
if(empty($class)) {
|
if(empty($class)) {
|
||||||
$class = get_called_class();
|
$class = get_called_class();
|
||||||
}
|
}
|
||||||
if($class === 'DataObject') {
|
return static::getSchema()->compositeFields($class, $aggregated);
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh cache
|
|
||||||
self::cache_database_fields($class);
|
|
||||||
|
|
||||||
// Get fields for this class
|
|
||||||
$compositeFields = self::$_cache_composite_fields[$class];
|
|
||||||
if(!$aggregated) {
|
|
||||||
return $compositeFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively merge
|
|
||||||
return array_merge(
|
|
||||||
$compositeFields,
|
|
||||||
self::composite_fields(get_parent_class($class))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1250,10 +1180,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* @param string $now Timestamp to use for the current time
|
* @param string $now Timestamp to use for the current time
|
||||||
* @param bool $isNewRecord Whether this should be treated as a new record write
|
* @param bool $isNewRecord Whether this should be treated as a new record write
|
||||||
* @param array $manipulation Manipulation to write to
|
* @param array $manipulation Manipulation to write to
|
||||||
* @param string $class Table and Class to select and write to
|
* @param string $class Class of table to manipulate
|
||||||
*/
|
*/
|
||||||
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
|
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
|
||||||
$manipulation[$class] = array();
|
$table = $this->getSchema()->tableName($class);
|
||||||
|
$manipulation[$table] = array();
|
||||||
|
|
||||||
// Extract records for this table
|
// Extract records for this table
|
||||||
foreach($this->record as $fieldName => $fieldValue) {
|
foreach($this->record as $fieldName => $fieldValue) {
|
||||||
@ -1261,7 +1192,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
// Check if this record pertains to this table, and
|
// Check if this record pertains to this table, and
|
||||||
// we're not attempting to reset the BaseTable->ID
|
// we're not attempting to reset the BaseTable->ID
|
||||||
if( empty($this->changed[$fieldName])
|
if( empty($this->changed[$fieldName])
|
||||||
|| ($class === $baseTable && $fieldName === 'ID')
|
|| ($table === $baseTable && $fieldName === 'ID')
|
||||||
|| (!self::has_own_table_database_field($class, $fieldName)
|
|| (!self::has_own_table_database_field($class, $fieldName)
|
||||||
&& !self::is_composite_field($class, $fieldName, false))
|
&& !self::is_composite_field($class, $fieldName, false))
|
||||||
) {
|
) {
|
||||||
@ -1276,25 +1207,26 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write to manipulation
|
// Write to manipulation
|
||||||
$fieldObj->writeToManipulation($manipulation[$class]);
|
$fieldObj->writeToManipulation($manipulation[$table]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure update of Created and LastEdited columns
|
// Ensure update of Created and LastEdited columns
|
||||||
if($baseTable === $class) {
|
if($baseTable === $table) {
|
||||||
$manipulation[$class]['fields']['LastEdited'] = $now;
|
$manipulation[$table]['fields']['LastEdited'] = $now;
|
||||||
if($isNewRecord) {
|
if($isNewRecord) {
|
||||||
$manipulation[$class]['fields']['Created']
|
$manipulation[$table]['fields']['Created']
|
||||||
= empty($this->record['Created'])
|
= empty($this->record['Created'])
|
||||||
? $now
|
? $now
|
||||||
: $this->record['Created'];
|
: $this->record['Created'];
|
||||||
$manipulation[$class]['fields']['ClassName'] = $this->class;
|
$manipulation[$table]['fields']['ClassName'] = $this->class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserts done one the base table are performed in another step, so the manipulation should instead
|
// Inserts done one the base table are performed in another step, so the manipulation should instead
|
||||||
// attempt an update, as though it were a normal update.
|
// attempt an update, as though it were a normal update.
|
||||||
$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
|
$manipulation[$table]['command'] = $isNewRecord ? 'insert' : 'update';
|
||||||
$manipulation[$class]['id'] = $this->record['ID'];
|
$manipulation[$table]['id'] = $this->record['ID'];
|
||||||
|
$manipulation[$table]['class'] = $class;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1379,7 +1311,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
if($hasChanges || $forceWrite || $isNewRecord) {
|
if($hasChanges || $forceWrite || $isNewRecord) {
|
||||||
// New records have their insert into the base data table done first, so that they can pass the
|
// New records have their insert into the base data table done first, so that they can pass the
|
||||||
// generated primary key on to the rest of the manipulation
|
// generated primary key on to the rest of the manipulation
|
||||||
$baseTable = ClassInfo::baseDataClass($this->class);
|
$baseTable = $this->baseTable();
|
||||||
$this->writeBaseRecord($baseTable, $now);
|
$this->writeBaseRecord($baseTable, $now);
|
||||||
|
|
||||||
// Write the DB manipulation for all changed fields
|
// Write the DB manipulation for all changed fields
|
||||||
@ -1730,7 +1662,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
case 'has_one': {
|
case 'has_one': {
|
||||||
// Mock has_many
|
// Mock has_many
|
||||||
$joinField = "{$remoteRelation}ID";
|
$joinField = "{$remoteRelation}ID";
|
||||||
$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
|
$componentClass = static::getSchema()->classForField($remoteClass, $joinField);
|
||||||
$result = HasManyList::create($componentClass, $joinField);
|
$result = HasManyList::create($componentClass, $joinField);
|
||||||
if ($this->model) {
|
if ($this->model) {
|
||||||
$result->setDataModel($this->model);
|
$result->setDataModel($this->model);
|
||||||
@ -1998,12 +1930,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* Return all of the database fields in this object
|
* Return all of the database fields in this object
|
||||||
*
|
*
|
||||||
* @param string $fieldName Limit the output to a specific field name
|
* @param string $fieldName Limit the output to a specific field name
|
||||||
* @param bool $includeTable If returning a single column, prefix the column with the table name
|
* @param bool $includeClass If returning a single column, prefix the column with the class name
|
||||||
* in Table.Column(spec) format
|
* in Table.Column(spec) format
|
||||||
* @return array|string|null The database fields, or if searching a single field, just this one field if found
|
* @return array|string|null The database fields, or if searching a single field,
|
||||||
* Field will be a string in ClassName(args) format, or Table.ClassName(args) format if $includeTable is true
|
* just this one field if found. Field will be a string in FieldClass(args)
|
||||||
|
* format, or RecordClass.FieldClass(args) format if $includeClass is true
|
||||||
*/
|
*/
|
||||||
public function db($fieldName = null, $includeTable = false) {
|
public function db($fieldName = null, $includeClass = false) {
|
||||||
$classes = ClassInfo::ancestry($this, true);
|
$classes = ClassInfo::ancestry($this, true);
|
||||||
|
|
||||||
// If we're looking for a specific field, we want to hit subclasses first as they may override field types
|
// If we're looking for a specific field, we want to hit subclasses first as they may override field types
|
||||||
@ -2021,17 +1954,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
// Check for search field
|
// Check for search field
|
||||||
if($fieldName && isset($db[$fieldName])) {
|
if($fieldName && isset($db[$fieldName])) {
|
||||||
// Return found field
|
// Return found field
|
||||||
if(!$includeTable) {
|
if(!$includeClass) {
|
||||||
return $db[$fieldName];
|
return $db[$fieldName];
|
||||||
}
|
}
|
||||||
|
return $class . "." . $db[$fieldName];
|
||||||
// Set table for the given field
|
|
||||||
if(in_array($fieldName, $this->config()->fixed_fields)) {
|
|
||||||
$table = $this->baseTable();
|
|
||||||
} else {
|
|
||||||
$table = $class;
|
|
||||||
}
|
|
||||||
return $table . "." . $db[$fieldName];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2179,54 +2105,60 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
foreach($classes as $class) {
|
foreach($classes as $class) {
|
||||||
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
|
$manyMany = Config::inst()->get($class, 'many_many', Config::UNINHERITED);
|
||||||
// Check if the component is defined in many_many on this class
|
// Check if the component is defined in many_many on this class
|
||||||
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
|
if(isset($manyMany[$component])) {
|
||||||
if($candidate) {
|
$candidate = $manyMany[$component];
|
||||||
$parentField = $class . "ID";
|
$classTable = static::getSchema()->tableName($class);
|
||||||
$childField = ($class == $candidate) ? "ChildID" : $candidate . "ID";
|
$candidateTable = static::getSchema()->tableName($candidate);
|
||||||
return array($class, $candidate, $parentField, $childField, "{$class}_$component");
|
$parentField = "{$classTable}ID";
|
||||||
|
$childField = $class === $candidate ? "ChildID" : "{$candidateTable}ID";
|
||||||
|
$joinTable = "{$classTable}_{$component}";
|
||||||
|
return array($class, $candidate, $parentField, $childField, $joinTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the component is defined in belongs_many_many on this class
|
// Check if the component is defined in belongs_many_many on this class
|
||||||
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
|
$belongsManyMany = Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
|
||||||
$candidate = (isset($belongsManyMany[$component])) ? $belongsManyMany[$component] : null;
|
if(!isset($belongsManyMany[$component])) {
|
||||||
if($candidate) {
|
continue;
|
||||||
// Extract class and relation name from dot-notation
|
}
|
||||||
if(strpos($candidate, '.') !== false) {
|
|
||||||
list($candidate, $relationName) = explode('.', $candidate, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
$childField = $candidate . "ID";
|
// Extract class and relation name from dot-notation
|
||||||
|
$candidate = $belongsManyMany[$component];
|
||||||
|
$relationName = null;
|
||||||
|
if(strpos($candidate, '.') !== false) {
|
||||||
|
list($candidate, $relationName) = explode('.', $candidate, 2);
|
||||||
|
}
|
||||||
|
$candidateTable = static::getSchema()->tableName($candidate);
|
||||||
|
$childField = $candidateTable . "ID";
|
||||||
|
|
||||||
// We need to find the inverse component name
|
// We need to find the inverse component name, if not explicitly given
|
||||||
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
|
$otherManyMany = Config::inst()->get($candidate, 'many_many', Config::UNINHERITED);
|
||||||
if(!$otherManyMany) {
|
if(!$relationName && $otherManyMany) {
|
||||||
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
|
foreach($otherManyMany as $inverseComponentName => $childClass) {
|
||||||
}
|
if($childClass === $class || is_subclass_of($class, $childClass)) {
|
||||||
|
$relationName = $inverseComponentName;
|
||||||
// If we've got a relation name (extracted from dot-notation), we can already work out
|
break;
|
||||||
// the join table and candidate class name...
|
|
||||||
if(isset($relationName) && isset($otherManyMany[$relationName])) {
|
|
||||||
$candidateClass = $otherManyMany[$relationName];
|
|
||||||
$joinTable = "{$candidate}_{$relationName}";
|
|
||||||
} else {
|
|
||||||
// ... otherwise, we need to loop over the many_manys and find a relation that
|
|
||||||
// matches up to this class
|
|
||||||
foreach($otherManyMany as $inverseComponentName => $candidateClass) {
|
|
||||||
if($candidateClass == $class || is_subclass_of($class, $candidateClass)) {
|
|
||||||
$joinTable = "{$candidate}_{$inverseComponentName}";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we could work out the join table, we've got all the info we need
|
|
||||||
if(isset($joinTable)) {
|
|
||||||
$parentField = ($class == $candidate) ? "ChildID" : $candidateClass . "ID";
|
|
||||||
return array($class, $candidate, $parentField, $childField, $joinTable);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new LogicException("Orphaned \$belongs_many_many value for $this->class.$component");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check valid relation found
|
||||||
|
if(!$relationName || !$otherManyMany || !isset($otherManyMany[$relationName])) {
|
||||||
|
throw new LogicException("Inverse component of $candidate not found ({$this->class})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've got a relation name (extracted from dot-notation), we can already work out
|
||||||
|
// the join table and candidate class name...
|
||||||
|
$childClass = $otherManyMany[$relationName];
|
||||||
|
$joinTable = "{$candidateTable}_{$relationName}";
|
||||||
|
|
||||||
|
// If we could work out the join table, we've got all the info we need
|
||||||
|
if ($childClass === $candidate) {
|
||||||
|
$parentField = "ChildID";
|
||||||
|
} else {
|
||||||
|
$childTable = static::getSchema()->tableName($childClass);
|
||||||
|
$parentField = "{$childTable}ID";
|
||||||
|
}
|
||||||
|
return array($class, $candidate, $parentField, $childField, $joinTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2470,16 +2402,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
/**
|
/**
|
||||||
* Loads all the stub fields that an initial lazy load didn't load fully.
|
* Loads all the stub fields that an initial lazy load didn't load fully.
|
||||||
*
|
*
|
||||||
* @param string $tableClass Base table to load the values from. Others are joined as required.
|
* @param string $class Class to load the values from. Others are joined as required.
|
||||||
* Not specifying a tableClass will load all lazy fields from all tables.
|
* Not specifying a tableClass will load all lazy fields from all tables.
|
||||||
* @return bool Flag if lazy loading succeeded
|
* @return bool Flag if lazy loading succeeded
|
||||||
*/
|
*/
|
||||||
protected function loadLazyFields($tableClass = null) {
|
protected function loadLazyFields($class = null) {
|
||||||
if(!$this->isInDB() || !is_numeric($this->ID)) {
|
if(!$this->isInDB() || !is_numeric($this->ID)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$tableClass) {
|
if (!$class) {
|
||||||
$loaded = array();
|
$loaded = array();
|
||||||
|
|
||||||
foreach ($this->record as $key => $value) {
|
foreach ($this->record as $key => $value) {
|
||||||
@ -2492,7 +2424,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dataQuery = new DataQuery($tableClass);
|
$dataQuery = new DataQuery($class);
|
||||||
|
|
||||||
// Reset query parameter context to that of this DataObject
|
// Reset query parameter context to that of this DataObject
|
||||||
if($params = $this->getSourceQueryParams()) {
|
if($params = $this->getSourceQueryParams()) {
|
||||||
@ -2503,16 +2435,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
|
|
||||||
// Limit query to the current record, unless it has the Versioned extension,
|
// Limit query to the current record, unless it has the Versioned extension,
|
||||||
// in which case it requires special handling through augmentLoadLazyFields()
|
// in which case it requires special handling through augmentLoadLazyFields()
|
||||||
$baseTable = ClassInfo::baseDataClass($this);
|
$baseIDColumn = static::getSchema()->sqlColumnForField($this, 'ID');
|
||||||
$dataQuery->where([
|
$dataQuery->where([
|
||||||
"\"{$baseTable}\".\"ID\"" => $this->record['ID']
|
$baseIDColumn => $this->record['ID']
|
||||||
])->limit(1);
|
])->limit(1);
|
||||||
|
|
||||||
$columns = array();
|
$columns = array();
|
||||||
|
|
||||||
// Add SQL for fields, both simple & multi-value
|
// Add SQL for fields, both simple & multi-value
|
||||||
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
|
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
|
||||||
$databaseFields = self::database_fields($tableClass);
|
$databaseFields = self::database_fields($class);
|
||||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||||
if(!isset($this->record[$k]) || $this->record[$k] === null) {
|
if(!isset($this->record[$k]) || $this->record[$k] === null) {
|
||||||
$columns[] = $k;
|
$columns[] = $k;
|
||||||
@ -2805,19 +2737,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function has_own_table($dataClass) {
|
public static function has_own_table($dataClass) {
|
||||||
if(!is_subclass_of($dataClass,'DataObject')) return false;
|
if(!is_subclass_of($dataClass, 'DataObject')) {
|
||||||
|
return false;
|
||||||
$dataClass = ClassInfo::class_name($dataClass);
|
|
||||||
if(!isset(self::$_cache_has_own_table[$dataClass])) {
|
|
||||||
if(get_parent_class($dataClass) == 'DataObject') {
|
|
||||||
self::$_cache_has_own_table[$dataClass] = true;
|
|
||||||
} else {
|
|
||||||
self::$_cache_has_own_table[$dataClass]
|
|
||||||
= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
|
|
||||||
|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return self::$_cache_has_own_table[$dataClass];
|
$fields = static::database_fields($dataClass);
|
||||||
|
return !empty($fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3263,12 +3187,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
* Reset all global caches associated with DataObject.
|
* Reset all global caches associated with DataObject.
|
||||||
*/
|
*/
|
||||||
public static function reset() {
|
public static function reset() {
|
||||||
|
// @todo Decouple these
|
||||||
DBClassName::clear_classname_cache();
|
DBClassName::clear_classname_cache();
|
||||||
|
ClassInfo::reset_db_cache();
|
||||||
|
static::getSchema()->reset();
|
||||||
self::$_cache_has_own_table = array();
|
self::$_cache_has_own_table = array();
|
||||||
self::$_cache_get_one = array();
|
self::$_cache_get_one = array();
|
||||||
self::$_cache_composite_fields = array();
|
|
||||||
self::$_cache_database_fields = array();
|
|
||||||
self::$_cache_get_class_ancestry = array();
|
|
||||||
self::$_cache_field_labels = array();
|
self::$_cache_field_labels = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3286,25 +3210,27 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
|
user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check filter column
|
// Pass to get_one
|
||||||
if(is_subclass_of($callerClass, 'DataObject')) {
|
$column = static::getSchema()->sqlColumnForField($callerClass, 'ID');
|
||||||
$baseClass = ClassInfo::baseDataClass($callerClass);
|
|
||||||
$column = "\"$baseClass\".\"ID\"";
|
|
||||||
} else{
|
|
||||||
// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
|
|
||||||
$column = '"ID"';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Relegate to get_one
|
|
||||||
return DataObject::get_one($callerClass, array($column => $id), $cache);
|
return DataObject::get_one($callerClass, array($column => $id), $cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of the base table for this object
|
* Get the name of the base table for this object
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function baseTable() {
|
public function baseTable() {
|
||||||
$tableClasses = ClassInfo::dataClassesFor($this->class);
|
return static::getSchema()->baseDataTable($this);
|
||||||
return array_shift($tableClasses);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base class for this object
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function baseClass() {
|
||||||
|
return static::getSchema()->baseDataClass($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3403,19 +3329,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
public function requireTable() {
|
public function requireTable() {
|
||||||
// Only build the table if we've actually got fields
|
// Only build the table if we've actually got fields
|
||||||
$fields = self::database_fields($this->class);
|
$fields = self::database_fields($this->class);
|
||||||
|
$table = static::getSchema()->tableName($this->class);
|
||||||
$extensions = self::database_extensions($this->class);
|
$extensions = self::database_extensions($this->class);
|
||||||
|
|
||||||
$indexes = $this->databaseIndexes();
|
$indexes = $this->databaseIndexes();
|
||||||
|
|
||||||
// Validate relationship configuration
|
// Validate relationship configuration
|
||||||
$this->validateModelDefinitions();
|
$this->validateModelDefinitions();
|
||||||
|
|
||||||
if($fields) {
|
if($fields) {
|
||||||
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
|
$hasAutoIncPK = get_parent_class($this) === 'DataObject';
|
||||||
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
|
DB::require_table(
|
||||||
$extensions);
|
$table, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'), $extensions
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
DB::dont_require_table($this->class);
|
DB::dont_require_table($table);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build any child tables for many_many items
|
// Build any child tables for many_many items
|
||||||
@ -3423,9 +3350,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
$extras = $this->uninherited('many_many_extraFields');
|
$extras = $this->uninherited('many_many_extraFields');
|
||||||
foreach($manyMany as $relationship => $childClass) {
|
foreach($manyMany as $relationship => $childClass) {
|
||||||
// Build field list
|
// Build field list
|
||||||
|
if($this->class === $childClass) {
|
||||||
|
$childField = "ChildID";
|
||||||
|
} else {
|
||||||
|
$childTable = $this->getSchema()->tableName($childClass);
|
||||||
|
$childField = "{$childTable}ID";
|
||||||
|
}
|
||||||
$manymanyFields = array(
|
$manymanyFields = array(
|
||||||
"{$this->class}ID" => "Int",
|
"{$table}ID" => "Int",
|
||||||
(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => "Int",
|
$childField => "Int",
|
||||||
);
|
);
|
||||||
if(isset($extras[$relationship])) {
|
if(isset($extras[$relationship])) {
|
||||||
$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
|
$manymanyFields = array_merge($manymanyFields, $extras[$relationship]);
|
||||||
@ -3433,12 +3366,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
|
|
||||||
// Build index list
|
// Build index list
|
||||||
$manymanyIndexes = array(
|
$manymanyIndexes = array(
|
||||||
"{$this->class}ID" => true,
|
"{$table}ID" => true,
|
||||||
(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
|
$childField => true,
|
||||||
);
|
);
|
||||||
|
$manyManyTable = "{$table}_$relationship";
|
||||||
DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
|
DB::require_table($manyManyTable, $manymanyFields, $manymanyIndexes, true, null, $extensions);
|
||||||
$extensions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
362
model/DataObjectSchema.php
Normal file
362
model/DataObjectSchema.php
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use SilverStripe\Framework\Core\Configurable;
|
||||||
|
use SilverStripe\Framework\Core\Injectable;
|
||||||
|
use SilverStripe\Model\FieldType\DBComposite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides dataobject and database schema mapping functionality
|
||||||
|
*/
|
||||||
|
class DataObjectSchema {
|
||||||
|
use Injectable;
|
||||||
|
use Configurable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default separate for table namespaces. Can be set to any string for
|
||||||
|
* databases that do not support some characters.
|
||||||
|
*
|
||||||
|
* Defaults to \ to to conform to 3.x convention.
|
||||||
|
*
|
||||||
|
* @config
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private static $table_namespace_separator = '\\';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of database fields
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $databaseFields = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of composite database field
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $compositeFields = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of table names
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $tableNames = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached table names
|
||||||
|
*/
|
||||||
|
public function reset() {
|
||||||
|
$this->tableNames = [];
|
||||||
|
$this->databaseFields = [];
|
||||||
|
$this->compositeFields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all table names
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getTableNames() {
|
||||||
|
$this->cacheTableNames();
|
||||||
|
return $this->tableNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a DataObject class and a field on that class, determine the appropriate SQL for
|
||||||
|
* selecting / filtering on in a SQL string. Note that $class must be a valid class, not an
|
||||||
|
* arbitrary table.
|
||||||
|
*
|
||||||
|
* The result will be a standard ANSI-sql quoted string in "Table"."Column" format.
|
||||||
|
*
|
||||||
|
* @param string $class Class name (not a table).
|
||||||
|
* @param string $field Name of field that belongs to this class (or a parent class)
|
||||||
|
* @return string The SQL identifier string for the corresponding column for this field
|
||||||
|
*/
|
||||||
|
public function sqlColumnForField($class, $field) {
|
||||||
|
$table = $this->tableForField($class, $field);
|
||||||
|
if(!$table) {
|
||||||
|
throw new InvalidArgumentException("\"{$field}\" is not a field on class \"{$class}\"");
|
||||||
|
}
|
||||||
|
return "\"{$table}\".\"{$field}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get table name for the given class.
|
||||||
|
*
|
||||||
|
* Note that this does not confirm a table actually exists (or should exist), but returns
|
||||||
|
* the name that would be used if this table did exist.
|
||||||
|
*
|
||||||
|
* @param string $class
|
||||||
|
* @return string Returns the table name, or null if there is no table
|
||||||
|
*/
|
||||||
|
public function tableName($class) {
|
||||||
|
$tables = $this->getTableNames();
|
||||||
|
$class = ClassInfo::class_name($class);
|
||||||
|
if(isset($tables[$class])) {
|
||||||
|
return $tables[$class];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the root class (the first to extend from DataObject) for the
|
||||||
|
* passed class.
|
||||||
|
*
|
||||||
|
* @param string|object $class
|
||||||
|
* @return string
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function baseDataClass($class) {
|
||||||
|
$class = ClassInfo::class_name($class);
|
||||||
|
$current = $class;
|
||||||
|
while ($next = get_parent_class($current)) {
|
||||||
|
if ($next === 'DataObject') {
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
$current = $next;
|
||||||
|
}
|
||||||
|
throw new InvalidArgumentException("$class is not a subclass of DataObject");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base table
|
||||||
|
*
|
||||||
|
* @param string|object $class
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function baseDataTable($class) {
|
||||||
|
return $this->tableName($this->baseDataClass($class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the class for the given table
|
||||||
|
*
|
||||||
|
* @param string $table
|
||||||
|
* @return string|null The FQN of the class, or null if not found
|
||||||
|
*/
|
||||||
|
public function tableClass($table) {
|
||||||
|
$tables = $this->getTableNames();
|
||||||
|
$class = array_search($table, $tables, true);
|
||||||
|
if($class) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no class for this table, strip table modifiers (e.g. _Live / _versions)
|
||||||
|
// from the end and re-attempt a search.
|
||||||
|
if(preg_match('/^(?<class>.+)(_[^_]+)$/i', $table, $matches)) {
|
||||||
|
$table = $matches['class'];
|
||||||
|
$class = array_search($table, $tables, true);
|
||||||
|
if($class) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache all table names if necessary
|
||||||
|
*/
|
||||||
|
protected function cacheTableNames() {
|
||||||
|
if($this->tableNames) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->tableNames = [];
|
||||||
|
foreach(ClassInfo::subclassesFor('DataObject') as $class) {
|
||||||
|
if($class === 'DataObject') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$table = $this->buildTableName($class);
|
||||||
|
|
||||||
|
// Check for conflicts
|
||||||
|
$conflict = array_search($table, $this->tableNames, true);
|
||||||
|
if($conflict) {
|
||||||
|
throw new LogicException(
|
||||||
|
"Multiple classes (\"{$class}\", \"{$conflict}\") map to the same table: \"{$table}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->tableNames[$class] = $table;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate table name for a class.
|
||||||
|
*
|
||||||
|
* Note: some DB schema have a hard limit on table name length. This is not enforced by this method.
|
||||||
|
* See dev/build errors for details in case of table name violation.
|
||||||
|
*
|
||||||
|
* @param string $class
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function buildTableName($class) {
|
||||||
|
$table = Config::inst()->get($class, 'table_name', Config::UNINHERITED);
|
||||||
|
|
||||||
|
// Generate default table name
|
||||||
|
if(!$table) {
|
||||||
|
$separator = $this->config()->table_namespace_separator;
|
||||||
|
$table = str_replace('\\', $separator, trim($class, '\\'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the complete map of fields to specification on this object, including fixed_fields.
|
||||||
|
* "ID" will be included on every table.
|
||||||
|
*
|
||||||
|
* @param string $class Class name to query from
|
||||||
|
* @return array Map of fieldname to specification, similiar to {@link DataObject::$db}.
|
||||||
|
*/
|
||||||
|
public function databaseFields($class) {
|
||||||
|
$class = ClassInfo::class_name($class);
|
||||||
|
if($class === 'DataObject') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$this->cacheDatabaseFields($class);
|
||||||
|
return $this->databaseFields[$class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all the composite if the given db field on the class is a composite field.
|
||||||
|
* Will check all applicable ancestor classes and aggregate results.
|
||||||
|
*
|
||||||
|
* Can be called directly on an object. E.g. Member::composite_fields(), or Member::composite_fields(null, true)
|
||||||
|
* to aggregate.
|
||||||
|
*
|
||||||
|
* Includes composite has_one (Polymorphic) fields
|
||||||
|
*
|
||||||
|
* @param string $class Name of class to check
|
||||||
|
* @param bool $aggregated Include fields in entire hierarchy, rather than just on this table
|
||||||
|
* @return array List of composite fields and their class spec
|
||||||
|
*/
|
||||||
|
public function compositeFields($class, $aggregated = true) {
|
||||||
|
$class = ClassInfo::class_name($class);
|
||||||
|
if($class === 'DataObject') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$this->cacheDatabaseFields($class);
|
||||||
|
|
||||||
|
// Get fields for this class
|
||||||
|
$compositeFields = $this->compositeFields[$class];
|
||||||
|
if(!$aggregated) {
|
||||||
|
return $compositeFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively merge
|
||||||
|
$parentFields = $this->compositeFields(get_parent_class($class));
|
||||||
|
return array_merge($compositeFields, $parentFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache all database and composite fields for the given class.
|
||||||
|
* Will do nothing if already cached
|
||||||
|
*
|
||||||
|
* @param string $class Class name to cache
|
||||||
|
*/
|
||||||
|
protected function cacheDatabaseFields($class) {
|
||||||
|
// Skip if already cached
|
||||||
|
if (isset($this->databaseFields[$class]) && isset($this->compositeFields[$class])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$compositeFields = array();
|
||||||
|
$dbFields = array();
|
||||||
|
|
||||||
|
// Ensure fixed fields appear at the start
|
||||||
|
$fixedFields = DataObject::config()->fixed_fields;
|
||||||
|
if(get_parent_class($class) === 'DataObject') {
|
||||||
|
// Merge fixed with ClassName spec and custom db fields
|
||||||
|
$dbFields = $fixedFields;
|
||||||
|
} else {
|
||||||
|
$dbFields['ID'] = $fixedFields['ID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each DB value as either a field or composite field
|
||||||
|
$db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array();
|
||||||
|
foreach($db as $fieldName => $fieldSpec) {
|
||||||
|
$fieldClass = strtok($fieldSpec, '(');
|
||||||
|
if(singleton($fieldClass) instanceof DBComposite) {
|
||||||
|
$compositeFields[$fieldName] = $fieldSpec;
|
||||||
|
} else {
|
||||||
|
$dbFields[$fieldName] = $fieldSpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in all has_ones
|
||||||
|
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array();
|
||||||
|
foreach($hasOne as $fieldName => $hasOneClass) {
|
||||||
|
if($hasOneClass === 'DataObject') {
|
||||||
|
$compositeFields[$fieldName] = 'PolymorphicForeignKey';
|
||||||
|
} else {
|
||||||
|
$dbFields["{$fieldName}ID"] = 'ForeignKey';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge composite fields into DB
|
||||||
|
foreach($compositeFields as $fieldName => $fieldSpec) {
|
||||||
|
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
|
||||||
|
$fieldObj->setTable($class);
|
||||||
|
$nestedFields = $fieldObj->compositeDatabaseFields();
|
||||||
|
foreach($nestedFields as $nestedName => $nestedSpec) {
|
||||||
|
$dbFields["{$fieldName}{$nestedName}"] = $nestedSpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent field-less tables
|
||||||
|
if(count($dbFields) < 2) {
|
||||||
|
$dbFields = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return cached results
|
||||||
|
$this->databaseFields[$class] = $dbFields;
|
||||||
|
$this->compositeFields[$class] = $compositeFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the table name in the class hierarchy which contains a given
|
||||||
|
* field column for a {@link DataObject}. If the field does not exist, this
|
||||||
|
* will return null.
|
||||||
|
*
|
||||||
|
* @param string $candidateClass
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function tableForField($candidateClass, $fieldName) {
|
||||||
|
$class = $this->classForField($candidateClass, $fieldName);
|
||||||
|
if($class) {
|
||||||
|
return $this->tableName($class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the class name in the class hierarchy which contains a given
|
||||||
|
* field column for a {@link DataObject}. If the field does not exist, this
|
||||||
|
* will return null.
|
||||||
|
*
|
||||||
|
* @param string $candidateClass
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function classForField($candidateClass, $fieldName) {
|
||||||
|
// normalise class name
|
||||||
|
$candidateClass = ClassInfo::class_name($candidateClass);
|
||||||
|
if($candidateClass === 'DataObject') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short circuit for fixed fields
|
||||||
|
$fixed = DataObject::config()->fixed_fields;
|
||||||
|
if(isset($fixed[$fieldName])) {
|
||||||
|
return $this->baseDataClass($candidateClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find regular field
|
||||||
|
while($candidateClass) {
|
||||||
|
$fields = $this->databaseFields($candidateClass);
|
||||||
|
if(isset($fields[$fieldName])) {
|
||||||
|
return $candidateClass;
|
||||||
|
}
|
||||||
|
$candidateClass = get_parent_class($candidateClass);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,16 @@ class DataQuery {
|
|||||||
protected $query;
|
protected $query;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Map of all field names to an array of conflicting column SQL
|
||||||
|
*
|
||||||
|
* E.g.
|
||||||
|
* array(
|
||||||
|
* 'Title' => array(
|
||||||
|
* '"MyTable"."Title"',
|
||||||
|
* '"AnotherTable"."Title"',
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $collidingFields = array();
|
protected $collidingFields = array();
|
||||||
@ -31,7 +41,7 @@ class DataQuery {
|
|||||||
private $queriedColumns = null;
|
private $queriedColumns = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Boolean
|
* @var bool
|
||||||
*/
|
*/
|
||||||
private $queryFinalised = false;
|
private $queryFinalised = false;
|
||||||
|
|
||||||
@ -131,15 +141,12 @@ class DataQuery {
|
|||||||
* Set up the simplest initial query
|
* Set up the simplest initial query
|
||||||
*/
|
*/
|
||||||
protected function initialiseQuery() {
|
protected function initialiseQuery() {
|
||||||
// Get the tables to join to.
|
// Join on base table and let lazy loading join subtables
|
||||||
// Don't get any subclass tables - let lazy loading do that.
|
$baseClass = DataObject::getSchema()->baseDataClass($this->dataClass());
|
||||||
$tableClasses = ClassInfo::ancestry($this->dataClass, true);
|
if(!$baseClass) {
|
||||||
if(!$tableClasses) {
|
|
||||||
throw new InvalidArgumentException("DataQuery::create() Can't find data classes for '{$this->dataClass}'");
|
throw new InvalidArgumentException("DataQuery::create() Can't find data classes for '{$this->dataClass}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseClass = array_shift($tableClasses);
|
|
||||||
|
|
||||||
// Build our intial query
|
// Build our intial query
|
||||||
$this->query = new SQLSelect(array());
|
$this->query = new SQLSelect(array());
|
||||||
$this->query->setDistinct(true);
|
$this->query->setDistinct(true);
|
||||||
@ -148,7 +155,8 @@ class DataQuery {
|
|||||||
$this->sort($sort);
|
$this->sort($sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->query->setFrom("\"$baseClass\"");
|
$baseTable = DataObject::getSchema()->tableName($baseClass);
|
||||||
|
$this->query->setFrom("\"{$baseTable}\"");
|
||||||
|
|
||||||
$obj = Injector::inst()->get($baseClass);
|
$obj = Injector::inst()->get($baseClass);
|
||||||
$obj->extend('augmentDataQueryCreation', $this->query, $this);
|
$obj->extend('augmentDataQueryCreation', $this->query, $this);
|
||||||
@ -165,13 +173,18 @@ class DataQuery {
|
|||||||
* @return SQLSelect The finalised sql query
|
* @return SQLSelect The finalised sql query
|
||||||
*/
|
*/
|
||||||
public function getFinalisedQuery($queriedColumns = null) {
|
public function getFinalisedQuery($queriedColumns = null) {
|
||||||
if(!$queriedColumns) $queriedColumns = $this->queriedColumns;
|
if(!$queriedColumns) {
|
||||||
|
$queriedColumns = $this->queriedColumns;
|
||||||
|
}
|
||||||
if($queriedColumns) {
|
if($queriedColumns) {
|
||||||
$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
|
$queriedColumns = array_merge($queriedColumns, array('Created', 'LastEdited', 'ClassName'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
$query = clone $this->query;
|
$query = clone $this->query;
|
||||||
$ancestorTables = ClassInfo::ancestry($this->dataClass, true);
|
$baseDataClass = $schema->baseDataClass($this->dataClass());
|
||||||
|
$baseIDColumn = $schema->sqlColumnForField($baseDataClass, 'ID');
|
||||||
|
$ancestorClasses = ClassInfo::ancestry($this->dataClass(), true);
|
||||||
|
|
||||||
// Generate the list of tables to iterate over and the list of columns required
|
// Generate the list of tables to iterate over and the list of columns required
|
||||||
// by any existing where clauses. This second step is skipped if we're fetching
|
// by any existing where clauses. This second step is skipped if we're fetching
|
||||||
@ -180,20 +193,25 @@ class DataQuery {
|
|||||||
// Specifying certain columns allows joining of child tables
|
// Specifying certain columns allows joining of child tables
|
||||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
||||||
|
|
||||||
|
// Ensure that any filtered columns are included in the selected columns
|
||||||
foreach ($query->getWhereParameterised($parameters) as $where) {
|
foreach ($query->getWhereParameterised($parameters) as $where) {
|
||||||
// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ?
|
// Check for any columns in the form '"Column" = ?' or '"Table"."Column"' = ?
|
||||||
if (preg_match('/^"([^"]+)"/', $where, $matches) ||
|
if(preg_match_all(
|
||||||
preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) {
|
'/(?:"(?<table>[^"]+)"\.)?"(?<column>[^"]+)"(?:[^\.]|$)/',
|
||||||
if (!in_array($matches[1], $queriedColumns)) $queriedColumns[] = $matches[1];
|
$where, $matches, PREG_SET_ORDER
|
||||||
|
)) {
|
||||||
|
foreach($matches as $match) {
|
||||||
|
$column = $match['column'];
|
||||||
|
if (!in_array($column, $queriedColumns)) {
|
||||||
|
$queriedColumns[] = $column;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$tableClasses = $ancestorTables;
|
$tableClasses = $ancestorClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tableNames = array_values($tableClasses);
|
|
||||||
$baseClass = $tableNames[0];
|
|
||||||
|
|
||||||
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
|
// Iterate over the tables and check what we need to select from them. If any selects are made (or the table is
|
||||||
// required for a select)
|
// required for a select)
|
||||||
foreach($tableClasses as $tableClass) {
|
foreach($tableClasses as $tableClass) {
|
||||||
@ -208,7 +226,9 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this is a subclass without any explicitly requested columns, omit this from the query
|
// If this is a subclass without any explicitly requested columns, omit this from the query
|
||||||
if(!in_array($tableClass, $ancestorTables) && empty($selectColumns)) continue;
|
if(!in_array($tableClass, $ancestorClasses) && empty($selectColumns)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Select necessary columns (unless an explicitly empty array)
|
// Select necessary columns (unless an explicitly empty array)
|
||||||
if($selectColumns !== array()) {
|
if($selectColumns !== array()) {
|
||||||
@ -216,51 +236,61 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Join if not the base table
|
// Join if not the base table
|
||||||
if($tableClass !== $baseClass) {
|
if($tableClass !== $baseDataClass) {
|
||||||
$query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"", $tableClass, 10);
|
$tableName = $schema->tableName($tableClass);
|
||||||
|
$query->addLeftJoin(
|
||||||
|
$tableName,
|
||||||
|
"\"{$tableName}\".\"ID\" = {$baseIDColumn}",
|
||||||
|
$tableName,
|
||||||
|
10
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Resolve colliding fields
|
// Resolve colliding fields
|
||||||
if($this->collidingFields) {
|
if($this->collidingFields) {
|
||||||
foreach($this->collidingFields as $k => $collisions) {
|
foreach($this->collidingFields as $collisionField => $collisions) {
|
||||||
$caseClauses = array();
|
$caseClauses = array();
|
||||||
foreach($collisions as $collision) {
|
foreach($collisions as $collision) {
|
||||||
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
|
if(preg_match('/^"(?<table>[^"]+)"\./', $collision, $matches)) {
|
||||||
$collisionBase = $matches[1];
|
$collisionTable = $matches['table'];
|
||||||
if(class_exists($collisionBase)) {
|
$collisionClass = $schema->tableClass($collisionTable);
|
||||||
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
|
if($collisionClass) {
|
||||||
$collisionClasses = Convert::raw2sql($collisionClasses, true);
|
$collisionClassColumn = $schema->sqlColumnForField($collisionClass, 'ClassName');
|
||||||
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ("
|
$collisionClasses = ClassInfo::subclassesFor($collisionClass);
|
||||||
. implode(", ", $collisionClasses) . ") THEN $collision";
|
$collisionClassesSQL = implode(', ', Convert::raw2sql($collisionClasses, true));
|
||||||
|
$caseClauses[] = "WHEN {$collisionClassColumn} IN ({$collisionClassesSQL}) THEN $collision";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user_error("Bad collision item '$collision'", E_USER_WARNING);
|
user_error("Bad collision item '$collision'", E_USER_WARNING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $k);
|
$query->selectField("CASE " . implode( " ", $caseClauses) . " ELSE NULL END", $collisionField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if($this->filterByClassName) {
|
if($this->filterByClassName) {
|
||||||
// If querying the base class, don't bother filtering on class name
|
// If querying the base class, don't bother filtering on class name
|
||||||
if($this->dataClass != $baseClass) {
|
if($this->dataClass != $baseDataClass) {
|
||||||
// Get the ClassName values to filter to
|
// Get the ClassName values to filter to
|
||||||
$classNames = ClassInfo::subclassesFor($this->dataClass);
|
$classNames = ClassInfo::subclassesFor($this->dataClass);
|
||||||
$classNamesPlaceholders = DB::placeholders($classNames);
|
$classNamesPlaceholders = DB::placeholders($classNames);
|
||||||
|
$baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName');
|
||||||
$query->addWhere(array(
|
$query->addWhere(array(
|
||||||
"\"$baseClass\".\"ClassName\" IN ($classNamesPlaceholders)" => $classNames
|
"{$baseClassColumn} IN ($classNamesPlaceholders)" => $classNames
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->selectField("\"$baseClass\".\"ID\"", "ID");
|
// Select ID
|
||||||
|
$query->selectField($baseIDColumn, "ID");
|
||||||
|
|
||||||
|
// Select RecordClassName
|
||||||
|
$baseClassColumn = $schema->sqlColumnForField($baseDataClass, 'ClassName');
|
||||||
$query->selectField("
|
$query->selectField("
|
||||||
CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\"
|
CASE WHEN {$baseClassColumn} IS NOT NULL THEN {$baseClassColumn}
|
||||||
ELSE ".Convert::raw2sql($baseClass, true)." END",
|
ELSE ".Convert::raw2sql($baseDataClass, true)." END",
|
||||||
"RecordClassName"
|
"RecordClassName"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -283,9 +313,6 @@ class DataQuery {
|
|||||||
* @return null
|
* @return null
|
||||||
*/
|
*/
|
||||||
protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) {
|
protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) {
|
||||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
|
||||||
$baseClass = array_shift($tableClasses);
|
|
||||||
|
|
||||||
if($orderby = $query->getOrderBy()) {
|
if($orderby = $query->getOrderBy()) {
|
||||||
$newOrderby = array();
|
$newOrderby = array();
|
||||||
$i = 0;
|
$i = 0;
|
||||||
@ -309,10 +336,9 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(count($parts) == 1) {
|
if(count($parts) == 1) {
|
||||||
|
// Get expression for sort value
|
||||||
if(DataObject::has_own_table_database_field($baseClass, $parts[0])) {
|
$qualCol = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $parts[0]);;
|
||||||
$qualCol = "\"$baseClass\".\"{$parts[0]}\"";
|
if(!$qualCol) {
|
||||||
} else {
|
|
||||||
$qualCol = "\"$parts[0]\"";
|
$qualCol = "\"$parts[0]\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,10 +395,12 @@ class DataQuery {
|
|||||||
/**
|
/**
|
||||||
* Return the number of records in this query.
|
* Return the number of records in this query.
|
||||||
* Note that this will issue a separate SELECT COUNT() query.
|
* Note that this will issue a separate SELECT COUNT() query.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function count() {
|
public function count() {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
$quotedColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
|
||||||
return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\"");
|
return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -450,7 +478,7 @@ class DataQuery {
|
|||||||
* Update the SELECT clause of the query with the columns from the given table
|
* Update the SELECT clause of the query with the columns from the given table
|
||||||
*
|
*
|
||||||
* @param SQLSelect $query
|
* @param SQLSelect $query
|
||||||
* @param string $tableClass
|
* @param string $tableClass Class to select from
|
||||||
* @param array $columns
|
* @param array $columns
|
||||||
*/
|
*/
|
||||||
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
|
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
|
||||||
@ -461,19 +489,23 @@ class DataQuery {
|
|||||||
foreach($databaseFields as $k => $v) {
|
foreach($databaseFields as $k => $v) {
|
||||||
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
|
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
|
||||||
// Update $collidingFields if necessary
|
// Update $collidingFields if necessary
|
||||||
if($expressionForField = $query->expressionForField($k)) {
|
$expressionForField = $query->expressionForField($k);
|
||||||
if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($expressionForField);
|
$quotedField = DataObject::getSchema()->sqlColumnForField($tableClass, $k);
|
||||||
$this->collidingFields[$k][] = "\"$tableClass\".\"$k\"";
|
if($expressionForField) {
|
||||||
|
if(!isset($this->collidingFields[$k])) {
|
||||||
|
$this->collidingFields[$k] = array($expressionForField);
|
||||||
|
}
|
||||||
|
$this->collidingFields[$k][] = $quotedField;
|
||||||
} else {
|
} else {
|
||||||
$query->selectField("\"$tableClass\".\"$k\"", $k);
|
$query->selectField($quotedField, $k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach($compositeFields as $k => $v) {
|
foreach($compositeFields as $k => $v) {
|
||||||
if((is_null($columns) || in_array($k, $columns)) && $v) {
|
if((is_null($columns) || in_array($k, $columns)) && $v) {
|
||||||
|
$tableName = DataObject::getSchema()->tableName($tableClass);
|
||||||
$dbO = Object::create_from_string($v, $k);
|
$dbO = Object::create_from_string($v, $k);
|
||||||
$dbO->setTable($tableClass);
|
$dbO->setTable($tableName);
|
||||||
$dbO->addToQuery($query);
|
$dbO->addToQuery($query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -724,18 +756,19 @@ class DataQuery {
|
|||||||
"Could not join polymorphic has_one relationship {$localField} on {$localClass}"
|
"Could not join polymorphic has_one relationship {$localField} on {$localClass}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
// Skip if already joined
|
// Skip if already joined
|
||||||
if($this->query->isJoinedTo($foreignClass)) {
|
$foreignBaseClass = $schema->baseDataClass($foreignClass);
|
||||||
|
$foreignBaseTable = $schema->tableName($foreignBaseClass);
|
||||||
|
if($this->query->isJoinedTo($foreignBaseTable)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$realModelClass = ClassInfo::table_for_object_field($localClass, "{$localField}ID");
|
// Join base table
|
||||||
$foreignBase = ClassInfo::baseDataClass($foreignClass);
|
$foreignIDColumn = $schema->sqlColumnForField($foreignBaseClass, 'ID');
|
||||||
$this->query->addLeftJoin(
|
$localColumn = $schema->sqlColumnForField($localClass, "{$localField}ID");
|
||||||
$foreignBase,
|
$this->query->addLeftJoin($foreignBaseTable, "{$foreignIDColumn} = {$localColumn}");
|
||||||
"\"$foreignBase\".\"ID\" = \"{$realModelClass}\".\"{$localField}ID\""
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add join clause to the component's ancestry classes so that the search filter could search on
|
* add join clause to the component's ancestry classes so that the search filter could search on
|
||||||
@ -745,8 +778,9 @@ class DataQuery {
|
|||||||
if(!empty($ancestry)){
|
if(!empty($ancestry)){
|
||||||
$ancestry = array_reverse($ancestry);
|
$ancestry = array_reverse($ancestry);
|
||||||
foreach($ancestry as $ancestor){
|
foreach($ancestry as $ancestor){
|
||||||
if($ancestor != $foreignBase) {
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
$this->query->addLeftJoin($ancestor, "\"$foreignBase\".\"ID\" = \"$ancestor\".\"ID\"");
|
if($ancestorTable !== $foreignBaseTable) {
|
||||||
|
$this->query->addLeftJoin($ancestorTable, "{$foreignIDColumn} = \"{$ancestorTable}\".\"ID\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -765,28 +799,30 @@ class DataQuery {
|
|||||||
if(!$foreignClass || $foreignClass === 'DataObject') {
|
if(!$foreignClass || $foreignClass === 'DataObject') {
|
||||||
throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}");
|
throw new InvalidArgumentException("Could not find a has_many relationship {$localField} on {$localClass}");
|
||||||
}
|
}
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
// Skip if already joined
|
// Skip if already joined
|
||||||
if($this->query->isJoinedTo($foreignClass)) {
|
$foreignTable = $schema->tableName($foreignClass);
|
||||||
|
if($this->query->isJoinedTo($foreignTable)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join table with associated has_one
|
// Join table with associated has_one
|
||||||
/** @var DataObject $model */
|
/** @var DataObject $model */
|
||||||
$model = singleton($localClass);
|
$model = singleton($localClass);
|
||||||
$ancestry = $model->getClassAncestry();
|
|
||||||
$foreignKey = $model->getRemoteJoinField($localField, 'has_many', $polymorphic);
|
$foreignKey = $model->getRemoteJoinField($localField, 'has_many', $polymorphic);
|
||||||
|
$localIDColumn = $schema->sqlColumnForField($localClass, 'ID');
|
||||||
if($polymorphic) {
|
if($polymorphic) {
|
||||||
|
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}ID");
|
||||||
|
$foreignKeyClassColumn = $schema->sqlColumnForField($foreignClass, "{$foreignKey}Class");
|
||||||
|
$localClassColumn = $schema->sqlColumnForField($localClass, 'ClassName');
|
||||||
$this->query->addLeftJoin(
|
$this->query->addLeftJoin(
|
||||||
$foreignClass,
|
$foreignTable,
|
||||||
"\"$foreignClass\".\"{$foreignKey}ID\" = \"{$ancestry[0]}\".\"ID\" AND "
|
"{$foreignKeyIDColumn} = {$localIDColumn} AND {$foreignKeyClassColumn} = {$localClassColumn}"
|
||||||
. "\"$foreignClass\".\"{$foreignKey}Class\" = \"{$ancestry[0]}\".\"ClassName\""
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$this->query->addLeftJoin(
|
$foreignKeyIDColumn = $schema->sqlColumnForField($foreignClass, $foreignKey);
|
||||||
$foreignClass,
|
$this->query->addLeftJoin($foreignTable, "{$foreignKeyIDColumn} = {$localIDColumn}");
|
||||||
"\"$foreignClass\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -795,9 +831,10 @@ class DataQuery {
|
|||||||
*/
|
*/
|
||||||
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
$ancestry = ClassInfo::ancestry($foreignClass, true);
|
||||||
$ancestry = array_reverse($ancestry);
|
$ancestry = array_reverse($ancestry);
|
||||||
foreach($ancestry as $ancestor){
|
foreach($ancestry as $ancestor) {
|
||||||
if($ancestor != $foreignClass){
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
$this->query->addInnerJoin($ancestor, "\"$foreignClass\".\"ID\" = \"$ancestor\".\"ID\"");
|
if($ancestorTable !== $foreignTable) {
|
||||||
|
$this->query->addInnerJoin($ancestorTable, "\"{$foreignTable}\".\"ID\" = \"{$ancestorTable}\".\"ID\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -812,16 +849,23 @@ class DataQuery {
|
|||||||
* @param string $relationTable Name of relation table
|
* @param string $relationTable Name of relation table
|
||||||
*/
|
*/
|
||||||
protected function joinManyManyRelationship($parentClass, $componentClass, $parentField, $componentField, $relationTable) {
|
protected function joinManyManyRelationship($parentClass, $componentClass, $parentField, $componentField, $relationTable) {
|
||||||
$parentBaseClass = ClassInfo::baseDataClass($parentClass);
|
$schema = DataObject::getSchema();
|
||||||
$componentBaseClass = ClassInfo::baseDataClass($componentClass);
|
|
||||||
|
// Join on parent table
|
||||||
|
$parentIDColumn = $schema->sqlColumnForField($parentClass, 'ID');
|
||||||
$this->query->addLeftJoin(
|
$this->query->addLeftJoin(
|
||||||
$relationTable,
|
$relationTable,
|
||||||
"\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""
|
"\"$relationTable\".\"$parentField\" = {$parentIDColumn}"
|
||||||
);
|
);
|
||||||
if (!$this->query->isJoinedTo($componentBaseClass)) {
|
|
||||||
|
// Join on base table of component class
|
||||||
|
$componentBaseClass = $schema->baseDataClass($componentClass);
|
||||||
|
$componentBaseTable = $schema->tableName($componentBaseClass);
|
||||||
|
$componentIDColumn = $schema->sqlColumnForField($componentBaseClass, 'ID');
|
||||||
|
if (!$this->query->isJoinedTo($componentBaseTable)) {
|
||||||
$this->query->addLeftJoin(
|
$this->query->addLeftJoin(
|
||||||
$componentBaseClass,
|
$componentBaseTable,
|
||||||
"\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""
|
"\"$relationTable\".\"$componentField\" = {$componentIDColumn}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -831,9 +875,10 @@ class DataQuery {
|
|||||||
*/
|
*/
|
||||||
$ancestry = ClassInfo::ancestry($componentClass, true);
|
$ancestry = ClassInfo::ancestry($componentClass, true);
|
||||||
$ancestry = array_reverse($ancestry);
|
$ancestry = array_reverse($ancestry);
|
||||||
foreach($ancestry as $ancestor){
|
foreach($ancestry as $ancestor) {
|
||||||
if($ancestor != $componentBaseClass && !$this->query->isJoinedTo($ancestor)){
|
$ancestorTable = $schema->tableName($ancestor);
|
||||||
$this->query->addInnerJoin($ancestor, "\"$componentBaseClass\".\"ID\" = \"$ancestor\".\"ID\"");
|
if($ancestorTable != $componentBaseTable && !$this->query->isJoinedTo($ancestorTable)) {
|
||||||
|
$this->query->addLeftJoin($ancestorTable, "{$componentIDColumn} = \"{$ancestorTable}\".\"ID\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -866,7 +911,7 @@ class DataQuery {
|
|||||||
*/
|
*/
|
||||||
public function selectFromTable($table, $fields) {
|
public function selectFromTable($table, $fields) {
|
||||||
$fieldExpressions = array_map(function($item) use($table) {
|
$fieldExpressions = array_map(function($item) use($table) {
|
||||||
return "\"$table\".\"$item\"";
|
return "\"{$table}\".\"{$item}\"";
|
||||||
}, $fields);
|
}, $fields);
|
||||||
|
|
||||||
$this->query->setSelect($fieldExpressions);
|
$this->query->setSelect($fieldExpressions);
|
||||||
@ -908,7 +953,7 @@ class DataQuery {
|
|||||||
|
|
||||||
// Special case for ID, if not provided
|
// Special case for ID, if not provided
|
||||||
if($field === 'ID') {
|
if($field === 'ID') {
|
||||||
return DataObject::quoted_column('ID', $this->dataClass);
|
return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -91,13 +91,14 @@ class DBClassName extends DBEnum {
|
|||||||
return $this->baseClass;
|
return $this->baseClass;
|
||||||
}
|
}
|
||||||
// Default to the basename of the record
|
// Default to the basename of the record
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
if($this->record) {
|
if($this->record) {
|
||||||
return ClassInfo::baseDataClass($this->record);
|
return $schema->baseDataClass($this->record);
|
||||||
}
|
}
|
||||||
// During dev/build only the table is assigned
|
// During dev/build only the table is assigned
|
||||||
$tableClass = $this->getClassNameFromTable($this->getTable());
|
$tableClass = $schema->tableClass($this->getTable());
|
||||||
if($tableClass) {
|
if($tableClass && ($baseClass = $schema->baseDataClass($tableClass))) {
|
||||||
return $tableClass;
|
return $baseClass;
|
||||||
}
|
}
|
||||||
// Fallback to global default
|
// Fallback to global default
|
||||||
return 'DataObject';
|
return 'DataObject';
|
||||||
@ -114,28 +115,6 @@ class DBClassName extends DBEnum {
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a table name, find the base data class
|
|
||||||
*
|
|
||||||
* @param string $table
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
protected function getClassNameFromTable($table) {
|
|
||||||
if(empty($table)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$class = ClassInfo::baseDataClass($table);
|
|
||||||
if($class) {
|
|
||||||
return $class;
|
|
||||||
}
|
|
||||||
// If there is no class for this table, strip table modifiers (_Live / _versions) off the end
|
|
||||||
if(preg_match('/^(?<class>.+)(_[^_]+)$/i', $table, $matches)) {
|
|
||||||
return $this->getClassNameFromTable($matches['class']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of classnames that should be selectable
|
* Get list of classnames that should be selectable
|
||||||
*
|
*
|
||||||
|
@ -35,15 +35,18 @@ class HasManyList extends RelationList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected function foreignIDFilter($id = null) {
|
protected function foreignIDFilter($id = null) {
|
||||||
if ($id === null) $id = $this->getForeignID();
|
if ($id === null) {
|
||||||
|
$id = $this->getForeignID();
|
||||||
|
}
|
||||||
|
|
||||||
// Apply relation filter
|
// Apply relation filter
|
||||||
$key = "\"$this->foreignKey\"";
|
$key = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $this->getForeignKey());
|
||||||
if(is_array($id)) {
|
if(is_array($id)) {
|
||||||
return array("$key IN (".DB::placeholders($id).")" => $id);
|
return array("$key IN (".DB::placeholders($id).")" => $id);
|
||||||
} else if($id !== null){
|
} else if($id !== null){
|
||||||
return array($key => $id);
|
return array($key => $id);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -449,32 +449,32 @@ class Hierarchy extends DataExtension {
|
|||||||
* Mark this DataObject as expanded.
|
* Mark this DataObject as expanded.
|
||||||
*/
|
*/
|
||||||
public function markExpanded() {
|
public function markExpanded() {
|
||||||
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
|
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
|
||||||
self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
|
self::$expanded[$this->owner->baseClass()][$this->owner->ID] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this DataObject as unexpanded.
|
* Mark this DataObject as unexpanded.
|
||||||
*/
|
*/
|
||||||
public function markUnexpanded() {
|
public function markUnexpanded() {
|
||||||
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
|
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
|
||||||
self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
|
self::$expanded[$this->owner->baseClass()][$this->owner->ID] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this DataObject's tree as opened.
|
* Mark this DataObject's tree as opened.
|
||||||
*/
|
*/
|
||||||
public function markOpened() {
|
public function markOpened() {
|
||||||
self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
|
self::$marked[$this->owner->baseClass()][$this->owner->ID] = true;
|
||||||
self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
|
self::$treeOpened[$this->owner->baseClass()][$this->owner->ID] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this DataObject's tree as closed.
|
* Mark this DataObject's tree as closed.
|
||||||
*/
|
*/
|
||||||
public function markClosed() {
|
public function markClosed() {
|
||||||
if(isset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID])) {
|
if(isset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID])) {
|
||||||
unset(self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID]);
|
unset(self::$treeOpened[$this->owner->baseClass()][$this->owner->ID]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,7 +484,7 @@ class Hierarchy extends DataExtension {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isMarked() {
|
public function isMarked() {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
$id = $this->owner->ID;
|
$id = $this->owner->ID;
|
||||||
return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
|
return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
|
||||||
}
|
}
|
||||||
@ -495,7 +495,7 @@ class Hierarchy extends DataExtension {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isExpanded() {
|
public function isExpanded() {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
$id = $this->owner->ID;
|
$id = $this->owner->ID;
|
||||||
return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
|
return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
|
||||||
}
|
}
|
||||||
@ -506,7 +506,7 @@ class Hierarchy extends DataExtension {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isTreeOpened() {
|
public function isTreeOpened() {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
$id = $this->owner->ID;
|
$id = $this->owner->ID;
|
||||||
return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
|
return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
|
||||||
}
|
}
|
||||||
@ -593,7 +593,7 @@ class Hierarchy extends DataExtension {
|
|||||||
public function doAllChildrenIncludingDeleted($context = null) {
|
public function doAllChildrenIncludingDeleted($context = null) {
|
||||||
if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
|
if(!$this->owner) user_error('Hierarchy::doAllChildrenIncludingDeleted() called without $this->owner');
|
||||||
|
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
if($baseClass) {
|
if($baseClass) {
|
||||||
$stageChildren = $this->owner->stageChildren(true);
|
$stageChildren = $this->owner->stageChildren(true);
|
||||||
|
|
||||||
@ -630,9 +630,13 @@ class Hierarchy extends DataExtension {
|
|||||||
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
|
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseClass=ClassInfo::baseDataClass($this->owner->class);
|
$baseTable = $this->owner->baseTable();
|
||||||
return Versioned::get_including_deleted($baseClass,
|
$parentIDColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'ParentID');
|
||||||
"\"ParentID\" = " . (int)$this->owner->ID, "\"$baseClass\".\"ID\" ASC");
|
return Versioned::get_including_deleted(
|
||||||
|
$this->owner->baseClass(),
|
||||||
|
[ $parentIDColumn => $this->owner->ID ],
|
||||||
|
"\"{$baseTable}\".\"ID\" ASC"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -646,8 +650,7 @@ class Hierarchy extends DataExtension {
|
|||||||
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
|
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
|
return $this->AllHistoricalChildren()->count();
|
||||||
"\"ParentID\" = " . (int)$this->owner->ID)->count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -689,7 +692,7 @@ class Hierarchy extends DataExtension {
|
|||||||
* @return DataList
|
* @return DataList
|
||||||
*/
|
*/
|
||||||
public function stageChildren($showAll = false) {
|
public function stageChildren($showAll = false) {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
|
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
|
||||||
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
|
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
|
||||||
$staged = $baseClass::get()
|
$staged = $baseClass::get()
|
||||||
@ -722,7 +725,7 @@ class Hierarchy extends DataExtension {
|
|||||||
throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
|
throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied');
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
|
$hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy;
|
||||||
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
|
$hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree;
|
||||||
$children = $baseClass::get()
|
$children = $baseClass::get()
|
||||||
@ -822,7 +825,7 @@ class Hierarchy extends DataExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$nextNode = null;
|
$nextNode = null;
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner->class);
|
$baseClass = $this->owner->baseClass();
|
||||||
|
|
||||||
$children = $baseClass::get()
|
$children = $baseClass::get()
|
||||||
->filter('ParentID', (int)$this->owner->ID)
|
->filter('ParentID', (int)$this->owner->ID)
|
||||||
@ -832,7 +835,7 @@ class Hierarchy extends DataExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try all the siblings of this node after the given node
|
// Try all the siblings of this node after the given node
|
||||||
/*if( $siblings = DataObject::get( ClassInfo::baseDataClass($this->owner->class),
|
/*if( $siblings = DataObject::get( $this->owner->baseClass(),
|
||||||
"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
|
"\"ParentID\"={$this->owner->ParentID}" . ( $afterNode ) ? "\"Sort\"
|
||||||
> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
|
> {$afterNode->Sort}" : "" , '\"Sort\" ASC' ) ) $searchNodes->merge( $siblings );*/
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class ManyManyList extends RelationList {
|
|||||||
* @param string $joinTable The name of the table whose entries define the content of this many_many relation.
|
* @param string $joinTable The name of the table whose entries define the content of this many_many relation.
|
||||||
* @param string $localKey The key in the join table that maps to the dataClass' PK.
|
* @param string $localKey The key in the join table that maps to the dataClass' PK.
|
||||||
* @param string $foreignKey The key in the join table that maps to joined class' PK.
|
* @param string $foreignKey The key in the join table that maps to joined class' PK.
|
||||||
* @param string $extraFields A map of field => fieldtype of extra fields on the join table.
|
* @param array $extraFields A map of field => fieldtype of extra fields on the join table.
|
||||||
*
|
*
|
||||||
* @example new ManyManyList('Group','Group_Members', 'GroupID', 'MemberID');
|
* @example new ManyManyList('Group','Group_Members', 'GroupID', 'MemberID');
|
||||||
*/
|
*/
|
||||||
@ -69,8 +69,11 @@ class ManyManyList extends RelationList {
|
|||||||
*/
|
*/
|
||||||
protected function linkJoinTable() {
|
protected function linkJoinTable() {
|
||||||
// Join to the many-many join table
|
// Join to the many-many join table
|
||||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
$dataClassIDColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
|
||||||
$this->dataQuery->innerJoin($this->joinTable, "\"{$this->joinTable}\".\"{$this->localKey}\" = \"{$baseClass}\".\"ID\"");
|
$this->dataQuery->innerJoin(
|
||||||
|
$this->joinTable,
|
||||||
|
"\"{$this->joinTable}\".\"{$this->localKey}\" = {$dataClassIDColumn}"
|
||||||
|
);
|
||||||
|
|
||||||
// Add the extra fields to the query
|
// Add the extra fields to the query
|
||||||
if($this->extraFields) {
|
if($this->extraFields) {
|
||||||
@ -184,6 +187,7 @@ class ManyManyList extends RelationList {
|
|||||||
* @param mixed $item
|
* @param mixed $item
|
||||||
* @param array $extraFields A map of additional columns to insert into the joinTable.
|
* @param array $extraFields A map of additional columns to insert into the joinTable.
|
||||||
* Column names should be ANSI quoted.
|
* Column names should be ANSI quoted.
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function add($item, $extraFields = array()) {
|
public function add($item, $extraFields = array()) {
|
||||||
// Ensure nulls or empty strings are correctly treated as empty arrays
|
// Ensure nulls or empty strings are correctly treated as empty arrays
|
||||||
@ -292,7 +296,9 @@ class ManyManyList extends RelationList {
|
|||||||
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
|
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->addWhere(array("\"{$this->localKey}\"" => $itemID));
|
$query->addWhere(array(
|
||||||
|
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
|
||||||
|
));
|
||||||
$query->execute();
|
$query->execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,15 +309,16 @@ class ManyManyList extends RelationList {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function removeAll() {
|
public function removeAll() {
|
||||||
$base = ClassInfo::baseDataClass($this->dataClass());
|
|
||||||
|
|
||||||
// Remove the join to the join table to avoid MySQL row locking issues.
|
// Remove the join to the join table to avoid MySQL row locking issues.
|
||||||
$query = $this->dataQuery();
|
$query = $this->dataQuery();
|
||||||
$foreignFilter = $query->getQueryParam('Foreign.Filter');
|
$foreignFilter = $query->getQueryParam('Foreign.Filter');
|
||||||
$query->removeFilterOn($foreignFilter);
|
$query->removeFilterOn($foreignFilter);
|
||||||
|
|
||||||
|
// Select ID column
|
||||||
$selectQuery = $query->query();
|
$selectQuery = $query->query();
|
||||||
$selectQuery->setSelect("\"{$base}\".\"ID\"");
|
$dataClassIDColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID');
|
||||||
|
$selectQuery->setSelect($dataClassIDColumn);
|
||||||
|
|
||||||
$from = $selectQuery->getFrom();
|
$from = $selectQuery->getFrom();
|
||||||
unset($from[$this->joinTable]);
|
unset($from[$this->joinTable]);
|
||||||
@ -364,7 +371,7 @@ class ManyManyList extends RelationList {
|
|||||||
user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING);
|
user_error("Can't call ManyManyList::getExtraData() until a foreign ID is set", E_USER_WARNING);
|
||||||
}
|
}
|
||||||
$query->addWhere(array(
|
$query->addWhere(array(
|
||||||
"\"{$this->localKey}\"" => $itemID
|
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
|
||||||
));
|
));
|
||||||
$queryResult = $query->execute()->current();
|
$queryResult = $query->execute()->current();
|
||||||
if ($queryResult) {
|
if ($queryResult) {
|
||||||
|
@ -174,26 +174,6 @@ abstract class DBConnector {
|
|||||||
*/
|
*/
|
||||||
abstract public function quoteString($value);
|
abstract public function quoteString($value);
|
||||||
|
|
||||||
/**
|
|
||||||
* Escapes an identifier (table / database name). Typically the value
|
|
||||||
* is simply double quoted. Don't pass in already escaped identifiers in,
|
|
||||||
* as this will double escape the value!
|
|
||||||
*
|
|
||||||
* @param string $value The identifier to escape
|
|
||||||
* @param string $separator optional identifier splitter
|
|
||||||
*/
|
|
||||||
public function escapeIdentifier($value, $separator = '.') {
|
|
||||||
// ANSI standard id escape is to surround with double quotes
|
|
||||||
if(empty($separator)) return '"'.trim($value).'"';
|
|
||||||
|
|
||||||
// Split, escape, and glue back multiple identifiers
|
|
||||||
$segments = array();
|
|
||||||
foreach(explode($separator, $value) as $item) {
|
|
||||||
$segments[] = $this->escapeIdentifier($item, null);
|
|
||||||
}
|
|
||||||
return implode($separator, $segments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the following query with the specified error level.
|
* Executes the following query with the specified error level.
|
||||||
* Implementations of this function should respect previewWrite and benchmarkQuery
|
* Implementations of this function should respect previewWrite and benchmarkQuery
|
||||||
|
@ -232,11 +232,18 @@ abstract class SS_Database {
|
|||||||
* is simply double quoted. Don't pass in already escaped identifiers in,
|
* is simply double quoted. Don't pass in already escaped identifiers in,
|
||||||
* as this will double escape the value!
|
* as this will double escape the value!
|
||||||
*
|
*
|
||||||
* @param string $value The identifier to escape
|
* @param string|array $value The identifier to escape or list of split components
|
||||||
* @param string $separator optional identifier splitter
|
* @param string $separator Splitter for each component
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function escapeIdentifier($value, $separator = '.') {
|
public function escapeIdentifier($value, $separator = '.') {
|
||||||
return $this->connector->escapeIdentifier($value, $separator);
|
// Split string into components
|
||||||
|
if(!is_array($value)) {
|
||||||
|
$value = explode($separator, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implode quoted column
|
||||||
|
return '"' . implode('"'.$separator.'"', $value) . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +117,7 @@ class ChangeSet extends DataObject {
|
|||||||
|
|
||||||
$references = [
|
$references = [
|
||||||
'ObjectID' => $object->ID,
|
'ObjectID' => $object->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($object)
|
'ObjectClass' => $object->baseClass(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get existing item in case already added
|
// Get existing item in case already added
|
||||||
@ -146,7 +146,7 @@ class ChangeSet extends DataObject {
|
|||||||
public function removeObject(DataObject $object) {
|
public function removeObject(DataObject $object) {
|
||||||
$item = ChangeSetItem::get()->filter([
|
$item = ChangeSetItem::get()->filter([
|
||||||
'ObjectID' => $object->ID,
|
'ObjectID' => $object->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($object),
|
'ObjectClass' => $object->baseClass(),
|
||||||
'ChangeSetID' => $this->ID
|
'ChangeSetID' => $this->ID
|
||||||
])->first();
|
])->first();
|
||||||
|
|
||||||
@ -159,9 +159,17 @@ class ChangeSet extends DataObject {
|
|||||||
$this->sync();
|
$this->sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function implicitKey($item) {
|
/**
|
||||||
if ($item instanceof ChangeSetItem) return $item->ObjectClass.'.'.$item->ObjectID;
|
* Build identifying string key for this object
|
||||||
return ClassInfo::baseDataClass($item).'.'.$item->ID;
|
*
|
||||||
|
* @param DataObject $item
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function implicitKey(DataObject $item) {
|
||||||
|
if ($item instanceof ChangeSetItem) {
|
||||||
|
return $item->ObjectClass.'.'.$item->ObjectID;
|
||||||
|
}
|
||||||
|
return $item->baseClass().'.'.$item->ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function calculateImplicit() {
|
protected function calculateImplicit() {
|
||||||
@ -174,16 +182,18 @@ class ChangeSet extends DataObject {
|
|||||||
/** @var string[][] $references List of which explicit items reference each thing in referenced */
|
/** @var string[][] $references List of which explicit items reference each thing in referenced */
|
||||||
$references = array();
|
$references = array();
|
||||||
|
|
||||||
|
/** @var ChangeSetItem $item */
|
||||||
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) {
|
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) {
|
||||||
$explicitKey = $this->implicitKey($item);
|
$explicitKey = $this->implicitKey($item);
|
||||||
$explicit[$explicitKey] = true;
|
$explicit[$explicitKey] = true;
|
||||||
|
|
||||||
foreach ($item->findReferenced() as $referee) {
|
foreach ($item->findReferenced() as $referee) {
|
||||||
|
/** @var DataObject $referee */
|
||||||
$key = $this->implicitKey($referee);
|
$key = $this->implicitKey($referee);
|
||||||
|
|
||||||
$referenced[$key] = [
|
$referenced[$key] = [
|
||||||
'ObjectID' => $referee->ID,
|
'ObjectID' => $referee->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($referee)
|
'ObjectClass' => $referee->baseClass(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$references[$key][] = $item->ID;
|
$references[$key][] = $item->ID;
|
||||||
@ -220,6 +230,7 @@ class ChangeSet extends DataObject {
|
|||||||
$implicit = $this->calculateImplicit();
|
$implicit = $this->calculateImplicit();
|
||||||
|
|
||||||
// Adjust the existing implicit ChangeSetItems for this ChangeSet
|
// Adjust the existing implicit ChangeSetItems for this ChangeSet
|
||||||
|
/** @var ChangeSetItem $item */
|
||||||
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
|
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
|
||||||
$objectKey = $this->implicitKey($item);
|
$objectKey = $this->implicitKey($item);
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
|
|||||||
|
|
||||||
public function onBeforeWrite() {
|
public function onBeforeWrite() {
|
||||||
// Make sure ObjectClass refers to the base data class in the case of old or wrong code
|
// Make sure ObjectClass refers to the base data class in the case of old or wrong code
|
||||||
$this->ObjectClass = ClassInfo::baseDataClass($this->ObjectClass);
|
$this->ObjectClass = $this->getSchema()->baseDataClass($this->ObjectClass);
|
||||||
parent::onBeforeWrite();
|
parent::onBeforeWrite();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +328,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
|
|||||||
public static function get_for_object($object) {
|
public static function get_for_object($object) {
|
||||||
return ChangeSetItem::get()->filter([
|
return ChangeSetItem::get()->filter([
|
||||||
'ObjectID' => $object->ID,
|
'ObjectID' => $object->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($object)
|
'ObjectClass' => $object->baseClass(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +342,7 @@ class ChangeSetItem extends DataObject implements Thumbnail {
|
|||||||
public static function get_for_object_by_id($objectID, $objectClass) {
|
public static function get_for_object_by_id($objectID, $objectClass) {
|
||||||
return ChangeSetItem::get()->filter([
|
return ChangeSetItem::get()->filter([
|
||||||
'ObjectID' => $objectID,
|
'ObjectID' => $objectID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($objectClass)
|
'ObjectClass' => static::getSchema()->baseDataClass($objectClass)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
model/versioning/VersionableExtension.php
Normal file
24
model/versioning/VersionableExtension.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum level extra fields required by extensions that are versonable
|
||||||
|
*/
|
||||||
|
interface VersionableExtension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the given table is versionable
|
||||||
|
*
|
||||||
|
* @param string $table
|
||||||
|
* @return bool True if versioned tables should be built for the given suffix
|
||||||
|
*/
|
||||||
|
public function isVersionedTable($table);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update fields and indexes for the versonable suffix table
|
||||||
|
*
|
||||||
|
* @param string $suffix Table suffix being built
|
||||||
|
* @param array $fields List of fields in this model
|
||||||
|
* @param array $indexes List of indexes in this model
|
||||||
|
*/
|
||||||
|
public function updateVersionableFields($suffix, &$fields, &$indexes);
|
||||||
|
}
|
@ -112,14 +112,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
*/
|
*/
|
||||||
private static $prepopulate_versionnumber_cache = true;
|
private static $prepopulate_versionnumber_cache = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep track of the archive tables that have been created.
|
|
||||||
*
|
|
||||||
* @config
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private static $archive_tables = array();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional database indexes for the new
|
* Additional database indexes for the new
|
||||||
* "_versions" table. Used in {@link augmentDatabase()}.
|
* "_versions" table. Used in {@link augmentDatabase()}.
|
||||||
@ -163,13 +155,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
|
* array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* Make sure your extension has a static $enabled-property that determines if it is
|
* Your extension must implement VersionableExtension interface in order to
|
||||||
* processed by Versioned.
|
* apply custom tables for versioned.
|
||||||
*
|
*
|
||||||
* @config
|
* @config
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
private static $versionableExtensions = array('Translatable' => 'lang');
|
private static $versionableExtensions = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
|
* Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
|
||||||
@ -275,7 +267,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
*/
|
*/
|
||||||
protected function getLastEditedForVersion($version) {
|
protected function getLastEditedForVersion($version) {
|
||||||
// Cache key
|
// Cache key
|
||||||
$baseTable = ClassInfo::baseDataClass($this->owner);
|
$baseTable = $this->baseTable();
|
||||||
$id = $this->owner->ID;
|
$id = $this->owner->ID;
|
||||||
$key = "{$baseTable}#{$id}/{$version}";
|
$key = "{$baseTable}#{$id}/{$version}";
|
||||||
|
|
||||||
@ -298,7 +290,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
return $date;
|
return $date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates query parameters of relations attached to versioned dataobjects
|
||||||
|
*
|
||||||
|
* @param array $params
|
||||||
|
*/
|
||||||
public function updateInheritableQueryParams(&$params) {
|
public function updateInheritableQueryParams(&$params) {
|
||||||
// Skip if versioned isn't set
|
// Skip if versioned isn't set
|
||||||
if(!isset($params['Versioned.mode'])) {
|
if(!isset($params['Versioned.mode'])) {
|
||||||
@ -306,7 +302,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adjust query based on original selection criterea
|
// Adjust query based on original selection criterea
|
||||||
$owner = $this->owner;
|
|
||||||
switch($params['Versioned.mode']) {
|
switch($params['Versioned.mode']) {
|
||||||
case 'all_versions': {
|
case 'all_versions': {
|
||||||
// Versioned.mode === all_versions doesn't inherit very well, so default to stage
|
// Versioned.mode === all_versions doesn't inherit very well, so default to stage
|
||||||
@ -350,8 +345,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
|
$baseTable = $this->baseTable();
|
||||||
|
|
||||||
$versionedMode = $dataQuery->getQueryParam('Versioned.mode');
|
$versionedMode = $dataQuery->getQueryParam('Versioned.mode');
|
||||||
switch($versionedMode) {
|
switch($versionedMode) {
|
||||||
// Reading a specific stage (Stage or Live)
|
// Reading a specific stage (Stage or Live)
|
||||||
@ -392,7 +386,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tempName = 'ExclusionarySource_'.$excluding;
|
$tempName = 'ExclusionarySource_'.$excluding;
|
||||||
$excludingTable = $baseTable . ($excluding && $excluding != static::DRAFT ? "_$excluding" : '');
|
$excludingTable = $this->baseTable($excluding);
|
||||||
|
|
||||||
$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
|
$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
|
||||||
$query->renameTable($tempName, $excludingTable);
|
$query->renameTable($tempName, $excludingTable);
|
||||||
@ -486,6 +480,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'all_versions':
|
||||||
default: {
|
default: {
|
||||||
// If all versions are requested, ensure that records are sorted by this field
|
// If all versions are requested, ensure that records are sorted by this field
|
||||||
$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
|
$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
|
||||||
@ -507,11 +502,26 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @return bool True if this table should be versioned
|
* @return bool True if this table should be versioned
|
||||||
*/
|
*/
|
||||||
protected function isTableVersioned($table) {
|
protected function isTableVersioned($table) {
|
||||||
if(!class_exists($table)) {
|
$schema = DataObject::getSchema();
|
||||||
|
$tableClass = $schema->tableClass($table);
|
||||||
|
if(empty($tableClass)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner);
|
|
||||||
return is_a($table, $baseClass, true);
|
// Check that this class belongs to the same tree
|
||||||
|
$baseClass = $schema->baseDataClass($this->owner);
|
||||||
|
if(!is_a($tableClass, $baseClass, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that this isn't a derived table
|
||||||
|
// (e.g. _Live, or a many_many table)
|
||||||
|
$mainTable = $schema->tableName($tableClass);
|
||||||
|
if($mainTable !== $table) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -527,7 +537,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
// metadata set on the query object. This prevents regular queries from
|
// metadata set on the query object. This prevents regular queries from
|
||||||
// accidentally querying the *_versions tables.
|
// accidentally querying the *_versions tables.
|
||||||
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
|
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
|
||||||
$dataClass = ClassInfo::baseDataClass($dataQuery->dataClass());
|
|
||||||
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
|
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
|
||||||
if(
|
if(
|
||||||
!empty($dataObject->Version) &&
|
!empty($dataObject->Version) &&
|
||||||
@ -535,36 +544,21 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
) {
|
) {
|
||||||
// This will ensure that augmentSQL will select only the same version as the owner,
|
// This will ensure that augmentSQL will select only the same version as the owner,
|
||||||
// regardless of how this object was initially selected
|
// regardless of how this object was initially selected
|
||||||
|
$versionColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'Version');
|
||||||
$dataQuery->where([
|
$dataQuery->where([
|
||||||
"\"$dataClass\".\"Version\"" => $dataObject->Version
|
$versionColumn => $dataObject->Version
|
||||||
]);
|
]);
|
||||||
$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
|
$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by {@link SapphireTest} when the database is reset.
|
|
||||||
*
|
|
||||||
* @todo Reduce the coupling between this and SapphireTest, somehow.
|
|
||||||
*/
|
|
||||||
public static function on_db_reset() {
|
|
||||||
// Drop all temporary tables
|
|
||||||
$db = DB::get_conn();
|
|
||||||
foreach(static::$archive_tables as $tableName) {
|
|
||||||
if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
|
|
||||||
else $db->query("DROP TABLE \"$tableName\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove references to them
|
|
||||||
static::$archive_tables = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function augmentDatabase() {
|
public function augmentDatabase() {
|
||||||
$owner = $this->owner;
|
$owner = $this->owner;
|
||||||
$classTable = $owner->class;
|
$class = get_class($owner);
|
||||||
|
$baseTable = $this->baseTable();
|
||||||
|
$classTable = $owner->getSchema()->tableName($owner);
|
||||||
|
|
||||||
$isRootClass = ($owner->class == ClassInfo::baseDataClass($owner->class));
|
$isRootClass = $class === $owner->baseClass();
|
||||||
|
|
||||||
// Build a list of suffixes whose tables need versioning
|
// Build a list of suffixes whose tables need versioning
|
||||||
$allSuffixes = array();
|
$allSuffixes = array();
|
||||||
@ -572,7 +566,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
if(count($versionableExtensions)){
|
if(count($versionableExtensions)){
|
||||||
foreach ($versionableExtensions as $versionableExtension => $suffixes) {
|
foreach ($versionableExtensions as $versionableExtension => $suffixes) {
|
||||||
if ($owner->hasExtension($versionableExtension)) {
|
if ($owner->hasExtension($versionableExtension)) {
|
||||||
$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
|
|
||||||
foreach ((array)$suffixes as $suffix) {
|
foreach ((array)$suffixes as $suffix) {
|
||||||
$allSuffixes[$suffix] = $versionableExtension;
|
$allSuffixes[$suffix] = $versionableExtension;
|
||||||
}
|
}
|
||||||
@ -581,49 +574,56 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the default table with an empty suffix to the list (table name = class name)
|
// Add the default table with an empty suffix to the list (table name = class name)
|
||||||
array_push($allSuffixes,'');
|
$allSuffixes[''] = null;
|
||||||
|
|
||||||
foreach ($allSuffixes as $key => $suffix) {
|
foreach ($allSuffixes as $suffix => $extension) {
|
||||||
// check that this is a valid suffix
|
// Check tables for this build
|
||||||
if (!is_int($key)) continue;
|
if ($suffix) {
|
||||||
|
$suffixBaseTable = "{$baseTable}_{$suffix}";
|
||||||
if ($suffix) $table = "{$classTable}_$suffix";
|
$suffixTable = "{$classTable}_{$suffix}";
|
||||||
else $table = $classTable;
|
} else {
|
||||||
|
$suffixBaseTable = $baseTable;
|
||||||
|
$suffixTable = $classTable;
|
||||||
|
}
|
||||||
|
|
||||||
$fields = DataObject::database_fields($owner->class);
|
$fields = DataObject::database_fields($owner->class);
|
||||||
unset($fields['ID']);
|
unset($fields['ID']);
|
||||||
if($fields) {
|
if($fields) {
|
||||||
$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
|
$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
|
||||||
$indexes = $owner->databaseIndexes();
|
$indexes = $owner->databaseIndexes();
|
||||||
if ($suffix && ($ext = $owner->getExtensionInstance($allSuffixes[$suffix]))) {
|
$extensionClass = $allSuffixes[$suffix];
|
||||||
if (!$ext->isVersionedTable($table)) continue;
|
if ($suffix && ($extension = $owner->getExtensionInstance($extensionClass))) {
|
||||||
$ext->setOwner($owner);
|
if (!$extension instanceof VersionableExtension) {
|
||||||
$fields = $ext->fieldsInExtraTables($suffix);
|
throw new LogicException(
|
||||||
$ext->clearOwner();
|
"Extension {$extensionClass} must implement VersionableExtension"
|
||||||
$indexes = $fields['indexes'];
|
);
|
||||||
$fields = $fields['db'];
|
}
|
||||||
|
// Allow versionable extension to customise table fields and indexes
|
||||||
|
$extension->setOwner($owner);
|
||||||
|
if ($extension->isVersionedTable($suffixTable)) {
|
||||||
|
$extension->updateVersionableFields($suffix, $fields, $indexes);
|
||||||
|
}
|
||||||
|
$extension->clearOwner();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create tables for other stages
|
// Build _Live table
|
||||||
if($this->hasStages()) {
|
if($this->hasStages()) {
|
||||||
// Extra tables for _Live, etc.
|
$liveTable = $this->stageTable($suffixTable, static::LIVE);
|
||||||
// Change unique indexes to 'index'. Versioned tables may run into unique indexing difficulties
|
|
||||||
// otherwise.
|
|
||||||
$liveTable = $this->stageTable($table, static::LIVE);
|
|
||||||
$indexes = $this->uniqueToIndex($indexes);
|
|
||||||
DB::require_table($liveTable, $fields, $indexes, false, $options);
|
DB::require_table($liveTable, $fields, $indexes, false, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build _versions table
|
||||||
|
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
|
||||||
|
$nonUniqueIndexes = $this->uniqueToIndex($indexes);
|
||||||
if($isRootClass) {
|
if($isRootClass) {
|
||||||
// Create table for all versions
|
// Create table for all versions
|
||||||
$versionFields = array_merge(
|
$versionFields = array_merge(
|
||||||
Config::inst()->get('Versioned', 'db_for_versions_table'),
|
Config::inst()->get('Versioned', 'db_for_versions_table'),
|
||||||
(array)$fields
|
(array)$fields
|
||||||
);
|
);
|
||||||
|
|
||||||
$versionIndexes = array_merge(
|
$versionIndexes = array_merge(
|
||||||
Config::inst()->get('Versioned', 'indexes_for_versions_table'),
|
Config::inst()->get('Versioned', 'indexes_for_versions_table'),
|
||||||
(array)$indexes
|
(array)$nonUniqueIndexes
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Create fields for any tables of subclasses
|
// Create fields for any tables of subclasses
|
||||||
@ -634,86 +634,74 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
),
|
),
|
||||||
(array)$fields
|
(array)$fields
|
||||||
);
|
);
|
||||||
|
|
||||||
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
|
|
||||||
$indexes = $this->uniqueToIndex($indexes);
|
|
||||||
$versionIndexes = array_merge(
|
$versionIndexes = array_merge(
|
||||||
array(
|
array(
|
||||||
'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
|
'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
|
||||||
'RecordID' => true,
|
'RecordID' => true,
|
||||||
'Version' => true,
|
'Version' => true,
|
||||||
),
|
),
|
||||||
(array)$indexes
|
(array)$nonUniqueIndexes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(DB::get_schema()->hasTable("{$table}_versions")) {
|
// Cleanup any orphans
|
||||||
// Fix data that lacks the uniqueness constraint (since this was added later and
|
$this->cleanupVersionedOrphans("{$suffixBaseTable}_versions", "{$suffixTable}_versions");
|
||||||
// bugs meant that the constraint was validated)
|
|
||||||
$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
|
|
||||||
FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
|
|
||||||
HAVING COUNT(*) > 1");
|
|
||||||
|
|
||||||
foreach($duplications as $dup) {
|
// Build versions table
|
||||||
DB::alteration_message("Removing {$table}_versions duplicate data for "
|
DB::require_table("{$suffixTable}_versions", $versionFields, $versionIndexes, true, $options);
|
||||||
."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
|
|
||||||
DB::prepared_query(
|
|
||||||
"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
|
|
||||||
AND \"Version\" = ? AND \"ID\" != ?",
|
|
||||||
array($dup['RecordID'], $dup['Version'], $dup['ID'])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove junk which has no data in parent classes. Only needs to run the following
|
|
||||||
// when versioned data is spread over multiple tables
|
|
||||||
if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
|
|
||||||
|
|
||||||
foreach($versionedTables as $child) {
|
|
||||||
if($table === $child) break; // only need subclasses
|
|
||||||
|
|
||||||
// Select all orphaned version records
|
|
||||||
$orphanedQuery = SQLSelect::create()
|
|
||||||
->selectField("\"{$table}_versions\".\"ID\"")
|
|
||||||
->setFrom("\"{$table}_versions\"");
|
|
||||||
|
|
||||||
// If we have a parent table limit orphaned records
|
|
||||||
// to only those that exist in this
|
|
||||||
if(DB::get_schema()->hasTable("{$child}_versions")) {
|
|
||||||
$orphanedQuery
|
|
||||||
->addLeftJoin(
|
|
||||||
"{$child}_versions",
|
|
||||||
"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
|
|
||||||
AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
|
|
||||||
)
|
|
||||||
->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = $orphanedQuery->count();
|
|
||||||
if($count > 0) {
|
|
||||||
DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
|
|
||||||
$ids = $orphanedQuery->execute()->column();
|
|
||||||
foreach($ids as $id) {
|
|
||||||
DB::prepared_query(
|
|
||||||
"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
|
|
||||||
array($id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
|
|
||||||
} else {
|
} else {
|
||||||
DB::dont_require_table("{$table}_versions");
|
DB::dont_require_table("{$suffixTable}_versions");
|
||||||
if($this->hasStages()) {
|
if($this->hasStages()) {
|
||||||
$liveTable = $this->stageTable($table, static::LIVE);
|
$liveTable = $this->stageTable($suffixTable, static::LIVE);
|
||||||
DB::dont_require_table($liveTable);
|
DB::dont_require_table($liveTable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup orphaned records in the _versions table
|
||||||
|
*
|
||||||
|
* @param string $baseTable base table to use as authoritative source of records
|
||||||
|
* @param string $childTable Sub-table to clean orphans from
|
||||||
|
*/
|
||||||
|
protected function cleanupVersionedOrphans($baseTable, $childTable) {
|
||||||
|
// Skip if child table doesn't exist
|
||||||
|
if(!DB::get_schema()->hasTable($childTable)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip if tables are the same
|
||||||
|
if($childTable === $baseTable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select all orphaned version records
|
||||||
|
$orphanedQuery = SQLSelect::create()
|
||||||
|
->selectField("\"{$childTable}\".\"ID\"")
|
||||||
|
->setFrom("\"{$childTable}\"");
|
||||||
|
|
||||||
|
// If we have a parent table limit orphaned records
|
||||||
|
// to only those that exist in this
|
||||||
|
if(DB::get_schema()->hasTable($baseTable)) {
|
||||||
|
$orphanedQuery
|
||||||
|
->addLeftJoin(
|
||||||
|
$baseTable,
|
||||||
|
"\"{$childTable}\".\"RecordID\" = \"{$baseTable}\".\"RecordID\"
|
||||||
|
AND \"{$childTable}\".\"Version\" = \"{$baseTable}\".\"Version\""
|
||||||
|
)
|
||||||
|
->addWhere("\"{$baseTable}\".\"ID\" IS NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = $orphanedQuery->count();
|
||||||
|
if($count > 0) {
|
||||||
|
DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
|
||||||
|
$ids = $orphanedQuery->execute()->column();
|
||||||
|
foreach($ids as $id) {
|
||||||
|
DB::prepared_query("DELETE FROM \"{$childTable}\" WHERE \"ID\" = ?", array($id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for augmentDatabase() to find unique indexes and convert them to non-unique
|
* Helper for augmentDatabase() to find unique indexes and convert them to non-unique
|
||||||
*
|
*
|
||||||
@ -747,23 +735,26 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* Generates a ($table)_version DB manipulation and injects it into the current $manipulation
|
* Generates a ($table)_version DB manipulation and injects it into the current $manipulation
|
||||||
*
|
*
|
||||||
* @param array $manipulation Source manipulation data
|
* @param array $manipulation Source manipulation data
|
||||||
* @param string $table Name of table
|
* @param string $class Class
|
||||||
|
* @param string $table Table Table for this class
|
||||||
* @param int $recordID ID of record to version
|
* @param int $recordID ID of record to version
|
||||||
*/
|
*/
|
||||||
protected function augmentWriteVersioned(&$manipulation, $table, $recordID) {
|
protected function augmentWriteVersioned(&$manipulation, $class, $table, $recordID) {
|
||||||
$baseDataClass = ClassInfo::baseDataClass($table);
|
$baseDataClass = DataObject::getSchema()->baseDataClass($class);
|
||||||
|
$baseDataTable = DataObject::getSchema()->tableName($baseDataClass);
|
||||||
|
|
||||||
// Set up a new entry in (table)_versions
|
// Set up a new entry in (table)_versions
|
||||||
$newManipulation = array(
|
$newManipulation = array(
|
||||||
"command" => "insert",
|
"command" => "insert",
|
||||||
"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
|
"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null,
|
||||||
|
"class" => $class,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add any extra, unchanged fields to the version record.
|
// Add any extra, unchanged fields to the version record.
|
||||||
$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record();
|
$data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record();
|
||||||
|
|
||||||
if ($data) {
|
if ($data) {
|
||||||
$fields = DataObject::database_fields($table);
|
$fields = DataObject::database_fields($class);
|
||||||
|
|
||||||
if (is_array($fields)) {
|
if (is_array($fields)) {
|
||||||
$data = array_intersect_key($data, $fields);
|
$data = array_intersect_key($data, $fields);
|
||||||
@ -784,13 +775,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
$nextVersion = 0;
|
$nextVersion = 0;
|
||||||
if($recordID) {
|
if($recordID) {
|
||||||
$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
|
$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
|
||||||
FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
|
FROM \"{$baseDataTable}_versions\" WHERE \"RecordID\" = ?",
|
||||||
array($recordID)
|
array($recordID)
|
||||||
)->value();
|
)->value();
|
||||||
}
|
}
|
||||||
$nextVersion = $nextVersion ?: 1;
|
$nextVersion = $nextVersion ?: 1;
|
||||||
|
|
||||||
if($table === $baseDataClass) {
|
if($class === $baseDataClass) {
|
||||||
// Write AuthorID for baseclass
|
// Write AuthorID for baseclass
|
||||||
$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
|
$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
|
||||||
$newManipulation['fields']['AuthorID'] = $userID;
|
$newManipulation['fields']['AuthorID'] = $userID;
|
||||||
@ -830,13 +821,13 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
// get Version number from base data table on write
|
// get Version number from base data table on write
|
||||||
$version = null;
|
$version = null;
|
||||||
$owner = $this->owner;
|
$owner = $this->owner;
|
||||||
$baseDataClass = ClassInfo::baseDataClass($owner->class);
|
$baseDataTable = DataObject::getSchema()->baseDataTable($owner);
|
||||||
if(isset($manipulation[$baseDataClass]['fields'])) {
|
if(isset($manipulation[$baseDataTable]['fields'])) {
|
||||||
if ($this->migratingVersion) {
|
if ($this->migratingVersion) {
|
||||||
$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion;
|
$manipulation[$baseDataTable]['fields']['Version'] = $this->migratingVersion;
|
||||||
}
|
}
|
||||||
if (isset($manipulation[$baseDataClass]['fields']['Version'])) {
|
if (isset($manipulation[$baseDataTable]['fields']['Version'])) {
|
||||||
$version = $manipulation[$baseDataClass]['fields']['Version'];
|
$version = $manipulation[$baseDataTable]['fields']['Version'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,7 +836,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
foreach($tables as $table) {
|
foreach($tables as $table) {
|
||||||
|
|
||||||
// Make sure that the augmented write is being applied to a table that can be versioned
|
// Make sure that the augmented write is being applied to a table that can be versioned
|
||||||
if( !$this->canBeVersioned($table) ) {
|
$class = isset($manipulation[$table]['class']) ? $manipulation[$table]['class'] : null;
|
||||||
|
if(!$class || !$this->canBeVersioned($class) ) {
|
||||||
unset($manipulation[$table]);
|
unset($manipulation[$table]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -864,7 +856,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
} elseif(empty($version)) {
|
} elseif(empty($version)) {
|
||||||
// If we haven't got a version #, then we're creating a new version.
|
// If we haven't got a version #, then we're creating a new version.
|
||||||
// Otherwise, we're just copying a version to another table
|
// Otherwise, we're just copying a version to another table
|
||||||
$this->augmentWriteVersioned($manipulation, $table, $id);
|
$this->augmentWriteVersioned($manipulation, $class, $table, $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove "Version" column from subclasses of baseDataClass
|
// Remove "Version" column from subclasses of baseDataClass
|
||||||
@ -879,7 +871,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
// If we're editing Live, then use (table)_Live instead of (table)
|
// If we're editing Live, then use (table)_Live instead of (table)
|
||||||
if($this->hasStages() && static::get_stage() === static::LIVE) {
|
if($this->hasStages() && static::get_stage() === static::LIVE) {
|
||||||
$this->augmentWriteStaged($manipulation, $table, $id);
|
$this->augmentWriteStaged($manipulation, $class, $id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1008,9 +1000,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
protected function lookupReverseOwners() {
|
protected function lookupReverseOwners() {
|
||||||
// Find all classes with 'owns' config
|
// Find all classes with 'owns' config
|
||||||
$lookup = array();
|
$lookup = array();
|
||||||
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
|
foreach(ClassInfo::subclassesFor('DataObject') as $class) {
|
||||||
// Ensure this class is versioned
|
// Ensure this class is versioned
|
||||||
if(!Object::has_extension($class, Versioned::class)) {
|
if(!Object::has_extension($class, 'Versioned')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1372,16 +1364,16 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a table is supporting the Versioned extensions (e.g.
|
* Determine if a class is supporting the Versioned extensions (e.g.
|
||||||
* $table_versions does exists).
|
* $table_versions does exists).
|
||||||
*
|
*
|
||||||
* @param string $table Table name
|
* @param string $class Class name
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public function canBeVersioned($table) {
|
public function canBeVersioned($class) {
|
||||||
return ClassInfo::exists($table)
|
return ClassInfo::exists($class)
|
||||||
&& is_subclass_of($table, 'DataObject')
|
&& is_subclass_of($class, 'DataObject')
|
||||||
&& DataObject::has_own_table($table);
|
&& DataObject::has_own_table($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1392,14 +1384,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @return boolean Returns false if the field isn't in the table, true otherwise
|
* @return boolean Returns false if the field isn't in the table, true otherwise
|
||||||
*/
|
*/
|
||||||
public function hasVersionField($table) {
|
public function hasVersionField($table) {
|
||||||
// Strip "_Live" from end of table
|
|
||||||
$live = static::LIVE;
|
|
||||||
if($this->hasStages() && preg_match("/^(?<table>.*)_{$live}$/", $table, $matches)) {
|
|
||||||
$table = $matches['table'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base table has version field
|
// Base table has version field
|
||||||
return $table === ClassInfo::baseDataClass($table);
|
$class = DataObject::getSchema()->tableClass($table);
|
||||||
|
return $class === DataObject::getSchema()->baseDataClass($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1416,11 +1403,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
if ($owner->hasExtension($versionableExtension)) {
|
if ($owner->hasExtension($versionableExtension)) {
|
||||||
$ext = $owner->getExtensionInstance($versionableExtension);
|
$ext = $owner->getExtensionInstance($versionableExtension);
|
||||||
$ext->setOwner($owner);
|
$ext->setOwner($owner);
|
||||||
$table = $ext->extendWithSuffix($table);
|
$table = $ext->extendWithSuffix($table);
|
||||||
$ext->clearOwner();
|
$ext->clearOwner();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return $table;
|
return $table;
|
||||||
}
|
}
|
||||||
@ -1433,12 +1420,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
public function latestPublished() {
|
public function latestPublished() {
|
||||||
// Get the root data object class - this will have the version field
|
// Get the root data object class - this will have the version field
|
||||||
$owner = $this->owner;
|
$owner = $this->owner;
|
||||||
$table1 = ClassInfo::baseDataClass($owner);
|
$draftTable = $this->baseTable();
|
||||||
$table2 = $this->stageTable($table1, static::LIVE);
|
$liveTable = $this->stageTable($draftTable, static::LIVE);
|
||||||
|
|
||||||
return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
|
return DB::prepared_query("SELECT \"$draftTable\".\"Version\" = \"$liveTable\".\"Version\" FROM \"$draftTable\"
|
||||||
INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
|
INNER JOIN \"$liveTable\" ON \"$draftTable\".\"ID\" = \"$liveTable\".\"ID\"
|
||||||
WHERE \"$table1\".\"ID\" = ?",
|
WHERE \"$draftTable\".\"ID\" = ?",
|
||||||
array($owner->ID)
|
array($owner->ID)
|
||||||
)->value();
|
)->value();
|
||||||
}
|
}
|
||||||
@ -1522,7 +1509,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
$joinClass = $owner->hasManyComponent($relationship);
|
$joinClass = $owner->hasManyComponent($relationship);
|
||||||
$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
|
$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
|
||||||
$idField = $polymorphic ? "{$joinField}ID" : $joinField;
|
$idField = $polymorphic ? "{$joinField}ID" : $joinField;
|
||||||
$joinTable = ClassInfo::table_for_object_field($joinClass, $idField);
|
$joinTable = DataObject::getSchema()->tableForField($joinClass, $idField);
|
||||||
|
|
||||||
// Generate update query which will unlink disowned objects
|
// Generate update query which will unlink disowned objects
|
||||||
$targetTable = $this->stageTable($joinTable, $targetStage);
|
$targetTable = $this->stageTable($joinTable, $targetStage);
|
||||||
@ -1604,14 +1591,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
$owner->invokeWithExtensions('onBeforeUnpublish');
|
$owner->invokeWithExtensions('onBeforeUnpublish');
|
||||||
|
|
||||||
$origStage = static::get_stage();
|
$origReadingMode = static::get_reading_mode();
|
||||||
static::set_stage(static::LIVE);
|
static::set_stage(static::LIVE);
|
||||||
|
|
||||||
// This way our ID won't be unset
|
// This way our ID won't be unset
|
||||||
$clone = clone $owner;
|
$clone = clone $owner;
|
||||||
$clone->delete();
|
$clone->delete();
|
||||||
|
|
||||||
static::set_stage($origStage);
|
static::set_reading_mode($origReadingMode);
|
||||||
|
|
||||||
$owner->invokeWithExtensions('onAfterUnpublish');
|
$owner->invokeWithExtensions('onAfterUnpublish');
|
||||||
return true;
|
return true;
|
||||||
@ -1688,7 +1675,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
$owner = $this->owner;
|
$owner = $this->owner;
|
||||||
$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
|
||||||
|
|
||||||
$baseClass = ClassInfo::baseDataClass($owner->class);
|
$baseClass = $owner->baseClass();
|
||||||
|
|
||||||
/** @var Versioned|DataObject $from */
|
/** @var Versioned|DataObject $from */
|
||||||
if(is_numeric($fromStage)) {
|
if(is_numeric($fromStage)) {
|
||||||
@ -1875,28 +1862,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
/**
|
/**
|
||||||
* Return the base table - the class that directly extends DataObject.
|
* Return the base table - the class that directly extends DataObject.
|
||||||
*
|
*
|
||||||
|
* Protected so it doesn't conflict with DataObject::baseTable()
|
||||||
|
*
|
||||||
* @param string $stage
|
* @param string $stage
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function baseTable($stage = null) {
|
protected function baseTable($stage = null) {
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner);
|
$baseTable = $this->owner->baseTable();
|
||||||
return $this->stageTable($baseClass, $stage);
|
return $this->stageTable($baseTable, $stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a class and stage determine the table name.
|
* Given a table and stage determine the table name.
|
||||||
*
|
*
|
||||||
* Note: Stages this asset does not exist in will default to the draft table.
|
* Note: Stages this asset does not exist in will default to the draft table.
|
||||||
*
|
*
|
||||||
* @param string $class
|
* @param string $table Main table
|
||||||
* @param string $stage
|
* @param string $stage
|
||||||
* @return string Table name
|
* @return string Staged table name
|
||||||
*/
|
*/
|
||||||
public function stageTable($class, $stage) {
|
public function stageTable($table, $stage) {
|
||||||
if($this->hasStages() && $stage === static::LIVE) {
|
if($this->hasStages() && $stage === static::LIVE) {
|
||||||
return "{$class}_{$stage}";
|
return "{$table}_{$stage}";
|
||||||
}
|
}
|
||||||
return $class;
|
return $table;
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------------------------//
|
//-----------------------------------------------------------------------------------------------//
|
||||||
@ -2025,8 +2014,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* Set the reading stage.
|
* Set the reading stage.
|
||||||
*
|
*
|
||||||
* @param string $stage New reading stage.
|
* @param string $stage New reading stage.
|
||||||
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public static function set_stage($stage) {
|
public static function set_stage($stage) {
|
||||||
|
if(!in_array($stage, [static::LIVE, static::DRAFT])) {
|
||||||
|
throw new \InvalidArgumentException("Invalid stage name \"{$stage}\"");
|
||||||
|
}
|
||||||
static::set_reading_mode('Stage.' . $stage);
|
static::set_reading_mode('Stage.' . $stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2069,8 +2062,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
|
public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
|
||||||
$baseClass = ClassInfo::baseDataClass($class);
|
$baseClass = DataObject::getSchema()->baseDataClass($class);
|
||||||
$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}";
|
$stageTable = DataObject::getSchema()->tableName($baseClass);
|
||||||
|
if($stage === static::LIVE) {
|
||||||
|
$stageTable .= "_{$stage}";
|
||||||
|
}
|
||||||
|
|
||||||
// cached call
|
// cached call
|
||||||
if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
|
if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
|
||||||
@ -2126,8 +2122,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
$parameters = $idList;
|
$parameters = $idList;
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseClass = ClassInfo::baseDataClass($class);
|
/** @var Versioned|DataObject $singleton */
|
||||||
$stageTable = ($stage == static::DRAFT) ? $baseClass : "{$baseClass}_{$stage}";
|
$singleton = DataObject::singleton($class);
|
||||||
|
$baseClass = $singleton->baseClass();
|
||||||
|
$baseTable = $singleton->baseTable();
|
||||||
|
$stageTable = $singleton->stageTable($baseTable, $stage);
|
||||||
|
|
||||||
$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
|
$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
|
||||||
|
|
||||||
@ -2173,7 +2172,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
Versioned::set_reading_mode($oldMode);
|
Versioned::set_reading_mode($oldMode);
|
||||||
|
|
||||||
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
|
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
|
||||||
$baseClass = ClassInfo::baseDataClass($owner->class);
|
$baseClass = $owner->baseClass();
|
||||||
self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
|
self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2214,7 +2213,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
public function onAfterRollback($version) {
|
public function onAfterRollback($version) {
|
||||||
// Find record at this version
|
// Find record at this version
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner);
|
$baseClass = DataObject::getSchema()->baseDataClass($this->owner);
|
||||||
/** @var Versioned|DataObject $recordVersion */
|
/** @var Versioned|DataObject $recordVersion */
|
||||||
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
|
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
|
||||||
|
|
||||||
@ -2234,7 +2233,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @return DataObject
|
* @return DataObject
|
||||||
*/
|
*/
|
||||||
public static function get_latest_version($class, $id) {
|
public static function get_latest_version($class, $id) {
|
||||||
$baseClass = ClassInfo::baseDataClass($class);
|
$baseClass = DataObject::getSchema()->baseDataClass($class);
|
||||||
$list = DataList::create($baseClass)
|
$list = DataList::create($baseClass)
|
||||||
->setDataQueryParam("Versioned.mode", "latest_versions");
|
->setDataQueryParam("Versioned.mode", "latest_versions");
|
||||||
|
|
||||||
@ -2277,8 +2276,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseClass = ClassInfo::baseDataClass($owner->class);
|
$table = $this->baseTable(static::LIVE);
|
||||||
$table = $this->stageTable($baseClass, static::LIVE);
|
|
||||||
$result = DB::prepared_query(
|
$result = DB::prepared_query(
|
||||||
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
||||||
array($owner->ID)
|
array($owner->ID)
|
||||||
@ -2297,7 +2295,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$table = ClassInfo::baseDataClass($owner->class);
|
$table = $this->baseTable();
|
||||||
$result = DB::prepared_query(
|
$result = DB::prepared_query(
|
||||||
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
|
||||||
array($owner->ID)
|
array($owner->ID)
|
||||||
@ -2341,7 +2339,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @return DataObject
|
* @return DataObject
|
||||||
*/
|
*/
|
||||||
public static function get_version($class, $id, $version) {
|
public static function get_version($class, $id, $version) {
|
||||||
$baseClass = ClassInfo::baseDataClass($class);
|
$baseClass = DataObject::getSchema()->baseDataClass($class);
|
||||||
$list = DataList::create($baseClass)
|
$list = DataList::create($baseClass)
|
||||||
->setDataQueryParam([
|
->setDataQueryParam([
|
||||||
"Versioned.mode" => 'version',
|
"Versioned.mode" => 'version',
|
||||||
|
@ -93,8 +93,11 @@ class SearchContext extends Object {
|
|||||||
*/
|
*/
|
||||||
protected function applyBaseTableFields() {
|
protected function applyBaseTableFields() {
|
||||||
$classes = ClassInfo::dataClassesFor($this->modelClass);
|
$classes = ClassInfo::dataClassesFor($this->modelClass);
|
||||||
$fields = array("\"".ClassInfo::baseDataClass($this->modelClass).'".*');
|
$baseTable = DataObject::getSchema()->baseDataTable($this->modelClass);
|
||||||
if($this->modelClass != $classes[0]) $fields[] = '"'.$classes[0].'".*';
|
$fields = array("\"{$baseTable}\".*");
|
||||||
|
if($this->modelClass != $classes[0]) {
|
||||||
|
$fields[] = '"'.$classes[0].'".*';
|
||||||
|
}
|
||||||
//$fields = array_keys($model->db());
|
//$fields = array_keys($model->db());
|
||||||
$fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"';
|
$fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"';
|
||||||
return $fields;
|
return $fields;
|
||||||
|
@ -51,6 +51,7 @@ class FulltextFilter extends SearchFilter {
|
|||||||
* MyDataObject::get()->filter('SearchFields:fulltext', 'search term')
|
* MyDataObject::get()->filter('SearchFields:fulltext', 'search term')
|
||||||
* </code>
|
* </code>
|
||||||
*
|
*
|
||||||
|
* @throws Exception
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getDbName() {
|
public function getDbName() {
|
||||||
@ -65,8 +66,11 @@ class FulltextFilter extends SearchFilter {
|
|||||||
if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) {
|
if(preg_match('/^fulltext\s+\((.+)\)$/i', $index, $matches)) {
|
||||||
return $this->prepareColumns($matches[1]);
|
return $this->prepareColumns($matches[1]);
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Invalid fulltext index format for '" . $this->getName()
|
throw new Exception(sprintf(
|
||||||
. "' on '" . $this->model . "'");
|
"Invalid fulltext index format for '%s' on '%s'",
|
||||||
|
$this->getName(),
|
||||||
|
$this->model
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,13 +82,14 @@ class FulltextFilter extends SearchFilter {
|
|||||||
* Adds table identifier to the every column.
|
* Adds table identifier to the every column.
|
||||||
* Columns must have table identifier to prevent duplicate column name error.
|
* Columns must have table identifier to prevent duplicate column name error.
|
||||||
*
|
*
|
||||||
|
* @param array $columns
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function prepareColumns($columns) {
|
protected function prepareColumns($columns) {
|
||||||
$cols = preg_split('/"?\s*,\s*"?/', trim($columns, '(") '));
|
$cols = preg_split('/"?\s*,\s*"?/', trim($columns, '(") '));
|
||||||
$class = ClassInfo::table_for_object_field($this->model, current($cols));
|
$table = DataObject::getSchema()->tableForField($this->model, current($cols));
|
||||||
$cols = array_map(function($col) use ($class) {
|
$cols = array_map(function($col) use ($table) {
|
||||||
return sprintf('"%s"."%s"', $class, $col);
|
return sprintf('"%s"."%s"', $table, $col);
|
||||||
}, $cols);
|
}, $cols);
|
||||||
return implode(',', $cols);
|
return implode(',', $cols);
|
||||||
}
|
}
|
||||||
|
@ -161,9 +161,10 @@ abstract class SearchFilter extends Object {
|
|||||||
*/
|
*/
|
||||||
public function getDbName() {
|
public function getDbName() {
|
||||||
// Special handler for "NULL" relations
|
// Special handler for "NULL" relations
|
||||||
if($this->name == "NULL") {
|
if($this->name === "NULL") {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that we're dealing with a DataObject.
|
// Ensure that we're dealing with a DataObject.
|
||||||
if (!is_subclass_of($this->model, 'DataObject')) {
|
if (!is_subclass_of($this->model, 'DataObject')) {
|
||||||
throw new InvalidArgumentException(
|
throw new InvalidArgumentException(
|
||||||
@ -171,19 +172,16 @@ abstract class SearchFilter extends Object {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidateClass = ClassInfo::table_for_object_field(
|
// Find table this field belongs to
|
||||||
$this->model,
|
$table = DataObject::getSchema()->tableForField($this->model, $this->name);
|
||||||
$this->name
|
if(!$table) {
|
||||||
);
|
|
||||||
|
|
||||||
if($candidateClass == 'DataObject') {
|
|
||||||
// fallback to the provided name in the event of a joined column
|
// fallback to the provided name in the event of a joined column
|
||||||
// name (as the candidate class doesn't check joined records)
|
// name (as the candidate class doesn't check joined records)
|
||||||
$parts = explode('.', $this->fullName);
|
$parts = explode('.', $this->fullName);
|
||||||
return '"' . implode('"."', $parts) . '"';
|
return '"' . implode('"."', $parts) . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf('"%s"."%s"', $candidateClass, $this->name);
|
return sprintf('"%s"."%s"', $table, $this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -992,33 +992,46 @@ class Security extends Controller implements TemplateGlobalProvider {
|
|||||||
*/
|
*/
|
||||||
public static function database_is_ready() {
|
public static function database_is_ready() {
|
||||||
// Used for unit tests
|
// Used for unit tests
|
||||||
if(self::$force_database_is_ready !== NULL) return self::$force_database_is_ready;
|
if(self::$force_database_is_ready !== null) {
|
||||||
|
return self::$force_database_is_ready;
|
||||||
|
}
|
||||||
|
|
||||||
if(self::$database_is_ready) return self::$database_is_ready;
|
if(self::$database_is_ready) {
|
||||||
|
return self::$database_is_ready;
|
||||||
|
}
|
||||||
|
|
||||||
$requiredTables = ClassInfo::dataClassesFor('Member');
|
$requiredClasses = ClassInfo::dataClassesFor('Member');
|
||||||
$requiredTables[] = 'Group';
|
$requiredClasses[] = 'Group';
|
||||||
$requiredTables[] = 'Permission';
|
$requiredClasses[] = 'Permission';
|
||||||
|
|
||||||
foreach($requiredTables as $table) {
|
foreach($requiredClasses as $class) {
|
||||||
// Skip test classes, as not all test classes are scaffolded at once
|
// Skip test classes, as not all test classes are scaffolded at once
|
||||||
if(is_subclass_of($table, 'TestOnly')) continue;
|
if(is_subclass_of($class, 'TestOnly')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// if any of the tables aren't created in the database
|
// if any of the tables aren't created in the database
|
||||||
if(!ClassInfo::hasTable($table)) return false;
|
$table = DataObject::getSchema()->tableName($class);
|
||||||
|
if(!ClassInfo::hasTable($table)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// HACK: DataExtensions aren't applied until a class is instantiated for
|
// HACK: DataExtensions aren't applied until a class is instantiated for
|
||||||
// the first time, so create an instance here.
|
// the first time, so create an instance here.
|
||||||
singleton($table);
|
singleton($class);
|
||||||
|
|
||||||
// if any of the tables don't have all fields mapped as table columns
|
// if any of the tables don't have all fields mapped as table columns
|
||||||
$dbFields = DB::field_list($table);
|
$dbFields = DB::field_list($table);
|
||||||
if(!$dbFields) return false;
|
if(!$dbFields) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$objFields = DataObject::database_fields($table);
|
$objFields = DataObject::database_fields($class);
|
||||||
$missingFields = array_diff_key($objFields, $dbFields);
|
$missingFields = array_diff_key($objFields, $dbFields);
|
||||||
|
|
||||||
if($missingFields) return false;
|
if($missingFields) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self::$database_is_ready = true;
|
self::$database_is_ready = true;
|
||||||
|
|
||||||
|
@ -8,10 +8,13 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
|
|
||||||
protected $extraDataObjects = array(
|
protected $extraDataObjects = array(
|
||||||
'ClassInfoTest_BaseClass',
|
'ClassInfoTest_BaseClass',
|
||||||
|
'ClassInfoTest_BaseDataClass',
|
||||||
'ClassInfoTest_ChildClass',
|
'ClassInfoTest_ChildClass',
|
||||||
'ClassInfoTest_GrandChildClass',
|
'ClassInfoTest_GrandChildClass',
|
||||||
'ClassInfoTest_BaseDataClass',
|
'ClassInfoTest_HasFields',
|
||||||
'ClassInfoTest_NoFields',
|
'ClassInfoTest_NoFields',
|
||||||
|
'ClassInfoTest_WithCustomTable',
|
||||||
|
'ClassInfoTest_WithRelation',
|
||||||
);
|
);
|
||||||
|
|
||||||
public function setUp() {
|
public function setUp() {
|
||||||
@ -26,6 +29,7 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
$this->assertTrue(ClassInfo::exists('CLASSINFOTEST'));
|
$this->assertTrue(ClassInfo::exists('CLASSINFOTEST'));
|
||||||
$this->assertTrue(ClassInfo::exists('stdClass'));
|
$this->assertTrue(ClassInfo::exists('stdClass'));
|
||||||
$this->assertTrue(ClassInfo::exists('stdCLASS'));
|
$this->assertTrue(ClassInfo::exists('stdCLASS'));
|
||||||
|
$this->assertFalse(ClassInfo::exists('SomeNonExistantClass'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSubclassesFor() {
|
public function testSubclassesFor() {
|
||||||
@ -50,12 +54,15 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testClassName() {
|
public function testClassName()
|
||||||
|
{
|
||||||
$this->assertEquals('ClassInfoTest', ClassInfo::class_name($this));
|
$this->assertEquals('ClassInfoTest', ClassInfo::class_name($this));
|
||||||
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('ClassInfoTest'));
|
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('ClassInfoTest'));
|
||||||
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('CLaSsInfOTEsT'));
|
$this->assertEquals('ClassInfoTest', ClassInfo::class_name('CLaSsInfOTEsT'));
|
||||||
|
}
|
||||||
|
|
||||||
// This is for backwards compatiblity and will be removed in 4.0
|
public function testNonClassName() {
|
||||||
|
$this->setExpectedException('ReflectionException', 'Class IAmAClassThatDoesNotExist does not exist');
|
||||||
$this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist'));
|
$this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,21 +83,6 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @covers ClassInfo::baseDataClass()
|
|
||||||
*/
|
|
||||||
public function testBaseDataClass() {
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_BaseClass'));
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('classinfotest_baseclass'));
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_ChildClass'));
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('CLASSINFOTEST_CHILDCLASS'));
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GrandChildClass'));
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseClass', ClassInfo::baseDataClass('ClassInfoTest_GRANDChildClass'));
|
|
||||||
|
|
||||||
$this->setExpectedException('InvalidArgumentException');
|
|
||||||
ClassInfo::baseDataClass('DataObject');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers ClassInfo::ancestry()
|
* @covers ClassInfo::ancestry()
|
||||||
*/
|
*/
|
||||||
@ -125,7 +117,8 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
$expect = array(
|
$expect = array(
|
||||||
'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass',
|
'ClassInfoTest_BaseDataClass' => 'ClassInfoTest_BaseDataClass',
|
||||||
'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields',
|
'ClassInfoTest_HasFields' => 'ClassInfoTest_HasFields',
|
||||||
'ClassInfoTest_WithRelation' => 'ClassInfoTest_WithRelation'
|
'ClassInfoTest_WithRelation' => 'ClassInfoTest_WithRelation',
|
||||||
|
'ClassInfoTest_WithCustomTable' => 'ClassInfoTest_WithCustomTable',
|
||||||
);
|
);
|
||||||
|
|
||||||
$classes = array(
|
$classes = array(
|
||||||
@ -152,62 +145,6 @@ class ClassInfoTest extends SapphireTest {
|
|||||||
$this->assertEquals($expect, ClassInfo::dataClassesFor(strtolower($classes[2])));
|
$this->assertEquals($expect, ClassInfo::dataClassesFor(strtolower($classes[2])));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testTableForObjectField() {
|
|
||||||
$this->assertEquals('ClassInfoTest_WithRelation',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_WithRelation', 'RelationID')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_WithRelation',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_withrelation', 'RelationID')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_BaseDataClass', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_NoFields', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('classinfotest_nofields', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertEquals('ClassInfoTest_HasFields',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'Description')
|
|
||||||
);
|
|
||||||
|
|
||||||
// existing behaviour fallback to DataObject? Should be null.
|
|
||||||
$this->assertEquals('DataObject',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_BaseClass', 'Nonexist')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertNull(
|
|
||||||
ClassInfo::table_for_object_field('SomeFakeClassHere', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertNull(
|
|
||||||
ClassInfo::table_for_object_field('Object', 'Title')
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertNull(
|
|
||||||
ClassInfo::table_for_object_field(null, null)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test fixed fields
|
|
||||||
$this->assertEquals(
|
|
||||||
'ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_HasFields', 'ID')
|
|
||||||
);
|
|
||||||
$this->assertEquals(
|
|
||||||
'ClassInfoTest_BaseDataClass',
|
|
||||||
ClassInfo::table_for_object_field('ClassInfoTest_NoFields', 'Created')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,6 +207,13 @@ class ClassInfoTest_HasFields extends ClassInfoTest_NoFields {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ClassInfoTest_WithCustomTable extends ClassInfoTest_NoFields {
|
||||||
|
private static $table_name = 'CITWithCustomTable';
|
||||||
|
private static $db = array(
|
||||||
|
'Description' => 'Text'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package framework
|
* @package framework
|
||||||
* @subpackage tests
|
* @subpackage tests
|
||||||
|
@ -28,7 +28,7 @@ class ChangeSetItemTest extends SapphireTest {
|
|||||||
|
|
||||||
$item = new ChangeSetItem([
|
$item = new ChangeSetItem([
|
||||||
'ObjectID' => $object->ID,
|
'ObjectID' => $object->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($object->ClassName)
|
'ObjectClass' => $object->baseClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
@ -80,7 +80,7 @@ class ChangeSetItemTest extends SapphireTest {
|
|||||||
|
|
||||||
$item = new ChangeSetItem([
|
$item = new ChangeSetItem([
|
||||||
'ObjectID' => $object->ID,
|
'ObjectID' => $object->ID,
|
||||||
'ObjectClass' => ClassInfo::baseDataClass($object)
|
'ObjectClass' => $object->baseClass(),
|
||||||
]);
|
]);
|
||||||
$item->write();
|
$item->write();
|
||||||
|
|
||||||
|
@ -150,7 +150,10 @@ class ChangeSetTest extends SapphireTest {
|
|||||||
$object = $this->objFromFixture($class, $identifier);
|
$object = $this->objFromFixture($class, $identifier);
|
||||||
|
|
||||||
foreach($items as $i => $item) {
|
foreach($items as $i => $item) {
|
||||||
if ($item->ObjectClass == ClassInfo::baseDataClass($object) && $item->ObjectID == $object->ID && $item->Added == $mode) {
|
if ( $item->ObjectClass == $object->baseClass()
|
||||||
|
&& $item->ObjectID == $object->ID
|
||||||
|
&& $item->Added == $mode
|
||||||
|
) {
|
||||||
unset($items[$i]);
|
unset($items[$i]);
|
||||||
continue 2;
|
continue 2;
|
||||||
}
|
}
|
||||||
|
@ -9,32 +9,10 @@ class DataListTest extends SapphireTest {
|
|||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
public function setUpOnce() {
|
||||||
// From DataObjectTest
|
$this->extraDataObjects = DataObjectTest::$extra_data_objects;
|
||||||
'DataObjectTest_Team',
|
parent::setUpOnce();
|
||||||
'DataObjectTest_Fixture',
|
}
|
||||||
'DataObjectTest_SubTeam',
|
|
||||||
'OtherSubclassWithSameField',
|
|
||||||
'DataObjectTest_FieldlessTable',
|
|
||||||
'DataObjectTest_FieldlessSubTable',
|
|
||||||
'DataObjectTest_ValidatedObject',
|
|
||||||
'DataObjectTest_Player',
|
|
||||||
'DataObjectTest_TeamComment',
|
|
||||||
'DataObjectTest_EquipmentCompany',
|
|
||||||
'DataObjectTest_SubEquipmentCompany',
|
|
||||||
'DataObjectTest\NamespacedClass',
|
|
||||||
'DataObjectTest\RelationClass',
|
|
||||||
'DataObjectTest_ExtendedTeamComment',
|
|
||||||
'DataObjectTest_Company',
|
|
||||||
'DataObjectTest_Staff',
|
|
||||||
'DataObjectTest_CEO',
|
|
||||||
'DataObjectTest_Fan',
|
|
||||||
'DataObjectTest_Play',
|
|
||||||
'DataObjectTest_Ploy',
|
|
||||||
'DataObjectTest_Bogey',
|
|
||||||
'ManyManyListTest_Product',
|
|
||||||
'ManyManyListTest_Category',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function testFilterDataObjectByCreatedDate() {
|
public function testFilterDataObjectByCreatedDate() {
|
||||||
// create an object to test with
|
// create an object to test with
|
||||||
|
354
tests/model/DataObjectSchemaTest.php
Normal file
354
tests/model/DataObjectSchemaTest.php
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests schema inspection of DataObjects
|
||||||
|
*/
|
||||||
|
class DataObjectSchemaTest extends SapphireTest
|
||||||
|
{
|
||||||
|
protected static $fixture_file = 'DataObjectSchemaTest.yml';
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
// Classes in base namespace
|
||||||
|
'DataObjectSchemaTest_BaseClass',
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
'DataObjectSchemaTest_ChildClass',
|
||||||
|
'DataObjectSchemaTest_GrandChildClass',
|
||||||
|
'DataObjectSchemaTest_HasFields',
|
||||||
|
'DataObjectSchemaTest_NoFields',
|
||||||
|
'DataObjectSchemaTest_WithCustomTable',
|
||||||
|
'DataObjectSchemaTest_WithRelation',
|
||||||
|
// Classes in sub-namespace (See DataObjectSchemaTest_Namespacejd.php)
|
||||||
|
'Namespaced\DOST\MyObject',
|
||||||
|
'Namespaced\DOST\MyObject_CustomTable',
|
||||||
|
'Namespaced\DOST\MyObject_NestedObject',
|
||||||
|
'Namespaced\DOST\MyObject_NamespacedTable',
|
||||||
|
'Namespaced\DOST\MyObject_Namespaced_Subclass',
|
||||||
|
'Namespaced\DOST\MyObject_NoFields',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test table name generation
|
||||||
|
*/
|
||||||
|
public function testTableName() {
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
|
// Non-namespaced tables
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithRelation',
|
||||||
|
$schema->tableName('DataObjectSchemaTest_WithRelation')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'DOSTWithCustomTable',
|
||||||
|
$schema->tableName('DataObjectSchemaTest_WithCustomTable')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Namespaced tables
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'CustomNamespacedTable',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject_CustomTable')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NestedObject',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject_NestedObject')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject_NamespacedTable')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\SubclassedTable',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject_Namespaced_Subclass')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NoFields',
|
||||||
|
$schema->tableName('Namespaced\DOST\MyObject_NoFields')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the class name is convertable from the table
|
||||||
|
*/
|
||||||
|
public function testClassNameForTable() {
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
|
// Tables that aren't classes
|
||||||
|
$this->assertNull($schema->tableClass('NotARealTable'));
|
||||||
|
|
||||||
|
|
||||||
|
// Non-namespaced tables
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithRelation',
|
||||||
|
$schema->tableClass('DataObjectSchemaTest_WithRelation')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithCustomTable',
|
||||||
|
$schema->tableClass('DOSTWithCustomTable')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Namespaced tables
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject',
|
||||||
|
$schema->tableClass('Namespaced\DOST\MyObject')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_CustomTable',
|
||||||
|
$schema->tableClass('CustomNamespacedTable')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NestedObject',
|
||||||
|
$schema->tableClass('Namespaced\DOST\MyObject_NestedObject')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NamespacedTable',
|
||||||
|
$schema->tableClass('Custom\NamespacedTable')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_Namespaced_Subclass',
|
||||||
|
$schema->tableClass('Custom\SubclassedTable')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NoFields',
|
||||||
|
$schema->tableClass('Namespaced\DOST\MyObject_NoFields')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test non-namespaced tables
|
||||||
|
*/
|
||||||
|
public function testTableForObjectField() {
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithRelation',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_WithRelation', 'RelationID')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithRelation',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_withrelation', 'RelationID')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_BaseDataClass', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_HasFields', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_NoFields', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_nofields', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_HasFields',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_HasFields', 'Description')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Class and table differ for this model
|
||||||
|
$this->assertEquals(
|
||||||
|
'DOSTWithCustomTable',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_WithCustomTable', 'Description')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_WithCustomTable',
|
||||||
|
$schema->classForField('DataObjectSchemaTest_WithCustomTable', 'Description')
|
||||||
|
);
|
||||||
|
$this->assertNull(
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_WithCustomTable', 'NotAField')
|
||||||
|
);
|
||||||
|
$this->assertNull(
|
||||||
|
$schema->classForField('DataObjectSchemaTest_WithCustomTable', 'NotAField')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Non-existant fields shouldn't match any table
|
||||||
|
$this->assertNull(
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_BaseClass', 'Nonexist')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertNull(
|
||||||
|
$schema->tableForField('Object', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test fixed fields
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_HasFields', 'ID')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'DataObjectSchemaTest_BaseDataClass',
|
||||||
|
$schema->tableForField('DataObjectSchemaTest_NoFields', 'Created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check table for fields with namespaced objects can be found
|
||||||
|
*/
|
||||||
|
public function testTableForNamespacedObjectField() {
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
|
// MyObject
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
// MyObject_CustomTable
|
||||||
|
$this->assertEquals(
|
||||||
|
'CustomNamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_CustomTable', 'Title')
|
||||||
|
);
|
||||||
|
|
||||||
|
// MyObject_NestedObject
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_NestedObject', 'Title')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NestedObject',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_NestedObject', 'Content')
|
||||||
|
);
|
||||||
|
|
||||||
|
// MyObject_NamespacedTable
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_NamespacedTable', 'Description')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_NamespacedTable', 'OwnerID')
|
||||||
|
);
|
||||||
|
|
||||||
|
// MyObject_Namespaced_Subclass
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'OwnerID')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'Title')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\NamespacedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'ID')
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
'Custom\SubclassedTable',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_Namespaced_Subclass', 'Details')
|
||||||
|
);
|
||||||
|
|
||||||
|
// MyObject_NoFields
|
||||||
|
$this->assertEquals(
|
||||||
|
'Namespaced\DOST\MyObject_NoFields',
|
||||||
|
$schema->tableForField('Namespaced\DOST\MyObject_NoFields', 'Created')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that relations join on the correct columns
|
||||||
|
*/
|
||||||
|
public function testRelationsQuery() {
|
||||||
|
$namespaced1 = $this->objFromFixture('Namespaced\DOST\MyObject_NamespacedTable', 'namespaced1');
|
||||||
|
$nofields = $this->objFromFixture('Namespaced\DOST\MyObject_NoFields', 'nofields1');
|
||||||
|
$subclass1 = $this->objFromFixture('Namespaced\DOST\MyObject_Namespaced_Subclass', 'subclass1');
|
||||||
|
$customtable1 = $this->objFromFixture('Namespaced\DOST\MyObject_CustomTable', 'customtable1');
|
||||||
|
$customtable3 = $this->objFromFixture('Namespaced\DOST\MyObject_CustomTable', 'customtable3');
|
||||||
|
|
||||||
|
// Check has_one / has_many
|
||||||
|
$this->assertEquals($nofields->ID, $namespaced1->Owner()->ID);
|
||||||
|
$this->assertDOSEquals([
|
||||||
|
['Title' => 'Namespaced 1'],
|
||||||
|
], $nofields->Owns());
|
||||||
|
|
||||||
|
// Check many_many / belongs_many_many
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Custom Table 1'],
|
||||||
|
['Title' => 'Custom Table 2'],
|
||||||
|
],
|
||||||
|
$subclass1->Children()
|
||||||
|
);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Subclass 1', 'Details' => 'Oh, Hi!',]]
|
||||||
|
,
|
||||||
|
$customtable1->Parents()
|
||||||
|
);
|
||||||
|
$this->assertEmpty($customtable3->Parents()->count());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers DataObjectSchema::baseDataClass()
|
||||||
|
*/
|
||||||
|
public function testBaseDataClass() {
|
||||||
|
$schema = DataObject::getSchema();
|
||||||
|
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_BaseClass'));
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_baseclass'));
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_ChildClass'));
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_CHILDCLASS'));
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GrandChildClass'));
|
||||||
|
$this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GRANDChildClass'));
|
||||||
|
|
||||||
|
$this->setExpectedException('InvalidArgumentException');
|
||||||
|
$schema->baseDataClass('DataObject');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_BaseClass extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_ChildClass extends DataObjectSchemaTest_BaseClass {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_GrandChildClass extends DataObjectSchemaTest_ChildClass {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_BaseDataClass extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_NoFields extends DataObjectSchemaTest_BaseDataClass {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_HasFields extends DataObjectSchemaTest_NoFields {
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Description' => 'Varchar'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_WithCustomTable extends DataObjectSchemaTest_NoFields {
|
||||||
|
private static $table_name = 'DOSTWithCustomTable';
|
||||||
|
private static $db = array(
|
||||||
|
'Description' => 'Text'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataObjectSchemaTest_WithRelation extends DataObjectSchemaTest_NoFields {
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Relation' => 'DataObjectSchemaTest_HasFields'
|
||||||
|
);
|
||||||
|
}
|
36
tests/model/DataObjectSchemaTest.yml
Normal file
36
tests/model/DataObjectSchemaTest.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
Namespaced\DOST\MyObject:
|
||||||
|
object1:
|
||||||
|
Title: 'Object 1'
|
||||||
|
Description: 'Description 1'
|
||||||
|
|
||||||
|
Namespaced\DOST\MyObject_CustomTable:
|
||||||
|
customtable1:
|
||||||
|
Title: 'Custom Table 1'
|
||||||
|
Description: 'Description A'
|
||||||
|
customtable2:
|
||||||
|
Title: 'Custom Table 2'
|
||||||
|
Description: 'Description B'
|
||||||
|
customtable3:
|
||||||
|
Title: 'Custom Table 3'
|
||||||
|
Description: 'Orphaned item'
|
||||||
|
|
||||||
|
Namespaced\DOST\MyObject_NestedObject:
|
||||||
|
nested1:
|
||||||
|
Title: 'Nested 1'
|
||||||
|
Description: 'Nested Description'
|
||||||
|
Content: '<p>Hello!</p>'
|
||||||
|
|
||||||
|
Namespaced\DOST\MyObject_NoFields:
|
||||||
|
nofields1: {}
|
||||||
|
|
||||||
|
Namespaced\DOST\MyObject_NamespacedTable:
|
||||||
|
namespaced1:
|
||||||
|
Title: 'Namespaced 1'
|
||||||
|
Owner: =>Namespaced\DOST\MyObject_NoFields.nofields1
|
||||||
|
|
||||||
|
Namespaced\DOST\MyObject_Namespaced_Subclass:
|
||||||
|
subclass1:
|
||||||
|
Title: 'Subclass 1'
|
||||||
|
Details: 'Oh, Hi!'
|
||||||
|
Children: =>Namespaced\DOST\MyObject_CustomTable.customtable1, =>Namespaced\DOST\MyObject_CustomTable.customtable2
|
||||||
|
|
78
tests/model/DataObjectSchemaTest_Namespaced.php
Normal file
78
tests/model/DataObjectSchemaTest_Namespaced.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespaced dataobjcets used by DataObjectSchemaTest
|
||||||
|
*/
|
||||||
|
namespace Namespaced\DOST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic namespaced object
|
||||||
|
*/
|
||||||
|
class MyObject extends \DataObject implements \TestOnly {
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Description' => 'Text',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespaced object with custom table
|
||||||
|
*/
|
||||||
|
class MyObject_CustomTable extends \DataObject implements \TestOnly {
|
||||||
|
private static $table_name = 'CustomNamespacedTable';
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Description' => 'Text',
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $belongs_many_many = [
|
||||||
|
'Parents' => 'Namespaced\DOST\MyObject_Namespaced_Subclass',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespaced subclassed object
|
||||||
|
*/
|
||||||
|
class MyObject_NestedObject extends MyObject implements \TestOnly {
|
||||||
|
private static $db = [
|
||||||
|
'Content' => 'HTMLText',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespaced object with custom table that itself is namespaced
|
||||||
|
*/
|
||||||
|
class MyObject_NamespacedTable extends \DataObject implements \TestOnly {
|
||||||
|
private static $table_name = 'Custom\NamespacedTable';
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Description' => 'Text',
|
||||||
|
];
|
||||||
|
private static $has_one = [
|
||||||
|
'Owner' => 'Namespaced\DOST\MyObject_NoFields',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass of a namespaced class
|
||||||
|
* Has a many_many to another namespaced table
|
||||||
|
*/
|
||||||
|
class MyObject_Namespaced_Subclass extends MyObject_NamespacedTable implements \TestOnly {
|
||||||
|
private static $table_name = 'Custom\SubclassedTable';
|
||||||
|
private static $db = [
|
||||||
|
'Details' => 'Varchar',
|
||||||
|
];
|
||||||
|
private static $many_many = [
|
||||||
|
'Children' => 'Namespaced\DOST\MyObject_CustomTable',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespaced class without any fields
|
||||||
|
* has a has_many to another namespaced table
|
||||||
|
*/
|
||||||
|
class MyObject_NoFields extends \DataObject implements \TestOnly {
|
||||||
|
private static $has_many = [
|
||||||
|
'Owns' => 'Namespaced\DOST\MyObject_NamespacedTable',
|
||||||
|
];
|
||||||
|
}
|
@ -10,7 +10,12 @@ class DataObjectTest extends SapphireTest {
|
|||||||
|
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
/**
|
||||||
|
* Standard set of dataobject test classes
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public static $extra_data_objects = array(
|
||||||
'DataObjectTest_Team',
|
'DataObjectTest_Team',
|
||||||
'DataObjectTest_Fixture',
|
'DataObjectTest_Fixture',
|
||||||
'DataObjectTest_SubTeam',
|
'DataObjectTest_SubTeam',
|
||||||
@ -32,10 +37,17 @@ class DataObjectTest extends SapphireTest {
|
|||||||
'DataObjectTest_Play',
|
'DataObjectTest_Play',
|
||||||
'DataObjectTest_Ploy',
|
'DataObjectTest_Ploy',
|
||||||
'DataObjectTest_Bogey',
|
'DataObjectTest_Bogey',
|
||||||
|
// From ManyManyListTest
|
||||||
|
'ManyManyListTest_ExtraFields',
|
||||||
'ManyManyListTest_Product',
|
'ManyManyListTest_Product',
|
||||||
'ManyManyListTest_Category',
|
'ManyManyListTest_Category',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public function setUpOnce() {
|
||||||
|
$this->extraDataObjects = static::$extra_data_objects;
|
||||||
|
parent::setUpOnce();
|
||||||
|
}
|
||||||
|
|
||||||
public function testDb() {
|
public function testDb() {
|
||||||
$obj = new DataObjectTest_TeamComment();
|
$obj = new DataObjectTest_TeamComment();
|
||||||
$dbFields = $obj->db();
|
$dbFields = $obj->db();
|
||||||
|
@ -75,7 +75,7 @@ class DataQueryTest extends SapphireTest {
|
|||||||
|
|
||||||
//test many_many with separate inheritance
|
//test many_many with separate inheritance
|
||||||
$newDQ = new DataQuery('DataQueryTest_C');
|
$newDQ = new DataQuery('DataQueryTest_C');
|
||||||
$baseDBTable = ClassInfo::baseDataClass('DataQueryTest_C');
|
$baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_C');
|
||||||
$newDQ->applyRelation('ManyTestAs');
|
$newDQ->applyRelation('ManyTestAs');
|
||||||
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
|
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
|
||||||
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
|
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
|
||||||
@ -84,7 +84,7 @@ class DataQueryTest extends SapphireTest {
|
|||||||
|
|
||||||
//test many_many with shared inheritance
|
//test many_many with shared inheritance
|
||||||
$newDQ = new DataQuery('DataQueryTest_E');
|
$newDQ = new DataQuery('DataQueryTest_E');
|
||||||
$baseDBTable = ClassInfo::baseDataClass('DataQueryTest_E');
|
$baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_E');
|
||||||
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
|
//check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses)
|
||||||
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
|
$this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable));
|
||||||
//check we are explicitly selecting "FROM" the DO's table
|
//check we are explicitly selecting "FROM" the DO's table
|
||||||
|
@ -5,32 +5,10 @@ class HasManyListTest extends SapphireTest {
|
|||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
public function setUpOnce() {
|
||||||
// From DataObjectTest
|
$this->extraDataObjects = DataObjectTest::$extra_data_objects;
|
||||||
'DataObjectTest_Team',
|
parent::setUpOnce();
|
||||||
'DataObjectTest_Fixture',
|
}
|
||||||
'DataObjectTest_SubTeam',
|
|
||||||
'OtherSubclassWithSameField',
|
|
||||||
'DataObjectTest_FieldlessTable',
|
|
||||||
'DataObjectTest_FieldlessSubTable',
|
|
||||||
'DataObjectTest_ValidatedObject',
|
|
||||||
'DataObjectTest_Player',
|
|
||||||
'DataObjectTest_TeamComment',
|
|
||||||
'DataObjectTest_EquipmentCompany',
|
|
||||||
'DataObjectTest_SubEquipmentCompany',
|
|
||||||
'DataObjectTest\NamespacedClass',
|
|
||||||
'DataObjectTest\RelationClass',
|
|
||||||
'DataObjectTest_ExtendedTeamComment',
|
|
||||||
'DataObjectTest_Company',
|
|
||||||
'DataObjectTest_Staff',
|
|
||||||
'DataObjectTest_CEO',
|
|
||||||
'DataObjectTest_Fan',
|
|
||||||
'DataObjectTest_Play',
|
|
||||||
'DataObjectTest_Ploy',
|
|
||||||
'DataObjectTest_Bogey',
|
|
||||||
'ManyManyListTest_Product',
|
|
||||||
'ManyManyListTest_Category',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function testRelationshipEmptyOnNewRecords() {
|
public function testRelationshipEmptyOnNewRecords() {
|
||||||
// Relies on the fact that (unrelated) comments exist in the fixture file already
|
// Relies on the fact that (unrelated) comments exist in the fixture file already
|
||||||
|
@ -10,35 +10,10 @@ class ManyManyListTest extends SapphireTest {
|
|||||||
|
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
public function setUpOnce() {
|
||||||
// From DataObjectTest
|
$this->extraDataObjects = DataObjectTest::$extra_data_objects;
|
||||||
'DataObjectTest_Team',
|
parent::setUpOnce();
|
||||||
'DataObjectTest_Fixture',
|
}
|
||||||
'DataObjectTest_SubTeam',
|
|
||||||
'OtherSubclassWithSameField',
|
|
||||||
'DataObjectTest_FieldlessTable',
|
|
||||||
'DataObjectTest_FieldlessSubTable',
|
|
||||||
'DataObjectTest_ValidatedObject',
|
|
||||||
'DataObjectTest_Player',
|
|
||||||
'DataObjectTest_TeamComment',
|
|
||||||
'DataObjectTest_EquipmentCompany',
|
|
||||||
'DataObjectTest_SubEquipmentCompany',
|
|
||||||
'DataObjectTest\NamespacedClass',
|
|
||||||
'DataObjectTest\RelationClass',
|
|
||||||
'DataObjectTest_ExtendedTeamComment',
|
|
||||||
'DataObjectTest_Company',
|
|
||||||
'DataObjectTest_Staff',
|
|
||||||
'DataObjectTest_CEO',
|
|
||||||
'DataObjectTest_Fan',
|
|
||||||
'DataObjectTest_Play',
|
|
||||||
'DataObjectTest_Ploy',
|
|
||||||
'DataObjectTest_Bogey',
|
|
||||||
// From ManyManyListTest
|
|
||||||
'ManyManyListTest_ExtraFields',
|
|
||||||
'ManyManyListTest_Product',
|
|
||||||
'ManyManyListTest_Category',
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
public function testAddCompositedExtraFields() {
|
public function testAddCompositedExtraFields() {
|
||||||
$obj = new ManyManyListTest_ExtraFields();
|
$obj = new ManyManyListTest_ExtraFields();
|
||||||
|
@ -9,32 +9,10 @@ class SS_MapTest extends SapphireTest {
|
|||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
public function setUpOnce() {
|
||||||
// From DataObjectTest
|
$this->extraDataObjects = DataObjectTest::$extra_data_objects;
|
||||||
'DataObjectTest_Team',
|
parent::setUpOnce();
|
||||||
'DataObjectTest_Fixture',
|
}
|
||||||
'DataObjectTest_SubTeam',
|
|
||||||
'OtherSubclassWithSameField',
|
|
||||||
'DataObjectTest_FieldlessTable',
|
|
||||||
'DataObjectTest_FieldlessSubTable',
|
|
||||||
'DataObjectTest_ValidatedObject',
|
|
||||||
'DataObjectTest_Player',
|
|
||||||
'DataObjectTest_TeamComment',
|
|
||||||
'DataObjectTest_EquipmentCompany',
|
|
||||||
'DataObjectTest_SubEquipmentCompany',
|
|
||||||
'DataObjectTest\NamespacedClass',
|
|
||||||
'DataObjectTest\RelationClass',
|
|
||||||
'DataObjectTest_ExtendedTeamComment',
|
|
||||||
'DataObjectTest_Company',
|
|
||||||
'DataObjectTest_Staff',
|
|
||||||
'DataObjectTest_CEO',
|
|
||||||
'DataObjectTest_Fan',
|
|
||||||
'DataObjectTest_Play',
|
|
||||||
'DataObjectTest_Ploy',
|
|
||||||
'DataObjectTest_Bogey',
|
|
||||||
'ManyManyListTest_Product',
|
|
||||||
'ManyManyListTest_Category',
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
public function testValues() {
|
public function testValues() {
|
||||||
|
@ -15,30 +15,10 @@ class PolymorphicHasManyListTest extends SapphireTest {
|
|||||||
// Borrow the model from DataObjectTest
|
// Borrow the model from DataObjectTest
|
||||||
protected static $fixture_file = 'DataObjectTest.yml';
|
protected static $fixture_file = 'DataObjectTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
public function setUpOnce() {
|
||||||
// From DataObjectTest
|
$this->extraDataObjects = DataObjectTest::$extra_data_objects;
|
||||||
'DataObjectTest_Team',
|
parent::setUpOnce();
|
||||||
'DataObjectTest_Fixture',
|
}
|
||||||
'DataObjectTest_SubTeam',
|
|
||||||
'OtherSubclassWithSameField',
|
|
||||||
'DataObjectTest_FieldlessTable',
|
|
||||||
'DataObjectTest_FieldlessSubTable',
|
|
||||||
'DataObjectTest_ValidatedObject',
|
|
||||||
'DataObjectTest_Player',
|
|
||||||
'DataObjectTest_TeamComment',
|
|
||||||
'DataObjectTest_EquipmentCompany',
|
|
||||||
'DataObjectTest_SubEquipmentCompany',
|
|
||||||
'DataObjectTest\NamespacedClass',
|
|
||||||
'DataObjectTest\RelationClass',
|
|
||||||
'DataObjectTest_ExtendedTeamComment',
|
|
||||||
'DataObjectTest_Company',
|
|
||||||
'DataObjectTest_Staff',
|
|
||||||
'DataObjectTest_CEO',
|
|
||||||
'DataObjectTest_Fan',
|
|
||||||
'DataObjectTest_Play',
|
|
||||||
'DataObjectTest_Ploy',
|
|
||||||
'DataObjectTest_Bogey',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function testRelationshipEmptyOnNewRecords() {
|
public function testRelationshipEmptyOnNewRecords() {
|
||||||
// Relies on the fact that (unrelated) comments exist in the fixture file already
|
// Relies on the fact that (unrelated) comments exist in the fixture file already
|
||||||
|
@ -68,30 +68,24 @@ class VersionableExtensionsTest_DataObject extends DataObject implements TestOnl
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class VersionableExtensionsTest_Extension extends DataExtension implements TestOnly {
|
class VersionableExtensionsTest_Extension extends DataExtension implements VersionableExtension, TestOnly {
|
||||||
|
|
||||||
|
|
||||||
public function isVersionedTable($table){
|
public function isVersionedTable($table) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fieldsInExtraTables function.
|
* Update fields and indexes for the versonable suffix table
|
||||||
*
|
*
|
||||||
* @access public
|
* @param string $suffix Table suffix being built
|
||||||
* @param mixed $suffix
|
* @param array $fields List of fields in this model
|
||||||
|
* @param array $indexes List of indexes in this model
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function fieldsInExtraTables($suffix){
|
public function updateVersionableFields($suffix, &$fields, &$indexes){
|
||||||
$fields = array();
|
$indexes['ExtraField'] = true;
|
||||||
//$fields['db'] = DataObject::database_fields($this->owner->class);
|
$fields['ExtraField'] = 'Varchar()';
|
||||||
$fields['indexes'] = $this->owner->databaseIndexes();
|
|
||||||
|
|
||||||
$fields['db'] = array_merge(
|
|
||||||
DataObject::database_fields($this->owner->class)
|
|
||||||
);
|
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
'VersionedTest_WithIndexes_versions' =>
|
'VersionedTest_WithIndexes_versions' =>
|
||||||
array('value' => false, 'message' => 'Unique indexes are no longer unique in _versions table'),
|
array('value' => false, 'message' => 'Unique indexes are no longer unique in _versions table'),
|
||||||
'VersionedTest_WithIndexes_Live' =>
|
'VersionedTest_WithIndexes_Live' =>
|
||||||
array('value' => false, 'message' => 'Unique indexes are no longer unique in _Live table'),
|
array('value' => true, 'message' => 'Unique indexes are unique in _Live table'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test each table's performance
|
// Test each table's performance
|
||||||
@ -56,7 +56,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
if (in_array($indexSpec['value'], $expectedColumns)) {
|
if (in_array($indexSpec['value'], $expectedColumns)) {
|
||||||
$isUnique = $indexSpec['type'] === 'unique';
|
$isUnique = $indexSpec['type'] === 'unique';
|
||||||
$this->assertEquals($isUnique, $expectation['value'], $expectation['message']);
|
$this->assertEquals($isUnique, $expectation['value'], $expectation['message']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,7 +314,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testWritingNewToStage() {
|
public function testWritingNewToStage() {
|
||||||
$origStage = Versioned::get_stage();
|
$origReadingMode = Versioned::get_reading_mode();
|
||||||
|
|
||||||
Versioned::set_stage(Versioned::DRAFT);
|
Versioned::set_stage(Versioned::DRAFT);
|
||||||
$page = new VersionedTest_DataObject();
|
$page = new VersionedTest_DataObject();
|
||||||
@ -333,7 +333,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
$this->assertEquals(1, $stage->count());
|
$this->assertEquals(1, $stage->count());
|
||||||
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
|
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
|
||||||
|
|
||||||
Versioned::set_stage($origStage);
|
Versioned::set_reading_mode($origReadingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -343,7 +343,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
* the VersionedTest_DataObject record though.
|
* the VersionedTest_DataObject record though.
|
||||||
*/
|
*/
|
||||||
public function testWritingNewToLive() {
|
public function testWritingNewToLive() {
|
||||||
$origStage = Versioned::get_stage();
|
$origReadingMode = Versioned::get_reading_mode();
|
||||||
|
|
||||||
Versioned::set_stage(Versioned::LIVE);
|
Versioned::set_stage(Versioned::LIVE);
|
||||||
$page = new VersionedTest_DataObject();
|
$page = new VersionedTest_DataObject();
|
||||||
@ -362,7 +362,7 @@ class VersionedTest extends SapphireTest {
|
|||||||
));
|
));
|
||||||
$this->assertEquals(0, $stage->count());
|
$this->assertEquals(0, $stage->count());
|
||||||
|
|
||||||
Versioned::set_stage($origStage);
|
Versioned::set_reading_mode($origReadingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,8 +144,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function testVersionedCache() {
|
public function testVersionedCache() {
|
||||||
|
$origReadingMode = Versioned::get_reading_mode();
|
||||||
$origStage = Versioned::get_stage();
|
|
||||||
|
|
||||||
// Run without caching in stage to prove data is uncached
|
// Run without caching in stage to prove data is uncached
|
||||||
$this->_reset(false);
|
$this->_reset(false);
|
||||||
@ -211,7 +210,7 @@ class SSViewerCacheBlockTest extends SapphireTest {
|
|||||||
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
$this->_runtemplate('<% cached %>$Inspect<% end_cached %>', $data)
|
||||||
);
|
);
|
||||||
|
|
||||||
Versioned::set_stage($origStage);
|
Versioned::set_reading_mode($origReadingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user