mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
Merge pull request #1360 from tractorcow/3.2-pdo-connector
API New Parameterised Database ORM for Silverstripe 3.2. Ticket #7429
This commit is contained in:
commit
ece95d3580
16
_config/database.yml
Normal file
16
_config/database.yml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: databaseconnectors
|
||||
---
|
||||
Injector:
|
||||
MySQLPDODatabase:
|
||||
class: 'MySQLDatabase'
|
||||
properties:
|
||||
connector: %$PDOConnector
|
||||
schemaManager: %$MySQLSchemaManager
|
||||
queryBuilder: %$DBQueryBuilder
|
||||
MySQLDatabase:
|
||||
class: 'MySQLDatabase'
|
||||
properties:
|
||||
connector: %$MySQLiConnector
|
||||
schemaManager: %$MySQLSchemaManager
|
||||
queryBuilder: %$DBQueryBuilder
|
@ -3,61 +3,29 @@
|
||||
// Register the SilverStripe provided databases
|
||||
$frameworkPath = defined('FRAMEWORK_PATH') ? FRAMEWORK_PATH : FRAMEWORK_NAME;
|
||||
|
||||
// Use MySQLi as default
|
||||
DatabaseAdapterRegistry::register(
|
||||
array(
|
||||
'class' => 'MySQLDatabase',
|
||||
'title' => 'MySQL 5.0+',
|
||||
'title' => 'MySQL 5.0+ (using MySQLi)',
|
||||
'helperPath' => $frameworkPath . '/dev/install/MySQLDatabaseConfigurationHelper.php',
|
||||
'supported' => class_exists('MySQLi'),
|
||||
'missingExtensionText' =>
|
||||
'The <a href="http://www.php.net/manual/en/book.mysqli.php">MySQLi</a>
|
||||
PHP extension is not available. Please install or enable it and refresh this page.'
|
||||
)
|
||||
);
|
||||
|
||||
// Setup MySQL PDO as alternate option
|
||||
DatabaseAdapterRegistry::register(
|
||||
array(
|
||||
'class' => 'MSSQLDatabase',
|
||||
'title' => 'SQL Server 2008',
|
||||
'helperPath' => 'mssql/code/MSSQLDatabaseConfigurationHelper.php',
|
||||
'supported' => (function_exists('mssql_connect') || function_exists('sqlsrv_connect')),
|
||||
'missingExtensionText' => 'Neither the <a href="http://php.net/mssql">mssql</a> or'
|
||||
. ' <a href="http://www.microsoft.com/sqlserver/2005/en/us/PHP-Driver.aspx">sqlsrv</a> PHP extensions are'
|
||||
. ' available. Please install or enable one of them and refresh this page.'
|
||||
'class' => 'MySQLPDODatabase',
|
||||
'title' => 'MySQL 5.0+ (using PDO)',
|
||||
'helperPath' => $frameworkPath . '/dev/install/MySQLDatabaseConfigurationHelper.php',
|
||||
'supported' => (class_exists('PDO') && in_array('mysql', PDO::getAvailableDrivers())),
|
||||
'missingExtensionText' =>
|
||||
'Either the <a href="http://www.php.net/manual/en/book.pdo.php">PDO Extension</a> or
|
||||
the <a href="http://www.php.net/manual/en/ref.pdo-mysql.php">MySQL PDO Driver</a>
|
||||
are unavailable. Please install or enable these and refresh this page.'
|
||||
)
|
||||
);
|
||||
|
||||
DatabaseAdapterRegistry::register(
|
||||
array(
|
||||
'class' => 'PostgreSQLDatabase',
|
||||
'title' => 'PostgreSQL 8.3+',
|
||||
'helperPath' => 'postgresql/code/PostgreSQLDatabaseConfigurationHelper.php',
|
||||
'supported' => function_exists('pg_query'),
|
||||
'missingExtensionText' => 'The <a href="http://php.net/pgsql">pgsql</a> PHP extension is not available. Please'
|
||||
. ' install or enable it and refresh this page.'
|
||||
)
|
||||
);
|
||||
|
||||
DatabaseAdapterRegistry::register(
|
||||
array(
|
||||
'class' => 'SQLiteDatabase',
|
||||
'title' => 'SQLite 3.3+',
|
||||
'helperPath' => 'sqlite3/code/SQLiteDatabaseConfigurationHelper.php',
|
||||
'supported' => (class_exists('SQLite3') || class_exists('PDO')),
|
||||
'missingExtensionText' => 'The <a href="http://php.net/manual/en/book.sqlite3.php">SQLite3</a> and'
|
||||
. ' <a href="http://php.net/manual/en/book.pdo.php">PDO</a> classes are not available. Please install or'
|
||||
. ' enable one of them and refresh this page.',
|
||||
'fields' => array(
|
||||
'path' => array(
|
||||
'title' => 'Database path<br /><small>Absolute path, writeable by the webserver user.<br />'
|
||||
. 'Recommended to be outside of your webroot</small>',
|
||||
'default' => realpath(dirname($_SERVER['SCRIPT_FILENAME'])) . DIRECTORY_SEPARATOR . 'assets'
|
||||
. DIRECTORY_SEPARATOR . '.db'
|
||||
),
|
||||
'database' => array(
|
||||
'title' => 'Database name',
|
||||
'default' => 'SS_mysite',
|
||||
'attributes' => array(
|
||||
"onchange" => "this.value = this.value.replace(/[\/\\:*?"<>|. \t]+/g,'');"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
);
|
@ -117,16 +117,10 @@ abstract class CMSBatchAction extends Object {
|
||||
|
||||
$applicableIDs = array();
|
||||
|
||||
$SQL_ids = implode(', ', array_filter($ids, 'is_numeric'));
|
||||
$draftPages = DataObject::get(
|
||||
$this->managedClass,
|
||||
sprintf(
|
||||
"\"%s\".\"ID\" IN (%s)",
|
||||
ClassInfo::baseDataClass($this->managedClass),
|
||||
$SQL_ids
|
||||
)
|
||||
);
|
||||
$managedClass = $this->managedClass;
|
||||
$draftPages = DataObject::get($managedClass)->byIDs($ids);
|
||||
|
||||
// Filter out the live-only ids
|
||||
$onlyOnLive = array_fill_keys($ids, true);
|
||||
if($checkStagePages) {
|
||||
foreach($draftPages as $obj) {
|
||||
@ -134,24 +128,13 @@ abstract class CMSBatchAction extends Object {
|
||||
if($obj->$methodName()) $applicableIDs[] = $obj->ID;
|
||||
}
|
||||
}
|
||||
$onlyOnLive = array_keys($onlyOnLive);
|
||||
|
||||
$managed_class = $this->managedClass;
|
||||
if($managed_class::has_extension('Versioned')) {
|
||||
if($checkLivePages && $onlyOnLive && $managedClass::has_extension('Versioned')) {
|
||||
// Get the pages that only exist on live (deleted from stage)
|
||||
if($checkLivePages && $onlyOnLive) {
|
||||
$SQL_ids = implode(', ', array_keys($onlyOnLive));
|
||||
$livePages = Versioned::get_by_stage(
|
||||
$this->managedClass, "Live",
|
||||
sprintf(
|
||||
"\"%s\".\"ID\" IN (%s)",
|
||||
ClassInfo::baseDataClass($this->managedClass),
|
||||
$SQL_ids
|
||||
)
|
||||
);
|
||||
|
||||
if($livePages) foreach($livePages as $obj) {
|
||||
if($obj->$methodName()) $applicableIDs[] = $obj->ID;
|
||||
}
|
||||
$livePages = Versioned::get_by_stage($managedClass, "Live")->byIDs($onlyOnLive);
|
||||
foreach($livePages as $obj) {
|
||||
if($obj->$methodName()) $applicableIDs[] = $obj->ID;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,14 +95,8 @@ class CMSBatchActionHandler extends RequestHandler {
|
||||
Translatable::disable_locale_filter();
|
||||
}
|
||||
|
||||
$pages = DataObject::get(
|
||||
$this->recordClass,
|
||||
sprintf(
|
||||
'"%s"."ID" IN (%s)',
|
||||
ClassInfo::baseDataClass($this->recordClass),
|
||||
implode(", ", $ids)
|
||||
)
|
||||
);
|
||||
$recordClass = $this->recordClass;
|
||||
$pages = DataObject::get($recordClass)->byIDs($ids);
|
||||
|
||||
if(class_exists('Translatable') && SiteTree::has_extension('Translatable')) {
|
||||
Translatable::enable_locale_filter();
|
||||
@ -112,16 +106,11 @@ class CMSBatchActionHandler extends RequestHandler {
|
||||
if($record_class::has_extension('Versioned')) {
|
||||
// If we didn't query all the pages, then find the rest on the live site
|
||||
if(!$pages || $pages->Count() < sizeof($ids)) {
|
||||
$idsFromLive = array();
|
||||
foreach($ids as $id) $idsFromLive[$id] = true;
|
||||
if($pages) foreach($pages as $page) unset($idsFromLive[$page->ID]);
|
||||
$idsFromLive = array_keys($idsFromLive);
|
||||
|
||||
$sql = sprintf(
|
||||
'"%s"."ID" IN (%s)',
|
||||
$this->recordClass,
|
||||
implode(", ", $idsFromLive)
|
||||
);
|
||||
$livePages = Versioned::get_by_stage($this->recordClass, 'Live', $sql);
|
||||
$livePages = Versioned::get_by_stage($this->recordClass, 'Live')->byIDs($idsFromLive);
|
||||
if($pages) {
|
||||
// Can't merge into a DataList, need to condense into an actual list first
|
||||
// (which will retrieve all records as objects, so its an expensive operation)
|
||||
|
@ -547,7 +547,7 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
* Return styling for the menu icon, if a custom icon is set for this class
|
||||
*
|
||||
* Example: static $menu-icon = '/path/to/image/';
|
||||
* @param type $class
|
||||
* @param string $class
|
||||
* @return string
|
||||
*/
|
||||
public static function menu_icon_for_class($class) {
|
||||
@ -975,14 +975,14 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
$className = $this->stat('tree_class');
|
||||
|
||||
// Existing or new record?
|
||||
$SQL_id = Convert::raw2sql($data['ID']);
|
||||
if(substr($SQL_id,0,3) != 'new') {
|
||||
$record = DataObject::get_by_id($className, $SQL_id);
|
||||
$id = $data['ID'];
|
||||
if(substr($id,0,3) != 'new') {
|
||||
$record = DataObject::get_by_id($className, $id);
|
||||
if($record && !$record->canEdit()) return Security::permissionFailure($this);
|
||||
if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$data['ID']);
|
||||
if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
|
||||
} else {
|
||||
if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
|
||||
$record = $this->getNewItem($SQL_id, false);
|
||||
$record = $this->getNewItem($id, false);
|
||||
}
|
||||
|
||||
// save form data into record
|
||||
@ -998,9 +998,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
public function delete($data, $form) {
|
||||
$className = $this->stat('tree_class');
|
||||
|
||||
$record = DataObject::get_by_id($className, Convert::raw2sql($data['ID']));
|
||||
$id = $data['ID'];
|
||||
$record = DataObject::get_by_id($className, $id);
|
||||
if($record && !$record->canDelete()) return Security::permissionFailure();
|
||||
if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$data['ID']);
|
||||
if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
|
||||
|
||||
$record->delete();
|
||||
|
||||
@ -1078,12 +1079,11 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
|
||||
// Update all dependent pages
|
||||
if(class_exists('VirtualPage')) {
|
||||
if($virtualPages = DataObject::get("VirtualPage", "\"CopyContentFromID\" = $node->ID")) {
|
||||
foreach($virtualPages as $virtualPage) {
|
||||
$statusUpdates['modified'][$virtualPage->ID] = array(
|
||||
'TreeTitle' => $virtualPage->TreeTitle()
|
||||
);
|
||||
}
|
||||
$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
|
||||
foreach($virtualPages as $virtualPage) {
|
||||
$statusUpdates['modified'][$virtualPage->ID] = array(
|
||||
'TreeTitle' => $virtualPage->TreeTitle()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1105,8 +1105,10 @@ class LeftAndMain extends Controller implements PermissionProvider {
|
||||
// Nodes that weren't "actually moved" shouldn't be registered as
|
||||
// having been edited; do a direct SQL update instead
|
||||
++$counter;
|
||||
DB::query(sprintf("UPDATE \"%s\" SET \"Sort\" = %d WHERE \"ID\" = '%d'",
|
||||
$className, $counter, $id));
|
||||
DB::prepared_query(
|
||||
"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
|
||||
array($counter, $id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1777,8 +1779,11 @@ class LeftAndMainMarkingFilter {
|
||||
// We need to recurse up the tree,
|
||||
// finding ParentIDs for each ID until we run out of parents
|
||||
while (!empty($parents)) {
|
||||
$res = DB::query('SELECT "ParentID", "ID" FROM "SiteTree"'
|
||||
. ' WHERE "ID" in ('.implode(',',array_keys($parents)).')');
|
||||
$parentsClause = DB::placeholders($parents);
|
||||
$res = DB::prepared_query(
|
||||
"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
|
||||
array_keys($parents)
|
||||
);
|
||||
$parents = array();
|
||||
|
||||
foreach($res as $row) {
|
||||
@ -1792,19 +1797,16 @@ class LeftAndMainMarkingFilter {
|
||||
protected function getQuery($params) {
|
||||
$where = array();
|
||||
|
||||
$SQL_params = Convert::raw2sql($params);
|
||||
if(isset($SQL_params['ID'])) unset($SQL_params['ID']);
|
||||
foreach($SQL_params as $name => $val) {
|
||||
switch($name) {
|
||||
default:
|
||||
// Partial string match against a variety of fields
|
||||
if(!empty($val) && singleton("SiteTree")->hasDatabaseField($name)) {
|
||||
$where[] = "\"$name\" LIKE '%$val%'";
|
||||
}
|
||||
if(isset($params['ID'])) unset($params['ID']);
|
||||
if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
|
||||
// Partial string match against a variety of fields
|
||||
if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
|
||||
$predicate = sprintf('"%s" LIKE ?', $name);
|
||||
$where[$predicate] = "%$val%";
|
||||
}
|
||||
}
|
||||
|
||||
return new SQLQuery(
|
||||
return new SQLSelect(
|
||||
array("ParentID", "ID"),
|
||||
'SiteTree',
|
||||
$where
|
||||
|
@ -63,7 +63,9 @@ class LeftAndMainTest extends FunctionalTest {
|
||||
$this->loginWithPermission('ADMIN');
|
||||
|
||||
// forcing sorting for non-MySQL
|
||||
$rootPages = DataObject::get('LeftAndMainTest_Object', '"ParentID" = 0', '"ID"');
|
||||
$rootPages = LeftAndMainTest_Object::get()
|
||||
->filter("ParentID", 0)
|
||||
->sort('"ID"');
|
||||
$siblingIDs = $rootPages->column('ID');
|
||||
$page1 = $rootPages->offsetGet(0);
|
||||
$page2 = $rootPages->offsetGet(1);
|
||||
@ -167,9 +169,11 @@ class LeftAndMainTest extends FunctionalTest {
|
||||
// restricted cms user
|
||||
$this->session()->inst_set('loggedInAs', $securityonlyuser->ID);
|
||||
$menuItems = singleton('LeftAndMain')->MainMenu(false);
|
||||
$menuItems = array_map($allValsFn, $menuItems->column('Code'));
|
||||
sort($menuItems);
|
||||
$this->assertEquals(
|
||||
array_map($allValsFn, $menuItems->column('Code')),
|
||||
array('SecurityAdmin','Help'),
|
||||
$menuItems,
|
||||
array('Help', 'SecurityAdmin'),
|
||||
'Groups with limited access can only access the interfaces they have permissions for'
|
||||
);
|
||||
|
||||
|
@ -141,3 +141,6 @@ if(defined('SS_USE_BASIC_AUTH') && SS_USE_BASIC_AUTH) {
|
||||
if(defined('SS_ERROR_LOG')) {
|
||||
SS_Log::add_writer(new SS_LogFileWriter(BASE_PATH . '/' . SS_ERROR_LOG), SS_Log::WARN, '<=');
|
||||
}
|
||||
|
||||
// Allow database adapters to handle their own configuration
|
||||
DatabaseAdapterRegistry::autoconfigure();
|
||||
|
@ -504,11 +504,11 @@ class Session {
|
||||
* Sets the appropriate form message in session, with type. This will be shown once,
|
||||
* for the form specified.
|
||||
*
|
||||
* @param formname the form name you wish to use ( usually $form->FormName() )
|
||||
* @param messsage the message you wish to add to it
|
||||
* @param type the type of message
|
||||
* @param string $formname the form name you wish to use ( usually $form->FormName() )
|
||||
* @param string $message the message you wish to add to it
|
||||
* @param string $type the type of message
|
||||
*/
|
||||
public static function setFormMessage($formname,$message,$type){
|
||||
public static function setFormMessage($formname, $message, $type){
|
||||
Session::set("FormInfo.$formname.formError.message", $message);
|
||||
Session::set("FormInfo.$formname.formError.type", $type);
|
||||
}
|
||||
|
@ -315,14 +315,10 @@ class Injector {
|
||||
* of a particular type is injected, and what items should be injected
|
||||
* for those properties / methods.
|
||||
*
|
||||
* @param type $class
|
||||
* The class to set a mapping for
|
||||
* @param type $property
|
||||
* The property to set the mapping for
|
||||
* @param type $injectType
|
||||
* The registered type that will be injected
|
||||
* @param string $injectVia
|
||||
* Whether to inject by setting a property or calling a setter
|
||||
* @param string $class The class to set a mapping for
|
||||
* @param string $property The property to set the mapping for
|
||||
* @param string $toInject The registered type that will be injected
|
||||
* @param string $injectVia Whether to inject by setting a property or calling a setter
|
||||
*/
|
||||
public function setInjectMapping($class, $property, $toInject, $injectVia = 'property') {
|
||||
$mapping = isset($this->injectMap[$class]) ? $this->injectMap[$class] : array();
|
||||
@ -757,8 +753,7 @@ class Injector {
|
||||
* Removes a named object from the cached list of objects managed
|
||||
* by the inject
|
||||
*
|
||||
* @param type $name
|
||||
* The name to unregister
|
||||
* @param string $name The name to unregister
|
||||
*/
|
||||
public function unregisterNamedObject($name) {
|
||||
unset($this->serviceCache[$name]);
|
||||
@ -793,7 +788,7 @@ class Injector {
|
||||
* Optional set of arguments to pass as constructor arguments
|
||||
* if this object is to be created from scratch
|
||||
* (ie asSingleton = false)
|
||||
*
|
||||
* @return mixed the instance of the specified object
|
||||
*/
|
||||
public function get($name, $asSingleton = true, $constructorArgs = null) {
|
||||
// reassign the name as it might actually be a compound name
|
||||
@ -866,7 +861,8 @@ class Injector {
|
||||
*
|
||||
* Additional parameters are passed through as
|
||||
*
|
||||
* @param type $name
|
||||
* @param string $name
|
||||
* @return mixed A new instance of the specified object
|
||||
*/
|
||||
public function create($name) {
|
||||
$constructorArgs = func_get_args();
|
||||
|
@ -25,7 +25,7 @@ class ClassInfo {
|
||||
/**
|
||||
* Cache for {@link hasTable()}
|
||||
*/
|
||||
private static $_cache_all_tables = null;
|
||||
private static $_cache_all_tables = array();
|
||||
|
||||
/**
|
||||
* @var Array Cache for {@link ancestry()}.
|
||||
@ -36,17 +36,11 @@ class ClassInfo {
|
||||
* @todo Move this to SS_Database or DB
|
||||
*/
|
||||
public static function hasTable($class) {
|
||||
if(DB::isActive()) {
|
||||
// Cache the list of all table names to reduce on DB traffic
|
||||
if(empty(self::$_cache_all_tables)) {
|
||||
self::$_cache_all_tables = array();
|
||||
$tables = DB::query(DB::getConn()->allTablesSQL())->column();
|
||||
foreach($tables as $table) self::$_cache_all_tables[strtolower($table)] = true;
|
||||
}
|
||||
return isset(self::$_cache_all_tables[strtolower($class)]);
|
||||
} else {
|
||||
return false;
|
||||
// Cache the list of all table names to reduce on DB traffic
|
||||
if(empty(self::$_cache_all_tables) && DB::is_active()) {
|
||||
self::$_cache_all_tables = DB::get_schema()->tableList();
|
||||
}
|
||||
return !empty(self::$_cache_all_tables[strtolower($class)]);
|
||||
}
|
||||
|
||||
public static function reset_db_cache() {
|
||||
@ -56,17 +50,21 @@ class ClassInfo {
|
||||
|
||||
/**
|
||||
* Returns the manifest of all classes which are present in the database.
|
||||
*
|
||||
* @param string $class Class name to check enum values for ClassName field
|
||||
* @param boolean $includeUnbacked Flag indicating whether or not to include
|
||||
* types that don't exist as implemented classes. By default these are excluded.
|
||||
* @return array List of subclasses
|
||||
*/
|
||||
public static function getValidSubClasses($class = 'SiteTree', $includeUnbacked = false) {
|
||||
$classes = DB::getConn()->enumValuesForField($class, 'ClassName');
|
||||
$classes = DB::get_schema()->enumValuesForField($class, 'ClassName');
|
||||
if (!$includeUnbacked) $classes = array_filter($classes, array('ClassInfo', 'exists'));
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the current class and all its ancestors and children
|
||||
* which have a DB table.
|
||||
* which require a DB table.
|
||||
*
|
||||
* @param string|object $class
|
||||
* @todo Move this into data object
|
||||
@ -81,10 +79,11 @@ class ClassInfo {
|
||||
|
||||
$classes = array_merge(
|
||||
self::ancestry($class),
|
||||
self::subclassesFor($class));
|
||||
self::subclassesFor($class)
|
||||
);
|
||||
|
||||
foreach ($classes as $class) {
|
||||
if (self::hasTable($class)) $result[$class] = $class;
|
||||
if (DataObject::has_own_table($class)) $result[$class] = $class;
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
@ -636,12 +636,12 @@ class Config {
|
||||
* every other source is filtered on request, so no amount of changes to parent's configuration etc can override a
|
||||
* remove call.
|
||||
*
|
||||
* @param $class string - The class to remove a configuration value from
|
||||
* @param $name string - The configuration name
|
||||
* @param $key any - An optional key to filter against.
|
||||
* @param string $class The class to remove a configuration value from
|
||||
* @param string $name The configuration name
|
||||
* @param mixed $key An optional key to filter against.
|
||||
* If referenced config value is an array, only members of that array that match this key will be removed
|
||||
* Must also match value if provided to be removed
|
||||
* @param $value any - And optional value to filter against.
|
||||
* @param mixed $value And optional value to filter against.
|
||||
* If referenced config value is an array, only members of that array that match this value will be removed
|
||||
* If referenced config value is not an array, value will be removed only if it matches this argument
|
||||
* Must also match key if provided and referenced config value is an array to be removed
|
||||
|
@ -141,12 +141,48 @@ class Convert {
|
||||
return self::raw2json($val);
|
||||
}
|
||||
|
||||
public static function raw2sql($val) {
|
||||
/**
|
||||
* Safely encodes a value (or list of values) using the current database's
|
||||
* safe string encoding method
|
||||
*
|
||||
* @param mixed|array $val Input value, or list of values as an array
|
||||
* @param boolean $quoted Flag indicating whether the value should be safely
|
||||
* quoted, instead of only being escaped. By default this function will
|
||||
* only escape the string (false).
|
||||
* @return string|array Safely encoded value in the same format as the input
|
||||
*/
|
||||
public static function raw2sql($val, $quoted = false) {
|
||||
if(is_array($val)) {
|
||||
foreach($val as $k => $v) $val[$k] = self::raw2sql($v);
|
||||
foreach($val as $k => $v) {
|
||||
$val[$k] = self::raw2sql($v, $quoted);
|
||||
}
|
||||
return $val;
|
||||
} else {
|
||||
return DB::getConn()->addslashes($val);
|
||||
if($quoted) {
|
||||
return DB::get_conn()->quoteString($val);
|
||||
} else {
|
||||
return DB::get_conn()->escapeString($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely encodes a SQL symbolic identifier (or list of identifiers), such as a database,
|
||||
* table, or column name. Supports encoding of multi identfiers separated by
|
||||
* a delimiter (e.g. ".")
|
||||
*
|
||||
* @param string|array $identifier The identifier to escape. E.g. 'SiteTree.Title'
|
||||
* @param string $separator The string that delimits subsequent identifiers
|
||||
* @return string|array The escaped identifier. E.g. '"SiteTree"."Title"'
|
||||
*/
|
||||
public static function symbol2sql($identifier, $separator = '.') {
|
||||
if(is_array($identifier)) {
|
||||
foreach($identifier as $k => $v) {
|
||||
$identifier[$k] = self::symbol2sql($v, $separator);
|
||||
}
|
||||
return $identifier;
|
||||
} else {
|
||||
return DB::get_conn()->escapeIdentifier($identifier, $separator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,6 +386,7 @@ class Convert {
|
||||
|
||||
/**
|
||||
* Normalises newline sequences to conform to (an) OS specific format.
|
||||
*
|
||||
* @param string $data Text containing potentially mixed formats of newline
|
||||
* sequences including \r, \r\n, \n, or unicode newline characters
|
||||
* @param string $nl The newline sequence to normalise to. Defaults to that
|
||||
|
@ -144,9 +144,9 @@ class PaginatedList extends SS_ListDecorator {
|
||||
* Sets the page length, page start and total items from a query object's
|
||||
* limit, offset and unlimited count. The query MUST have a limit clause.
|
||||
*
|
||||
* @param SQLQuery $query
|
||||
* @param SQLSelect $query
|
||||
*/
|
||||
public function setPaginationFromQuery(SQLQuery $query) {
|
||||
public function setPaginationFromQuery(SQLSelect $query) {
|
||||
if ($limit = $query->getLimit()) {
|
||||
$this->setPageLength($limit['limit']);
|
||||
$this->setPageStart($limit['start']);
|
||||
|
@ -208,19 +208,17 @@ class CsvBulkLoader extends BulkLoader {
|
||||
|
||||
foreach($this->duplicateChecks as $fieldName => $duplicateCheck) {
|
||||
if(is_string($duplicateCheck)) {
|
||||
$SQL_fieldName = Convert::raw2sql($duplicateCheck);
|
||||
|
||||
if(!isset($record[$SQL_fieldName]) || empty($record[$SQL_fieldName])) {
|
||||
//skip current duplicate check if field value is empty
|
||||
continue;
|
||||
}
|
||||
// Skip current duplicate check if field value is empty
|
||||
if(empty($record[$duplicateCheck])) continue;
|
||||
|
||||
$SQL_fieldValue = Convert::raw2sql($record[$SQL_fieldName]);
|
||||
$existingRecord = DataObject::get_one($this->objectClass, "\"$SQL_fieldName\" = '{$SQL_fieldValue}'");
|
||||
// Check existing record with this value
|
||||
$dbFieldValue = $record[$duplicateCheck];
|
||||
$existingRecord = DataObject::get($this->objectClass)
|
||||
->filter($duplicateCheck, $dbFieldValue)
|
||||
->first();
|
||||
|
||||
if($existingRecord) {
|
||||
return $existingRecord;
|
||||
}
|
||||
if($existingRecord) return $existingRecord;
|
||||
} elseif(is_array($duplicateCheck) && isset($duplicateCheck['callback'])) {
|
||||
if($this->hasMethod($duplicateCheck['callback'])) {
|
||||
$existingRecord = $this->{$duplicateCheck['callback']}($record[$fieldName], $record);
|
||||
|
@ -326,11 +326,10 @@ class Debug {
|
||||
}
|
||||
|
||||
if(!headers_sent()) {
|
||||
$currController = Controller::has_curr() ? Controller::curr() : null;
|
||||
// Ensure the error message complies with the HTTP 1.1 spec
|
||||
$msg = strip_tags(str_replace(array("\n", "\r"), '', $friendlyErrorMessage));
|
||||
if($currController) {
|
||||
$response = $currController->getResponse();
|
||||
if(Controller::has_curr()) {
|
||||
$response = Controller::curr()->getResponse();
|
||||
$response->setStatusCode($statusCode, $msg);
|
||||
} else {
|
||||
header($_SERVER['SERVER_PROTOCOL'] . " $statusCode $msg");
|
||||
@ -466,24 +465,22 @@ class Debug {
|
||||
// being called again.
|
||||
// This basically calls Permission::checkMember($_SESSION['loggedInAs'], 'ADMIN');
|
||||
|
||||
// @TODO - Rewrite safely using DataList::filter
|
||||
$memberID = $_SESSION['loggedInAs'];
|
||||
|
||||
$groups = DB::query("SELECT \"GroupID\" from \"Group_Members\" WHERE \"MemberID\" = " . $memberID);
|
||||
$groupCSV = implode($groups->column(), ',');
|
||||
|
||||
$permission = DB::query("
|
||||
SELECT \"ID\"
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" = 'ADMIN'
|
||||
AND \"Type\" = " . Permission::GRANT_PERMISSION . "
|
||||
AND \"GroupID\" IN ($groupCSV)
|
||||
$permission = DB::prepared_query('
|
||||
SELECT "ID" FROM "Permission"
|
||||
INNER JOIN "Group_Members" ON "Permission"."GroupID" = "Group_Members"."GroupID"
|
||||
WHERE "Permission"."Code" = ?
|
||||
AND "Permission"."Type" = ?
|
||||
AND "Group_Members"."MemberID" = ?',
|
||||
array(
|
||||
'ADMIN', // Code
|
||||
Permission::GRANT_PERMISSION, // Type
|
||||
$memberID // MemberID
|
||||
)
|
||||
")->value();
|
||||
)->value();
|
||||
|
||||
if($permission) {
|
||||
return;
|
||||
}
|
||||
if($permission) return;
|
||||
}
|
||||
|
||||
// This basically does the same as
|
||||
|
@ -86,7 +86,7 @@ class FixtureBlueprint {
|
||||
$obj->ID = $data['ID'];
|
||||
|
||||
// The database needs to allow inserting values into the foreign key column (ID in our case)
|
||||
$conn = DB::getConn();
|
||||
$conn = DB::get_conn();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) {
|
||||
$conn->allowPrimaryKeyEditing(ClassInfo::baseDataClass($class), true);
|
||||
}
|
||||
@ -187,12 +187,12 @@ class FixtureBlueprint {
|
||||
// If LastEdited was set in the fixture, set it here
|
||||
if($data && array_key_exists('LastEdited', $data)) {
|
||||
$edited = $this->parseValue($data['LastEdited'], $fixtures);
|
||||
DB::manipulate(array(
|
||||
$class => array(
|
||||
"command" => "update", "id" => $obj->id,
|
||||
"fields" => array("LastEdited" => "'".$edited."'")
|
||||
)
|
||||
));
|
||||
$update = new SQLUpdate(
|
||||
$class,
|
||||
array('"LastEdited"' => $edited),
|
||||
array('"ID"' => $obj->id)
|
||||
);
|
||||
$update->execute();
|
||||
}
|
||||
} catch(Exception $e) {
|
||||
Config::inst()->update('DataObject', 'validation_enabled', $validationenabled);
|
||||
|
@ -89,12 +89,13 @@ class FixtureFactory {
|
||||
* @return Int Database identifier
|
||||
*/
|
||||
public function createRaw($table, $identifier, $data) {
|
||||
$manipulation = array($table => array("fields" => array(), "command" => "insert"));
|
||||
foreach($data as $fieldName => $fieldVal) {
|
||||
$manipulation[$table]["fields"][$fieldName] = "'" . $this->parseValue($fieldVal) . "'";
|
||||
$fields = array();
|
||||
foreach($data as $fieldName => $fieldVal) {
|
||||
$fields["\"$fieldName\""] = $this->parseValue($fieldVal);
|
||||
}
|
||||
DB::manipulate($manipulation);
|
||||
$id = DB::getGeneratedID($table);
|
||||
$insert = new SQLInsert($table, $fields);
|
||||
$insert->execute();
|
||||
$id = DB::get_generated_id($table);
|
||||
$this->fixtures[$table][$identifier] = $id;
|
||||
|
||||
return $id;
|
||||
@ -171,10 +172,10 @@ class FixtureFactory {
|
||||
$class::get()->byId($dbId)->delete();
|
||||
} else {
|
||||
$table = $class;
|
||||
DB::manipulate(array(
|
||||
$table => array("fields" => array('ID' => $dbId),
|
||||
"command" => "delete")
|
||||
$delete = new SQLDelete("\"$table\"", array(
|
||||
"\"$table\".\"ID\"" => $dbId
|
||||
));
|
||||
$delete->execute();
|
||||
}
|
||||
|
||||
unset($this->fixtures[$class][$id]);
|
||||
|
@ -41,8 +41,8 @@ class SS_LogErrorEmailFormatter implements Zend_Log_Formatter_Interface {
|
||||
$data = '';
|
||||
$data .= '<style type="text/css">html, body, table {font-family: sans-serif; font-size: 12px;}</style>';
|
||||
$data .= "<div style=\"border: 5px $colour solid;\">\n";
|
||||
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">"
|
||||
. "[$errorType] $errstr<br />$errfile:$errline\n<br />\n<br />\n</p>\n";
|
||||
$data .= "<p style=\"color: white; background-color: $colour; margin: 0\">[$errorType] ";
|
||||
$data .= nl2br(htmlspecialchars($errstr))."<br />$errfile:$errline\n<br />\n<br />\n</p>\n";
|
||||
|
||||
// Render the provided backtrace
|
||||
$data .= SS_Backtrace::get_rendered_backtrace($errcontext);
|
||||
|
@ -224,7 +224,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
// Set up fixture
|
||||
if($fixtureFile || $this->usesDatabase || !self::using_temp_db()) {
|
||||
if(substr(DB::getConn()->currentDatabase(), 0, strlen($prefix) + 5)
|
||||
if(substr(DB::get_conn()->getSelectedDatabase(), 0, strlen($prefix) + 5)
|
||||
!= strtolower(sprintf('%stmpdb', $prefix))) {
|
||||
|
||||
//echo "Re-creating temp database... ";
|
||||
@ -700,6 +700,74 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes sequences of repeated whitespace characters from SQL queries
|
||||
* making them suitable for string comparison
|
||||
*
|
||||
* @param string $sql
|
||||
* @return string The cleaned and normalised SQL string
|
||||
*/
|
||||
protected function normaliseSQL($sql) {
|
||||
return trim(preg_replace('/\s+/m', ' ', $sql));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that two SQL queries are equivalent
|
||||
*
|
||||
* @param string $expectedSQL
|
||||
* @param string $actualSQL
|
||||
* @param string $message
|
||||
* @param float $delta
|
||||
* @param integer $maxDepth
|
||||
* @param boolean $canonicalize
|
||||
* @param boolean $ignoreCase
|
||||
*/
|
||||
public function assertSQLEquals($expectedSQL, $actualSQL, $message = '', $delta = 0, $maxDepth = 10,
|
||||
$canonicalize = false, $ignoreCase = false
|
||||
) {
|
||||
// Normalise SQL queries to remove patterns of repeating whitespace
|
||||
$expectedSQL = $this->normaliseSQL($expectedSQL);
|
||||
$actualSQL = $this->normaliseSQL($actualSQL);
|
||||
|
||||
$this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a SQL query contains a SQL fragment
|
||||
*
|
||||
* @param string $needleSQL
|
||||
* @param string $haystackSQL
|
||||
* @param string $message
|
||||
* @param boolean $ignoreCase
|
||||
* @param boolean $checkForObjectIdentity
|
||||
*/
|
||||
public function assertSQLContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
|
||||
$checkForObjectIdentity = true
|
||||
) {
|
||||
$needleSQL = $this->normaliseSQL($needleSQL);
|
||||
$haystackSQL = $this->normaliseSQL($haystackSQL);
|
||||
|
||||
$this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a SQL query contains a SQL fragment
|
||||
*
|
||||
* @param string $needleSQL
|
||||
* @param string $haystackSQL
|
||||
* @param string $message
|
||||
* @param boolean $ignoreCase
|
||||
* @param boolean $checkForObjectIdentity
|
||||
*/
|
||||
public function assertSQLNotContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
|
||||
$checkForObjectIdentity = true
|
||||
) {
|
||||
$needleSQL = $this->normaliseSQL($needleSQL);
|
||||
$haystackSQL = $this->normaliseSQL($haystackSQL);
|
||||
|
||||
$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for the DOS matchers
|
||||
@ -724,18 +792,18 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
* Returns true if we are currently using a temporary database
|
||||
*/
|
||||
public static function using_temp_db() {
|
||||
$dbConn = DB::getConn();
|
||||
$dbConn = DB::get_conn();
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
return $dbConn && (substr($dbConn->currentDatabase(), 0, strlen($prefix) + 5)
|
||||
return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
|
||||
== strtolower(sprintf('%stmpdb', $prefix)));
|
||||
}
|
||||
|
||||
public static function kill_temp_db() {
|
||||
// Delete our temporary database
|
||||
if(self::using_temp_db()) {
|
||||
$dbConn = DB::getConn();
|
||||
$dbName = $dbConn->currentDatabase();
|
||||
if($dbName && DB::getConn()->databaseExists($dbName)) {
|
||||
$dbConn = DB::get_conn();
|
||||
$dbName = $dbConn->getSelectedDatabase();
|
||||
if($dbName && DB::get_conn()->databaseExists($dbName)) {
|
||||
// Some DataExtensions keep a static cache of information that needs to
|
||||
// be reset whenever the database is killed
|
||||
foreach(ClassInfo::subclassesFor('DataExtension') as $class) {
|
||||
@ -744,7 +812,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
}
|
||||
|
||||
// echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
|
||||
$dbConn->dropDatabase();
|
||||
$dbConn->dropSelectedDatabase();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -754,8 +822,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
*/
|
||||
public static function empty_temp_db() {
|
||||
if(self::using_temp_db()) {
|
||||
$dbadmin = new DatabaseAdmin();
|
||||
$dbadmin->clearAllData();
|
||||
DB::get_conn()->clearAllData();
|
||||
|
||||
// Some DataExtensions keep a static cache of information that needs to
|
||||
// be reset whenever the database is cleaned out
|
||||
@ -775,15 +842,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
global $databaseConfig;
|
||||
$databaseConfig['timezone'] = '+0:00';
|
||||
DB::connect($databaseConfig);
|
||||
$dbConn = DB::getConn();
|
||||
$dbConn = DB::get_conn();
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
|
||||
while(!$dbname || $dbConn->databaseExists($dbname)) {
|
||||
$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
|
||||
}
|
||||
|
||||
$dbConn->selectDatabase($dbname);
|
||||
$dbConn->createDatabase();
|
||||
$dbConn->selectDatabase($dbname, true);
|
||||
|
||||
$st = Injector::inst()->create('SapphireTest');
|
||||
$st->resetDBSchema();
|
||||
@ -796,9 +862,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
public static function delete_all_temp_dbs() {
|
||||
$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
|
||||
foreach(DB::getConn()->allDatabaseNames() as $dbName) {
|
||||
foreach(DB::get_schema()->databaseList() as $dbName) {
|
||||
if(preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
|
||||
DB::getConn()->dropDatabaseByName($dbName);
|
||||
DB::get_schema()->dropDatabase($dbName);
|
||||
if(Director::is_cli()) {
|
||||
echo "Dropped database \"$dbName\"" . PHP_EOL;
|
||||
} else {
|
||||
@ -823,27 +889,26 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$dataClasses = ClassInfo::subclassesFor('DataObject');
|
||||
array_shift($dataClasses);
|
||||
|
||||
$conn = DB::getConn();
|
||||
$conn->beginSchemaUpdate();
|
||||
DB::quiet();
|
||||
|
||||
foreach($dataClasses as $dataClass) {
|
||||
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
|
||||
if(class_exists($dataClass)) {
|
||||
$SNG = singleton($dataClass);
|
||||
if(!($SNG instanceof TestOnly)) $SNG->requireTable();
|
||||
$schema = DB::get_schema();
|
||||
$extraDataObjects = $includeExtraDataObjects ? $this->extraDataObjects : null;
|
||||
$schema->schemaUpdate(function() use($dataClasses, $extraDataObjects){
|
||||
foreach($dataClasses as $dataClass) {
|
||||
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
|
||||
if(class_exists($dataClass)) {
|
||||
$SNG = singleton($dataClass);
|
||||
if(!($SNG instanceof TestOnly)) $SNG->requireTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have additional dataobjects which need schema, do so here:
|
||||
if($includeExtraDataObjects && $this->extraDataObjects) {
|
||||
foreach($this->extraDataObjects as $dataClass) {
|
||||
$SNG = singleton($dataClass);
|
||||
if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
|
||||
|
||||
// If we have additional dataobjects which need schema, do so here:
|
||||
if($extraDataObjects) {
|
||||
foreach($extraDataObjects as $dataClass) {
|
||||
$SNG = singleton($dataClass);
|
||||
if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$conn->endSchemaUpdate();
|
||||
});
|
||||
|
||||
ClassInfo::reset_db_cache();
|
||||
singleton('DataObject')->flushCache();
|
||||
@ -865,7 +930,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
$permission->write();
|
||||
$group->Permissions()->add($permission);
|
||||
|
||||
$member = DataObject::get_one('Member', sprintf('"Email" = \'%s\'', "$permCode@example.org"));
|
||||
$member = DataObject::get_one('Member', array(
|
||||
'"Member"."Email"' => "$permCode@example.org"
|
||||
));
|
||||
if(!$member) $member = Injector::inst()->create('Member');
|
||||
|
||||
$member->FirstName = $permCode;
|
||||
|
@ -99,8 +99,7 @@ class TestRunner extends Controller {
|
||||
|
||||
// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
|
||||
// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
|
||||
DataObject::clear_classname_spec_cache();
|
||||
PolymorphicForeignKey::clear_classname_spec_cache();
|
||||
DataObject::reset();
|
||||
}
|
||||
|
||||
public function init() {
|
||||
|
@ -9,39 +9,47 @@
|
||||
* @author Tom Rix
|
||||
*/
|
||||
class DatabaseAdapterRegistry {
|
||||
|
||||
|
||||
/**
|
||||
* Default database connector registration fields
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $default_fields = array(
|
||||
'server' => array(
|
||||
'title' => 'Database server',
|
||||
'envVar' => 'SS_DATABASE_SERVER',
|
||||
'title' => 'Database server',
|
||||
'envVar' => 'SS_DATABASE_SERVER',
|
||||
'default' => 'localhost'
|
||||
),
|
||||
'username' => array(
|
||||
'title' => 'Database username',
|
||||
'envVar' => 'SS_DATABASE_USERNAME',
|
||||
'title' => 'Database username',
|
||||
'envVar' => 'SS_DATABASE_USERNAME',
|
||||
'default' => 'root'
|
||||
),
|
||||
'password' => array(
|
||||
'title' => 'Database password',
|
||||
'envVar' => 'SS_DATABASE_PASSWORD',
|
||||
'title' => 'Database password',
|
||||
'envVar' => 'SS_DATABASE_PASSWORD',
|
||||
'default' => 'password'
|
||||
),
|
||||
'database' => array(
|
||||
'title' => 'Database name',
|
||||
'title' => 'Database name',
|
||||
'default' => 'SS_mysite',
|
||||
'attributes' => array(
|
||||
"onchange" => "this.value = this.value.replace(/[\/\\:*?"<>|. \t]+/g,'');"
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Internal array of registered database adapters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $adapters = array();
|
||||
|
||||
|
||||
/**
|
||||
* Add new adapter to the registry
|
||||
*
|
||||
* @param array $config Associative array of configuration details
|
||||
*/
|
||||
public static function register($config) {
|
||||
@ -55,20 +63,28 @@ class DatabaseAdapterRegistry {
|
||||
? $config['missingModuleText']
|
||||
: 'The SilverStripe module, '.$moduleName.', is missing or incomplete.'
|
||||
. ' Please <a href="http://silverstripe.org/modules">download it</a>.';
|
||||
|
||||
|
||||
$config['missingModuleText'] = $missingModuleText;
|
||||
$config['missingExtensionText'] = $missingExtensionText;
|
||||
|
||||
|
||||
// set default fields if none are defined already
|
||||
if(!isset($config['fields'])) $config['fields'] = self::$default_fields;
|
||||
|
||||
|
||||
self::$adapters[$config['class']] = $config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unregisters a database connector by classname
|
||||
*
|
||||
* @param string $class
|
||||
*/
|
||||
public static function unregister($class) {
|
||||
if(isset($adapters[$class])) unset($adapters[$class]);
|
||||
unset(self::$adapters[$class]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects all _register_database.php files and invokes them
|
||||
*/
|
||||
public static function autodiscover() {
|
||||
foreach(glob(dirname(__FILE__) . '/../../../*', GLOB_ONLYDIR) as $directory) {
|
||||
if(file_exists($directory . '/_register_database.php')) {
|
||||
@ -76,9 +92,45 @@ class DatabaseAdapterRegistry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects all _configure_database.php files and invokes them
|
||||
* Called by ConfigureFromEnv.php
|
||||
*/
|
||||
public static function autoconfigure() {
|
||||
foreach(glob(dirname(__FILE__) . '/../../../*', GLOB_ONLYDIR) as $directory) {
|
||||
if(file_exists($directory . '/_configure_database.php')) {
|
||||
include_once($directory . '/_configure_database.php');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all registered adapters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_adapters() {
|
||||
return self::$adapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns registry data for a class
|
||||
*
|
||||
* @param string $class
|
||||
* @return array List of adapter properties
|
||||
*/
|
||||
public static function get_adapter($class) {
|
||||
if(isset(self::$adapters[$class])) return self::$adapters[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves default field configuration
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_default_fields() {
|
||||
return self::$default_fields;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Interface for database helper classes.
|
||||
*
|
||||
* @package framework
|
||||
*/
|
||||
interface DatabaseConfigurationHelper {
|
||||
@ -16,6 +18,7 @@ interface DatabaseConfigurationHelper {
|
||||
|
||||
/**
|
||||
* Ensure that the database server exists.
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('okay' => true, 'error' => 'details of error')
|
||||
*/
|
||||
@ -29,6 +32,22 @@ interface DatabaseConfigurationHelper {
|
||||
* @return array Result - e.g. array('okay' => true, 'connection' => mysql link, 'error' => 'details of error')
|
||||
*/
|
||||
public function requireDatabaseConnection($databaseConfig);
|
||||
|
||||
/**
|
||||
* Determines the version of the database server
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return string Version of database server or false on failure
|
||||
*/
|
||||
public function getDatabaseVersion($databaseConfig);
|
||||
|
||||
/**
|
||||
* Check database version is greater than the minimum supported
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
||||
*/
|
||||
public function requireDatabaseVersion($databaseConfig);
|
||||
|
||||
/**
|
||||
* Ensure that the database connection is able to use an existing database,
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This is a helper class for the SS installer.
|
||||
*
|
||||
@ -11,31 +12,77 @@
|
||||
class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
||||
|
||||
/**
|
||||
* Ensure that the database function for connectivity is available.
|
||||
* If it is, we assume the PHP module for this database has been setup correctly.
|
||||
* Create a connection of the appropriate type
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return boolean
|
||||
* @param array $databaseConfig
|
||||
* @param string $error Error message passed by value
|
||||
* @return mixed|null Either the connection object, or null if error
|
||||
*/
|
||||
public function requireDatabaseFunctions($databaseConfig) {
|
||||
return class_exists('MySQLi');
|
||||
protected function createConnection($databaseConfig, &$error) {
|
||||
$error = null;
|
||||
try {
|
||||
switch($databaseConfig['type']) {
|
||||
case 'MySQLDatabase':
|
||||
$conn = @new MySQLi($databaseConfig['server'], $databaseConfig['username'],
|
||||
$databaseConfig['password']);
|
||||
if($conn && empty($conn->connect_errno)) {
|
||||
$conn->query("SET sql_mode = 'ANSI'");
|
||||
return $conn;
|
||||
} else {
|
||||
$error = ($conn->connect_errno)
|
||||
? $conn->connect_error
|
||||
: 'Unknown connection error';
|
||||
return null;
|
||||
}
|
||||
case 'MySQLPDODatabase':
|
||||
// May throw a PDOException if fails
|
||||
$conn = @new PDO('mysql:host='.$databaseConfig['server'], $databaseConfig['username'],
|
||||
$databaseConfig['password']);
|
||||
if($conn) {
|
||||
$conn->query("SET sql_mode = 'ANSI'");
|
||||
return $conn;
|
||||
} else {
|
||||
$error = 'Unknown connection error';
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
$error = 'Invalid connection type';
|
||||
return null;
|
||||
}
|
||||
} catch(Exception $ex) {
|
||||
$error = $ex->getMessage();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the database server exists.
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
||||
* Helper function to quickly extract a column from a mysqi_result
|
||||
*
|
||||
* @param mixed $results mysqli_result or enumerable list of rows
|
||||
* @return array Resulting data
|
||||
*/
|
||||
public function requireDatabaseServer($databaseConfig) {
|
||||
$success = false;
|
||||
$error = '';
|
||||
$conn = @new MySQLi($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||
if($conn && $conn->connect_errno) {
|
||||
$success = false;
|
||||
$error = $conn->connect_error;
|
||||
protected function column($results) {
|
||||
$array = array();
|
||||
if($results instanceof mysqli_result) {
|
||||
while($row = $results->fetch_array()) {
|
||||
$array[] = $row[0];
|
||||
}
|
||||
} else {
|
||||
$success = true;
|
||||
foreach($results as $row) {
|
||||
$array[] = $row[0];
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
public function requireDatabaseFunctions($databaseConfig) {
|
||||
$data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
|
||||
return !empty($data['supported']);
|
||||
}
|
||||
|
||||
public function requireDatabaseServer($databaseConfig) {
|
||||
$connection = $this->createConnection($databaseConfig, $error);
|
||||
$success = !empty($connection);
|
||||
|
||||
return array(
|
||||
'success' => $success,
|
||||
@ -43,28 +90,21 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database version for the MySQL connection, given the
|
||||
* database parameters.
|
||||
* @return mixed string Version number as string | boolean FALSE on failure
|
||||
*/
|
||||
public function getDatabaseVersion($databaseConfig) {
|
||||
$conn = new MySQLi($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||
if(!$conn) return false;
|
||||
$version = $conn->server_info;
|
||||
if(!$version) {
|
||||
// fallback to trying a query
|
||||
$result = $conn->query('SELECT VERSION()');
|
||||
$row = $result->fetch_array();
|
||||
if($row && isset($row[0])) {
|
||||
$version = trim($row[0]);
|
||||
}
|
||||
$conn = $this->createConnection($databaseConfig, $error);
|
||||
if(!$conn) {
|
||||
return false;
|
||||
} elseif($conn instanceof MySQLi) {
|
||||
return $conn->server_info;
|
||||
} elseif($conn instanceof PDO) {
|
||||
return $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
return $version;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the MySQL server version is at least 5.0.
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
||||
*/
|
||||
@ -86,21 +126,16 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a database connection is possible using credentials provided.
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
||||
*/
|
||||
public function requireDatabaseConnection($databaseConfig) {
|
||||
$success = false;
|
||||
$error = '';
|
||||
$conn = new MySQLi($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||
if($conn) {
|
||||
$success = true;
|
||||
} else {
|
||||
$conn = $this->createConnection($databaseConfig, $error);
|
||||
$success = !empty($conn);
|
||||
|
||||
// Check database name only uses valid characters
|
||||
if($success && !$this->checkValidDatabaseName($databaseConfig['database'])) {
|
||||
$success = false;
|
||||
$error = ($conn) ? $conn->connect_error : '';
|
||||
$error = 'Invalid characters in database name.';
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => $success,
|
||||
'error' => $error
|
||||
@ -108,68 +143,89 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the database connection is able to use an existing database,
|
||||
* or be able to create one if it doesn't exist.
|
||||
* Determines if a given database name is a valid Silverstripe name.
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('success' => true, 'alreadyExists' => 'true')
|
||||
* @param string $database Candidate database name
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkValidDatabaseName($database) {
|
||||
|
||||
// Reject filename unsafe characters (cross platform)
|
||||
if(preg_match('/[\\\\\/\?%\*\:\|"\<\>\.]+/', $database)) return false;
|
||||
|
||||
// Restricted to characters in the ASCII and Extended ASCII range
|
||||
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
|
||||
return preg_match('/^[\x{0001}-\x{FFFF}]+$/u', $database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specified grant proves that the current user has the specified
|
||||
* permission on the specified database
|
||||
*
|
||||
* @param string $database Database name
|
||||
* @param string $permission Permission to check for
|
||||
* @param string $grant MySQL syntax grant to check within
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkDatabasePermissionGrant($database, $permission, $grant) {
|
||||
// Filter out invalid database names
|
||||
if(!$this->checkValidDatabaseName($database)) return false;
|
||||
|
||||
// Escape all valid database patterns (permission must exist on all tables)
|
||||
$dbPattern = sprintf(
|
||||
'((%s)|(%s)|(%s))',
|
||||
preg_quote("\"$database\".*"),
|
||||
preg_quote('"%".*'),
|
||||
preg_quote('*.*')
|
||||
);
|
||||
$expression = '/GRANT[ ,\w]+((ALL PRIVILEGES)|('.$permission.'(?! ((VIEW)|(ROUTINE)))))[ ,\w]+ON '.
|
||||
$dbPattern.'/i';
|
||||
return preg_match($expression, $grant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user has the specified permission on the specified database
|
||||
*
|
||||
* @param mixed $conn Connection object
|
||||
* @param string $database Database name
|
||||
* @param string $permission Permission to check
|
||||
* @return boolean
|
||||
*/
|
||||
public function checkDatabasePermission($conn, $database, $permission) {
|
||||
$grants = $this->column($conn->query("SHOW GRANTS FOR CURRENT_USER"));
|
||||
foreach($grants as $grant) {
|
||||
if($this->checkDatabasePermissionGrant($database, $permission, $grant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function requireDatabaseOrCreatePermissions($databaseConfig) {
|
||||
$success = false;
|
||||
$alreadyExists = false;
|
||||
$conn = new MySQLi($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||
if($conn && $conn->select_db($databaseConfig['database'])) {
|
||||
$success = true;
|
||||
$alreadyExists = true;
|
||||
} else {
|
||||
if($conn && $conn->query('CREATE DATABASE testing123')) {
|
||||
$conn->query('DROP DATABASE testing123');
|
||||
$conn = $this->createConnection($databaseConfig, $error);
|
||||
if($conn) {
|
||||
$list = $this->column($conn->query("SHOW DATABASES"));
|
||||
if(in_array($databaseConfig['database'], $list)) {
|
||||
$success = true;
|
||||
$alreadyExists = true;
|
||||
} else{
|
||||
// If no database exists then check DDL permissions
|
||||
$alreadyExists = false;
|
||||
$success = $this->checkDatabasePermission($conn, $databaseConfig['database'], 'CREATE');
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => $success,
|
||||
'alreadyExists' => $alreadyExists
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we have permissions to alter tables.
|
||||
*
|
||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||
* @return array Result - e.g. array('okay' => true, 'applies' => true), where applies is whether
|
||||
* the test is relevant for the database
|
||||
*/
|
||||
public function requireDatabaseAlterPermissions($databaseConfig) {
|
||||
$success = false;
|
||||
$conn = new MySQLi($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||
if($conn) {
|
||||
if ($res = $conn->query('SHOW GRANTS')) {
|
||||
// Annoyingly, MySQL 'escapes' the database, so we need to do it too.
|
||||
$db = str_replace(array('%', '_', '`'), array('\%', '\_', '``'), $databaseConfig['database']);
|
||||
while ($row = $res->fetch_array()) {
|
||||
if (preg_match('/^GRANT (.+) ON (.+) TO/', $row[0], $matches)) {
|
||||
// Need to change to an array of permissions, because ALTER is contained in ALTER ROUTINES.
|
||||
$permission = array_map('trim', explode(',', $matches[1]));
|
||||
$on_database = $matches[2];
|
||||
// The use of both ` and " is because of ANSI mode.
|
||||
if (in_array('ALL PRIVILEGES', $permission) and (
|
||||
($on_database == '*.*') or ($on_database == '`' . $db . '`.*')
|
||||
or ($on_database == '"' . $db . '".*'))) {
|
||||
$success = true;
|
||||
break;
|
||||
}
|
||||
if (in_array('ALTER', $permission) and (
|
||||
($on_database == '*.*') or ($on_database == '`' . $db . '`.*')
|
||||
or ($on_database == '"' . $db . '".*'))) {
|
||||
$success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$conn = $this->createConnection($databaseConfig, $error);
|
||||
$success = $this->checkDatabasePermission($conn, $databaseConfig['database'], 'ALTER');
|
||||
return array(
|
||||
'success' => $success,
|
||||
'applies' => true
|
||||
|
@ -133,13 +133,12 @@
|
||||
if(isset($details['fields'])) foreach($details['fields'] as $fieldName => $fieldSpec) {
|
||||
$fieldTitle = $fieldSpec['title'];
|
||||
$fieldType = ($fieldName == 'password') ? 'password' : 'text';
|
||||
|
||||
// values
|
||||
$defaultValue = (isset($fieldSpec['default'])) ? $fieldSpec['default'] : null;
|
||||
if($usingEnv && isset($fieldSpec['envVar']) && defined($fieldSpec['envVar'])) {
|
||||
$value = constant($fieldSpec['envVar']);
|
||||
} else {
|
||||
$value = (isset($databaseConfig[$fieldName])) ? $databaseConfig[$fieldName] : $defaultValue;
|
||||
$value = (isset($databaseConfig[$fieldName]) && $databaseConfig['type'] == $class) ? $databaseConfig[$fieldName] : $defaultValue;
|
||||
}
|
||||
|
||||
// attributes
|
||||
|
@ -165,8 +165,11 @@ foreach(DatabaseAdapterRegistry::get_adapters() as $class => $details) {
|
||||
|
||||
// Load database config
|
||||
if(isset($_REQUEST['db'])) {
|
||||
if(isset($_REQUEST['db']['type'])) $type = $_REQUEST['db']['type'];
|
||||
else $type = $_REQUEST['db']['type'] = defined('SS_DATABASE_CLASS') ? SS_DATABASE_CLASS : 'MySQLDatabase';
|
||||
if(isset($_REQUEST['db']['type'])) {
|
||||
$type = $_REQUEST['db']['type'];
|
||||
} else {
|
||||
$type = $_REQUEST['db']['type'] = defined('SS_DATABASE_CLASS') ? SS_DATABASE_CLASS : 'MySQLDatabase';
|
||||
}
|
||||
|
||||
// Disabled inputs don't submit anything - we need to use the environment (except the database name)
|
||||
if($usingEnv) {
|
||||
@ -301,9 +304,13 @@ class InstallRequirements {
|
||||
* Check the database configuration. These are done one after another
|
||||
* starting with checking the database function exists in PHP, and
|
||||
* continuing onto more difficult checks like database permissions.
|
||||
*
|
||||
* @param array $databaseConfig The list of database parameters
|
||||
* @return boolean Validity of database configuration details
|
||||
*/
|
||||
function checkDatabase($databaseConfig) {
|
||||
if($this->requireDatabaseFunctions(
|
||||
public function checkDatabase($databaseConfig) {
|
||||
// Check if support is available
|
||||
if(!$this->requireDatabaseFunctions(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
@ -311,57 +318,66 @@ class InstallRequirements {
|
||||
"Database support in PHP",
|
||||
$this->getDatabaseTypeNice($databaseConfig['type'])
|
||||
)
|
||||
)) {
|
||||
if($this->requireDatabaseServer(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database server",
|
||||
$databaseConfig['type'] == 'SQLiteDatabase' ? "I couldn't write to path '$databaseConfig[path]'" : "I couldn't find a database server on '$databaseConfig[server]'",
|
||||
$databaseConfig['type'] == 'SQLiteDatabase' ? $databaseConfig['path'] : $databaseConfig['server']
|
||||
)
|
||||
)) {
|
||||
if($this->requireDatabaseConnection(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database access credentials",
|
||||
"That username/password doesn't work"
|
||||
)
|
||||
)) {
|
||||
if($this->requireDatabaseVersion(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database server version requirement",
|
||||
'',
|
||||
'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
|
||||
)
|
||||
)) {
|
||||
if($this->requireDatabaseOrCreatePermissions(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Can I access/create the database",
|
||||
"I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
|
||||
)
|
||||
)) {
|
||||
$this->requireDatabaseAlterPermissions(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Can I ALTER tables",
|
||||
"I don't have permission to ALTER tables"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)) return false;
|
||||
|
||||
// Check if the server is available
|
||||
$usePath = !empty($databaseConfig['path']) && empty($databaseConfig['server']);
|
||||
if(!$this->requireDatabaseServer(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database server",
|
||||
$usePath ? "I couldn't write to path '$databaseConfig[path]'" : "I couldn't find a database server on '$databaseConfig[server]'",
|
||||
$usePath ? $databaseConfig['path'] : $databaseConfig['server']
|
||||
)
|
||||
)) return false;
|
||||
|
||||
// Check if the connection credentials allow access to the server / database
|
||||
if(!$this->requireDatabaseConnection(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database access credentials",
|
||||
"That username/password doesn't work"
|
||||
)
|
||||
)) return false;
|
||||
|
||||
// Check the necessary server version is available
|
||||
if(!$this->requireDatabaseVersion(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Database server version requirement",
|
||||
'',
|
||||
'Version ' . $this->getDatabaseConfigurationHelper($databaseConfig['type'])->getDatabaseVersion($databaseConfig)
|
||||
)
|
||||
)) return false;
|
||||
|
||||
// Check that database creation permissions are available
|
||||
if(!$this->requireDatabaseOrCreatePermissions(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Can I access/create the database",
|
||||
"I can't create new databases and the database '$databaseConfig[database]' doesn't exist"
|
||||
)
|
||||
)) return false;
|
||||
|
||||
// Check alter permission (necessary to create tables etc)
|
||||
if(!$this->requireDatabaseAlterPermissions(
|
||||
$databaseConfig,
|
||||
array(
|
||||
"Database Configuration",
|
||||
"Can I ALTER tables",
|
||||
"I don't have permission to ALTER tables"
|
||||
)
|
||||
)) return false;
|
||||
|
||||
// Success!
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkAdminConfig($adminConfig) {
|
||||
public function checkAdminConfig($adminConfig) {
|
||||
if(!$adminConfig['username']) {
|
||||
$this->error(array('', 'Please enter a username!'));
|
||||
}
|
||||
@ -374,14 +390,14 @@ class InstallRequirements {
|
||||
* Check if the web server is IIS and version greater than the given version.
|
||||
* @return boolean
|
||||
*/
|
||||
function isIIS($fromVersion = 7) {
|
||||
public function isIIS($fromVersion = 7) {
|
||||
if(strpos($this->findWebserver(), 'IIS/') === false) {
|
||||
return false;
|
||||
}
|
||||
return substr(strstr($this->findWebserver(), '/'), -3, 1) >= $fromVersion;
|
||||
}
|
||||
|
||||
function isApache() {
|
||||
public function isApache() {
|
||||
if(strpos($this->findWebserver(), 'Apache') !== false) {
|
||||
return true;
|
||||
} else {
|
||||
@ -393,7 +409,7 @@ class InstallRequirements {
|
||||
* Find the webserver software running on the PHP host.
|
||||
* @return string|boolean Server software or boolean FALSE
|
||||
*/
|
||||
function findWebserver() {
|
||||
public function findWebserver() {
|
||||
// Try finding from SERVER_SIGNATURE or SERVER_SOFTWARE
|
||||
if(!empty($_SERVER['SERVER_SIGNATURE'])) {
|
||||
$webserver = $_SERVER['SERVER_SIGNATURE'];
|
||||
@ -409,7 +425,7 @@ class InstallRequirements {
|
||||
/**
|
||||
* Check everything except the database
|
||||
*/
|
||||
function check() {
|
||||
public function check() {
|
||||
$this->errors = null;
|
||||
$isApache = $this->isApache();
|
||||
$isIIS = $this->isIIS();
|
||||
@ -657,7 +673,7 @@ class InstallRequirements {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
function suggestPHPSetting($settingName, $settingValues, $testDetails) {
|
||||
public function suggestPHPSetting($settingName, $settingValues, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
// special case for display_errors, check the original value before
|
||||
@ -675,7 +691,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requirePHPSetting($settingName, $settingValues, $testDetails) {
|
||||
public function requirePHPSetting($settingName, $settingValues, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
$val = ini_get($settingName);
|
||||
@ -685,7 +701,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function suggestClass($class, $testDetails) {
|
||||
public function suggestClass($class, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
if(!class_exists($class)) {
|
||||
@ -693,7 +709,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function suggestFunction($class, $testDetails) {
|
||||
public function suggestFunction($class, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
if(!function_exists($class)) {
|
||||
@ -701,7 +717,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireDateTimezone($testDetails) {
|
||||
public function requireDateTimezone($testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
$result = ini_get('date.timezone') && in_array(ini_get('date.timezone'), timezone_identifiers_list());
|
||||
@ -710,7 +726,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireMemory($min, $recommended, $testDetails) {
|
||||
public function requireMemory($min, $recommended, $testDetails) {
|
||||
$_SESSION['forcemem'] = false;
|
||||
|
||||
$mem = $this->getPHPMemory();
|
||||
@ -735,7 +751,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function getPHPMemory() {
|
||||
public function getPHPMemory() {
|
||||
$memString = ini_get("memory_limit");
|
||||
|
||||
switch(strtolower(substr($memString, -1))) {
|
||||
@ -753,7 +769,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function listErrors() {
|
||||
public function listErrors() {
|
||||
if($this->errors) {
|
||||
echo "<p>The following problems are preventing me from installing SilverStripe CMS:</p>\n\n";
|
||||
foreach($this->errors as $error) {
|
||||
@ -762,7 +778,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function showTable($section = null) {
|
||||
public function showTable($section = null) {
|
||||
if($section) {
|
||||
$tests = $this->tests[$section];
|
||||
$id = strtolower(str_replace(' ', '_', $section));
|
||||
@ -816,7 +832,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireFunction($funcName, $testDetails) {
|
||||
public function requireFunction($funcName, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
if(!function_exists($funcName)) {
|
||||
@ -826,7 +842,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireClass($className, $testDetails) {
|
||||
public function requireClass($className, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
if(!class_exists($className)) {
|
||||
$this->error($testDetails);
|
||||
@ -838,7 +854,7 @@ class InstallRequirements {
|
||||
/**
|
||||
* Require that the given class doesn't exist
|
||||
*/
|
||||
function requireNoClasses($classNames, $testDetails) {
|
||||
public function requireNoClasses($classNames, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$badClasses = array();
|
||||
foreach($classNames as $className) {
|
||||
@ -852,7 +868,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function checkApacheVersion($testDetails) {
|
||||
public function checkApacheVersion($testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
$is1pointx = preg_match('#Apache[/ ]1\.#', $testDetails[3]);
|
||||
@ -863,7 +879,7 @@ class InstallRequirements {
|
||||
return true;
|
||||
}
|
||||
|
||||
function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails) {
|
||||
public function requirePHPVersion($recommendedVersion, $requiredVersion, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
$installedVersion = phpversion();
|
||||
@ -891,7 +907,7 @@ class InstallRequirements {
|
||||
/**
|
||||
* Check that a module exists
|
||||
*/
|
||||
function checkModuleExists($dirname) {
|
||||
public function checkModuleExists($dirname) {
|
||||
$path = $this->getBaseDir() . $dirname;
|
||||
return file_exists($path) && ($dirname == 'mysite' || file_exists($path . '/_config.php'));
|
||||
}
|
||||
@ -900,7 +916,7 @@ class InstallRequirements {
|
||||
* The same as {@link requireFile()} but does additional checks
|
||||
* to ensure the module directory is intact.
|
||||
*/
|
||||
function requireModule($dirname, $testDetails) {
|
||||
public function requireModule($dirname, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$path = $this->getBaseDir() . $dirname;
|
||||
if(!file_exists($path)) {
|
||||
@ -913,7 +929,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireFile($filename, $testDetails) {
|
||||
public function requireFile($filename, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$filename = $this->getBaseDir() . $filename;
|
||||
if(!file_exists($filename)) {
|
||||
@ -922,7 +938,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireWriteable($filename, $testDetails, $absolute = false) {
|
||||
public function requireWriteable($filename, $testDetails, $absolute = false) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
if($absolute) {
|
||||
@ -974,7 +990,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireTempFolder($testDetails) {
|
||||
public function requireTempFolder($testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
try {
|
||||
@ -991,7 +1007,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireApacheModule($moduleName, $testDetails) {
|
||||
public function requireApacheModule($moduleName, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
if(!in_array($moduleName, apache_get_modules())) {
|
||||
$this->error($testDetails);
|
||||
@ -1001,7 +1017,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function testApacheRewriteExists($moduleName = 'mod_rewrite') {
|
||||
public function testApacheRewriteExists($moduleName = 'mod_rewrite') {
|
||||
if(function_exists('apache_get_modules') && in_array($moduleName, apache_get_modules())) {
|
||||
return true;
|
||||
} elseif(isset($_SERVER['HTTP_MOD_REWRITE']) && $_SERVER['HTTP_MOD_REWRITE'] == 'On') {
|
||||
@ -1011,7 +1027,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule') {
|
||||
public function testIISRewriteModuleExists($moduleName = 'IIS_UrlRewriteModule') {
|
||||
if(isset($_SERVER[$moduleName]) && $_SERVER[$moduleName]) {
|
||||
return true;
|
||||
} else {
|
||||
@ -1019,7 +1035,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireApacheRewriteModule($moduleName, $testDetails) {
|
||||
public function requireApacheRewriteModule($moduleName, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
if($this->testApacheRewriteExists()) {
|
||||
return true;
|
||||
@ -1033,11 +1049,11 @@ class InstallRequirements {
|
||||
* Determines if the web server has any rewriting capability.
|
||||
* @return boolean
|
||||
*/
|
||||
function hasRewritingCapability() {
|
||||
public function hasRewritingCapability() {
|
||||
return ($this->testApacheRewriteExists() || $this->testIISRewriteModuleExists());
|
||||
}
|
||||
|
||||
function requireIISRewriteModule($moduleName, $testDetails) {
|
||||
public function requireIISRewriteModule($moduleName, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
if($this->testIISRewriteModuleExists()) {
|
||||
return true;
|
||||
@ -1047,7 +1063,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function getDatabaseTypeNice($databaseClass) {
|
||||
public function getDatabaseTypeNice($databaseClass) {
|
||||
return substr($databaseClass, 0, -8);
|
||||
}
|
||||
|
||||
@ -1055,7 +1071,7 @@ class InstallRequirements {
|
||||
* Get an instance of a helper class for the specific database.
|
||||
* @param string $databaseClass e.g. MySQLDatabase or MSSQLDatabase
|
||||
*/
|
||||
function getDatabaseConfigurationHelper($databaseClass) {
|
||||
public function getDatabaseConfigurationHelper($databaseClass) {
|
||||
$adapters = DatabaseAdapterRegistry::get_adapters();
|
||||
if(isset($adapters[$databaseClass])) {
|
||||
$helperPath = $adapters[$databaseClass]['helperPath'];
|
||||
@ -1064,7 +1080,7 @@ class InstallRequirements {
|
||||
return (class_exists($class)) ? new $class() : false;
|
||||
}
|
||||
|
||||
function requireDatabaseFunctions($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseFunctions($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
if (!$helper) {
|
||||
@ -1080,7 +1096,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireDatabaseConnection($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseConnection($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
$result = $helper->requireDatabaseConnection($databaseConfig);
|
||||
@ -1093,7 +1109,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireDatabaseVersion($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseVersion($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
if(method_exists($helper, 'requireDatabaseVersion')) {
|
||||
@ -1110,7 +1126,7 @@ class InstallRequirements {
|
||||
return true;
|
||||
}
|
||||
|
||||
function requireDatabaseServer($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseServer($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
$result = $helper->requireDatabaseServer($databaseConfig);
|
||||
@ -1123,7 +1139,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseOrCreatePermissions($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
$result = $helper->requireDatabaseOrCreatePermissions($databaseConfig);
|
||||
@ -1144,7 +1160,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireDatabaseAlterPermissions($databaseConfig, $testDetails) {
|
||||
public function requireDatabaseAlterPermissions($databaseConfig, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$helper = $this->getDatabaseConfigurationHelper($databaseConfig['type']);
|
||||
$result = $helper->requireDatabaseAlterPermissions($databaseConfig);
|
||||
@ -1158,7 +1174,7 @@ class InstallRequirements {
|
||||
}
|
||||
}
|
||||
|
||||
function requireServerVariables($varNames, $testDetails) {
|
||||
public function requireServerVariables($varNames, $testDetails) {
|
||||
$this->testing($testDetails);
|
||||
$missing = array();
|
||||
|
||||
@ -1177,7 +1193,7 @@ class InstallRequirements {
|
||||
}
|
||||
|
||||
|
||||
function requirePostSupport($testDetails) {
|
||||
public function requirePostSupport($testDetails) {
|
||||
$this->testing($testDetails);
|
||||
|
||||
if(!isset($_POST)) {
|
||||
@ -1189,7 +1205,7 @@ class InstallRequirements {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isRunningWebServer($testDetails) {
|
||||
public function isRunningWebServer($testDetails) {
|
||||
$this->testing($testDetails);
|
||||
if($testDetails[3]) {
|
||||
return true;
|
||||
@ -1202,14 +1218,14 @@ class InstallRequirements {
|
||||
// Must be PHP4 compatible
|
||||
var $baseDir;
|
||||
|
||||
function getBaseDir() {
|
||||
public function getBaseDir() {
|
||||
// Cache the value so that when the installer mucks with SCRIPT_FILENAME half way through, this method
|
||||
// still returns the correct value.
|
||||
if(!$this->baseDir) $this->baseDir = realpath(dirname($_SERVER['SCRIPT_FILENAME'])) . DIRECTORY_SEPARATOR;
|
||||
return $this->baseDir;
|
||||
}
|
||||
|
||||
function testing($testDetails) {
|
||||
public function testing($testDetails) {
|
||||
if(!$testDetails) return;
|
||||
|
||||
$section = $testDetails[0];
|
||||
@ -1221,7 +1237,7 @@ class InstallRequirements {
|
||||
$this->tests[$section][$test] = array("good", $message);
|
||||
}
|
||||
|
||||
function error($testDetails) {
|
||||
public function error($testDetails) {
|
||||
$section = $testDetails[0];
|
||||
$test = $testDetails[1];
|
||||
|
||||
@ -1229,7 +1245,7 @@ class InstallRequirements {
|
||||
$this->errors[] = $testDetails;
|
||||
}
|
||||
|
||||
function warning($testDetails) {
|
||||
public function warning($testDetails) {
|
||||
$section = $testDetails[0];
|
||||
$test = $testDetails[1];
|
||||
|
||||
@ -1237,23 +1253,23 @@ class InstallRequirements {
|
||||
$this->warnings[] = $testDetails;
|
||||
}
|
||||
|
||||
function hasErrors() {
|
||||
public function hasErrors() {
|
||||
return sizeof($this->errors);
|
||||
}
|
||||
|
||||
function hasWarnings() {
|
||||
public function hasWarnings() {
|
||||
return sizeof($this->warnings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Installer extends InstallRequirements {
|
||||
function __construct() {
|
||||
public function __construct() {
|
||||
// Cache the baseDir value
|
||||
$this->getBaseDir();
|
||||
}
|
||||
|
||||
function install($config) {
|
||||
public function install($config) {
|
||||
?>
|
||||
<html>
|
||||
<head>
|
||||
@ -1307,7 +1323,9 @@ class Installer extends InstallRequirements {
|
||||
$databaseVersion = $config['db']['type'];
|
||||
$helper = $this->getDatabaseConfigurationHelper($dbType);
|
||||
if($helper && method_exists($helper, 'getDatabaseVersion')) {
|
||||
$databaseVersion = urlencode($dbType . ': ' . $helper->getDatabaseVersion($config['db'][$dbType]));
|
||||
$versionConfig = $config['db'][$dbType];
|
||||
$versionConfig['type'] = $dbType;
|
||||
$databaseVersion = urlencode($dbType . ': ' . $helper->getDatabaseVersion($versionConfig));
|
||||
}
|
||||
|
||||
$url = "http://ss2stat.silverstripe.com/Installation/add?SilverStripe=$silverstripe_version&PHP=$phpVersion&Database=$databaseVersion&WebServer=$encWebserver";
|
||||
@ -1343,7 +1361,6 @@ class Installer extends InstallRequirements {
|
||||
// Write the config file
|
||||
global $usingEnv;
|
||||
if($usingEnv) {
|
||||
|
||||
$this->statusMessage("Setting up 'mysite/_config.php' for use with _ss_environment.php...");
|
||||
$this->writeToFile("mysite/_config.php", <<<PHP
|
||||
<?php
|
||||
@ -1358,12 +1375,20 @@ require_once('conf/ConfigureFromEnv.php');
|
||||
|
||||
// Set the site locale
|
||||
i18n::set_locale('$locale');
|
||||
|
||||
PHP
|
||||
);
|
||||
|
||||
} else {
|
||||
$this->statusMessage("Setting up 'mysite/_config.php'...");
|
||||
$escapedPassword = addslashes($dbConfig['password']);
|
||||
// Create databaseConfig
|
||||
$lines = array(
|
||||
$lines[] = "\t'type' => '$type'"
|
||||
);
|
||||
foreach($dbConfig as $key => $value) {
|
||||
$lines[] = "\t'{$key}' => '$value'";
|
||||
}
|
||||
$databaseConfigContent = implode(",\n", $lines);
|
||||
$this->writeToFile("mysite/_config.php", <<<PHP
|
||||
<?php
|
||||
|
||||
@ -1372,27 +1397,39 @@ global \$project;
|
||||
|
||||
global \$databaseConfig;
|
||||
\$databaseConfig = array(
|
||||
"type" => '{$type}',
|
||||
"server" => '{$dbConfig['server']}',
|
||||
"username" => '{$dbConfig['username']}',
|
||||
"password" => '{$escapedPassword}',
|
||||
"database" => '{$dbConfig['database']}',
|
||||
"path" => '{$dbConfig['path']}',
|
||||
{$databaseConfigContent}
|
||||
);
|
||||
|
||||
// Set the site locale
|
||||
i18n::set_locale('$locale');
|
||||
|
||||
PHP
|
||||
);
|
||||
}
|
||||
|
||||
$this->statusMessage("Setting up 'mysite/_config/config.yml'");
|
||||
$this->writeToFile("mysite/_config/config.yml", <<<YML
|
||||
---
|
||||
Name: mysite
|
||||
After:
|
||||
- 'framework/*'
|
||||
- 'cms/*'
|
||||
---
|
||||
# YAML configuration for SilverStripe
|
||||
# See http://doc.silverstripe.org/framework/en/topics/configuration
|
||||
# Caution: Indentation through two spaces, not tabs
|
||||
SSViewer:
|
||||
theme: '$theme'
|
||||
YML
|
||||
);
|
||||
|
||||
if(!$this->checkModuleExists('cms')) {
|
||||
$this->writeToFile("mysite/code/RootURLController.php", <<<PHP
|
||||
<?php
|
||||
|
||||
class RootURLController extends Controller {
|
||||
|
||||
function index() {
|
||||
public function index() {
|
||||
echo "<html>Your site is now set up. Start adding controllers to mysite to get started.</html>";
|
||||
}
|
||||
|
||||
@ -1442,7 +1479,7 @@ PHP
|
||||
$adminMember = Security::findAnAdministrator();
|
||||
$adminMember->Email = $config['admin']['username'];
|
||||
$adminMember->Password = $config['admin']['password'];
|
||||
$adminMember->PasswordEncryption = Security::get_password_encryption_algorithm();
|
||||
$adminMember->PasswordEncryption = Security::config()->encryption_algorithm;
|
||||
|
||||
try {
|
||||
$this->statusMessage('Creating default CMS admin account...');
|
||||
@ -1496,7 +1533,7 @@ HTML;
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
function writeToFile($filename, $content) {
|
||||
public function writeToFile($filename, $content) {
|
||||
$base = $this->getBaseDir();
|
||||
$this->statusMessage("Setting up $base$filename");
|
||||
|
||||
@ -1507,7 +1544,7 @@ HTML;
|
||||
}
|
||||
}
|
||||
|
||||
function createHtaccess() {
|
||||
public function createHtaccess() {
|
||||
$start = "### SILVERSTRIPE START ###\n";
|
||||
$end = "\n### SILVERSTRIPE END ###";
|
||||
|
||||
@ -1548,7 +1585,7 @@ ErrorDocument 500 /assets/error-500.html
|
||||
RewriteRule ^vendor(/|$) - [F,L,NC]
|
||||
RewriteRule silverstripe-cache(/|$) - [F,L,NC]
|
||||
RewriteRule composer\.(json|lock) - [F,L,NC]
|
||||
|
||||
|
||||
RewriteCond %{REQUEST_URI} ^(.*)$
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_URI} !\.php$
|
||||
@ -1576,7 +1613,7 @@ TEXT;
|
||||
* Writes basic configuration to the web.config for IIS
|
||||
* so that rewriting capability can be use.
|
||||
*/
|
||||
function createWebConfig() {
|
||||
public function createWebConfig() {
|
||||
$modulePath = FRAMEWORK_NAME;
|
||||
$content = <<<TEXT
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
@ -1614,7 +1651,7 @@ TEXT;
|
||||
$this->writeToFile('web.config', $content);
|
||||
}
|
||||
|
||||
function checkRewrite() {
|
||||
public function checkRewrite() {
|
||||
require_once 'core/startup/ParameterConfirmationToken.php';
|
||||
$token = new ParameterConfirmationToken('flush');
|
||||
$params = http_build_query($token->params());
|
||||
@ -1659,7 +1696,7 @@ TEXT;
|
||||
HTML;
|
||||
}
|
||||
|
||||
function var_export_array_nokeys($array) {
|
||||
public function var_export_array_nokeys($array) {
|
||||
$retval = "array(\n";
|
||||
foreach($array as $item) {
|
||||
$retval .= "\t'";
|
||||
@ -1674,7 +1711,7 @@ HTML;
|
||||
* Show an installation status message.
|
||||
* The output differs depending on whether this is CLI or web based
|
||||
*/
|
||||
function statusMessage($msg) {
|
||||
public function statusMessage($msg) {
|
||||
echo "<li>$msg</li>\n";
|
||||
flush();
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
## Changelog
|
||||
|
||||
### CMS
|
||||
|
||||
### DataObject::validate() method visibility changed to public
|
||||
|
||||
The visibility of `DataObject::validate()` has been changed from `protected` to `public`.
|
||||
@ -83,3 +85,424 @@ including "MMM" or "MMMM", consider deleting those formats to fall back to
|
||||
the global (and more stable) default.
|
||||
|
||||
### Bugfixes
|
||||
* Migration of code to use new parameterised framework
|
||||
|
||||
### Framework
|
||||
|
||||
* Implementation of a parameterised query framework eliminating the need to manually escape variables for
|
||||
use in SQL queries. This has been integrated into nearly every level of the database ORM.
|
||||
* Refactor of database connectivity classes into separate components linked together through dependency injection
|
||||
* Refactor of `SQLQuery` into separate objects for each query type: `SQLSelect`, `SQLDelete`, `SQLUpdate` and `SQLInsert`
|
||||
* Rename of API methods to conform to coding conventions
|
||||
* PDO is now a standard connector, and is available for all database interfaces
|
||||
* Additional database and query generation tools
|
||||
|
||||
## Bugfixes
|
||||
|
||||
* Reduced database regeneration chances on subsequent rebuilds after the initial dev/build
|
||||
* Elimination of various SQL injection vulnerability points
|
||||
* `DataObject::writeComponents()` now called correctly during `DataObject::write()`
|
||||
* Fixed missing theme declaration in installer
|
||||
* Fixed incorrect use of non-existing exception classes (e.g. `HTTPResponse_exception`)
|
||||
* `GridState` fixed to distinguish between check for missing values, and creation of
|
||||
nested state values, in order to prevent non-empty values being returned for
|
||||
missing keys. This was breaking `DataObject::get_by_id` by passing in an object
|
||||
for the ID.
|
||||
* Fixed order of `File` fulltext searchable fields to use same order as actual fields.
|
||||
This is required to prevent unnecessary rebuild of MS SQL databases when fulltext
|
||||
searching is enabled.
|
||||
|
||||
## Upgrading
|
||||
|
||||
### Update code that uses SQLQuery
|
||||
|
||||
SQLQuery is still implemented, but now extends the new SQLSelect class and has some methods
|
||||
deprecated. Previously this class was used for both selecting and deleting, but these
|
||||
have been superceded by the specialised SQLSelect and SQLDelete classes. Additionally,
|
||||
3.2 now provides SQLUpdate and SQLInsert to generate parameterised query friendly
|
||||
data updates.
|
||||
|
||||
SQLSelect, SQLDelete and SQLUpdate all inherit from SQLConditionalExpression, which
|
||||
implements toSelect, toDelete, and toUpdate to generate basic transformations
|
||||
between query types.
|
||||
|
||||
In the past SQLQuery->setDelete(true) would be used to turn a select into a delete,
|
||||
although now a new SQLDelete object should be created from a separate SQLSelect.
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$query = new SQLQuery('*');
|
||||
$query->setFrom('"SiteTree"');
|
||||
$query->setWhere('"SiteTree"."ShowInMenus" = 0');
|
||||
$query->setDelete(true);
|
||||
$query->execute();
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$query = SQLDelete::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
|
||||
$query->execute();
|
||||
|
||||
Alternatively:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
|
||||
->toDelete();
|
||||
$query->execute();
|
||||
|
||||
Also, take care for any code or functions which expect an object of type `SQLQuery`, as
|
||||
these references should be replaced with `SQLSelect`. Legacy code which generates
|
||||
`SQLQuery` can still communicate with new code that expects `SQLSelect` as it is a
|
||||
subclass of `SQLSelect`, but the inverse is not true.
|
||||
|
||||
### Update code that interacts with SQL strings to use parameters
|
||||
|
||||
The Silverstripe ORM (object relation model) has moved from using escaped SQL strings
|
||||
to query the database, to a combination of parameterised SQL expressions alongside
|
||||
a related list of parameter values. As a result of this, it is necessary to assume
|
||||
that any `SQLSelect` object (previously `SQLQuery`) may, and will usually, have
|
||||
un-injected parameters.
|
||||
|
||||
All database queries performed through `DataList`, `DataQuery` and `SQLQuery` will continue
|
||||
to work, as will those through `DataObject::get()` (which returns a filterable `DataList`).
|
||||
However, any conditional expression that includes values escaped with `Convert::raw2sql()`
|
||||
should use the new standard syntax. This new querying standard method enforces a much
|
||||
higher level of security than was previously available, and all code using manual
|
||||
escaping should be upgraded.
|
||||
|
||||
See [the security topic](/topics/security#parameterised-queries) for details on why this is necessary, or
|
||||
[the databamodel topic](/topics/datamodel#raw-sql-options-for-advanced-users) for more information.
|
||||
|
||||
As a result of this upgrade there are now very few cases where `Convert::raw2sql` needs to be used.
|
||||
|
||||
Examples of areas where queries should be upgraded are below:
|
||||
|
||||
1. #### Querying the database directly through DB, including non-SELECT queries
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
// Note: No deprecation notices will be caused here
|
||||
DB::query("UPDATE \"SiteTree\" SET \"Title\" LIKE '%" . Convert::raw2sql($myTitle) . "%' WHERE \"ID\" = 1");
|
||||
$myPages = DB::query(sprintf('SELECT "ID" FROM "MyObject" WHERE "Title" = \'%s\'', Convert::raw2sql($parentTitle)));
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
DB::prepared_query(
|
||||
'UPDATE "SiteTree" SET "Title" LIKE ? WHERE "ID" = ?',
|
||||
array("%{$myTitle}%", 1)
|
||||
);
|
||||
$myPages = DB::prepared_query(
|
||||
'SELECT "ID" FROM "MyObject" WHERE "Title" = ?',
|
||||
array($parentTitle)
|
||||
);
|
||||
|
||||
2. #### Querying the database through `SQLQuery` (deprecated)
|
||||
|
||||
Before:
|
||||
|
||||
Note: Use of SQLQuery would generate a deprecation notice if left un-upgraded.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$query = new SQLQuery('*', '"SiteTree"', "\"URLSegment\" = '".Convert::raw2sql($testURL)."'");
|
||||
|
||||
$query->addWhere(array(
|
||||
'"ParentID" = \''.intval($parentID).'\'',
|
||||
'"ID" IN (SELECT "PageID" FROM "MyObject")'
|
||||
));
|
||||
|
||||
$query->addWhere("\"Title\" LIKE '%".Convert::raw2sql($myText)."' OR \"Title\" LIKE '".Convert::raw2sql($myText)."%'");
|
||||
|
||||
After, substituting `SQLSelect` for the deprecated `SQLQuery`:
|
||||
|
||||
Note: The inclusion of properly ANSI quoted symbols with the table name included,
|
||||
as per best coding practices.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$query = SQLSelect::create('*', '"SiteTree"', array('"SiteTree"."URLSegment" = ?' => $testURL));
|
||||
|
||||
$query->addWhere(array(
|
||||
'"SiteTree"."ParentID"' => // Note that the " = ?" is optional for simple comparison
|
||||
array( // Syntax for parameter casting for supporting databases
|
||||
'value' => $parentID,
|
||||
'type' => 'integer'
|
||||
),
|
||||
'"SiteTree"."ID" IN (SELECT "MyObject"."PageID" FROM "MyObject")' // Raw SQL condition with no parameters
|
||||
));
|
||||
|
||||
// Multiple parameters may be assigned for a single query (this should not be associative)
|
||||
$query->addWhere(array(
|
||||
'"SiteTree"."Title" LIKE %? OR "SiteTree"."Title" LIKE %?' => array($myText, $myText)
|
||||
));
|
||||
|
||||
3. #### Querying the database through `DataList`, `DataQuery`, and `DataObject`
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$items = DataObject::get_one('MyObject', '"Details" = \''.Convert::raw2sql($details).'\'');
|
||||
$things = MyObject::get()->where('"Name" = \''.Convert::raw2sql($name).'\'');
|
||||
$list = DataList::create('Banner')->where(array(
|
||||
'"ParentID" IS NOT NULL',
|
||||
'"Title" = \'' . Convert::raw2sql($title) . '\''
|
||||
);
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$items = DataObject::get_one('MyObject', array('"MyObject"."Details"' => $details));
|
||||
$things = MyObject::get()->where(array('"MyObject"."Name" = ?' => $name));
|
||||
$list = DataList::create('Banner')->where(array(
|
||||
'"ParentID" IS NOT NULL',
|
||||
'"Title" = ?', $title
|
||||
);
|
||||
|
||||
4. #### Interaction with the `DataList::sql()`, `DataQuery::sql()` or `SQLSelect::sql()` methods
|
||||
|
||||
The place where legacy code would almost certainly fail is any code that calls
|
||||
`SQLQuery::sql`, `DataList::sql` or `DataQuery::sql`, as the api requires that user
|
||||
code passes in an argument here to retrieve SQL parameters by value.
|
||||
|
||||
User code that assumes parameterless queries will likely fail, and need to be
|
||||
updated to handle this case properly.
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
// Generate query
|
||||
$argument = 'whatever';
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array("\"SiteTree\".\"Title\" LIKE '" . Convert::raw2sql($argument) . "'"));
|
||||
|
||||
// Inspect elements of the query
|
||||
$sql = $query->sql();
|
||||
$sql = preg_replace('/LIKE \'(.+)\'/', 'LIKE \'%${1}%\'', $sql); // Adds %% around the argument
|
||||
|
||||
// Pass new query to database connector
|
||||
DB::query($sql);
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
// Generate query
|
||||
$argument = 'whatever';
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."Title" LIKE ?' => $argument));
|
||||
|
||||
// Inspect elements of the query
|
||||
$sql = $query->sql($parameters);
|
||||
foreach($parameters as $key => $value) {
|
||||
// Adds %% around arguments
|
||||
$parameters[$key] = "%{$value}%";
|
||||
}
|
||||
|
||||
// Pass new query to database connector
|
||||
// Note that DB::query($sql) would fail, as it would contain ? with missing parameters
|
||||
DB::prepared_query($sql, $parameters);
|
||||
|
||||
Also note that the parameters may not be a single level array, as certain values
|
||||
may be forced to be cast as a certain type (where supported by the current API).
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$parameters = array(
|
||||
10,
|
||||
array('value' => 0, 'type' => 'boolean') // May also contain other database API specific options
|
||||
)
|
||||
DB::prepared_query('DELETE FROM "MyObject" WHERE ParentID = ? OR IsValid = ?', $parameters);
|
||||
|
||||
5. #### Update implementations of augmentSQL
|
||||
|
||||
Since this method now takes a `SQLSelect` as a first parameter, existing code referencing the deprecated `SQLQuery`
|
||||
type will raise a PHP error.
|
||||
|
||||
Furthermore, it's important to note that even though the signature of `SQLSelect::getWhere` is similar to the old
|
||||
`SQLQuery::getWhere` the result will actually be an associative array of SQL fragments mapped to arrays of
|
||||
parameters, and any transformation of these values will require parameters to be maintained.
|
||||
|
||||
If your code doesn't modify the parameters then `SQLSelect::getWhereParameterised` can be used in order to return
|
||||
these SQL statements as a simple array of strings. The resulting parameters are still maintained, but are
|
||||
instead be returned by referenced through the first parameter to this method.
|
||||
|
||||
E.g.
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
function augmentSQL(SQLQuery $query, DataQuery $dataQuery = null) {
|
||||
$locale = Translatable::get_current_locale();
|
||||
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhere()))) {
|
||||
$qry = sprintf('"Locale" = \'%s\'', Convert::raw2sql($locale));
|
||||
$query->addWhere($qry);
|
||||
}
|
||||
}
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
|
||||
$locale = Translatable::get_current_locale();
|
||||
if(!preg_match('/("|\'|`)Locale("|\'|`)/', implode(' ', $query->getWhereParameterised($parameters)))) {
|
||||
$query->addWhere(array(
|
||||
'"Locale"' => $locale
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
### Update code that interacts with the DB schema
|
||||
|
||||
Updating database schema is now done by `updateSchema` with a callback, rather than relying
|
||||
on user code to call `beginSchemaUpdate` and `endSchemaUpdate` around the call.
|
||||
|
||||
Since the schema management object is separate from the database controller you
|
||||
interact with it via `DB::get_schema` instead of `DB::get_conn` (previously named
|
||||
`DB::getConn`)
|
||||
|
||||
Before:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$conn = DB::getConn();
|
||||
$conn->beginSchemaUpdate();
|
||||
foreach($dataClasses as $dataClass) {
|
||||
singleton($dataClass)->requireTable();
|
||||
}
|
||||
$conn->endSchemaUpdate();
|
||||
|
||||
After:
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$schema = DB::get_schema();
|
||||
$schema->schemaUpdate(function() use($dataClasses){
|
||||
foreach($dataClasses as $dataClass) {
|
||||
singleton($dataClass)->requireTable();
|
||||
}
|
||||
});
|
||||
|
||||
Also should be noted is that many functions have been renamed to conform better with
|
||||
coding conventions. E.g. `DB::requireTable` is now `DB::require_table`
|
||||
|
||||
### Other
|
||||
|
||||
* Helper function `DB::placeholders` can be used to generate a comma separated list of placeholders
|
||||
useful for creating "WHERE ... IN (?,...)" SQL fragments
|
||||
* Implemented Convert::symbol2sql to safely encode database and table names and identifiers.
|
||||
E.g. `Convert::symbol2sql('table.column') => '"table"."column"';`
|
||||
* `Convert::raw2sql` may now quote the escaped value, as well as safely escape it, according to the current
|
||||
database adaptor's preference.
|
||||
* `DB` class has been updated and many static methods have been renamed to conform to coding convention.
|
||||
* Renamed API:
|
||||
- `affectedRows` -> `affected_rows`
|
||||
- `checkAndRepairTable` -> `check_and_repair_table`
|
||||
- `createDatabase` -> `create_database`
|
||||
- `createField` -> `createField`
|
||||
- `createTable` -> `createTable`
|
||||
- `dontRequireField` -> `dont_require_field`
|
||||
- `dontRequireTable` -> `dont_require_table`
|
||||
- `fieldList` -> `field_list`
|
||||
- `getConn` -> `get_conn`
|
||||
- `getGeneratedID` -> `get_generated_id`
|
||||
- `isActive` -> `is_active`
|
||||
- `requireField` -> `require_field`
|
||||
- `requireIndex` -> `require_index`
|
||||
- `requireTable` -> `require_table`
|
||||
- `setConn` -> `set_conn`
|
||||
- `tableList` -> `table_list`
|
||||
* Deprecated API:
|
||||
- `getConnect` (Was placeholder for PDO connection string building code, but is made
|
||||
redundant after the PDOConnector being fully abstracted)
|
||||
* New API:
|
||||
- `build_sql` - Hook into new SQL generation code
|
||||
- `get_connector` (Nothing to do with getConnect)
|
||||
- `get_schema`
|
||||
- `placeholders`
|
||||
- `prepared_query`
|
||||
* `SS_Database` class has been updated and many functions have been deprecated, or refactored into
|
||||
the various other database classes. Most of the database management classes remain in the database
|
||||
controller, due to individual databases (changing, creating of, etc) varying quite a lot from
|
||||
API to API, but schema updates within a database itself is managed by an attached DBSchemaManager
|
||||
* Refactored into DBSchemaManager:
|
||||
- `createTable`
|
||||
- `alterTable`
|
||||
- `renameTable`
|
||||
- `createField`
|
||||
- `renameField`
|
||||
- `fieldList`
|
||||
- `tableList`
|
||||
- `hasTable`
|
||||
- `enumValuesForField`
|
||||
- `beginSchemaUpdate` and `endSchemaUpdate` -> Use `schemaUpdate` with a callback
|
||||
- `cancelSchemaUpdate`
|
||||
- `isSchemaUpdating`
|
||||
- `doesSchemaNeedUpdating`
|
||||
- `transCreateTable`
|
||||
- `transAlterTable`
|
||||
- `transCreateField`
|
||||
- `transCreateField`
|
||||
- `transCreateIndex`
|
||||
- `transAlterField`
|
||||
- `transAlterIndex`
|
||||
- `requireTable`
|
||||
- `dontRequireTable`
|
||||
- `requireIndex`
|
||||
- `hasField`
|
||||
- `requireField`
|
||||
- `dontRequireField`
|
||||
* Refactored into DBQueryBuilder
|
||||
- `sqlQueryToString`
|
||||
* Deprecated:
|
||||
- `getConnect` - Was intended for use with PDO, but was never implemented, and is now
|
||||
redundant, now that there is a stand-alone `PDOConnector`
|
||||
- `prepStringForDB` - Use `quoteString` instead
|
||||
- `dropDatabase` - Use `dropSelectedDatabase`
|
||||
- `createDatabase` - Use `selectDatabase` with the second parameter set to true instead
|
||||
- `allDatabaseNames` - Use `databaseList` instead
|
||||
- `currentDatabase` - Use `getSelectedDatabase` instead
|
||||
- `addslashes` - Use `escapeString` instead
|
||||
* LogErrorEmailFormatter now better displays SQL queries in errors by respecting line breaks
|
||||
* Installer has been majorly upgraded to handle the new database configuration options
|
||||
and additional PDO functionality.
|
||||
* Created SS_DatabaseException to emit database errors. Query information such as SQL
|
||||
and any relevant parameters may be used by error handling user code that catches
|
||||
this exception.
|
||||
* The SQLConditionGroup interface has been created to represent dynamically
|
||||
evaluated SQL conditions. This may be used to wrap a class that generates
|
||||
a custom SQL clause(s) to be evaluated at the time of execution.
|
||||
* DataObject constants CHANGE_NONE, CHANGE_STRICT, and CHANGE_VALUE have been created
|
||||
to provide more verbosity to field modification detection. This replaces the use of
|
||||
various magic numbers with the same meaning.
|
||||
* create_table_options now uses constants as API specific filters rather than strings.
|
||||
This is in order to promote better referencing of elements across the codebase.
|
||||
See `FulltextSearchable->enable` for example.
|
||||
|
@ -9,6 +9,8 @@ For information on how to upgrade to newer versions consult the [upgrading](/ins
|
||||
|
||||
## Stable Releases
|
||||
|
||||
* [3.2.0](3.2.0) - Unreleased
|
||||
|
||||
* [3.1.5](3.1.5) - 13 May 2014
|
||||
* [3.1.4](3.1.4) - 8 April 2014
|
||||
* [3.1.0](3.1.0) - 1 October 2013
|
||||
|
@ -433,7 +433,15 @@ Put code into the classes in the following order (where applicable).
|
||||
### SQL Format
|
||||
|
||||
If you have to use raw SQL, make sure your code works across databases make sure you escape your queries like below,
|
||||
with the column or table name escaped with double quotes and values with single quotes.
|
||||
with the column or table name escaped with double quotes as below.
|
||||
|
||||
:::php
|
||||
MyClass::get()->where(array("\"Score\" > ?" => 50));
|
||||
|
||||
It is preferable to use parameterised queries whenever necessary to provide conditions
|
||||
to a SQL query, where values placeholders are each replaced with a single unquoted question mark.
|
||||
If it's absolutely necessary to use literal values in a query make sure that values
|
||||
are single quoted.
|
||||
|
||||
:::php
|
||||
MyClass::get()->where("\"Title\" = 'my title'");
|
||||
|
@ -225,9 +225,8 @@ The other queries that you will want to customise are the selection queries,
|
||||
called by get & get_one. For example, the Versioned object has code to redirect
|
||||
every request to ClassName_live, if you are browsing the live site.
|
||||
|
||||
To do this, define the **augmentSQL(SQLQuery &$query)** method. Again, the
|
||||
`$query` object is passed by reference and can be modified as needed by your
|
||||
method. Instead of a manipulation array, we have a `[api:SQLQuery]` object.
|
||||
To do this, define the **augmentSQL(SQLSelect $query)** method. Again, the $query object is passed by reference and can
|
||||
be modified as needed by your method. Instead of a manipulation array, we have a `[api:SQLSelect]` object.
|
||||
|
||||
### Additional methods
|
||||
|
||||
|
@ -25,7 +25,7 @@ Reference articles complement our auto-generated [API docs](http://api.silverstr
|
||||
* [Site Reports](site-reports): Tabular reports in a specialized CMS interface
|
||||
* [SiteConfig](siteconfig): Global configuration stored in the database
|
||||
* [SiteTree](sitetree): Base class for a "page" in the CMS
|
||||
* [SQLQuery](sqlquery): Wrapper around a SQL query allowing modification before execution
|
||||
* [SQLSelect](sqlquery): Wrapper around a SQL query allowing modification before execution
|
||||
* [StaticPublisher](staticpublisher): Export a page tree as static HTML for better performance and portability
|
||||
* [TableField](tablefield): Add and edit records with inline edits in this form field
|
||||
* [TableListField](tablelistfield): View and delete records in the CMS
|
||||
|
@ -6,7 +6,7 @@ Manages searching of properties on one or more `[api:DataObject]` types, based o
|
||||
`[api:SearchContext]` is intentionally decoupled from any controller-logic,
|
||||
it just receives a set of search parameters and an object class it acts on.
|
||||
|
||||
The default output of a `[api:SearchContext]` is either a `[api:SQLQuery]` object for further refinement, or a
|
||||
The default output of a `[api:SearchContext]` is either a `[api:SQLSelect]` object for further refinement, or a
|
||||
`[api:DataObject]` instance.
|
||||
|
||||
In case you need multiple contexts, consider namespacing your request parameters by using `FieldList->namespace()` on
|
||||
@ -86,7 +86,7 @@ method, we're building our own `getCustomSearchContext()` variant.
|
||||
### Pagination
|
||||
|
||||
For pagination records on multiple pages, you need to wrap the results in a
|
||||
`PaginatedList` object. This object is also passed the generated `SQLQuery`
|
||||
`PaginatedList` object. This object is also passed the generated `SQLSelect`
|
||||
in order to read page limit information. It is also passed the current
|
||||
`SS_HTTPRequest` object so it can read the current page from a GET var.
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# SQL Query
|
||||
# SQL Select
|
||||
|
||||
## Introduction
|
||||
|
||||
An object representing a SQL query, which can be serialized into a SQL statement.
|
||||
An object representing a SQL select query, which can be serialized into a SQL statement.
|
||||
It is easier to deal with object-wrappers than string-parsing a raw SQL-query.
|
||||
This object is used by the SilverStripe ORM internally.
|
||||
|
||||
@ -18,8 +18,8 @@ the following three statements are functionally equivalent:
|
||||
:::php
|
||||
// Through raw SQL
|
||||
$count = DB::query('SELECT COUNT(*) FROM "Member"')->value();
|
||||
// Through SQLQuery abstraction layer
|
||||
$query = new SQLQuery();
|
||||
// Through SQLSelect abstraction layer
|
||||
$query = new SQLSelect();
|
||||
$count = $query->setFrom('Member')->setSelect('COUNT(*)')->value();
|
||||
// Through the ORM
|
||||
$count = Member::get()->count();
|
||||
@ -38,51 +38,179 @@ but still maintain a connection to the ORM where possible.
|
||||
|
||||
<div class="warning" markdown="1">
|
||||
Please read our ["security" topic](/topics/security) to find out
|
||||
how to sanitize user input before using it in SQL queries.
|
||||
how to properly prepare user input and variables for use in queries
|
||||
</div>
|
||||
|
||||
## Usage
|
||||
|
||||
### SELECT
|
||||
|
||||
Selection can be done by creating an instance of `SQLSelect`, which allows
|
||||
management of all elements of a SQL SELECT query, including columns, joined tables,
|
||||
conditional filters, grouping, limiting, and sorting.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->selectField('FieldName', 'Name');
|
||||
$sqlQuery->selectField('YEAR("Birthday")', 'Birthyear');
|
||||
$sqlQuery->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||
$sqlQuery->addWhere('YEAR("Birthday") = 1982');
|
||||
// $sqlQuery->setOrderBy(...);
|
||||
// $sqlQuery->setGroupBy(...);
|
||||
// $sqlQuery->setHaving(...);
|
||||
// $sqlQuery->setLimit(...);
|
||||
// $sqlQuery->setDistinct(true);
|
||||
<?php
|
||||
|
||||
$sqlSelect = new SQLSelect();
|
||||
$sqlSelect->setFrom('Player');
|
||||
$sqlSelect->selectField('FieldName', 'Name');
|
||||
$sqlSelect->selectField('YEAR("Birthday")', 'Birthyear');
|
||||
$sqlSelect->addLeftJoin('Team','"Player"."TeamID" = "Team"."ID"');
|
||||
$sqlSelect->addWhere(array('YEAR("Birthday") = ?' => 1982));
|
||||
// $sqlSelect->setOrderBy(...);
|
||||
// $sqlSelect->setGroupBy(...);
|
||||
// $sqlSelect->setHaving(...);
|
||||
// $sqlSelect->setLimit(...);
|
||||
// $sqlSelect->setDistinct(true);
|
||||
|
||||
// Get the raw SQL (optional)
|
||||
$rawSQL = $sqlQuery->sql();
|
||||
// Get the raw SQL (optional) and parameters
|
||||
$rawSQL = $sqlSelect->sql($parameters);
|
||||
|
||||
// Execute and return a Query object
|
||||
$result = $sqlQuery->execute();
|
||||
$result = $sqlSelect->execute();
|
||||
|
||||
// Iterate over results
|
||||
foreach($result as $row) {
|
||||
echo $row['BirthYear'];
|
||||
}
|
||||
|
||||
The result is an array lightly wrapped in a database-specific subclass of `[api:Query]`.
|
||||
The result of `SQLSelect::execute()` is an array lightly wrapped in a database-specific subclass of `[api:SS_Query]`.
|
||||
This class implements the *Iterator*-interface, and provides convenience-methods for accessing the data.
|
||||
|
||||
### DELETE
|
||||
|
||||
Deletion can be done either by calling `DB::query`/`DB::prepared_query` directly,
|
||||
by creating a `SQLDelete` object, or by transforming a `SQLSelect` into a `SQLDelete`
|
||||
object instead.
|
||||
|
||||
For example, creating a `SQLDelete` object
|
||||
|
||||
:::php
|
||||
$sqlQuery->setDelete(true);
|
||||
<?php
|
||||
|
||||
$query = SQLDelete::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0));
|
||||
$query->execute();
|
||||
|
||||
Alternatively, turning an existing `SQLSelect` into a delete
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"SiteTree"')
|
||||
->setWhere(array('"SiteTree"."ShowInMenus"' => 0))
|
||||
->toDelete();
|
||||
$query->execute();
|
||||
|
||||
Directly querying the database
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
DB::prepared_query('DELETE FROM "SiteTree" WHERE "SiteTree"."ShowInMenus" = ?', array(0));
|
||||
|
||||
### INSERT/UPDATE
|
||||
|
||||
Currently not supported through the `SQLQuery` class, please use raw `DB::query()` calls instead.
|
||||
INSERT and UPDATE can be performed using the `SQLInsert` and `SQLUpdate` classes.
|
||||
These both have similar aspects in that they can modify content in
|
||||
the database, but each are different in the way in which they behave.
|
||||
|
||||
Previously, similar operations could be performed by using the `DB::manipulate`
|
||||
function which would build the INSERT and UPDATE queries on the fly. This method
|
||||
still exists, but internally uses `SQLUpdate` / `SQLInsert`, although the actual
|
||||
query construction is now done by the `DBQueryBuilder` object.
|
||||
|
||||
Each of these classes implements the interface `SQLWriteExpression`, noting that each
|
||||
accepts write key/value pairs in a number of similar ways. These include the following
|
||||
api methods:
|
||||
|
||||
* `addAssignments` - Takes a list of assignments as an associative array of key -> value pairs,
|
||||
but also supports SQL expressions as values if necessary.
|
||||
* `setAssignments` - Replaces all existing assignments with the specified list
|
||||
* `getAssignments` - Returns all currently given assignments, as an associative array
|
||||
in the format `array('Column' => array('SQL' => array('parameters)))`
|
||||
* `assign` - Singular form of addAssignments, but only assigns a single column value.
|
||||
* `assignSQL` - Assigns a column the value of a specified SQL expression without parameters
|
||||
`assignSQL('Column', 'SQL)` is shorthand for `assign('Column', array('SQL' => array()))`
|
||||
|
||||
SQLUpdate also includes the following api methods:
|
||||
|
||||
* `clear` - Clears all assignments
|
||||
* `getTable` - Gets the table to update
|
||||
* `setTable` - Sets the table to update. This should be ANSI quoted.
|
||||
E.g. `$query->setTable('"SiteTree"');`
|
||||
|
||||
SQLInsert also includes the following api methods:
|
||||
* `clear` - Clears all rows
|
||||
* `clearRow` - Clears all assignments on the current row
|
||||
* `addRow` - Adds another row of assignments, and sets the current row to the new row
|
||||
* `addRows` - Adds a number of arrays, each representing a list of assignment rows,
|
||||
and sets the current row to the last one.
|
||||
* `getColumns` - Gets the names of all distinct columns assigned
|
||||
* `getInto` - Gets the table to insert into
|
||||
* `setInto` - Sets the table to insert into. This should be ANSI quoted.
|
||||
E.g. `$query->setInto('"SiteTree"');`
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
DB::query('UPDATE "Player" SET "Status"=\'Active\'');
|
||||
<?php
|
||||
$update = SQLUpdate::create('"SiteTree"')->where(array('ID' => 3));
|
||||
|
||||
// assigning a list of items
|
||||
$update->addAssignments(array(
|
||||
'"Title"' => 'Our Products',
|
||||
'"MenuTitle"' => 'Products'
|
||||
));
|
||||
|
||||
// Assigning a single value
|
||||
$update->assign('"MenuTitle"', 'Products');
|
||||
|
||||
// Assigning a value using parameterised expression
|
||||
$title = 'Products';
|
||||
$update->assign('"MenuTitle"', array(
|
||||
'CASE WHEN LENGTH("MenuTitle") > LENGTH(?) THEN "MenuTitle" ELSE ? END' =>
|
||||
array($title, $title)
|
||||
));
|
||||
|
||||
// Assigning a value using a pure SQL expression
|
||||
$update->assignSQL('"Date"', 'NOW()');
|
||||
|
||||
// Perform the update
|
||||
$update->execute();
|
||||
|
||||
In addition to assigning values, the SQLInsert object also supports multi-row
|
||||
inserts. For database connectors and API that don't have multi-row insert support
|
||||
these are translated internally as multiple single row inserts.
|
||||
|
||||
For example,
|
||||
|
||||
:::php
|
||||
<?php
|
||||
$insert = SQLInsert::create('"SiteTree"');
|
||||
|
||||
// Add multiple rows in a single call. Note that column names do not need
|
||||
// to be symmetric
|
||||
$insert->addRows(array(
|
||||
array('"Title"' => 'Home', '"Content"' => '<p>This is our home page</p>'),
|
||||
array('"Title"' => 'About Us', '"ClassName"' => 'AboutPage')
|
||||
));
|
||||
|
||||
// Adjust an assignment on the last row
|
||||
$insert->assign('"Content"', '<p>This is about us</p>');
|
||||
|
||||
// Add another row
|
||||
$insert->addRow(array('"Title"' => 'Contact Us'));
|
||||
|
||||
$columns = $insert->getColumns();
|
||||
// $columns will be array('"Title"', '"Content"', '"ClassName"');
|
||||
|
||||
$insert->execute();
|
||||
|
||||
### Value Checks
|
||||
|
||||
@ -92,12 +220,12 @@ e.g. when you want a single column rather than a full-blown object representatio
|
||||
Example: Get the count from a relationship.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->addSelect('COUNT("Player"."ID")');
|
||||
$sqlQuery->addWhere('"Team"."ID" = 99');
|
||||
$sqlQuery->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||
$count = $sqlQuery->execute()->value();
|
||||
$sqlSelect = new SQLSelect();
|
||||
$sqlSelect->setFrom('Player');
|
||||
$sqlSelect->addSelect('COUNT("Player"."ID")');
|
||||
$sqlSelect->addWhere(array('"Team"."ID"' => 99));
|
||||
$sqlSelect->addLeftJoin('Team', '"Team"."ID" = "Player"."TeamID"');
|
||||
$count = $sqlSelect->execute()->value();
|
||||
|
||||
Note that in the ORM, this call would be executed in an efficient manner as well:
|
||||
|
||||
@ -112,14 +240,14 @@ This can be useful for creating dropdowns.
|
||||
Example: Show player names with their birth year, but set their birth dates as values.
|
||||
|
||||
:::php
|
||||
$sqlQuery = new SQLQuery();
|
||||
$sqlQuery->setFrom('Player');
|
||||
$sqlQuery->setSelect('Birthdate');
|
||||
$sqlQuery->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlQuery->execute()->map();
|
||||
$sqlSelect = new SQLSelect();
|
||||
$sqlSelect->setFrom('Player');
|
||||
$sqlSelect->setSelect('Birthdate');
|
||||
$sqlSelect->selectField('CONCAT("Name", ' - ', YEAR("Birthdate")', 'NameWithBirthyear');
|
||||
$map = $sqlSelect->execute()->map();
|
||||
$field = new DropdownField('Birthdates', 'Birthdates', $map);
|
||||
|
||||
Note that going through SQLQuery is just necessary here
|
||||
Note that going through SQLSelect is just necessary here
|
||||
because of the custom SQL value transformation (`YEAR()`).
|
||||
An alternative approach would be a custom getter in the object definition.
|
||||
|
||||
|
@ -348,13 +348,134 @@ necessary. If the ORM doesn't do quite what you need it to, you may also
|
||||
consider extending the ORM with new data types or filter modifiers (that
|
||||
documentation still needs to be written)
|
||||
|
||||
#### Where clauses
|
||||
<div class="notice" markdown="1">
|
||||
See [the security topic](/topics/security#parameterised-queries) for details on safe database querying and why parameterised queries
|
||||
are so necessary here.
|
||||
</div>
|
||||
|
||||
You can specify a WHERE clause fragment (that will be combined with other
|
||||
filters using AND) with the `where()` method:
|
||||
#### SQL WHERE Predicates with Parameters
|
||||
|
||||
If using `DataObject::get()` (which returns a `DataList` instance) you can specify a WHERE clause fragment
|
||||
(that will be combined with other filters using AND) with the `where()` method, or `whereAny()` to add a list
|
||||
of clauses combined with OR.
|
||||
|
||||
Placeholders within a predicate are denoted by the question mark symbol, and should not be quoted.
|
||||
|
||||
For example:
|
||||
|
||||
:::php
|
||||
$members = Member::get()->where("\"FirstName\" = 'Sam'")
|
||||
$members = Member::get()->where(array('"FirstName" = ?' => 'Sam'));
|
||||
|
||||
If using `SQLSelect` you should use `addWhere`, `setWhere`, `addWhereAny`, or `setWhereAny` to modify the query.
|
||||
|
||||
Using the parameterised query syntax you can either provide a single variable as a parameter, an array of parameters
|
||||
if the SQL has multiple value placeholders, or simply pass an indexed array of strings for literal SQL.
|
||||
|
||||
Although parameters can be escaped and directly inserted into the SQL condition (See `Convert::raw2sql()'),
|
||||
the parameterised syntax is the preferred method of declaring conditions on a query.
|
||||
|
||||
Column names must still be double quoted, and for consistency and compatibility with other code, should also
|
||||
be prefixed with the table name.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
$query = Table::get();
|
||||
|
||||
// multiple predicates with parameters
|
||||
$query = $query->where(array(
|
||||
'"Table"."Column" = ?' => $column,
|
||||
'"Table"."Name" = ?' => $value
|
||||
));
|
||||
|
||||
// Shorthand for simple column comparison (as above), omitting the '?'
|
||||
// These will each be expanded internally to '"Table"."Column" = ?'
|
||||
$query = $query->where(array(
|
||||
'"Table"."Column"' => $column,
|
||||
'"Table"."Name"' => $value
|
||||
));
|
||||
|
||||
// Multiple predicates, some with multiple parameters.
|
||||
// The parameters should ideally not be an associative array.
|
||||
$query = $query->where(array(
|
||||
'"Table"."ColumnOne" = ? OR "Table"."ColumnTwo" != ?' => array(1, 4),
|
||||
'"Table"."ID" != ?' => $value
|
||||
));
|
||||
|
||||
// Multiple predicates, each with explicitly typed parameters.
|
||||
//
|
||||
// The purpose of this syntax is to provide not only parameter values, but
|
||||
// to also instruct the database connector on how to treat this value
|
||||
// internally (subject to the database API supporting this feature).
|
||||
//
|
||||
// SQLQuery distinguishes these from predicates with multiple parameters
|
||||
// by checking for the 'value' key in any array parameter given
|
||||
$query = $query->whereAny(array(
|
||||
'"Table"."Column"' => array(
|
||||
'value' => $value,
|
||||
'type' => 'string' // or any php type
|
||||
),
|
||||
'"Table"."HasValue"' => array(
|
||||
'value' => 0,
|
||||
'type' => 'boolean'
|
||||
)
|
||||
));
|
||||
|
||||
#### Run-Time Evaluated Conditions with SQLConditionGroup
|
||||
|
||||
Conditional expressions and groups may be encapsulated within a class (implementing
|
||||
the SQLConditionGroup interface) and evaluated at the time of execution.
|
||||
|
||||
This is useful for conditions which may be placed into a query before the details
|
||||
of that condition are fully specified.
|
||||
|
||||
E.g.
|
||||
|
||||
:::php
|
||||
<?php
|
||||
|
||||
class RandomGroup implements SQLConditionGroup {
|
||||
public $field = null;
|
||||
public function conditionSQL(&$parameters) {
|
||||
$parameters = array();
|
||||
return "{$this->field} < RAND()";
|
||||
}
|
||||
}
|
||||
|
||||
$query = SQLSelect::create()
|
||||
->setFrom('"MyObject"')
|
||||
->setWhere($condition = new RandomCondition());
|
||||
$condition->field = '"Score"';
|
||||
$items = $query->execute();
|
||||
|
||||
#### Direct SQL Predicate
|
||||
|
||||
Conditions can be a literal piece of SQL which doesn't involve any parameters or values
|
||||
at all, or can using safely SQL-encoded values, as it was originally.
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
In nearly every instance it's preferrable to use the parameterised syntax, especially dealing
|
||||
with variable parameters, even if those values were not submitted by the user.
|
||||
See [the security topic](/topics/security#parameterised-queries) for details.
|
||||
</div>
|
||||
|
||||
For instance, the following are all valid ways of adding SQL conditions directly to a query
|
||||
|
||||
:::php
|
||||
<?php
|
||||
// the entire predicate as a single string
|
||||
$query->addWhere("\"Column\" = 'Value'");
|
||||
|
||||
// multiple predicates as an array
|
||||
$query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
||||
|
||||
// Shorthand for the above using argument expansion
|
||||
$query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
|
||||
|
||||
// Literal SQL condition
|
||||
$query->addWhere('"Created" > NOW()"');
|
||||
|
||||
#### Joining
|
||||
|
||||
|
@ -8,14 +8,57 @@ See our "[Release Process](/misc/release-process#security-releases) on how to re
|
||||
## SQL Injection
|
||||
|
||||
The [coding-conventions](/misc/coding-conventions) help guard against SQL injection attacks but still require developer
|
||||
diligence: ensure that any variable you insert into a filter / sort / join clause has been escaped.
|
||||
diligence: ensure that any variable you insert into a filter / sort / join clause is either parameterised, or has been
|
||||
escaped.
|
||||
|
||||
See [http://shiflett.org/articles/sql-injection](http://shiflett.org/articles/sql-injection).
|
||||
|
||||
### Parameterised queries
|
||||
|
||||
Parameterised queries, or prepared statements, allow the logic around the query and its structure to be separated from
|
||||
the parameters passed in to be executed. Many DB adaptors support these as standard including [PDO](http://php.net/manual/en/pdo.prepare.php),
|
||||
[MySQL](http://php.net/manual/en/mysqli.prepare.php), [SQL Server](http://php.net/manual/en/function.sqlsrv-prepare.php),
|
||||
[SQLite](http://php.net/manual/en/sqlite3.prepare.php), and [PostgreSQL](http://php.net/manual/en/function.pg-prepare.php).
|
||||
|
||||
The use of parameterised queries whenever possible will safeguard your code in most cases, but care
|
||||
must still be taken when working with literal values or table/column identifiers that may
|
||||
come from user input.
|
||||
|
||||
Example:
|
||||
|
||||
:::php
|
||||
$records = DB::preparedQuery('SELECT * FROM "MyClass" WHERE "ID" = ?', array(3));
|
||||
$records = MyClass::get()->where(array('"ID" = ?' => 3));
|
||||
$records = MyClass::get()->where(array('"ID"' => 3));
|
||||
$records = DataObject::get_by_id('MyClass', 3);
|
||||
$records = DataObject::get_one('MyClass', array('"ID" = ?' => 3));
|
||||
$records = MyClass::get()->byID(3);
|
||||
$records = SQLSelect::create()->addWhere(array('"ID"' => 3))->execute();
|
||||
|
||||
Parameterised updates and inserts are also supported, but the syntax is a little different
|
||||
|
||||
:::php
|
||||
SQLInsert::create('"MyClass"')
|
||||
->assign('"Name"', 'Daniel')
|
||||
->addAssignments(array(
|
||||
'"Position"' => 'Accountant',
|
||||
'"Age"' => array(
|
||||
'GREATEST(0,?,?)' => array(24, 28)
|
||||
)
|
||||
))
|
||||
->assignSQL('"Created"', 'NOW()')
|
||||
->execute();
|
||||
DB::preparedQuery(
|
||||
'INSERT INTO "MyClass" ("Name", "Position", "Age", "Created") VALUES(?, ?, GREATEST(0,?,?), NOW())'
|
||||
array('Daniel', 'Accountant', 24, 28)
|
||||
);
|
||||
|
||||
|
||||
### Automatic escaping
|
||||
|
||||
SilverStripe automatically escapes data in SQL statements wherever possible,
|
||||
through database-specific methods (see `[api:Database->addslashes()]`).
|
||||
SilverStripe internally will use parameterised queries in SQL statements wherever possible.
|
||||
|
||||
If necessary Silverstripe performs any required escaping through database-specific methods (see `[api:Database->addslashes()]`).
|
||||
For `[api:MySQLDatabase]`, this will be `[mysql_real_escape_string()](http://de3.php.net/mysql_real_escape_string)`.
|
||||
|
||||
* Most `[api:DataList]` accessors (see escaping note in method documentation)
|
||||
@ -29,7 +72,8 @@ For `[api:MySQLDatabase]`, this will be `[mysql_real_escape_string()](http://de3
|
||||
* FormField->saveInto()
|
||||
* DBField->saveInto()
|
||||
|
||||
Data is escaped when saving back to the database, not when writing to object-properties.
|
||||
Data is not escaped when writing to object-properties, as inserts and updates are normally
|
||||
handled via prepared statements.
|
||||
|
||||
Example:
|
||||
|
||||
@ -38,23 +82,25 @@ Example:
|
||||
$members = Member::get()->filter('Name', $_GET['name']);
|
||||
// automatically escaped/quoted
|
||||
$members = Member::get()->filter(array('Name' => $_GET['name']));
|
||||
// needs to be escaped/quoted manually
|
||||
$members = Member::get()->where(sprintf('"Name" = \'%s\'', Convert::raw2sql($_GET['name'])));
|
||||
// parameterised condition
|
||||
$members = Member::get()->where(array('"Name" = ?' => $_GET['name']));
|
||||
// needs to be escaped and quoted manually (note raw2sql called with the $quote parameter set to true)
|
||||
$members = Member::get()->where(sprintf('"Name" = %s', Convert::raw2sql($_GET['name'], true)));
|
||||
|
||||
<div class="warning" markdown='1'>
|
||||
It is NOT good practice to "be sure" and convert the data passed to the functions below manually. This might
|
||||
It is NOT good practice to "be sure" and convert the data passed to the functions above manually. This might
|
||||
result in *double escaping* and alters the actually saved data (e.g. by adding slashes to your content).
|
||||
</div>
|
||||
|
||||
### Manual escaping
|
||||
|
||||
As a rule of thumb, whenever you're creating raw queries (or just chunks of SQL), you need to take care of escaping
|
||||
yourself. See [coding-conventions](/misc/coding-conventions) and [datamodel](/topics/datamodel) for ways to cast and convert
|
||||
your data.
|
||||
As a rule of thumb, whenever you're creating SQL queries (or just chunks of SQL) you should use parameterisation,
|
||||
but there may be cases where you need to take care of escaping yourself. See [coding-conventions](/misc/coding-conventions)
|
||||
and [datamodel](/topics/datamodel) for ways to parameterise, cast, and convert your data.
|
||||
|
||||
* `SQLQuery`
|
||||
* `DataObject::buildSQL()`
|
||||
* `SQLSelect`
|
||||
* `DB::query()`
|
||||
* `DB::preparedQuery()`
|
||||
* `Director::urlParams()`
|
||||
* `Controller->requestParams`, `Controller->urlParams`
|
||||
* `SS_HTTPRequest` data
|
||||
@ -64,11 +110,12 @@ Example:
|
||||
|
||||
:::php
|
||||
class MyForm extends Form {
|
||||
public function save($RAW_data, $form) {
|
||||
$SQL_data = Convert::raw2sql($RAW_data); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = '{$SQL_data[name]}'");
|
||||
// ...
|
||||
}
|
||||
public function save($RAW_data, $form) {
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_data = Convert::raw2sql($RAW_data, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['name']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -79,32 +126,34 @@ Example:
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
private static $allowed_actions = array('myurlaction');
|
||||
public function myurlaction($RAW_urlParams) {
|
||||
$SQL_urlParams = Convert::raw2sql($RAW_urlParams); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = '{$SQL_data[OtherID]}'");
|
||||
// ...
|
||||
}
|
||||
private static $allowed_actions = array('myurlaction');
|
||||
public function myurlaction($RAW_urlParams) {
|
||||
// Pass true as the second parameter of raw2sql to quote the value safely
|
||||
$SQL_urlParams = Convert::raw2sql($RAW_urlParams, true); // works recursively on an array
|
||||
$objs = Player::get()->where("Name = " . $SQL_data['OtherID']);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
As a rule of thumb, you should escape your data **as close to querying as possible**.
|
||||
This means if you've got a chain of functions passing data through, escaping should happen at the end of the chain.
|
||||
As a rule of thumb, you should escape your data **as close to querying as possible**
|
||||
(or preferably, use parameterised queries). This means if you've got a chain of functions
|
||||
passing data through, escaping should happen at the end of the chain.
|
||||
|
||||
:::php
|
||||
class MyController extends Controller {
|
||||
/**
|
||||
* @param array $RAW_data All names in an indexed array (not SQL-safe)
|
||||
*/
|
||||
public function saveAllNames($RAW_data) {
|
||||
// $SQL_data = Convert::raw2sql($RAW_data); // premature escaping
|
||||
foreach($RAW_data as $item) $this->saveName($item);
|
||||
}
|
||||
|
||||
public function saveName($RAW_name) {
|
||||
$SQL_name = Convert::raw2sql($RAW_name);
|
||||
DB::query("UPDATE Player SET Name = '{$SQL_name}'");
|
||||
}
|
||||
/**
|
||||
* @param array $RAW_data All names in an indexed array (not SQL-safe)
|
||||
*/
|
||||
public function saveAllNames($RAW_data) {
|
||||
// $SQL_data = Convert::raw2sql($RAW_data); // premature escaping
|
||||
foreach($RAW_data as $item) $this->saveName($item);
|
||||
}
|
||||
|
||||
public function saveName($RAW_name) {
|
||||
$SQL_name = Convert::raw2sql($RAW_name, true);
|
||||
DB::query("UPDATE Player SET Name = {$SQL_name}");
|
||||
}
|
||||
}
|
||||
|
||||
This might not be applicable in all cases - especially if you are building an API thats likely to be customized. If
|
||||
@ -171,10 +220,10 @@ PHP:
|
||||
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
private static $db = array(
|
||||
'MyEscapedValue' => 'Text', // Example value: <b>not bold</b>
|
||||
'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b>
|
||||
);
|
||||
private static $db = array(
|
||||
'MyEscapedValue' => 'Text', // Example value: <b>not bold</b>
|
||||
'MyUnescapedValue' => 'HTMLText' // Example value: <b>bold</b>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -182,8 +231,8 @@ Template:
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
</ul>
|
||||
|
||||
|
||||
@ -199,11 +248,11 @@ Template (see above):
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
// output: <a href="#" title="foo & &#quot;bar"">foo & "bar"</a>
|
||||
<li><a href="#" title="$Title.ATT">$Title</a></li>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
<li>$MyUnescapedValue.XML</li> // output: <b>bold<b>
|
||||
// output: <a href="#" title="foo & &#quot;bar"">foo & "bar"</a>
|
||||
<li><a href="#" title="$Title.ATT">$Title</a></li>
|
||||
<li>$MyEscapedValue</li> // output: <b>not bold<b>
|
||||
<li>$MyUnescapedValue</li> // output: <b>bold</b>
|
||||
<li>$MyUnescapedValue.XML</li> // output: <b>bold<b>
|
||||
</ul>
|
||||
|
||||
|
||||
@ -217,7 +266,7 @@ PHP:
|
||||
:::php
|
||||
class MyObject extends DataObject {
|
||||
public $Title = '<b>not bold</b>'; // will be escaped due to Text casting
|
||||
|
||||
|
||||
$casting = array(
|
||||
"Title" => "Text", // forcing a casting
|
||||
'TitleWithHTMLSuffix' => 'HTMLText' // optional, as HTMLText is the default casting
|
||||
@ -234,9 +283,9 @@ Template:
|
||||
|
||||
:::php
|
||||
<ul>
|
||||
<li>$Title</li> // output: <b>not bold<b>
|
||||
<li>$Title.RAW</li> // output: <b>not bold</b>
|
||||
<li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
|
||||
<li>$Title</li> // output: <b>not bold<b>
|
||||
<li>$Title.RAW</li> // output: <b>not bold</b>
|
||||
<li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
|
||||
</ul>
|
||||
|
||||
|
||||
@ -349,17 +398,17 @@ Below is an example with different ways you would use this casting technique:
|
||||
:::php
|
||||
public function CaseStudies() {
|
||||
|
||||
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
|
||||
$anotherID = (int)Director::urlParam['ID'];
|
||||
|
||||
// perform a calculation, the prerequisite being $anotherID must be an integer
|
||||
$calc = $anotherID + (5 - 2) / 2;
|
||||
|
||||
// cast the 'category' GET variable as an integer
|
||||
$categoryID = (int)$_GET['category'];
|
||||
|
||||
// perform a byID(), which ensures the ID is an integer before querying
|
||||
return CaseStudy::get()->byID($categoryID);
|
||||
// cast an ID from URL parameters e.g. (mysite.com/home/action/ID)
|
||||
$anotherID = (int)Director::urlParam['ID'];
|
||||
|
||||
// perform a calculation, the prerequisite being $anotherID must be an integer
|
||||
$calc = $anotherID + (5 - 2) / 2;
|
||||
|
||||
// cast the 'category' GET variable as an integer
|
||||
$categoryID = (int)$_GET['category'];
|
||||
|
||||
// perform a byID(), which ensures the ID is an integer before querying
|
||||
return CaseStudy::get()->byID($categoryID);
|
||||
}
|
||||
|
||||
|
||||
@ -390,11 +439,10 @@ disallow certain filetypes.
|
||||
Example configuration for Apache2:
|
||||
|
||||
<VirtualHost *:80>
|
||||
...
|
||||
<LocationMatch assets/>
|
||||
php_flag engine off
|
||||
Options -ExecCGI -Includes -Indexes
|
||||
</LocationMatch>
|
||||
<LocationMatch assets/>
|
||||
php_flag engine off
|
||||
Options -ExecCGI -Includes -Indexes
|
||||
</LocationMatch>
|
||||
</VirtualHost>
|
||||
|
||||
|
||||
@ -490,7 +538,6 @@ controller's `init()` method:
|
||||
class MyController extends Controller {
|
||||
public function init() {
|
||||
parent::init();
|
||||
|
||||
$this->response->addHeader('X-Frame-Options', 'SAMEORIGIN');
|
||||
}
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ class File extends DataObject {
|
||||
|
||||
if (!$record) {
|
||||
if(class_exists('ErrorPage')) {
|
||||
$record = DataObject::get_one('ErrorPage', '"ErrorCode" = \'404\'');
|
||||
$record = ErrorPage::get()->filter("ErrorCode", 404)->first();
|
||||
}
|
||||
|
||||
if (!$record) return; // There were no suitable matches at all.
|
||||
@ -237,8 +237,10 @@ class File extends DataObject {
|
||||
$item = null;
|
||||
foreach($parts as $part) {
|
||||
if($part == ASSETS_DIR && !$parentID) continue;
|
||||
$SQL_part = Convert::raw2sql($part);
|
||||
$item = DataObject::get_one("File", "\"Name\" = '$SQL_part' AND \"ParentID\" = $parentID");
|
||||
$item = File::get()->filter(array(
|
||||
'Name' => $part,
|
||||
'ParentID' => $parentID
|
||||
))->first();
|
||||
if(!$item) break;
|
||||
$parentID = $item->ID;
|
||||
}
|
||||
@ -472,7 +474,9 @@ class File extends DataObject {
|
||||
* Delete the database record (recursively for folders) without touching the filesystem
|
||||
*/
|
||||
public function deleteDatabaseOnly() {
|
||||
if(is_numeric($this->ID)) DB::query("DELETE FROM \"File\" WHERE \"ID\" = $this->ID");
|
||||
if(is_numeric($this->ID)) {
|
||||
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($this->ID));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,33 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* A collection of static methods for manipulating the filesystem.
|
||||
*
|
||||
* A collection of static methods for manipulating the filesystem.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
class Filesystem extends Object {
|
||||
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var integer Integer
|
||||
*/
|
||||
private static $file_create_mask = 02775;
|
||||
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var integer Integer
|
||||
*/
|
||||
private static $folder_create_mask = 02775;
|
||||
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected static $cache_folderModTime;
|
||||
|
||||
|
||||
/**
|
||||
* @config
|
||||
*
|
||||
* Array of file / folder regex expressions to exclude from the
|
||||
* Array of file / folder regex expressions to exclude from the
|
||||
* {@link Filesystem::sync()}
|
||||
*
|
||||
* @var array
|
||||
@ -45,17 +45,17 @@ class Filesystem extends Object {
|
||||
* Uses {@link Filesystem::$folder_create_mask} to set filesystem permissions.
|
||||
* Use {@link Folder::findOrMake()} to create a {@link Folder} database
|
||||
* record automatically.
|
||||
*
|
||||
*
|
||||
* @param String $folder Absolute folder path
|
||||
*/
|
||||
public static function makeFolder($folder) {
|
||||
if(!file_exists($base = dirname($folder))) self::makeFolder($base);
|
||||
if(!file_exists($folder)) mkdir($folder, Config::inst()->get('Filesystem', 'folder_create_mask'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove a directory and all subdirectories and files.
|
||||
*
|
||||
*
|
||||
* @param String $folder Absolute folder path
|
||||
* @param Boolean $contentsOnly If this is true then the contents of the folder will be removed but not the
|
||||
* folder itself
|
||||
@ -77,13 +77,13 @@ class Filesystem extends Object {
|
||||
if(!$contentsOnly) rmdir($folder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cleanup function to reset all the Filename fields. Visit File/fixfiles to call.
|
||||
*/
|
||||
public function fixfiles() {
|
||||
if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
|
||||
|
||||
|
||||
$files = DataObject::get("File");
|
||||
foreach($files as $file) {
|
||||
$file->updateFilesystem();
|
||||
@ -92,10 +92,10 @@ class Filesystem extends Object {
|
||||
}
|
||||
echo "<p>Done!";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the most recent modification time of anything in the folder.
|
||||
*
|
||||
*
|
||||
* @param $folder The folder, relative to the site root
|
||||
* @param $extensionList An option array of file extensions to limit the search to
|
||||
* @return String Same as filemtime() format.
|
||||
@ -103,17 +103,17 @@ class Filesystem extends Object {
|
||||
public static function folderModTime($folder, $extensionList = null, $recursiveCall = false) {
|
||||
//$cacheID = $folder . ',' . implode(',', $extensionList);
|
||||
//if(!$recursiveCall && self::$cache_folderModTime[$cacheID]) return self::$cache_folderModTime[$cacheID];
|
||||
|
||||
|
||||
$modTime = 0;
|
||||
if(!Filesystem::isAbsolute($folder)) $folder = Director::baseFolder() . '/' . $folder;
|
||||
|
||||
|
||||
$items = scandir($folder);
|
||||
foreach($items as $item) {
|
||||
if($item[0] != '.') {
|
||||
// Recurse into folders
|
||||
if(is_dir("$folder/$item")) {
|
||||
$modTime = max($modTime, self::folderModTime("$folder/$item", $extensionList, true));
|
||||
|
||||
|
||||
// Check files
|
||||
} else {
|
||||
if($extensionList) $extension = strtolower(substr($item,strrpos($item,'.')+1));
|
||||
@ -127,11 +127,11 @@ class Filesystem extends Object {
|
||||
//if(!$recursiveCall) self::$cache_folderModTime[$cacheID] = $modTime;
|
||||
return $modTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given filename is an absolute file reference.
|
||||
* Works on Linux and Windows.
|
||||
*
|
||||
*
|
||||
* @param String $filename Absolute or relative filename, with or without path.
|
||||
* @return Boolean
|
||||
*/
|
||||
@ -142,16 +142,16 @@ class Filesystem extends Object {
|
||||
|
||||
/**
|
||||
* This function ensures the file table is correct with the files in the assets folder.
|
||||
*
|
||||
*
|
||||
* If a Folder record ID is given, all of that folder's children will be synchronised.
|
||||
* If the given Folder ID isn't found, or not specified at all, then everything will
|
||||
* be synchronised from the root folder (singleton Folder).
|
||||
*
|
||||
* See {@link File->updateFilesystem()} to sync properties of a single database record
|
||||
*
|
||||
* See {@link File->updateFilesystem()} to sync properties of a single database record
|
||||
* back to the equivalent filesystem record.
|
||||
*
|
||||
*
|
||||
* @param int $folderID Folder ID to sync along with all it's children
|
||||
* @param Boolean $syncLinkTracking Determines if the link tracking data should also
|
||||
* @param Boolean $syncLinkTracking Determines if the link tracking data should also
|
||||
* be updated via {@link SiteTree->syncLinkTracking()}. Setting this to FALSE
|
||||
* means that broken links inside page content are not noticed, at least until the next
|
||||
* call to {@link SiteTree->write()} on this page.
|
||||
@ -160,13 +160,13 @@ class Filesystem extends Object {
|
||||
public static function sync($folderID = null, $syncLinkTracking = true) {
|
||||
$folder = DataObject::get_by_id('Folder', (int) $folderID);
|
||||
if(!($folder && $folder->exists())) $folder = singleton('Folder');
|
||||
|
||||
|
||||
$results = $folder->syncChildren();
|
||||
$finished = false;
|
||||
while(!$finished) {
|
||||
$orphans = DB::query("SELECT \"C\".\"ID\" FROM \"File\" AS \"C\"
|
||||
LEFT JOIN \"File\" AS \"P\" ON \"C\".\"ParentID\" = \"P\".\"ID\"
|
||||
WHERE \"P\".\"ID\" IS NULL AND \"C\".\"ParentID\" > 0");
|
||||
$orphans = DB::query('SELECT "ChildFile"."ID" FROM "File" AS "ChildFile"
|
||||
LEFT JOIN "File" AS "ParentFile" ON "ChildFile"."ParentID" = "ParentFile"."ID"
|
||||
WHERE "ParentFile"."ID" IS NULL AND "ChildFile"."ParentID" > 0');
|
||||
$finished = true;
|
||||
if($orphans) foreach($orphans as $orphan) {
|
||||
$finished = false;
|
||||
@ -208,13 +208,13 @@ class Filesystem extends Object {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return _t(
|
||||
'Filesystem.SYNCRESULTS',
|
||||
'Sync complete: {createdcount} items created, {deletedcount} items deleted',
|
||||
'Sync complete: {createdcount} items created, {deletedcount} items deleted',
|
||||
array('createdcount' => (int)$results['added'], 'deletedcount' => (int)$results['deleted'])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -2,18 +2,18 @@
|
||||
/**
|
||||
* Represents a folder in the assets/ directory.
|
||||
* The folder path is stored in the "Filename" property.
|
||||
*
|
||||
*
|
||||
* Updating the "Name" or "Filename" properties on
|
||||
* a folder object also updates all associated children
|
||||
* (both {@link File} and {@link Folder} records).
|
||||
*
|
||||
*
|
||||
* Deleting a folder will also remove the folder from the filesystem,
|
||||
* including any subfolders and contained files. Use {@link deleteDatabaseOnly()}
|
||||
* to avoid touching the filesystem.
|
||||
*
|
||||
*
|
||||
* See {@link File} documentation for more details about the
|
||||
* relationship between the database and filesystem in the SilverStripe file APIs.
|
||||
*
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage filesystem
|
||||
*/
|
||||
@ -24,13 +24,13 @@ class Folder extends File {
|
||||
private static $plural_name = "Folders";
|
||||
|
||||
private static $default_sort = "\"Name\"";
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
parent::populateDefaults();
|
||||
|
||||
|
||||
if(!$this->Name) $this->Name = _t('AssetAdmin.NEWFOLDER',"NewFolder");
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ class Folder extends File {
|
||||
* and on the filesystem. If necessary, creates parent folders as well. If it's
|
||||
* unable to find or make the folder, it will return null (as /assets is unable
|
||||
* to be represented by a Folder DataObject)
|
||||
*
|
||||
*
|
||||
* @param $folderPath string Absolute or relative path to the file.
|
||||
* If path is relative, its interpreted relative to the "assets/" directory.
|
||||
* @return Folder|null
|
||||
@ -58,7 +58,7 @@ class Folder extends File {
|
||||
$filter = FileNameFilter::create();
|
||||
foreach($parts as $part) {
|
||||
if(!$part) continue; // happens for paths with a trailing slash
|
||||
|
||||
|
||||
// Ensure search includes folders with illegal characters removed, but
|
||||
// err in favour of matching existing folders if $folderPath
|
||||
// includes illegal characters itself.
|
||||
@ -67,7 +67,7 @@ class Folder extends File {
|
||||
'ParentID' => $parentID,
|
||||
'Name' => array($partSafe, $part)
|
||||
))->first();
|
||||
|
||||
|
||||
if(!$item) {
|
||||
$item = new Folder();
|
||||
$item->ParentID = $parentID;
|
||||
@ -83,9 +83,9 @@ class Folder extends File {
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Synchronize the file database with the actual content of the assets
|
||||
* Synchronize the file database with the actual content of the assets
|
||||
* folder.
|
||||
*/
|
||||
public function syncChildren() {
|
||||
@ -95,19 +95,23 @@ class Folder extends File {
|
||||
$skipped = 0;
|
||||
|
||||
// First, merge any children that are duplicates
|
||||
$duplicateChildrenNames = DB::query("SELECT \"Name\" FROM \"File\""
|
||||
. " WHERE \"ParentID\" = $parentID GROUP BY \"Name\" HAVING count(*) > 1")->column();
|
||||
$duplicateChildrenNames = DB::prepared_query(
|
||||
'SELECT "Name" FROM "File" WHERE "ParentID" = ? GROUP BY "Name" HAVING count(*) > 1',
|
||||
array($parentID)
|
||||
)->column();
|
||||
if($duplicateChildrenNames) foreach($duplicateChildrenNames as $childName) {
|
||||
$childName = Convert::raw2sql($childName);
|
||||
// Note, we do this in the database rather than object-model; otherwise we get all sorts of problems
|
||||
// about deleting files
|
||||
$children = DB::query("SELECT \"ID\" FROM \"File\""
|
||||
. " WHERE \"Name\" = '$childName' AND \"ParentID\" = $parentID")->column();
|
||||
$children = DB::prepared_query(
|
||||
'SELECT "ID" FROM "File" WHERE "Name" = ? AND "ParentID" = ?',
|
||||
array($childName, $parentID)
|
||||
)->column();
|
||||
if($children) {
|
||||
$keptChild = array_shift($children);
|
||||
foreach($children as $removedChild) {
|
||||
DB::query("UPDATE \"File\" SET \"ParentID\" = $keptChild WHERE \"ParentID\" = $removedChild");
|
||||
DB::query("DELETE FROM \"File\" WHERE \"ID\" = $removedChild");
|
||||
DB::prepared_query('UPDATE "File" SET "ParentID" = ? WHERE "ParentID" = ?',
|
||||
array($keptChild, $removedChild));
|
||||
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($removedChild));
|
||||
}
|
||||
} else {
|
||||
user_error("Inconsistent database issue: SELECT ID FROM \"File\" WHERE Name = '$childName'"
|
||||
@ -115,10 +119,10 @@ class Folder extends File {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get index of database content
|
||||
// We don't use DataObject so that things like subsites doesn't muck with this.
|
||||
$dbChildren = DB::query("SELECT * FROM \"File\" WHERE \"ParentID\" = $parentID");
|
||||
$dbChildren = DB::prepared_query('SELECT * FROM "File" WHERE "ParentID" = ?', array($parentID));
|
||||
$hasDbChild = array();
|
||||
|
||||
if($dbChildren) {
|
||||
@ -150,7 +154,7 @@ class Folder extends File {
|
||||
|
||||
foreach($actualChildren as $actualChild) {
|
||||
$skip = false;
|
||||
|
||||
|
||||
// Check ignore patterns
|
||||
if($ignoreRules) foreach($ignoreRules as $rule) {
|
||||
if(preg_match($rule, $actualChild)) {
|
||||
@ -158,7 +162,7 @@ class Folder extends File {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check allowed extensions, unless admin users are allowed to bypass these exclusions
|
||||
if($checkExtensions
|
||||
&& ($extension = self::get_file_extension($actualChild))
|
||||
@ -175,13 +179,13 @@ class Folder extends File {
|
||||
// A record with a bad class type doesn't deserve to exist. It must be purged!
|
||||
if(isset($hasDbChild[$actualChild])) {
|
||||
$child = $hasDbChild[$actualChild];
|
||||
if(( !( $child instanceof Folder ) && is_dir($baseDir . $actualChild) )
|
||||
if(( !( $child instanceof Folder ) && is_dir($baseDir . $actualChild) )
|
||||
|| (( $child instanceof Folder ) && !is_dir($baseDir . $actualChild)) ) {
|
||||
DB::query("DELETE FROM \"File\" WHERE \"ID\" = $child->ID");
|
||||
unset($hasDbChild[$actualChild]);
|
||||
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($child->ID));
|
||||
unset($hasDbChild[$actualChild]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(isset($hasDbChild[$actualChild])) {
|
||||
$child = $hasDbChild[$actualChild];
|
||||
unset($unwantedDbChildren[$actualChild]);
|
||||
@ -190,30 +194,30 @@ class Folder extends File {
|
||||
$childID = $this->constructChild($actualChild);
|
||||
$child = DataObject::get_by_id("File", $childID);
|
||||
}
|
||||
|
||||
|
||||
if( $child && is_dir($baseDir . $actualChild)) {
|
||||
$childResult = $child->syncChildren();
|
||||
$added += $childResult['added'];
|
||||
$deleted += $childResult['deleted'];
|
||||
$skipped += $childResult['skipped'];
|
||||
}
|
||||
|
||||
|
||||
// Clean up the child record from memory after use. Important!
|
||||
$child->destroy();
|
||||
$child = null;
|
||||
}
|
||||
|
||||
|
||||
// Iterate through the unwanted children, removing them all
|
||||
if(isset($unwantedDbChildren)) foreach($unwantedDbChildren as $unwantedDbChild) {
|
||||
DB::query("DELETE FROM \"File\" WHERE \"ID\" = $unwantedDbChild->ID");
|
||||
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($unwantedDbChild->ID));
|
||||
$deleted++;
|
||||
}
|
||||
} else {
|
||||
DB::query("DELETE FROM \"File\" WHERE \"ID\" = $this->ID");
|
||||
DB::prepared_query('DELETE FROM "File" WHERE "ID" = ?', array($this->ID));
|
||||
}
|
||||
|
||||
|
||||
return array(
|
||||
'added' => $added,
|
||||
'added' => $added,
|
||||
'deleted' => $deleted,
|
||||
'skipped' => $skipped
|
||||
);
|
||||
@ -223,6 +227,9 @@ class Folder extends File {
|
||||
* Construct a child of this Folder with the given name.
|
||||
* It does this without actually using the object model, as this starts messing
|
||||
* with all the data. Rather, it does a direct database insert.
|
||||
*
|
||||
* @param string $name Name of the file or folder
|
||||
* @return integer the ID of the newly saved File record
|
||||
*/
|
||||
public function constructChild($name) {
|
||||
// Determine the class name - File, Folder or Image
|
||||
@ -233,20 +240,19 @@ class Folder extends File {
|
||||
$className = File::get_class_for_file_extension(pathinfo($name, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
if(Member::currentUser()) $ownerID = Member::currentUser()->ID;
|
||||
else $ownerID = 0;
|
||||
|
||||
$filename = Convert::raw2sql($this->Filename . $name);
|
||||
$ownerID = Member::currentUserID();
|
||||
|
||||
$filename = $this->Filename . $name;
|
||||
if($className == 'Folder' ) $filename .= '/';
|
||||
|
||||
$name = Convert::raw2sql($name);
|
||||
|
||||
DB::query("INSERT INTO \"File\"
|
||||
$nowExpression = DB::get_conn()->now();
|
||||
DB::prepared_query("INSERT INTO \"File\"
|
||||
(\"ClassName\", \"ParentID\", \"OwnerID\", \"Name\", \"Filename\", \"Created\", \"LastEdited\", \"Title\")
|
||||
VALUES ('$className', $this->ID, $ownerID, '$name', '$filename', "
|
||||
. DB::getConn()->now() . ',' . DB::getConn()->now() . ", '$name')");
|
||||
|
||||
return DB::getGeneratedID("File");
|
||||
VALUES (?, ?, ?, ?, ?, $nowExpression, $nowExpression, ?)",
|
||||
array($className, $this->ID, $ownerID, $name, $filename, $name)
|
||||
);
|
||||
|
||||
return DB::get_generated_id("File");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,7 +268,7 @@ class Folder extends File {
|
||||
if(!isset($tmpFile['size'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$base = BASE_PATH;
|
||||
// $parentFolder = Folder::findOrMake("Uploads");
|
||||
|
||||
@ -275,14 +281,14 @@ class Folder extends File {
|
||||
|
||||
$file = $this->RelativePath . $file;
|
||||
Filesystem::makeFolder(dirname("$base/$file"));
|
||||
|
||||
|
||||
$doubleBarrelledExts = array('.gz', '.bz', '.bz2');
|
||||
|
||||
|
||||
$ext = "";
|
||||
if(preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) {
|
||||
$file = $matches[1];
|
||||
$ext = $matches[2];
|
||||
// Special case for double-barrelled
|
||||
// Special case for double-barrelled
|
||||
if(in_array($ext, $doubleBarrelledExts) && preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) {
|
||||
$file = $matches[1];
|
||||
$ext = $matches[2] . $ext;
|
||||
@ -294,7 +300,7 @@ class Folder extends File {
|
||||
while(file_exists("$base/$file$ext")) {
|
||||
$i++;
|
||||
$oldFile = $file;
|
||||
|
||||
|
||||
if(strpos($file, '.') !== false) {
|
||||
$file = preg_replace('/[0-9]*(\.[^.]+$)/', $i . '\\1', $file);
|
||||
} elseif(strpos($file, '_') !== false) {
|
||||
@ -305,7 +311,7 @@ class Folder extends File {
|
||||
|
||||
if($oldFile == $file && $i > 2) user_error("Couldn't fix $file$ext with $i", E_USER_ERROR);
|
||||
}
|
||||
|
||||
|
||||
if (move_uploaded_file($tmpFile['tmp_name'], "$base/$file$ext")) {
|
||||
// Update with the new image
|
||||
return $this->constructChild(basename($file . $ext));
|
||||
@ -319,18 +325,18 @@ class Folder extends File {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function validate() {
|
||||
return new ValidationResult(true);
|
||||
}
|
||||
|
||||
|
||||
//-------------------------------------------------------------------------------------------------
|
||||
// Data Model Definition
|
||||
|
||||
public function getRelativePath() {
|
||||
return parent::getRelativePath() . "/";
|
||||
}
|
||||
|
||||
|
||||
public function onBeforeDelete() {
|
||||
if($this->ID && ($children = $this->AllChildren())) {
|
||||
foreach($children as $child) {
|
||||
@ -345,11 +351,11 @@ class Folder extends File {
|
||||
// Do this after so a folder's contents are removed before we delete the folder.
|
||||
if($this->Filename && $this->Name && file_exists($this->getFullPath())) {
|
||||
$files = glob( $this->getFullPath() . '/*' );
|
||||
|
||||
|
||||
if( !$files || ( count( $files ) == 1 && preg_match( '/\/_resampled$/', $files[0] ) ) )
|
||||
Filesystem::removeFolder( $this->getFullPath() );
|
||||
}
|
||||
|
||||
|
||||
parent::onBeforeDelete();
|
||||
}
|
||||
|
||||
@ -376,13 +382,13 @@ class Folder extends File {
|
||||
|
||||
/**
|
||||
* A folder doesn't have a (meaningful) file size.
|
||||
*
|
||||
*
|
||||
* @return Null
|
||||
*/
|
||||
public function getSize() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete the database record (recursively for folders) without touching the filesystem
|
||||
*/
|
||||
@ -393,40 +399,34 @@ class Folder extends File {
|
||||
|
||||
parent::deleteDatabaseOnly();
|
||||
}
|
||||
|
||||
public function myChildren() {
|
||||
// Ugly, but functional.
|
||||
$ancestors = ClassInfo::ancestry($this->class);
|
||||
foreach($ancestors as $i => $a) {
|
||||
if(isset($baseClass) && $baseClass === -1) {
|
||||
$baseClass = $a;
|
||||
break;
|
||||
}
|
||||
if($a == "DataObject") $baseClass = -1;
|
||||
}
|
||||
|
||||
$g = DataObject::get($baseClass, "\"ParentID\" = " . $this->ID);
|
||||
return $g;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if this folder has children
|
||||
* Returns all children of this folder
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function hasChildren() {
|
||||
return (bool)DB::query("SELECT COUNT(*) FROM \"File\" WHERE ParentID = "
|
||||
. (int)$this->ID)->value();
|
||||
public function myChildren() {
|
||||
return File::get()->filter("ParentID", $this->ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this folder has children
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren() {
|
||||
return $this->myChildren()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this folder has children
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildFolders() {
|
||||
$SQL_folderClasses = Convert::raw2sql(ClassInfo::subclassesFor('Folder'));
|
||||
|
||||
return (bool)DB::query("SELECT COUNT(*) FROM \"File\" WHERE \"ParentID\" = " . (int)$this->ID
|
||||
. " AND \"ClassName\" IN ('" . implode("','", $SQL_folderClasses) . "')")->value();
|
||||
return $this->ChildFolders()->exists();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overloaded to call recursively on all contained {@link File} records.
|
||||
*/
|
||||
@ -442,7 +442,7 @@ class Folder extends File {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the FieldList used to edit this folder in the CMS.
|
||||
* You can modify this FieldList by subclassing folder, or by creating a {@link DataExtension}
|
||||
@ -451,27 +451,29 @@ class Folder extends File {
|
||||
public function getCMSFields() {
|
||||
// Hide field on root level, which can't be renamed
|
||||
if(!$this->ID || $this->ID === "root") {
|
||||
$titleField = new HiddenField("Name");
|
||||
$titleField = new HiddenField("Name");
|
||||
} else {
|
||||
$titleField = new TextField("Name", $this->fieldLabel('Name'));
|
||||
}
|
||||
|
||||
|
||||
$fields = new FieldList(
|
||||
$titleField,
|
||||
new HiddenField('ParentID')
|
||||
);
|
||||
$this->extend('updateCMSFields', $fields);
|
||||
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the children of this folder that are also folders.
|
||||
*
|
||||
* @return DataList
|
||||
*/
|
||||
public function ChildFolders() {
|
||||
return Folder::get()->filter('ParentID', $this->ID);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return String
|
||||
*/
|
||||
@ -481,14 +483,14 @@ class Folder extends File {
|
||||
if(!$this->canDelete())
|
||||
$classes .= " nodelete";
|
||||
|
||||
if(!$this->canEdit())
|
||||
if(!$this->canEdit())
|
||||
$classes .= " disabled";
|
||||
|
||||
|
||||
$classes .= $this->markingClasses();
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
@ -1110,8 +1110,8 @@ class Form extends RequestHandler {
|
||||
/**
|
||||
* Set a status message for the form.
|
||||
*
|
||||
* @param message the text of the message
|
||||
* @param type Should be set to good, bad, or warning.
|
||||
* @param string $message the text of the message
|
||||
* @param string $type Should be set to good, bad, or warning.
|
||||
*/
|
||||
public function setMessage($message, $type) {
|
||||
$this->message = $message;
|
||||
@ -1122,15 +1122,15 @@ class Form extends RequestHandler {
|
||||
/**
|
||||
* Set a message to the session, for display next time this form is shown.
|
||||
*
|
||||
* @param message the text of the message
|
||||
* @param type Should be set to good, bad, or warning.
|
||||
* @param string $message the text of the message
|
||||
* @param string $type Should be set to good, bad, or warning.
|
||||
*/
|
||||
public function sessionMessage($message, $type) {
|
||||
Session::set("FormInfo.{$this->FormName()}.formError.message", $message);
|
||||
Session::set("FormInfo.{$this->FormName()}.formError.type", $type);
|
||||
}
|
||||
|
||||
public static function messageForForm( $formName, $message, $type ) {
|
||||
public static function messageForForm($formName, $message, $type ) {
|
||||
Session::set("FormInfo.{$formName}.formError.message", $message);
|
||||
Session::set("FormInfo.{$formName}.formError.type", $type);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class HtmlEditorField extends TextareaField {
|
||||
private static $sanitise_server_side = false;
|
||||
|
||||
protected $rows = 30;
|
||||
|
||||
|
||||
/**
|
||||
* Includes the JavaScript neccesary for this field to work using the {@link Requirements} system.
|
||||
*/
|
||||
@ -50,20 +50,20 @@ class HtmlEditorField extends TextareaField {
|
||||
|
||||
} else {
|
||||
Requirements::javascript(MCE_ROOT . 'tiny_mce_src.js');
|
||||
}
|
||||
}
|
||||
|
||||
Requirements::customScript($configObj->generateJS(), 'htmlEditorConfig');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see TextareaField::__construct()
|
||||
*/
|
||||
public function __construct($name, $title = null, $value = '') {
|
||||
parent::__construct($name, $title, $value);
|
||||
|
||||
|
||||
self::include_js();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@ -73,7 +73,7 @@ class HtmlEditorField extends TextareaField {
|
||||
|
||||
if($links = $value->getElementsByTagName('a')) foreach($links as $link) {
|
||||
$matches = array();
|
||||
|
||||
|
||||
if(preg_match('/\[sitetree_link(?:\s*|%20|,)?id=([0-9]+)\]/i', $link->getAttribute('href'), $matches)) {
|
||||
if(!DataObject::get_by_id('SiteTree', $matches[1])) {
|
||||
$class = $link->getAttribute('class');
|
||||
@ -105,14 +105,14 @@ class HtmlEditorField extends TextareaField {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function saveInto(DataObjectInterface $record) {
|
||||
if($record->hasField($this->name) && $record->escapeTypeForField($this->name) != 'xml') {
|
||||
throw new Exception (
|
||||
'HtmlEditorField->saveInto(): This field should save into a HTMLText or HTMLVarchar field.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$htmlValue = Injector::inst()->create('HTMLValue', $this->value);
|
||||
|
||||
// Sanitise if requested
|
||||
@ -153,10 +153,10 @@ class HtmlEditorField extends TextareaField {
|
||||
public function performReadonlyTransformation() {
|
||||
$field = $this->castedCopy('HtmlEditorField_Readonly');
|
||||
$field->dontEscape = true;
|
||||
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
||||
public function performDisabledTransformation() {
|
||||
return $this->performReadonlyTransformation();
|
||||
}
|
||||
@ -182,7 +182,7 @@ class HtmlEditorField_Readonly extends ReadonlyField {
|
||||
/**
|
||||
* Toolbar shared by all instances of {@link HTMLEditorField}, to avoid too much markup duplication.
|
||||
* Needs to be inserted manually into the template in order to function - see {@link LeftAndMain->EditorToolbar()}.
|
||||
*
|
||||
*
|
||||
* @package forms
|
||||
* @subpackage fields-formattedinput
|
||||
*/
|
||||
@ -200,7 +200,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
protected $templateViewFile = 'HtmlEditorField_viewfile';
|
||||
|
||||
protected $controller, $name;
|
||||
|
||||
|
||||
public function __construct($controller, $name) {
|
||||
parent::__construct();
|
||||
|
||||
@ -211,7 +211,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
Requirements::javascript(FRAMEWORK_DIR ."/javascript/HtmlEditorField.js");
|
||||
|
||||
Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
|
||||
|
||||
|
||||
$this->controller = $controller;
|
||||
$this->name = $name;
|
||||
}
|
||||
@ -226,17 +226,20 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
|
||||
/**
|
||||
* Searches the SiteTree for display in the dropdown
|
||||
*
|
||||
*
|
||||
* @return callback
|
||||
*/
|
||||
*/
|
||||
public function siteTreeSearchCallback($sourceObject, $labelField, $search) {
|
||||
return DataObject::get($sourceObject, "\"MenuTitle\" LIKE '%$search%' OR \"Title\" LIKE '%$search%'");
|
||||
return DataObject::get($sourceObject)->filterAny(array(
|
||||
'MenuTitle:PartialMatch' => $search,
|
||||
'Title:PartialMatch' => $search
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@link Form} instance allowing a user to
|
||||
* add links in the TinyMCE content editor.
|
||||
*
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function LinkForm() {
|
||||
@ -244,16 +247,16 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
'SiteTree', 'ID', 'MenuTitle', true);
|
||||
// mimic the SiteTree::getMenuTitle(), which is bypassed when the search is performed
|
||||
$siteTree->setSearchFunction(array($this, 'siteTreeSearchCallback'));
|
||||
|
||||
|
||||
$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
|
||||
. '<strong class="title">%s</strong></span>';
|
||||
$form = new Form(
|
||||
$this->controller,
|
||||
"{$this->name}/LinkForm",
|
||||
"{$this->name}/LinkForm",
|
||||
new FieldList(
|
||||
$headerWrap = new CompositeField(
|
||||
new LiteralField(
|
||||
'Heading',
|
||||
'Heading',
|
||||
sprintf('<h3 class="htmleditorfield-mediaform-heading insert">%s</h3>',
|
||||
_t('HtmlEditorField.LINK', 'Insert Link'))
|
||||
)
|
||||
@ -298,22 +301,22 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
)
|
||||
);
|
||||
|
||||
$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
|
||||
$headerWrap->addExtraClass('CompositeField composite cms-content-header nolabel ');
|
||||
$contentComposite->addExtraClass('ss-insert-link content');
|
||||
|
||||
|
||||
$form->unsetValidator();
|
||||
$form->loadDataFrom($this);
|
||||
$form->addExtraClass('htmleditorfield-form htmleditorfield-linkform cms-dialog-content');
|
||||
|
||||
|
||||
$this->extend('updateLinkForm', $form);
|
||||
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link Form} instance allowing a user to
|
||||
* add images and flash objects to the TinyMCE content editor.
|
||||
*
|
||||
*
|
||||
* @return Form
|
||||
*/
|
||||
public function MediaForm() {
|
||||
@ -340,17 +343,17 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
'CMSThumbnail' => false,
|
||||
'Name' => _t('File.Name'),
|
||||
));
|
||||
|
||||
|
||||
$numericLabelTmpl = '<span class="step-label"><span class="flyout">%d</span><span class="arrow"></span>'
|
||||
. '<strong class="title">%s</strong></span>';
|
||||
|
||||
$fromCMS = new CompositeField(
|
||||
new LiteralField('headerSelect',
|
||||
new LiteralField('headerSelect',
|
||||
'<h4>'.sprintf($numericLabelTmpl, '1', _t('HtmlEditorField.FindInFolder', 'Find in Folder')).'</h4>'),
|
||||
$select = TreeDropdownField::create('ParentID', "", 'Folder')->addExtraClass('noborder'),
|
||||
$select = TreeDropdownField::create('ParentID', "", 'Folder')->addExtraClass('noborder'),
|
||||
$fileField
|
||||
);
|
||||
|
||||
|
||||
$fromCMS->addExtraClass('content ss-uploadfield');
|
||||
$select->addExtraClass('content-select');
|
||||
|
||||
@ -424,7 +427,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
$headings,
|
||||
$allFields
|
||||
);
|
||||
|
||||
|
||||
$actions = new FieldList(
|
||||
FormAction::create('insertmedia', _t('HtmlEditorField.BUTTONINSERT', 'Insert'))
|
||||
->addExtraClass('ss-ui-action-constructive media-insert')
|
||||
@ -442,7 +445,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
$fields,
|
||||
$actions
|
||||
);
|
||||
|
||||
|
||||
|
||||
$form->unsetValidator();
|
||||
$form->disableSecurityToken();
|
||||
@ -451,9 +454,9 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
// TODO Re-enable once we remove $.metadata dependency which currently breaks the JS due to $.ui.widget
|
||||
// $form->setAttribute('data-urlViewfile', $this->controller->Link($this->name));
|
||||
|
||||
// Allow other people to extend the fields being added to the imageform
|
||||
// Allow other people to extend the fields being added to the imageform
|
||||
$this->extend('updateMediaForm', $form);
|
||||
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
@ -470,15 +473,15 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
$file = new File(array(
|
||||
'Title' => basename($url),
|
||||
'Filename' => $url
|
||||
));
|
||||
));
|
||||
} else {
|
||||
$url = Director::makeRelative($request->getVar('FileURL'));
|
||||
$url = preg_replace('/_resampled\/[^-]+-/', '', $url);
|
||||
$file = File::get()->filter('Filename', $url)->first();
|
||||
$file = File::get()->filter('Filename', $url)->first();
|
||||
if(!$file) $file = new File(array(
|
||||
'Title' => basename($url),
|
||||
'Filename' => $url
|
||||
));
|
||||
));
|
||||
}
|
||||
} elseif($id = $request->getVar('ID')) {
|
||||
$file = DataObject::get_by_id('File', $id);
|
||||
@ -510,7 +513,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
* Similar to {@link File->getCMSFields()}, but only returns fields
|
||||
* for manipulating the instance of the file as inserted into the HTML content,
|
||||
* not the "master record" in the database - hence there's no form or saving logic.
|
||||
*
|
||||
*
|
||||
* @param String Relative or absolute URL to file
|
||||
* @return FieldList
|
||||
*/
|
||||
@ -605,7 +608,7 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
if($file->Type == 'photo') {
|
||||
$fields->insertBefore('CaptionText', new TextField(
|
||||
'AltText',
|
||||
_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
|
||||
_t('HtmlEditorField.IMAGEALTTEXT', 'Alternative text (alt) - shown if image can\'t be displayed'),
|
||||
$file->Title,
|
||||
80
|
||||
));
|
||||
@ -745,13 +748,10 @@ class HtmlEditorField_Toolbar extends RequestHandler {
|
||||
* @return DataList
|
||||
*/
|
||||
protected function getFiles($parentID = null) {
|
||||
// TODO Use array('Filename:EndsWith' => $exts) once that's supported
|
||||
$exts = $this->getAllowedExtensions();
|
||||
$wheres = array();
|
||||
foreach($exts as $ext) $wheres[] = '"Filename" LIKE \'%.' . $ext . '\'';
|
||||
$dotExts = array_map(function($ext) { return ".{$ext}"; }, $exts);
|
||||
$files = File::get()->filter('Filename:EndsWith', $dotExts);
|
||||
|
||||
$files = File::get()->where(implode(' OR ', $wheres));
|
||||
|
||||
// Limit by folder (if required)
|
||||
if($parentID) {
|
||||
$files = $files->filter('ParentID', $parentID);
|
||||
@ -795,7 +795,7 @@ class HtmlEditorField_File extends ViewableData {
|
||||
|
||||
/**
|
||||
* @param String
|
||||
* @param File
|
||||
* @param File
|
||||
*/
|
||||
public function __construct($url, $file = null) {
|
||||
$this->url = $url;
|
||||
@ -942,7 +942,7 @@ class HtmlEditorField_Embed extends HtmlEditorField_File {
|
||||
public function appCategory() {
|
||||
return 'embed';
|
||||
}
|
||||
|
||||
|
||||
public function getInfo() {
|
||||
return $this->oembed->info;
|
||||
}
|
||||
@ -981,7 +981,7 @@ class HtmlEditorField_Image extends HtmlEditorField_File {
|
||||
|
||||
/**
|
||||
* Provide an initial width for inserted image, restricted based on $embed_width
|
||||
*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInsertWidth() {
|
||||
@ -992,7 +992,7 @@ class HtmlEditorField_Image extends HtmlEditorField_File {
|
||||
|
||||
/**
|
||||
* Provide an initial height for inserted image, scaled proportionally to the initial width
|
||||
*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getInsertHeight() {
|
||||
|
@ -245,7 +245,7 @@ class TreeDropdownField extends FormField {
|
||||
// Regular source specification
|
||||
$isSubTree = false;
|
||||
|
||||
$this->search = Convert::Raw2SQL($request->requestVar('search'));
|
||||
$this->search = $request->requestVar('search');
|
||||
$ID = (is_numeric($request->latestparam('ID')))
|
||||
? (int)$request->latestparam('ID')
|
||||
: (int)$request->requestVar('ID');
|
||||
@ -432,19 +432,19 @@ class TreeDropdownField extends FormField {
|
||||
$res = call_user_func($this->searchCallback, $this->sourceObject, $this->labelField, $this->search);
|
||||
} else {
|
||||
$sourceObject = $this->sourceObject;
|
||||
$wheres = array();
|
||||
$filters = array();
|
||||
if(singleton($sourceObject)->hasDatabaseField($this->labelField)) {
|
||||
$wheres[] = "\"$this->labelField\" LIKE '%$this->search%'";
|
||||
$filters["{$this->labelField}:PartialMatch"] = $this->search;
|
||||
} else {
|
||||
if(singleton($sourceObject)->hasDatabaseField('Title')) {
|
||||
$wheres[] = "\"Title\" LIKE '%$this->search%'";
|
||||
$filters["Title:PartialMatch"] = $this->search;
|
||||
}
|
||||
if(singleton($sourceObject)->hasDatabaseField('Name')) {
|
||||
$wheres[] = "\"Name\" LIKE '%$this->search%'";
|
||||
$filters["Name:PartialMatch"] = $this->search;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$wheres) {
|
||||
if(empty($filters)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Cannot query by %s.%s, not a valid database column',
|
||||
$sourceObject,
|
||||
@ -452,7 +452,7 @@ class TreeDropdownField extends FormField {
|
||||
));
|
||||
}
|
||||
|
||||
$res = DataObject::get($this->sourceObject, implode(' OR ', $wheres));
|
||||
$res = DataObject::get($this->sourceObject)->filterAny($filters);
|
||||
}
|
||||
|
||||
if( $res ) {
|
||||
@ -462,8 +462,11 @@ class TreeDropdownField extends FormField {
|
||||
$this->searchIds[$row->ID] = true;
|
||||
}
|
||||
while (!empty($parents)) {
|
||||
$res = DB::query('SELECT "ParentID", "ID" FROM "' . $this->sourceObject
|
||||
. '" WHERE "ID" in ('.implode(',',array_keys($parents)).')');
|
||||
$idsClause = DB::placeholders($parents);
|
||||
$res = DB::prepared_query(
|
||||
"SELECT \"ParentID\", \"ID\" FROM \"{$this->sourceObject}\" WHERE \"ID\" in ($idsClause)",
|
||||
array_keys($parents)
|
||||
);
|
||||
$parents = array();
|
||||
|
||||
foreach($res as $row) {
|
||||
@ -482,11 +485,9 @@ class TreeDropdownField extends FormField {
|
||||
* @return DataObject
|
||||
*/
|
||||
protected function objectForKey($key) {
|
||||
if($this->keyField == 'ID') {
|
||||
return DataObject::get_by_id($this->sourceObject, $key);
|
||||
} else {
|
||||
return DataObject::get_one($this->sourceObject, "\"{$this->keyField}\" = '".Convert::raw2sql($key)."'");
|
||||
}
|
||||
return DataObject::get($this->sourceObject)
|
||||
->filter($this->keyField, $key)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -662,7 +662,7 @@ class GridField extends FormField {
|
||||
* @param string $actionName
|
||||
* @param mixed $args
|
||||
* @param arrray $data - send data from a form
|
||||
* @return type
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handleAlterAction($actionName, $args, $data) {
|
||||
@ -798,10 +798,10 @@ class GridField_FormAction extends FormAction {
|
||||
|
||||
/**
|
||||
* @param GridField $gridField
|
||||
* @param type $name
|
||||
* @param type $label
|
||||
* @param type $actionName
|
||||
* @param type $args
|
||||
* @param string $name
|
||||
* @param string $label
|
||||
* @param string $actionName
|
||||
* @param array $args
|
||||
*/
|
||||
public function __construct(GridField $gridField, $name, $title, $actionName, $args) {
|
||||
$this->gridField = $gridField;
|
||||
|
@ -55,6 +55,7 @@ interface GridField_ColumnProvider extends GridFieldComponent {
|
||||
* @see {@link GridFieldDataColumns}.
|
||||
*
|
||||
* @param GridField $gridField
|
||||
* @param arary $columns List of columns
|
||||
* @param array - List reference of all column names.
|
||||
*/
|
||||
public function augmentColumns($gridField, &$columns);
|
||||
|
@ -42,7 +42,7 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
|
||||
/**
|
||||
* Add a column 'Delete'
|
||||
*
|
||||
* @param type $gridField
|
||||
* @param GridField $gridField
|
||||
* @param array $columns
|
||||
*/
|
||||
public function augmentColumns($gridField, &$columns) {
|
||||
@ -79,8 +79,8 @@ class GridFieldDeleteAction implements GridField_ColumnProvider, GridField_Actio
|
||||
/**
|
||||
* Which columns are handled by this component
|
||||
*
|
||||
* @param type $gridField
|
||||
* @return type
|
||||
* @param GridField $gridField
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnsHandled($gridField) {
|
||||
return array('Actions');
|
||||
|
@ -72,8 +72,8 @@ class GridFieldDetailForm implements GridField_URLHandler {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param type $gridField
|
||||
* @param type $request
|
||||
* @param GridField $gridField
|
||||
* @param SS_HTTPRequest $request
|
||||
* @return GridFieldDetailForm_ItemRequest
|
||||
*/
|
||||
public function handleItem($gridField, $request) {
|
||||
|
@ -18,7 +18,7 @@ class GridFieldEditButton implements GridField_ColumnProvider {
|
||||
/**
|
||||
* Add a column 'Delete'
|
||||
*
|
||||
* @param type $gridField
|
||||
* @param GridField $gridField
|
||||
* @param array $columns
|
||||
*/
|
||||
public function augmentColumns($gridField, &$columns) {
|
||||
@ -54,8 +54,8 @@ class GridFieldEditButton implements GridField_ColumnProvider {
|
||||
/**
|
||||
* Which columns are handled by this component
|
||||
*
|
||||
* @param type $gridField
|
||||
* @return type
|
||||
* @param GridField $gridField
|
||||
* @return array
|
||||
*/
|
||||
public function getColumnsHandled($gridField) {
|
||||
return array('Actions');
|
||||
|
@ -100,7 +100,7 @@ class GridState extends HiddenField {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return type
|
||||
* @return string
|
||||
*/
|
||||
public function attrValue() {
|
||||
return Convert::raw2att($this->Value());
|
||||
@ -108,7 +108,7 @@ class GridState extends HiddenField {
|
||||
|
||||
/**
|
||||
*
|
||||
* @return type
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->Value();
|
||||
|
@ -82,10 +82,10 @@ class Aggregate extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the SQLQuery to calculate the aggregate
|
||||
* Build the SQLSelect to calculate the aggregate
|
||||
* This is a seperate function so that subtypes of Aggregate can change just this bit
|
||||
* @param string $attr - the SQL field statement for selection (i.e. "MAX(LastUpdated)")
|
||||
* @return SQLQuery
|
||||
* @return SQLSelect
|
||||
*/
|
||||
protected function query($attr) {
|
||||
$query = DataList::create($this->type)->where($this->filter);
|
||||
@ -108,7 +108,7 @@ class Aggregate extends ViewableData {
|
||||
$table = null;
|
||||
|
||||
foreach (ClassInfo::ancestry($this->type, true) as $class) {
|
||||
$fields = DataObject::database_fields($class);
|
||||
$fields = DataObject::database_fields($class, false);
|
||||
if (array_key_exists($attribute, $fields)) { $table = $class; break; }
|
||||
}
|
||||
|
||||
@ -117,7 +117,8 @@ class Aggregate extends ViewableData {
|
||||
$query = $this->query("$func(\"$table\".\"$attribute\")");
|
||||
|
||||
// Cache results of this specific SQL query until flushCache() is triggered.
|
||||
$cachekey = sha1($query->sql());
|
||||
$sql = $query->sql($parameters);
|
||||
$cachekey = sha1($sql.'-'.var_export($parameters, true));
|
||||
$cache = self::cache();
|
||||
|
||||
if (!($result = $cache->load($cachekey))) {
|
||||
|
@ -275,8 +275,8 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
|
||||
/**
|
||||
* Returns a map of this list
|
||||
*
|
||||
* @param type $keyfield - the 'key' field of the result array
|
||||
* @param type $titlefield - the value field of the result array
|
||||
* @param string $keyfield - the 'key' field of the result array
|
||||
* @param string $titlefield - the value field of the result array
|
||||
* @return array
|
||||
*/
|
||||
public function map($keyfield = 'ID', $titlefield = 'Title') {
|
||||
@ -295,9 +295,9 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta
|
||||
/**
|
||||
* Find the first item of this list where the given key = value
|
||||
*
|
||||
* @param type $key
|
||||
* @param type $value
|
||||
* @return type
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function find($key, $value) {
|
||||
foreach ($this->items as $item) {
|
||||
|
378
model/DB.php
378
model/DB.php
@ -2,11 +2,12 @@
|
||||
/**
|
||||
* Global database interface, complete with static methods.
|
||||
* Use this class for interacting with the database.
|
||||
*
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class DB {
|
||||
|
||||
/**
|
||||
* This constant was added in SilverStripe 2.4 to indicate that SQL-queries
|
||||
* should now use ANSI-compatible syntax. The most notable affect of this
|
||||
@ -14,7 +15,7 @@ class DB {
|
||||
* and not backticks
|
||||
*/
|
||||
const USE_ANSI_SQL = true;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The global database connection.
|
||||
@ -37,38 +38,99 @@ class DB {
|
||||
* Set the global database connection.
|
||||
* Pass an object that's a subclass of SS_Database. This object will be used when {@link DB::query()}
|
||||
* is called.
|
||||
*
|
||||
* @param $connection The connecton object to set as the connection.
|
||||
* @param $name The name to give to this connection. If you omit this argument, the connection
|
||||
* will be the default one used by the ORM. However, you can store other named connections to
|
||||
* be accessed through DB::getConn($name). This is useful when you have an application that
|
||||
* be accessed through DB::get_conn($name). This is useful when you have an application that
|
||||
* needs to connect to more than one database.
|
||||
*/
|
||||
public static function setConn(SS_Database $connection, $name = 'default') {
|
||||
public static function set_conn(SS_Database $connection, $name = 'default') {
|
||||
self::$connections[$name] = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::set_conn instead
|
||||
*/
|
||||
public static function setConn(SS_Database $connection, $name = 'default') {
|
||||
Deprecation::notice('3.3', 'Use DB::set_conn instead');
|
||||
self::set_conn($connection, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the global database connection.
|
||||
* @param $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
*
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* the default connection is returned.
|
||||
* @return SS_Database
|
||||
*/
|
||||
public static function getConn($name = 'default') {
|
||||
public static function get_conn($name = 'default') {
|
||||
if(isset(self::$connections[$name])) {
|
||||
return self::$connections[$name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an alternative database in a browser cookie,
|
||||
* @deprecated since version 3.3 Use DB::get_conn instead
|
||||
*/
|
||||
public static function getConn($name = 'default') {
|
||||
Deprecation::notice('3.3', 'Use DB::get_conn instead');
|
||||
return self::get_conn($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the schema manager for the current database
|
||||
*
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* the default connection is returned.
|
||||
* @return DBSchemaManager
|
||||
*/
|
||||
public static function get_schema($name = 'default') {
|
||||
$connection = self::get_conn($name);
|
||||
if($connection) return $connection->getSchemaManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a sql query with the specified connection
|
||||
*
|
||||
* @param SQLExpression $expression The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* the default connection is returned.
|
||||
* @return string The resulting SQL as a string
|
||||
*/
|
||||
public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default') {
|
||||
$connection = self::get_conn($name);
|
||||
if($connection) {
|
||||
return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
|
||||
} else {
|
||||
$parameters = array();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the connector object for the current database
|
||||
*
|
||||
* @param string $name An optional name given to a connection in the DB::setConn() call. If omitted,
|
||||
* the default connection is returned.
|
||||
* @return DBConnector
|
||||
*/
|
||||
public static function get_connector($name = 'default') {
|
||||
$connection = self::get_conn($name);
|
||||
if($connection) return $connection->getConnector();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an alternative database in a browser cookie,
|
||||
* with the cookie lifetime set to the browser session.
|
||||
* This is useful for integration testing on temporary databases.
|
||||
*
|
||||
* There is a strict naming convention for temporary databases to avoid abuse:
|
||||
* There is a strict naming convention for temporary databases to avoid abuse:
|
||||
* <prefix> (default: 'ss_') + tmpdb + <7 digits>
|
||||
* As an additional security measure, temporary databases will
|
||||
* be ignored in "live" mode.
|
||||
*
|
||||
*
|
||||
* Note that the database will be set on the next request.
|
||||
* Set it to null to revert to the main database.
|
||||
*/
|
||||
@ -104,7 +166,7 @@ class DB {
|
||||
Cookie::set("alternativeDatabaseNameIv", null, 0, null, null, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the name of the database in use
|
||||
*/
|
||||
@ -133,7 +195,7 @@ class DB {
|
||||
/**
|
||||
* Determines if the name is valid, as a security
|
||||
* measure against setting arbitrary databases.
|
||||
*
|
||||
*
|
||||
* @param String $name
|
||||
* @return Boolean
|
||||
*/
|
||||
@ -148,7 +210,7 @@ class DB {
|
||||
/**
|
||||
* Connect to a database.
|
||||
*
|
||||
* Given the database configuration, this method will create the correct
|
||||
* Given the database configuration, this method will create the correct
|
||||
* subclass of {@link SS_Database}.
|
||||
*
|
||||
* @param array $database A map of options. The 'type' is the name of the subclass of SS_Database to use. For the
|
||||
@ -158,7 +220,7 @@ class DB {
|
||||
* @return SS_Database
|
||||
*/
|
||||
public static function connect($databaseConfig, $label = 'default') {
|
||||
|
||||
|
||||
// This is used by the "testsession" module to test up a test session using an alternative name
|
||||
if($name = self::get_alternative_database_name()) {
|
||||
$databaseConfig['database'] = $name;
|
||||
@ -171,13 +233,17 @@ class DB {
|
||||
self::$connection_attempted = true;
|
||||
|
||||
$dbClass = $databaseConfig['type'];
|
||||
$conn = new $dbClass($databaseConfig);
|
||||
|
||||
self::setConn($conn, $label);
|
||||
// Using Injector->get allows us to use registered configurations
|
||||
// which may or may not map to explicit objects
|
||||
$conn = Injector::inst()->get($dbClass);
|
||||
$conn->connect($databaseConfig);
|
||||
|
||||
self::set_conn($conn, $label);
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if a database connection has been attempted.
|
||||
* In particular, it lets the caller know if we're still so early in the execution pipeline that
|
||||
@ -188,12 +254,10 @@ class DB {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the connection string from input.
|
||||
* @param array $parameters The connection details.
|
||||
* @return string $connect The connection string.
|
||||
**/
|
||||
* @deprecated since version 3.2 DB::getConnect was never implemented and is obsolete
|
||||
*/
|
||||
public static function getConnect($parameters) {
|
||||
return self::getConn()->getConnect($parameters);
|
||||
Deprecation::notice('3.2', 'DB::getConnect was never implemented and is obsolete');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -204,8 +268,43 @@ class DB {
|
||||
*/
|
||||
public static function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
self::$lastQuery = $sql;
|
||||
|
||||
return self::getConn()->query($sql, $errorLevel);
|
||||
|
||||
return self::get_conn()->query($sql, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for generating a list of parameter placeholders for the
|
||||
* given argument(s)
|
||||
*
|
||||
* @param array|integer $input An array of items needing placeholders, or a
|
||||
* number to specify the number of placeholders
|
||||
* @param string The string to join each placeholder together with
|
||||
* @return string|null Either a list of placeholders, or null
|
||||
*/
|
||||
public static function placeholders($input, $join = ', ') {
|
||||
if(is_array($input)) {
|
||||
$number = count($input);
|
||||
} elseif(is_numeric($input)) {
|
||||
$number = intval($input);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if($number === 0) return null;
|
||||
return implode($join, array_fill(0, $number, '?'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given SQL parameterised query with the specified arguments
|
||||
*
|
||||
* @param string $sql The SQL query to execute. The ? character will denote parameters.
|
||||
* @param array $parameters An ordered list of arguments.
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||
self::$lastQuery = $sql;
|
||||
|
||||
return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,7 +313,7 @@ class DB {
|
||||
* and the values are map containing 'command' and 'fields'. Command should be 'insert' or 'update',
|
||||
* and fields should be a map of field names to field values, including quotes. The field value can
|
||||
* also be a SQL function or similar.
|
||||
*
|
||||
*
|
||||
* Example:
|
||||
* <code>
|
||||
* array(
|
||||
@ -228,7 +327,7 @@ class DB {
|
||||
* ),
|
||||
* "id" => 234 // an alternative to providing ID in the fields list
|
||||
* ),
|
||||
*
|
||||
*
|
||||
* // Command: update
|
||||
* "other table" => array(
|
||||
* "command" => "update",
|
||||
@ -241,61 +340,97 @@ class DB {
|
||||
* ),
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* You'll note that only one command on a given table can be called.
|
||||
* That's a limitation of the system that's due to it being written for {@link DataObject::write()},
|
||||
*
|
||||
* You'll note that only one command on a given table can be called.
|
||||
* That's a limitation of the system that's due to it being written for {@link DataObject::write()},
|
||||
* which needs to do a single write on a number of different tables.
|
||||
*
|
||||
*
|
||||
* @todo Update this to support paramaterised queries
|
||||
*
|
||||
* @param array $manipulation
|
||||
*/
|
||||
public static function manipulate($manipulation) {
|
||||
self::$lastQuery = $manipulation;
|
||||
return self::getConn()->manipulate($manipulation);
|
||||
return self::get_conn()->manipulate($manipulation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the autogenerated ID from the previous INSERT query.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_generated_id($table) {
|
||||
return self::get_conn()->getGeneratedID($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_generated_id instead
|
||||
*/
|
||||
public static function getGeneratedID($table) {
|
||||
return self::getConn()->getGeneratedID($table);
|
||||
Deprecation::notice('3.3', 'Use DB::get_generated_id instead');
|
||||
return self::get_generated_id($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the connection to the database is active.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_active() {
|
||||
return ($conn = self::get_conn()) && $conn->isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::is_active instead
|
||||
*/
|
||||
public static function isActive() {
|
||||
if($conn = self::getConn()) return $conn->isActive();
|
||||
else return false;
|
||||
Deprecation::notice('3.3', 'Use DB::is_active instead');
|
||||
return self::is_active();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the database and connect to it. This can be called if the
|
||||
* initial database connection is not successful because the database
|
||||
* does not exist.
|
||||
* @param string $connect Connection string
|
||||
* @param string $username SS_Database username
|
||||
* @param string $password SS_Database Password
|
||||
* @param string $database SS_Database to which to create
|
||||
*
|
||||
* @param string $database Name of database to create
|
||||
* @return boolean Returns true if successful
|
||||
*/
|
||||
public static function create_database($database) {
|
||||
return self::get_conn()->selectDatabase($database, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::create_database instead
|
||||
*/
|
||||
public static function createDatabase($connect, $username, $password, $database) {
|
||||
return self::getConn()->createDatabase($connect, $username, $password, $database);
|
||||
Deprecation::notice('3.3', 'Use DB::create_database instead');
|
||||
return self::create_database($database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new table.
|
||||
* @param $tableName The name of the table
|
||||
* @param $fields A map of field names to field types
|
||||
* @param $indexes A map of indexes
|
||||
* @param $options An map of additional options. The available keys are as follows:
|
||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
||||
* @param string $tableName The name of the table
|
||||
* @param array$fields A map of field names to field types
|
||||
* @param array $indexes A map of indexes
|
||||
* @param array $options An map of additional options. The available keys are as follows:
|
||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
|
||||
* for MySQL.
|
||||
* - 'temporary' - If true, then a temporary table will be created
|
||||
* @return The table name generated. This may be different from the table name, for example with temporary tables.
|
||||
* @return string The table name generated. This may be different from the table name, for example with
|
||||
* temporary tables.
|
||||
*/
|
||||
public static function create_table($table, $fields = null, $indexes = null, $options = null,
|
||||
$advancedOptions = null
|
||||
) {
|
||||
return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::create_table instead
|
||||
*/
|
||||
public static function createTable($table, $fields = null, $indexes = null, $options = null) {
|
||||
return self::getConn()->createTable($table, $fields, $indexes, $options);
|
||||
Deprecation::notice('3.3', 'Use DB::create_table instead');
|
||||
return self::create_table($table, $fields, $indexes, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -304,115 +439,210 @@ class DB {
|
||||
* @param string $field Name of the field to add.
|
||||
* @param string $spec The field specification, eg 'INTEGER NOT NULL'
|
||||
*/
|
||||
public static function create_field($table, $field, $spec) {
|
||||
return self::get_schema()->createField($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::create_field instead
|
||||
*/
|
||||
public static function createField($table, $field, $spec) {
|
||||
return self::getConn()->createField($table, $field, $spec);
|
||||
Deprecation::notice('3.3', 'Use DB::create_field instead');
|
||||
return self::create_field($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the following table in the database, modifying whatever already exists
|
||||
* as necessary.
|
||||
*
|
||||
* @param string $table The name of the table
|
||||
* @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
|
||||
* @param string $indexSchema A list of indexes to create. The keys of the array are the names of the index.
|
||||
* @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
|
||||
* The values of the array can be one of:
|
||||
* - true: Create a single column index on the field named the same as the index.
|
||||
* - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
|
||||
* control over the index.
|
||||
* @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
|
||||
* @param string $options SQL statement to append to the CREATE TABLE call.
|
||||
* @param array $extensions List of extensions
|
||||
*/
|
||||
public static function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK=true,
|
||||
$options = null, $extensions=null) {
|
||||
|
||||
return self::getConn()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
|
||||
public static function require_table($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
|
||||
$options = null, $extensions = null
|
||||
) {
|
||||
return self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options,
|
||||
$extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_table instead
|
||||
*/
|
||||
public static function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
|
||||
$options = null, $extensions = null
|
||||
) {
|
||||
Deprecation::notice('3.3', 'Use DB::require_table instead');
|
||||
return self::require_table($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given field on the table, modifying whatever already exists as necessary.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $field The field name.
|
||||
* @param string $spec The field specification.
|
||||
*/
|
||||
public static function require_field($table, $field, $spec) {
|
||||
return self::get_schema()->requireField($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_field instead
|
||||
*/
|
||||
public static function requireField($table, $field, $spec) {
|
||||
return self::getConn()->requireField($table, $field, $spec);
|
||||
Deprecation::notice('3.3', 'Use DB::require_field instead');
|
||||
return self::require_field($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given index in the database, modifying whatever already exists as necessary.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $index The index name.
|
||||
* @param string|boolean $spec The specification of the index. See requireTable() for more information.
|
||||
*/
|
||||
public static function require_index($table, $index, $spec) {
|
||||
self::get_schema()->requireIndex($table, $index, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_index instead
|
||||
*/
|
||||
public static function requireIndex($table, $index, $spec) {
|
||||
return self::getConn()->requireIndex($table, $index, $spec);
|
||||
Deprecation::notice('3.3', 'Use DB::require_index instead');
|
||||
self::require_index($table, $index, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
|
||||
*
|
||||
* @param string $table The table name.
|
||||
*/
|
||||
public static function dont_require_table($table) {
|
||||
self::get_schema()->dontRequireTable($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::dont_require_table instead
|
||||
*/
|
||||
public static function dontRequireTable($table) {
|
||||
return self::getConn()->dontRequireTable($table);
|
||||
Deprecation::notice('3.3', 'Use DB::dont_require_table instead');
|
||||
self::dont_require_table($table);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* See {@link SS_Database->dontRequireField()}.
|
||||
*
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $fieldName
|
||||
* @param string $fieldName The field name not to require
|
||||
*/
|
||||
public static function dont_require_field($table, $fieldName) {
|
||||
self::get_schema()->dontRequireField($table, $fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::dont_require_field instead
|
||||
*/
|
||||
public static function dontRequireField($table, $fieldName) {
|
||||
return self::getConn()->dontRequireField($table, $fieldName);
|
||||
Deprecation::notice('3.3', 'Use DB::dont_require_field instead');
|
||||
self::dont_require_field($table, $fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a table's integrity and repairs it if necessary.
|
||||
* @var string $tableName The name of the table.
|
||||
*
|
||||
* @param string $tableName The name of the table.
|
||||
* @return boolean Return true if the table has integrity after the method is complete.
|
||||
*/
|
||||
public static function check_and_repair_table($table) {
|
||||
return self::get_schema()->checkAndRepairTable($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::check_and_repair_table instead
|
||||
*/
|
||||
public static function checkAndRepairTable($table) {
|
||||
return self::getConn()->checkAndRepairTable($table);
|
||||
Deprecation::notice('3.3', 'Use DB::check_and_repair_table instead');
|
||||
self::check_and_repair_table($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of rows affected by the previous operation.
|
||||
* @return int
|
||||
*
|
||||
* @return integer The number of affected rows
|
||||
*/
|
||||
public static function affected_rows() {
|
||||
return self::get_conn()->affectedRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::affected_rows instead
|
||||
*/
|
||||
public static function affectedRows() {
|
||||
return self::getConn()->affectedRows();
|
||||
Deprecation::notice('3.3', 'Use DB::affected_rows instead');
|
||||
return self::affected_rows();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all tables in the database.
|
||||
* The table names will be in lower case.
|
||||
* @return array
|
||||
*
|
||||
* @return array The list of tables
|
||||
*/
|
||||
public static function table_list() {
|
||||
return self::get_schema()->tableList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::table_list instead
|
||||
*/
|
||||
public static function tableList() {
|
||||
return self::getConn()->tableList();
|
||||
Deprecation::notice('3.3', 'Use DB::table_list instead');
|
||||
return self::table_list();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of all the fields for the given table.
|
||||
* Returns a map of field name => field spec.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @return array
|
||||
* @return array The list of fields
|
||||
*/
|
||||
public static function field_list($table) {
|
||||
return self::get_schema()->fieldList($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::field_list instead
|
||||
*/
|
||||
public static function fieldList($table) {
|
||||
return self::getConn()->fieldList($table);
|
||||
Deprecation::notice('3.3', 'Use DB::field_list instead');
|
||||
return self::field_list($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable supression of database messages.
|
||||
*/
|
||||
public static function quiet() {
|
||||
return self::getConn()->quiet();
|
||||
self::get_schema()->quiet();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show a message about database alteration.
|
||||
* Show a message about database alteration
|
||||
*
|
||||
* @param string $message to display
|
||||
* @param string $type one of [created|changed|repaired|obsolete|deleted|error]
|
||||
*/
|
||||
public static function alteration_message($message,$type="") {
|
||||
return self::getConn()->alterationMessage($message, $type);
|
||||
public static function alteration_message($message, $type = "") {
|
||||
self::get_schema()->alterationMessage($message, $type);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -40,9 +40,9 @@ abstract class DataExtension extends Extension {
|
||||
/**
|
||||
* Edit the given query object to support queries for this extension
|
||||
*
|
||||
* @param SQLQuery $query Query to augment.
|
||||
* @param SQLSelect $query Query to augment.
|
||||
*/
|
||||
public function augmentSQL(SQLQuery &$query) {
|
||||
public function augmentSQL(SQLSelect $query) {
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,7 +57,7 @@ abstract class DataExtension extends Extension {
|
||||
/**
|
||||
* Augment a write-record request.
|
||||
*
|
||||
* @param SQLQuery $manipulation Query to augment.
|
||||
* @param array $manipulation Array of operations to augment.
|
||||
*/
|
||||
public function augmentWrite(&$manipulation) {
|
||||
}
|
||||
@ -180,4 +180,3 @@ abstract class DataExtension extends Extension {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -153,6 +153,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DataList instance with the specified query parameter assigned
|
||||
*
|
||||
* @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set
|
||||
* @param mixed $val If $keyOrArray is not an array, this is the value to set
|
||||
* @return DataList
|
||||
*/
|
||||
public function setDataQueryParam($keyOrArray, $val = null) {
|
||||
$clone = clone $this;
|
||||
|
||||
@ -171,16 +178,28 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
/**
|
||||
* Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-)
|
||||
*
|
||||
* @return SQLQuery
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @param string The resulting SQL query (may be paramaterised)
|
||||
*/
|
||||
public function sql() {
|
||||
return $this->dataQuery->query()->sql();
|
||||
public function sql(&$parameters = array()) {
|
||||
if(func_num_args() == 0) {
|
||||
Deprecation::notice(
|
||||
'3.2',
|
||||
'DataList::sql() now may produce parameters which are necessary to execute this query'
|
||||
);
|
||||
}
|
||||
return $this->dataQuery->query()->sql($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new DataList instance with a WHERE clause added to this list's query.
|
||||
*
|
||||
* Supports parameterised queries.
|
||||
* See SQLSelect::addWhere() for syntax examples, although DataList
|
||||
* won't expand multiple method arguments as SQLSelect does.
|
||||
*
|
||||
* @param string $filter Escaped SQL statement
|
||||
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
|
||||
* paramaterised queries
|
||||
* @return DataList
|
||||
*/
|
||||
public function where($filter) {
|
||||
@ -188,6 +207,26 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
$query->where($filter);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new DataList instance with a WHERE clause added to this list's query.
|
||||
* All conditions provided in the filter will be joined with an OR
|
||||
*
|
||||
* Supports parameterised queries.
|
||||
* See SQLSelect::addWhere() for syntax examples, although DataList
|
||||
* won't expand multiple method arguments as SQLSelect does.
|
||||
*
|
||||
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
|
||||
* paramaterised queries
|
||||
* @return DataList
|
||||
*/
|
||||
public function whereAny($filter) {
|
||||
return $this->alterDataQuery(function($query) use ($filter){
|
||||
$query->whereAny($filter);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if this DataList can be sorted by the given field.
|
||||
@ -229,7 +268,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* order set.
|
||||
*
|
||||
* @see SS_List::sort()
|
||||
* @see SQLQuery::orderby
|
||||
* @see SQLSelect::orderby
|
||||
* @example $list = $list->sort('Name'); // default ASC sorting
|
||||
* @example $list = $list->sort('Name DESC'); // DESC sorting
|
||||
* @example $list = $list->sort('Name', 'ASC');
|
||||
@ -310,7 +349,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
*
|
||||
* @todo extract the sql from $customQuery into a SQLGenerator class
|
||||
*
|
||||
* @param string|array Key and Value pairs, the array values are automatically sanitised for the DB quesry
|
||||
* @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
|
||||
* @return DataList
|
||||
*/
|
||||
public function filter() {
|
||||
@ -499,7 +538,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
*
|
||||
* @todo extract the sql from this method into a SQLGenerator class
|
||||
*
|
||||
* @param string|array Escaped SQL statement. If passed as array, all keys and values are assumed to be escaped.
|
||||
* @param string|array Escaped SQL statement. If passed as array, all keys and values will be escaped internally
|
||||
* @return DataList
|
||||
*/
|
||||
public function exclude() {
|
||||
@ -527,13 +566,13 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
$t = singleton($list->dataClass())->dbObject($field);
|
||||
if($filterType) {
|
||||
$className = "{$filterType}Filter";
|
||||
} else {
|
||||
} else {
|
||||
$className = 'ExactMatchFilter';
|
||||
}
|
||||
}
|
||||
if(!class_exists($className)){
|
||||
$className = 'ExactMatchFilter';
|
||||
array_unshift($modifiers, $filterType);
|
||||
}
|
||||
}
|
||||
$t = new $className($field, $value, $modifiers);
|
||||
$t->exclude($subquery);
|
||||
}
|
||||
@ -608,7 +647,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
/**
|
||||
* Return this list as an array and every object it as an sub array as well
|
||||
*
|
||||
* @return type
|
||||
* @return array
|
||||
*/
|
||||
public function toNestedArray() {
|
||||
$result = array();
|
||||
@ -793,14 +832,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* @return DataObject|null
|
||||
*/
|
||||
public function find($key, $value) {
|
||||
if($key == 'ID') {
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
$SQL_col = sprintf('"%s"."%s"', $baseClass, Convert::raw2sql($key));
|
||||
} else {
|
||||
$SQL_col = sprintf('"%s"', Convert::raw2sql($key));
|
||||
}
|
||||
|
||||
return $this->where("$SQL_col = '" . Convert::raw2sql($value) . "'")->First();
|
||||
return $this->filter($key, $value)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -818,13 +850,11 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
/**
|
||||
* Filter this list to only contain the given Primary IDs
|
||||
*
|
||||
* @param array $ids Array of integers, will be automatically cast/escaped.
|
||||
* @param array $ids Array of integers
|
||||
* @return DataList
|
||||
*/
|
||||
public function byIDs(array $ids) {
|
||||
$ids = array_map('intval', $ids); // sanitize
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
return $this->where("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")");
|
||||
return $this->filter('ID', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -834,8 +864,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* @return DataObject
|
||||
*/
|
||||
public function byID($id) {
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
return $this->where("\"$baseClass\".\"ID\" = " . (int)$id)->First();
|
||||
return $this->filter('ID', $id)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -965,7 +994,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab
|
||||
* This method are overloaded by HasManyList and ManyMany list to perform more sophisticated
|
||||
* list manipulation
|
||||
*
|
||||
* @param type $item
|
||||
* @param mixed $item
|
||||
*/
|
||||
public function add($item) {
|
||||
// Nothing needs to happen by default
|
||||
|
1127
model/DataObject.php
1127
model/DataObject.php
@ -5,16 +5,16 @@
|
||||
* <h2>Extensions</h2>
|
||||
*
|
||||
* See {@link Extension} and {@link DataExtension}.
|
||||
*
|
||||
*
|
||||
* <h2>Permission Control</h2>
|
||||
*
|
||||
*
|
||||
* Object-level access control by {@link Permission}. Permission codes are arbitrary
|
||||
* strings which can be selected on a group-by-group basis.
|
||||
*
|
||||
*
|
||||
* <code>
|
||||
* class Article extends DataObject implements PermissionProvider {
|
||||
* static $api_access = true;
|
||||
*
|
||||
*
|
||||
* function canView($member = false) {
|
||||
* return Permission::check('ARTICLE_VIEW');
|
||||
* }
|
||||
@ -36,13 +36,13 @@
|
||||
* );
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
* </code>
|
||||
*
|
||||
* Object-level access control by {@link Group} membership:
|
||||
* Object-level access control by {@link Group} membership:
|
||||
* <code>
|
||||
* class Article extends DataObject {
|
||||
* static $api_access = true;
|
||||
*
|
||||
*
|
||||
* function canView($member = false) {
|
||||
* if(!$member) $member = Member::currentUser();
|
||||
* return $member->inGroup('Subscribers');
|
||||
@ -51,18 +51,18 @@
|
||||
* if(!$member) $member = Member::currentUser();
|
||||
* return $member->inGroup('Editors');
|
||||
* }
|
||||
*
|
||||
*
|
||||
* // ...
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* If any public method on this class is prefixed with an underscore,
|
||||
*
|
||||
* If any public method on this class is prefixed with an underscore,
|
||||
* the results are cached in memory through {@link cachedCall()}.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @todo Add instance specific removeExtension() which undos loadExtraStatics()
|
||||
* and defineMethods()
|
||||
*
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*
|
||||
@ -72,21 +72,21 @@
|
||||
* @property string Created Date and time of DataObject creation.
|
||||
*/
|
||||
class DataObject extends ViewableData implements DataObjectInterface, i18nEntityProvider {
|
||||
|
||||
|
||||
/**
|
||||
* Human-readable singular name.
|
||||
* @var string
|
||||
* @config
|
||||
*/
|
||||
private static $singular_name = null;
|
||||
|
||||
|
||||
/**
|
||||
* Human-readable pluaral name
|
||||
* @var string
|
||||
* @config
|
||||
*/
|
||||
private static $plural_name = null;
|
||||
|
||||
|
||||
/**
|
||||
* Allow API access to this object?
|
||||
* @todo Define the options that can be set here
|
||||
@ -99,27 +99,27 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @var boolean
|
||||
*/
|
||||
public $destroyed = false;
|
||||
|
||||
|
||||
/**
|
||||
* The DataModel from this this object comes
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
|
||||
/**
|
||||
* Data stored in this objects database record. An array indexed by fieldname.
|
||||
*
|
||||
* Data stored in this objects database record. An array indexed by fieldname.
|
||||
*
|
||||
* Use {@link toMap()} if you want an array representation
|
||||
* of this object, as the $record array might contain lazy loaded field aliases.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $record;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a field that hasn't changed (before === after, thus before == after)
|
||||
*/
|
||||
const CHANGE_NONE = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a field that has changed type, although not the loosely defined value.
|
||||
* (before !== after && before == after)
|
||||
@ -127,19 +127,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Value changes are by nature also considered strict changes.
|
||||
*/
|
||||
const CHANGE_STRICT = 1;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a field that has changed the loosely defined value
|
||||
* (before != after, thus, before !== after))
|
||||
* E.g. change false to true, but not false to 0
|
||||
*/
|
||||
const CHANGE_VALUE = 2;
|
||||
|
||||
|
||||
/**
|
||||
* An array indexed by fieldname, true if the field has been changed.
|
||||
* Use {@link getChangedFields()} and {@link isChanged()} to inspect
|
||||
* the changed state.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $changed;
|
||||
@ -156,13 +156,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @var boolean
|
||||
*/
|
||||
protected $brokenOnDelete = false;
|
||||
|
||||
|
||||
/**
|
||||
* Used by onBeforeWrite() to ensure child classes call parent::onBeforeWrite()
|
||||
* @var boolean
|
||||
*/
|
||||
protected $brokenOnWrite = false;
|
||||
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var boolean Should dataobjects be validated before they are written?
|
||||
@ -176,7 +176,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Static caches used by relevant functions.
|
||||
*/
|
||||
public static $cache_has_own_table = array();
|
||||
public static $cache_has_own_table_field = array();
|
||||
protected static $_cache_db = array();
|
||||
protected static $_cache_get_one;
|
||||
protected static $_cache_get_class_ancestry;
|
||||
@ -204,7 +203,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Returns when validation on DataObjects is enabled.
|
||||
*
|
||||
*
|
||||
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
|
||||
* @return bool
|
||||
*/
|
||||
@ -212,14 +211,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead');
|
||||
return Config::inst()->get('DataObject', 'validation_enabled');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether DataObjects should be validated before they are written.
|
||||
*
|
||||
*
|
||||
* Caution: Validation can contain safeguards against invalid/malicious data,
|
||||
* and check permission levels (e.g. on {@link Group}). Therefore it is recommended
|
||||
* to only disable validation for very specific use cases.
|
||||
*
|
||||
*
|
||||
* @param $enable bool
|
||||
* @see DataObject::validate()
|
||||
* @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead
|
||||
@ -233,13 +232,41 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @var [string] - class => ClassName field definition cache for self::database_fields
|
||||
*/
|
||||
private static $classname_spec_cache = array();
|
||||
|
||||
|
||||
/**
|
||||
* Clear all cached classname specs. It's necessary to clear all cached subclassed names
|
||||
* for any classes if a new class manifest is generated.
|
||||
*/
|
||||
public static function clear_classname_spec_cache() {
|
||||
self::$classname_spec_cache = array();
|
||||
PolymorphicForeignKey::clear_classname_spec_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the specification for the ClassName field for the given class
|
||||
*
|
||||
* @param string $class
|
||||
* @param boolean $queryDB Determine if the DB may be queried for additional information
|
||||
* @return string Resulting ClassName spec. If $queryDB is true this will include all
|
||||
* legacy types that no longer have concrete classes in PHP
|
||||
*/
|
||||
public static function get_classname_spec($class, $queryDB = true) {
|
||||
// Check cache
|
||||
if(!empty(self::$classname_spec_cache[$class])) return self::$classname_spec_cache[$class];
|
||||
|
||||
// Build known class names
|
||||
$classNames = ClassInfo::subclassesFor($class);
|
||||
|
||||
// Enhance with existing classes in order to prevent legacy details being lost
|
||||
if($queryDB && DB::get_schema()->hasField($class, 'ClassName')) {
|
||||
$existing = DB::query("SELECT DISTINCT \"ClassName\" FROM \"{$class}\"")->column();
|
||||
$classNames = array_unique(array_merge($classNames, $existing));
|
||||
}
|
||||
$spec = "Enum('" . implode(', ', $classNames) . "')";
|
||||
|
||||
// Only cache full information if queried
|
||||
if($queryDB) self::$classname_spec_cache[$class] = $spec;
|
||||
return $spec;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,30 +274,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* See {@link custom_database_fields()} for a getter that excludes these "base fields".
|
||||
*
|
||||
* @param string $class
|
||||
* @param boolean $queryDB Determine if the DB may be queried for additional information
|
||||
* @return array
|
||||
*/
|
||||
public static function database_fields($class) {
|
||||
public static function database_fields($class, $queryDB = true) {
|
||||
if(get_parent_class($class) == 'DataObject') {
|
||||
if(empty(self::$classname_spec_cache[$class])) {
|
||||
$classNames = ClassInfo::subclassesFor($class);
|
||||
|
||||
$db = DB::getConn();
|
||||
if($db->hasField($class, 'ClassName')) {
|
||||
$existing = $db->query("SELECT DISTINCT \"ClassName\" FROM \"$class\"")->column();
|
||||
$classNames = array_unique(array_merge($classNames, $existing));
|
||||
}
|
||||
|
||||
self::$classname_spec_cache[$class] = "Enum('" . implode(', ', $classNames) . "')";
|
||||
}
|
||||
|
||||
return array_merge (
|
||||
// TODO: should this be using self::$fixed_fields? only difference is ID field
|
||||
// and ClassName creates an Enum with all values
|
||||
array (
|
||||
'ClassName' => self::$classname_spec_cache[$class],
|
||||
'Created' => 'SS_Datetime',
|
||||
'LastEdited' => 'SS_Datetime'
|
||||
),
|
||||
// Merge fixed with ClassName spec and custom db fields
|
||||
$fixed = self::$fixed_fields;
|
||||
unset($fixed['ID']);
|
||||
return array_merge(
|
||||
$fixed,
|
||||
array('ClassName' => self::get_classname_spec($class, $queryDB)),
|
||||
self::custom_database_fields($class)
|
||||
);
|
||||
}
|
||||
@ -279,14 +293,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all database columns explicitly defined on a class in {@link DataObject::$db}
|
||||
* and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField}
|
||||
* into the actual database fields, rather than the name of the field which
|
||||
* Get all database columns explicitly defined on a class in {@link DataObject::$db}
|
||||
* and {@link DataObject::$has_one}. Resolves instances of {@link CompositeDBField}
|
||||
* into the actual database fields, rather than the name of the field which
|
||||
* might not equate a database column.
|
||||
*
|
||||
*
|
||||
* Does not include "base fields" like "ID", "ClassName", "Created", "LastEdited",
|
||||
* see {@link database_fields()}.
|
||||
*
|
||||
*
|
||||
* @uses CompositeDBField->compositeDatabaseFields()
|
||||
*
|
||||
* @param string $class
|
||||
@ -302,18 +316,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) {
|
||||
// Remove the original fieldname, it's not an actual database column
|
||||
unset($fields[$fieldName]);
|
||||
|
||||
|
||||
// Add all composite columns
|
||||
$compositeFields = singleton($fieldClass)->compositeDatabaseFields();
|
||||
if($compositeFields) foreach($compositeFields as $compositeName => $spec) {
|
||||
$fields["{$fieldName}{$compositeName}"] = $spec;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add has_one relationships
|
||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
||||
if($hasOne) foreach(array_keys($hasOne) as $field) {
|
||||
|
||||
|
||||
// Check if this is a polymorphic relation, in which case the relation
|
||||
// is a composite field
|
||||
if($hasOne[$field] === 'DataObject') {
|
||||
@ -335,17 +349,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the field class if the given db field on the class is a composite field.
|
||||
* Will check all applicable ancestor classes and aggregate results.
|
||||
*
|
||||
* @param string $class Class to check
|
||||
* @param string $name Field to check
|
||||
* @param boolean $aggregated True if parent classes should be checked, or false to limit to this class
|
||||
* @return string Class name of composite field if it exists
|
||||
*/
|
||||
public static function is_composite_field($class, $name, $aggregated = true) {
|
||||
if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
|
||||
|
||||
|
||||
if(isset(DataObject::$_cache_composite_fields[$class][$name])) {
|
||||
return DataObject::$_cache_composite_fields[$class][$name];
|
||||
|
||||
|
||||
} else if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
|
||||
return self::is_composite_field($parentClass, $name);
|
||||
}
|
||||
@ -357,14 +376,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public static function composite_fields($class, $aggregated = true) {
|
||||
if(!isset(DataObject::$_cache_composite_fields[$class])) self::cache_composite_fields($class);
|
||||
|
||||
|
||||
$compositeFields = DataObject::$_cache_composite_fields[$class];
|
||||
|
||||
|
||||
if($aggregated && $class != 'DataObject' && ($parentClass=get_parent_class($class)) != 'DataObject') {
|
||||
$compositeFields = array_merge($compositeFields,
|
||||
$compositeFields = array_merge($compositeFields,
|
||||
self::composite_fields($parentClass));
|
||||
}
|
||||
|
||||
|
||||
return $compositeFields;
|
||||
}
|
||||
|
||||
@ -373,7 +392,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
private static function cache_composite_fields($class) {
|
||||
$compositeFields = array();
|
||||
|
||||
|
||||
$fields = Config::inst()->get($class, 'db', Config::UNINHERITED);
|
||||
if($fields) foreach($fields as $fieldName => $fieldClass) {
|
||||
if(!is_string($fieldClass)) continue;
|
||||
@ -381,16 +400,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Strip off any parameters
|
||||
$bPos = strpos('(', $fieldClass);
|
||||
if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass);
|
||||
|
||||
|
||||
// Test to see if it implements CompositeDBField
|
||||
if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) {
|
||||
$compositeFields[$fieldName] = $fieldClass;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DataObject::$_cache_composite_fields[$class] = $compositeFields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new DataObject.
|
||||
*
|
||||
@ -465,7 +484,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// prevent populateDefaults() and setField() from marking overwritten defaults as changed
|
||||
$this->changed = array();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the DataModel
|
||||
* @param DataModel $model
|
||||
@ -498,14 +517,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$className = $this->class;
|
||||
$clone = new $className( $this->toMap(), false, $this->model );
|
||||
$clone->ID = 0;
|
||||
|
||||
|
||||
$clone->invokeWithExtensions('onBeforeDuplicate', $this, $doWrite);
|
||||
if($doWrite) {
|
||||
$clone->write();
|
||||
$this->duplicateManyManyRelations($this, $clone);
|
||||
}
|
||||
$clone->invokeWithExtensions('onAfterDuplicate', $this, $doWrite);
|
||||
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
@ -543,7 +562,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Helper function to duplicate relations from one object to another
|
||||
* @param $sourceObject the source object to duplicate from
|
||||
* @param $destinationObject the destination object to populate with the duplicated relations
|
||||
* @param $name the name of the relation to duplicate (e.g. members)
|
||||
* @param $name the name of the relation to duplicate (e.g. members)
|
||||
*/
|
||||
private function duplicateRelations($sourceObject, $destinationObject, $name) {
|
||||
$relations = $sourceObject->$name();
|
||||
@ -570,7 +589,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if (!ClassInfo::exists($className)) return get_class($this);
|
||||
return $className;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the ClassName attribute. {@link $class} is also updated.
|
||||
* Warning: This will produce an inconsistent record, as the object
|
||||
@ -596,7 +615,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* it ensures that the instance of the class is a match for the className of the
|
||||
* record. Don't set the {@link DataObject->class} or {@link DataObject->ClassName}
|
||||
* property manually before calling this method, as it will confuse change detection.
|
||||
*
|
||||
*
|
||||
* If the new class is different to the original class, defaults are populated again
|
||||
* because this will only occur automatically on instantiation of a DataObject if
|
||||
* there is no record, or the record has no ID. In this case, we do have an ID but
|
||||
@ -615,7 +634,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
'RecordClassName' => $originalClass,
|
||||
)
|
||||
), false, $this->model);
|
||||
|
||||
|
||||
if($newClassName != $originalClass) {
|
||||
$newInstance->setClassName($newClassName);
|
||||
$newInstance->populateDefaults();
|
||||
@ -684,9 +703,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Returns TRUE if all values (other than "ID") are
|
||||
* considered empty (by weak boolean comparison).
|
||||
* Only checks for fields listed in {@link custom_database_fields()}
|
||||
*
|
||||
*
|
||||
* @todo Use DBField->hasValue()
|
||||
*
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEmpty(){
|
||||
@ -696,7 +715,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
foreach($map as $k=>$v){
|
||||
// only look at custom fields
|
||||
if(!array_key_exists($k, $customFields)) continue;
|
||||
|
||||
|
||||
$dbObj = ($v instanceof DBField) ? $v : $this->dbObject($k);
|
||||
$isEmpty = ($isEmpty && !$dbObj->exists());
|
||||
}
|
||||
@ -715,7 +734,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(!$name = $this->stat('singular_name')) {
|
||||
$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $this->class))));
|
||||
}
|
||||
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
@ -768,14 +787,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$name = $this->plural_name();
|
||||
return _t($this->class.'.PLURALNAME', $name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Standard implementation of a title/label for a specific
|
||||
* record. Tries to find properties 'Title' or 'Name',
|
||||
* and falls back to the 'ID'. Useful to provide
|
||||
* user-friendly identification of a record, e.g. in errormessages
|
||||
* or UI-selections.
|
||||
*
|
||||
*
|
||||
* Overload this method to have a more specialized implementation,
|
||||
* e.g. for an Address record this could be:
|
||||
* <code>
|
||||
@ -789,7 +808,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public function getTitle() {
|
||||
if($this->hasDatabaseField('Title')) return $this->getField('Title');
|
||||
if($this->hasDatabaseField('Name')) return $this->getField('Name');
|
||||
|
||||
|
||||
return "#{$this->ID}";
|
||||
}
|
||||
|
||||
@ -827,11 +846,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Update a number of fields on this object, given a map of the desired changes.
|
||||
*
|
||||
*
|
||||
* The field names can be simple names, or you can use a dot syntax to access $has_one relations.
|
||||
* For example, array("Author.FirstName" => "Jim") will set $this->Author()->FirstName to "Jim".
|
||||
*
|
||||
* update() doesn't write the main object, but if you use the dot syntax, it will write()
|
||||
*
|
||||
* update() doesn't write the main object, but if you use the dot syntax, it will write()
|
||||
* the related objects that it alters.
|
||||
*
|
||||
* @param array $data A map of field name to data values to update.
|
||||
@ -859,8 +878,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
} else {
|
||||
user_error(
|
||||
"DataObject::update(): Can't traverse relationship '$relation'," .
|
||||
"it has to be a has_one relationship or return a single DataObject",
|
||||
"DataObject::update(): Can't traverse relationship '$relation'," .
|
||||
"it has to be a has_one relationship or return a single DataObject",
|
||||
E_USER_NOTICE
|
||||
);
|
||||
// unset relation object so we don't write properties to the wrong object
|
||||
@ -884,7 +903,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pass changes as a map, and try to
|
||||
* get automatic casting for these fields.
|
||||
@ -996,7 +1015,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Forces the record to think that all its data has changed.
|
||||
* Doesn't write to the database. Only sets fields as changed
|
||||
* if they are not already marked as changed.
|
||||
*
|
||||
*
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function forceChange() {
|
||||
@ -1005,32 +1024,32 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// $this->record might not contain the blank values so we loop on $this->inheritedDatabaseFields() as well
|
||||
$fieldNames = array_unique(array_merge(
|
||||
array_keys($this->record),
|
||||
array_keys($this->record),
|
||||
array_keys($this->inheritedDatabaseFields())));
|
||||
|
||||
|
||||
foreach($fieldNames as $fieldName) {
|
||||
if(!isset($this->changed[$fieldName])) $this->changed[$fieldName] = self::CHANGE_STRICT;
|
||||
// Populate the null values in record so that they actually get written
|
||||
if(!isset($this->record[$fieldName])) $this->record[$fieldName] = null;
|
||||
}
|
||||
|
||||
|
||||
// @todo Find better way to allow versioned to write a new version after forceChange
|
||||
if($this->isChanged('Version')) unset($this->changed['Version']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate the current object.
|
||||
*
|
||||
* By default, there is no validation - objects are always valid! However, you can overload this method in your
|
||||
* DataObject sub-classes to specify custom validation, or use the hook through DataExtension.
|
||||
*
|
||||
*
|
||||
* Invalid objects won't be able to be written - a warning will be thrown and no write will occur. onBeforeWrite()
|
||||
* and onAfterWrite() won't get called either.
|
||||
*
|
||||
*
|
||||
* It is expected that you call validate() in your own application to test that an object is valid before
|
||||
* attempting a write, and respond appropriately if it isn't.
|
||||
*
|
||||
*
|
||||
* @return A {@link ValidationResult} object
|
||||
*/
|
||||
public function validate() {
|
||||
@ -1045,12 +1064,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* database. Don't forget to call parent::onBeforeWrite(), though!
|
||||
*
|
||||
* This called after {@link $this->validate()}, so you can be sure that your data is valid.
|
||||
*
|
||||
*
|
||||
* @uses DataExtension->onBeforeWrite()
|
||||
*/
|
||||
protected function onBeforeWrite() {
|
||||
$this->brokenOnWrite = false;
|
||||
|
||||
|
||||
$dummy = null;
|
||||
$this->extend('onBeforeWrite', $dummy);
|
||||
}
|
||||
@ -1077,11 +1096,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
protected function onBeforeDelete() {
|
||||
$this->brokenOnDelete = false;
|
||||
|
||||
|
||||
$dummy = null;
|
||||
$this->extend('onBeforeDelete', $dummy);
|
||||
}
|
||||
|
||||
|
||||
protected function onAfterDelete() {
|
||||
$this->extend('onAfterDelete');
|
||||
}
|
||||
@ -1090,22 +1109,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Load the default values in from the self::$defaults array.
|
||||
* Will traverse the defaults of the current class and all its parent classes.
|
||||
* Called by the constructor when creating new records.
|
||||
*
|
||||
*
|
||||
* @uses DataExtension->populateDefaults()
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function populateDefaults() {
|
||||
$classes = array_reverse(ClassInfo::ancestry($this));
|
||||
|
||||
|
||||
foreach($classes as $class) {
|
||||
$defaults = Config::inst()->get($class, 'defaults', Config::UNINHERITED);
|
||||
|
||||
|
||||
if($defaults && !is_array($defaults)) {
|
||||
user_error("Bad '$this->class' defaults given: " . var_export($defaults, true),
|
||||
E_USER_WARNING);
|
||||
$defaults = null;
|
||||
}
|
||||
|
||||
|
||||
if($defaults) foreach($defaults as $fieldName => $fieldValue) {
|
||||
// SRM 2007-03-06: Stricter check
|
||||
if(!isset($this->$fieldName) || $this->$fieldName === null) {
|
||||
@ -1121,18 +1140,194 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->extend('populateDefaults');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine validation of this object prior to write
|
||||
*
|
||||
* @return ValidationException Exception generated by this write, or null if valid
|
||||
*/
|
||||
protected function validateWrite() {
|
||||
if ($this->ObsoleteClassName) {
|
||||
return new ValidationException(
|
||||
"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
|
||||
"you need to change the ClassName before you can write it",
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
if(Config::inst()->get('DataObject', 'validation_enabled')) {
|
||||
$result = $this->validate();
|
||||
if (!$result->valid()) {
|
||||
return new ValidationException(
|
||||
$result,
|
||||
$result->message(),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare an object prior to write
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function preWrite() {
|
||||
// Validate this object
|
||||
if($writeException = $this->validateWrite()) {
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
throw $writeException;
|
||||
}
|
||||
|
||||
// Check onBeforeWrite
|
||||
$this->brokenOnWrite = true;
|
||||
$this->onBeforeWrite();
|
||||
if($this->brokenOnWrite) {
|
||||
user_error("$this->class has a broken onBeforeWrite() function."
|
||||
. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects and updates all changes made to this object
|
||||
*
|
||||
* @param bool $forceChanges If set to true, force all fields to be treated as changed
|
||||
* @return bool True if any changes are detected
|
||||
*/
|
||||
protected function updateChanges($forceChanges = false) {
|
||||
// Update the changed array with references to changed obj-fields
|
||||
foreach($this->record as $field => $value) {
|
||||
// Only mark ID as changed if $forceChanges
|
||||
if($field === 'ID' && !$forceChanges) continue;
|
||||
// Determine if this field should be forced, or can mark itself, changed
|
||||
if($forceChanges
|
||||
|| !$this->isInDB()
|
||||
|| (is_object($value) && method_exists($value, 'isChanged') && $value->isChanged())
|
||||
) {
|
||||
$this->changed[$field] = self::CHANGE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
// Check changes exist, abort if there are no changes
|
||||
return $this->changed && (bool)array_filter($this->changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a subset of changes for a specific table to the given manipulation
|
||||
*
|
||||
* @param string $baseTable Base table
|
||||
* @param string $now Timestamp to use for the current time
|
||||
* @param bool $isNewRecord Whether this should be treated as a new record write
|
||||
* @param array $manipulation Manipulation to write to
|
||||
* @param string $class Table and Class to select and write to
|
||||
*/
|
||||
protected function prepareManipulationTable($baseTable, $now, $isNewRecord, &$manipulation, $class) {
|
||||
$manipulation[$class] = array();
|
||||
|
||||
// Extract records for this table
|
||||
foreach($this->record as $fieldName => $fieldValue) {
|
||||
|
||||
// Check if this record pertains to this table, and
|
||||
// we're not attempting to reset the BaseTable->ID
|
||||
if( empty($this->changed[$fieldName])
|
||||
|| ($class === $baseTable && $fieldName === 'ID')
|
||||
|| (!self::has_own_table_database_field($class, $fieldName)
|
||||
&& !self::is_composite_field($class, $fieldName, false))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if database column doesn't correlate to a DBField instance...
|
||||
$fieldObj = $this->dbObject($fieldName);
|
||||
if(!$fieldObj) {
|
||||
$fieldObj = DBField::create_field('Varchar', $fieldValue, $fieldName);
|
||||
}
|
||||
|
||||
// Ensure DBField is repopulated and written to the manipulation
|
||||
$fieldObj->setValue($fieldValue, $this->record);
|
||||
$fieldObj->writeToManipulation($manipulation[$class]);
|
||||
}
|
||||
|
||||
// Ensure update of Created and LastEdited columns
|
||||
if($baseTable === $class) {
|
||||
$manipulation[$class]['fields']['LastEdited'] = $now;
|
||||
if($isNewRecord) {
|
||||
$manipulation[$class]['fields']['Created']
|
||||
= empty($this->record['Created'])
|
||||
? $now
|
||||
: $this->record['Created'];
|
||||
$manipulation[$class]['fields']['ClassName'] = $this->class;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
$manipulation[$class]['command'] = $isNewRecord ? 'insert' : 'update';
|
||||
$manipulation[$class]['id'] = $this->record['ID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a blank base record exists with the basic fixed fields for this dataobject
|
||||
*
|
||||
* Does nothing if an ID is already assigned for this record
|
||||
*
|
||||
* @param string $baseTable Base table
|
||||
* @param string $now Timestamp to use for the current time
|
||||
*/
|
||||
protected function writeBaseRecord($baseTable, $now) {
|
||||
// Generate new ID if not specified
|
||||
if($this->isInDB()) return;
|
||||
|
||||
// Perform an insert on the base table
|
||||
$insert = new SQLInsert('"'.$baseTable.'"');
|
||||
$insert
|
||||
->assign('"Created"', $now)
|
||||
->execute();
|
||||
$this->changed['ID'] = self::CHANGE_VALUE;
|
||||
$this->record['ID'] = DB::get_generated_id($baseTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and write the database manipulation for all changed fields
|
||||
*
|
||||
* @param string $baseTable Base table
|
||||
* @param string $now Timestamp to use for the current time
|
||||
* @param bool $isNewRecord If this is a new record
|
||||
*/
|
||||
protected function writeManipulation($baseTable, $now, $isNewRecord) {
|
||||
// Generate database manipulations for each class
|
||||
$manipulation = array();
|
||||
foreach($this->getClassAncestry() as $class) {
|
||||
if(self::has_own_table($class)) {
|
||||
$this->prepareManipulationTable($baseTable, $now, $isNewRecord, $manipulation, $class);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow extensions to extend this manipulation
|
||||
$this->extend('augmentWrite', $manipulation);
|
||||
|
||||
// New records have their insert into the base data table done first, so that they can pass the
|
||||
// generated ID on to the rest of the manipulation
|
||||
if($isNewRecord) {
|
||||
$manipulation[$baseTable]['command'] = 'update';
|
||||
}
|
||||
|
||||
// Perform the manipulation
|
||||
DB::manipulate($manipulation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes all changes to this object to the database.
|
||||
* - It will insert a record whenever ID isn't set, otherwise update.
|
||||
* - All relevant tables will be updated.
|
||||
* - $this->onBeforeWrite() gets called beforehand.
|
||||
* - Extensions such as Versioned will ammend the database-write to ensure that a version is saved.
|
||||
*
|
||||
*
|
||||
* @uses DataExtension->augmentWrite()
|
||||
*
|
||||
* @param boolean $showDebug Show debugging information
|
||||
@ -1145,198 +1340,66 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @throws ValidationException Exception that can be caught and handled by the calling function
|
||||
*/
|
||||
public function write($showDebug = false, $forceInsert = false, $forceWrite = false, $writeComponents = false) {
|
||||
$firstWrite = false;
|
||||
$this->brokenOnWrite = true;
|
||||
$isNewRecord = false;
|
||||
$now = SS_Datetime::now()->Rfc2822();
|
||||
|
||||
$writeException = null;
|
||||
// Execute pre-write tasks
|
||||
$this->preWrite();
|
||||
|
||||
if ($this->ObsoleteClassName) {
|
||||
$writeException = new ValidationException(
|
||||
"Object is of class '{$this->ObsoleteClassName}' which doesn't exist - ".
|
||||
"you need to change the ClassName before you can write it",
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
else if(Config::inst()->get('DataObject', 'validation_enabled')) {
|
||||
$valid = $this->validate();
|
||||
if (!$valid->valid()) {
|
||||
$writeException = new ValidationException(
|
||||
$valid,
|
||||
$valid->message(),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
}
|
||||
// Check if we are doing an update or an insert
|
||||
$isNewRecord = !$this->isInDB() || $forceInsert;
|
||||
|
||||
if($writeException) {
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
throw $writeException;
|
||||
return false;
|
||||
}
|
||||
// Check changes exist, abort if there are none
|
||||
$hasChanges = $this->updateChanges($forceInsert);
|
||||
if($hasChanges || $forceWrite || $isNewRecord) {
|
||||
// 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
|
||||
$baseTable = ClassInfo::baseDataClass($this->class);
|
||||
$this->writeBaseRecord($baseTable, $now);
|
||||
|
||||
$this->onBeforeWrite();
|
||||
if($this->brokenOnWrite) {
|
||||
user_error("$this->class has a broken onBeforeWrite() function."
|
||||
. " Make sure that you call parent::onBeforeWrite().", E_USER_ERROR);
|
||||
}
|
||||
|
||||
// New record = everything has changed
|
||||
|
||||
if(($this->ID && is_numeric($this->ID)) && !$forceInsert) {
|
||||
$dbCommand = 'update';
|
||||
|
||||
// Update the changed array with references to changed obj-fields
|
||||
foreach($this->record as $k => $v) {
|
||||
if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
|
||||
$this->changed[$k] = self::CHANGE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
} else{
|
||||
$dbCommand = 'insert';
|
||||
// Write the DB manipulation for all changed fields
|
||||
$this->writeManipulation($baseTable, $now, $isNewRecord);
|
||||
|
||||
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
|
||||
$this->writeRelations();
|
||||
$this->onAfterWrite();
|
||||
$this->changed = array();
|
||||
foreach($this->record as $k => $v) {
|
||||
$this->changed[$k] = self::CHANGE_VALUE;
|
||||
}
|
||||
|
||||
$firstWrite = true;
|
||||
}
|
||||
|
||||
// No changes made
|
||||
if($this->changed || $forceWrite) {
|
||||
foreach($this->getClassAncestry() as $ancestor) {
|
||||
if(self::has_own_table($ancestor))
|
||||
$ancestry[] = $ancestor;
|
||||
}
|
||||
|
||||
// Look for some changes to make
|
||||
if(!$forceInsert) unset($this->changed['ID']);
|
||||
|
||||
$hasChanges = false;
|
||||
if (!$forceWrite) {
|
||||
foreach ($this->changed as $fieldName => $changed) {
|
||||
if ($changed) {
|
||||
$hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if($hasChanges || $forceWrite || !$this->record['ID']) {
|
||||
|
||||
// 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
|
||||
$baseTable = $ancestry[0];
|
||||
|
||||
if((!isset($this->record['ID']) || !$this->record['ID']) && isset($ancestry[0])) {
|
||||
|
||||
DB::query("INSERT INTO \"{$baseTable}\" (\"Created\") VALUES (" . DB::getConn()->now() . ")");
|
||||
$this->record['ID'] = DB::getGeneratedID($baseTable);
|
||||
$this->changed['ID'] = self::CHANGE_VALUE;
|
||||
|
||||
$isNewRecord = true;
|
||||
}
|
||||
|
||||
// Divvy up field saving into a number of database manipulations
|
||||
$manipulation = array();
|
||||
if(isset($ancestry) && is_array($ancestry)) {
|
||||
foreach($ancestry as $idx => $class) {
|
||||
$classSingleton = singleton($class);
|
||||
|
||||
foreach($this->record as $fieldName => $fieldValue) {
|
||||
if(isset($this->changed[$fieldName]) && $this->changed[$fieldName]
|
||||
&& $fieldType = $classSingleton->hasOwnTableDatabaseField($fieldName)) {
|
||||
|
||||
$fieldObj = $this->dbObject($fieldName);
|
||||
if(!isset($manipulation[$class])) $manipulation[$class] = array();
|
||||
|
||||
// if database column doesn't correlate to a DBField instance...
|
||||
if(!$fieldObj) {
|
||||
$fieldObj = DBField::create_field('Varchar', $this->record[$fieldName], $fieldName);
|
||||
}
|
||||
|
||||
// Both CompositeDBFields and regular fields need to be repopulated
|
||||
$fieldObj->setValue($this->record[$fieldName], $this->record);
|
||||
|
||||
if($class != $baseTable || $fieldName!='ID')
|
||||
$fieldObj->writeToManipulation($manipulation[$class]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the class name to the base object
|
||||
if($idx == 0) {
|
||||
$manipulation[$class]['fields']["LastEdited"] = "'".SS_Datetime::now()->Rfc2822()."'";
|
||||
if($dbCommand == 'insert') {
|
||||
if(!empty($this->record["Created"])) {
|
||||
$manipulation[$class]['fields']["Created"]
|
||||
= DB::getConn()->prepStringForDB($this->record["Created"]);
|
||||
} else {
|
||||
$manipulation[$class]['fields']["Created"]
|
||||
= DB::getConn()->prepStringForDB(SS_Datetime::now()->Rfc2822());
|
||||
}
|
||||
//echo "<li>$this->class - " .get_class($this);
|
||||
$manipulation[$class]['fields']["ClassName"]
|
||||
= DB::getConn()->prepStringForDB($this->class);
|
||||
}
|
||||
}
|
||||
|
||||
// In cases where there are no fields, this 'stub' will get picked up on
|
||||
if(self::has_own_table($class)) {
|
||||
$manipulation[$class]['command'] = $dbCommand;
|
||||
$manipulation[$class]['id'] = $this->record['ID'];
|
||||
} else {
|
||||
unset($manipulation[$class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->extend('augmentWrite', $manipulation);
|
||||
|
||||
// New records have their insert into the base data table done first, so that they can pass the
|
||||
// generated ID on to the rest of the manipulation
|
||||
if(isset($isNewRecord) && $isNewRecord && isset($manipulation[$baseTable])) {
|
||||
$manipulation[$baseTable]['command'] = 'update';
|
||||
}
|
||||
|
||||
DB::manipulate($manipulation);
|
||||
|
||||
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
|
||||
if($this->unsavedRelations) {
|
||||
foreach($this->unsavedRelations as $name => $list) {
|
||||
$list->changeToList($this->$name());
|
||||
}
|
||||
$this->unsavedRelations = array();
|
||||
}
|
||||
|
||||
$this->onAfterWrite();
|
||||
|
||||
$this->changed = null;
|
||||
} elseif ( $showDebug ) {
|
||||
echo "<b>Debug:</b> no changes for DataObject<br />";
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
}
|
||||
|
||||
// Clears the cache for this object so get_one returns the correct object.
|
||||
$this->flushCache();
|
||||
|
||||
if(!isset($this->record['Created'])) {
|
||||
$this->record['Created'] = SS_Datetime::now()->Rfc2822();
|
||||
}
|
||||
$this->record['LastEdited'] = SS_Datetime::now()->Rfc2822();
|
||||
} else {
|
||||
if($showDebug) Debug::message("no changes for DataObject");
|
||||
|
||||
// Used by DODs to clean up after themselves, eg, Versioned
|
||||
$this->invokeWithExtensions('onAfterSkippedWrite');
|
||||
}
|
||||
|
||||
// Ensure Created and LastEdited are populated
|
||||
if(!isset($this->record['Created'])) {
|
||||
$this->record['Created'] = $now;
|
||||
}
|
||||
$this->record['LastEdited'] = $now;
|
||||
|
||||
// Write relations as necessary
|
||||
if($writeComponents) {
|
||||
$this->writeComponents(true);
|
||||
}
|
||||
if($writeComponents) $this->writeComponents(true);
|
||||
|
||||
// Clears the cache for this object so get_one returns the correct object.
|
||||
$this->flushCache();
|
||||
|
||||
return $this->record['ID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes cached relation lists to the database, if possible
|
||||
*/
|
||||
public function writeRelations() {
|
||||
if(!$this->isInDB()) return;
|
||||
|
||||
// If there's any relations that couldn't be saved before, save them now (we have an ID here)
|
||||
if($this->unsavedRelations) {
|
||||
foreach($this->unsavedRelations as $name => $list) {
|
||||
$list->changeToList($this->$name());
|
||||
}
|
||||
$this->unsavedRelations = array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cached components to the database. Cached components could refer to two different instances of the
|
||||
* same record.
|
||||
@ -1346,13 +1409,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function writeComponents($recursive = false) {
|
||||
if(!$this->components) return $this;
|
||||
|
||||
|
||||
foreach($this->components as $component) {
|
||||
$component->write(false, false, false, $recursive);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete this data object.
|
||||
* $this->onBeforeDelete() gets called.
|
||||
@ -1366,7 +1429,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
user_error("$this->class has a broken onBeforeDelete() function."
|
||||
. " Make sure that you call parent::onBeforeDelete().", E_USER_ERROR);
|
||||
}
|
||||
|
||||
|
||||
// Deleting a record without an ID shouldn't do anything
|
||||
if(!$this->ID) throw new LogicException("DataObject::delete() called on a DataObject without an ID");
|
||||
|
||||
@ -1376,14 +1439,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// obviously, that means getting requireTable() to configure cascading deletes ;-)
|
||||
$srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query();
|
||||
foreach($srcQuery->queriedTables() as $table) {
|
||||
$query = new SQLQuery("*", array('"' . $table . '"'));
|
||||
$query->setWhere("\"ID\" = $this->ID");
|
||||
$query->setDelete(true);
|
||||
$query->execute();
|
||||
$delete = new SQLDelete("\"$table\"", array('"ID"' => $this->ID));
|
||||
$delete->execute();
|
||||
}
|
||||
// Remove this item out of any caches
|
||||
$this->flushCache();
|
||||
|
||||
|
||||
$this->onAfterDelete();
|
||||
|
||||
$this->OldID = $this->ID;
|
||||
@ -1436,17 +1497,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(isset($this->components[$componentName])) {
|
||||
return $this->components[$componentName];
|
||||
}
|
||||
|
||||
|
||||
if($class = $this->has_one($componentName)) {
|
||||
$joinField = $componentName . 'ID';
|
||||
$joinID = $this->getField($joinField);
|
||||
|
||||
|
||||
// Extract class name for polymorphic relations
|
||||
if($class === 'DataObject') {
|
||||
$class = $this->getField($componentName . 'Class');
|
||||
if(empty($class)) return null;
|
||||
}
|
||||
|
||||
|
||||
if($joinID) {
|
||||
$component = $this->model->$class->byID($joinID);
|
||||
}
|
||||
@ -1455,18 +1516,23 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$component = $this->model->$class->newObject();
|
||||
}
|
||||
} elseif($class = $this->belongs_to($componentName)) {
|
||||
|
||||
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic);
|
||||
$joinID = $this->ID;
|
||||
|
||||
|
||||
if($joinID) {
|
||||
|
||||
$filter = $polymorphic
|
||||
? "\"{$joinField}ID\" = '".Convert::raw2sql($joinID)."' AND
|
||||
\"{$joinField}Class\" = '".Convert::raw2sql($this->class)."'"
|
||||
: "\"{$joinField}\" = '".Convert::raw2sql($joinID)."'";
|
||||
$component = DataObject::get_one($class, $filter);
|
||||
? array(
|
||||
"{$joinField}ID" => $joinID,
|
||||
"{$joinField}Class" => $this->class
|
||||
)
|
||||
: array(
|
||||
$joinField => $joinID
|
||||
);
|
||||
$component = DataObject::get($class)->filter($filter)->first();
|
||||
}
|
||||
|
||||
|
||||
if(empty($component)) {
|
||||
$component = $this->model->$class->newObject();
|
||||
if($polymorphic) {
|
||||
@ -1479,7 +1545,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
} else {
|
||||
throw new Exception("DataObject->getComponent(): Could not find component '$componentName'.");
|
||||
}
|
||||
|
||||
|
||||
$this->components[$componentName] = $component;
|
||||
return $component;
|
||||
}
|
||||
@ -1514,13 +1580,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(!$this->ID) {
|
||||
if(!isset($this->unsavedRelations[$componentName])) {
|
||||
$this->unsavedRelations[$componentName] =
|
||||
new UnsavedRelationList($this->class, $componentName, $componentClass);
|
||||
new UnsavedRelationList($this->class, $componentName, $componentClass);
|
||||
}
|
||||
return $this->unsavedRelations[$componentName];
|
||||
}
|
||||
|
||||
// Determine type and nature of foreign relation
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
|
||||
$joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic);
|
||||
if($polymorphic) {
|
||||
$result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class);
|
||||
} else {
|
||||
@ -1578,7 +1644,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Tries to find the database key on another object that is used to store a
|
||||
* relationship to this class. If no join field can be found it defaults to 'ParentID'.
|
||||
*
|
||||
*
|
||||
* If the remote field is polymorphic then $polymorphic is set to true, and the return value
|
||||
* is in the form 'Relation' instead of 'RelationID', referencing the composite DBField.
|
||||
*
|
||||
@ -1589,7 +1655,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) {
|
||||
|
||||
// Extract relation from current object
|
||||
$remoteClass = $this->$type($component, false);
|
||||
if(empty($remoteClass)) {
|
||||
@ -1600,16 +1665,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
"Class '$remoteClass' not found, but used in $type component '$component' on class '$this->class'"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// If presented with an explicit field name (using dot notation) then extract field name
|
||||
$remoteField = null;
|
||||
if(strpos($remoteClass, '.') !== false) {
|
||||
list($remoteClass, $remoteField) = explode('.', $remoteClass);
|
||||
}
|
||||
|
||||
|
||||
// Reference remote has_one to check against
|
||||
$remoteRelations = Config::inst()->get($remoteClass, 'has_one');
|
||||
|
||||
|
||||
// Without an explicit field name, attempt to match the first remote field
|
||||
// with the same type as the current class
|
||||
if(empty($remoteField)) {
|
||||
@ -1622,7 +1687,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// In case of an indeterminate remote field show an error
|
||||
if(empty($remoteField)) {
|
||||
$polymorphic = false;
|
||||
@ -1634,10 +1699,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
|
||||
// If given an explicit field name ensure the related class specifies this
|
||||
if(empty($remoteRelations[$remoteField])) {
|
||||
throw new Exception("Missing expected has_one named '$remoteField'
|
||||
throw new Exception("Missing expected has_one named '$remoteField'
|
||||
on class '$remoteClass' referenced by $type named '$component'
|
||||
on class {$this->class}"
|
||||
);
|
||||
@ -1652,7 +1717,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return $remoteField . 'ID';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a many-to-many component, as a ManyManyList.
|
||||
* @param string $componentName Name of the many-many component
|
||||
@ -1667,11 +1732,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(!$this->ID) {
|
||||
if(!isset($this->unsavedRelations[$componentName])) {
|
||||
$this->unsavedRelations[$componentName] =
|
||||
new UnsavedRelationList($parentClass, $componentName, $componentClass);
|
||||
new UnsavedRelationList($parentClass, $componentName, $componentClass);
|
||||
}
|
||||
return $this->unsavedRelations[$componentName];
|
||||
}
|
||||
|
||||
|
||||
$result = ManyManyList::create(
|
||||
$componentClass, $table, $componentField, $parentField,
|
||||
$this->many_many_extraFields($componentName)
|
||||
@ -1686,9 +1751,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
->sort($sort)
|
||||
->limit($limit);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
||||
* Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and
|
||||
* their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type.
|
||||
*
|
||||
* @param string $component Name of component
|
||||
@ -1705,7 +1770,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
if($component) {
|
||||
$hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED);
|
||||
|
||||
|
||||
if(isset($hasOne[$component])) {
|
||||
return $hasOne[$component];
|
||||
}
|
||||
@ -1714,7 +1779,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
|
||||
user_error("$class::\$has_one has a bad entry: "
|
||||
user_error("$class::\$has_one has a bad entry: "
|
||||
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
|
||||
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
|
||||
}
|
||||
@ -1724,7 +1789,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
return isset($items) ? $items : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and
|
||||
* their class name will be returned.
|
||||
@ -1736,7 +1801,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function belongs_to($component = null, $classOnly = true) {
|
||||
$belongsTo = $this->config()->belongs_to;
|
||||
|
||||
|
||||
if($component) {
|
||||
if($belongsTo && array_key_exists($component, $belongsTo)) {
|
||||
$belongsTo = $belongsTo[$component];
|
||||
@ -1744,14 +1809,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($belongsTo && $classOnly) {
|
||||
return preg_replace('/(.+)?\..+/', '$1', $belongsTo);
|
||||
} else {
|
||||
return $belongsTo ? $belongsTo : array();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return all of the database fields defined in self::$db and all the parent classes.
|
||||
* Doesn't include any fields specified by self::$has_one. Use $this->has_one() to get these fields
|
||||
@ -1788,7 +1853,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Validate the data
|
||||
foreach($dbItems as $k => $v) {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
|
||||
user_error("$class::\$db has a bad entry: "
|
||||
user_error("$class::\$db has a bad entry: "
|
||||
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
|
||||
. " property name, and the map value should be the property type.", E_USER_ERROR);
|
||||
}
|
||||
@ -1812,7 +1877,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function has_many($component = null, $classOnly = true) {
|
||||
$hasMany = $this->config()->has_many;
|
||||
|
||||
|
||||
if($component) {
|
||||
if($hasMany && array_key_exists($component, $hasMany)) {
|
||||
$hasMany = $hasMany[$component];
|
||||
@ -1820,7 +1885,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($hasMany && $classOnly) {
|
||||
return preg_replace('/(.+)?\..+/', '$1', $hasMany);
|
||||
} else {
|
||||
@ -1830,10 +1895,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Return the many-to-many extra fields specification.
|
||||
*
|
||||
*
|
||||
* If you don't specify a component name, it returns all
|
||||
* extra fields for all components available.
|
||||
*
|
||||
*
|
||||
* @param string $component Name of component
|
||||
* @return array
|
||||
*/
|
||||
@ -1853,13 +1918,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(isset($extraFields[$component])) {
|
||||
return $extraFields[$component];
|
||||
}
|
||||
|
||||
|
||||
$manyMany = $SNG_class->stat('many_many');
|
||||
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
|
||||
if($candidate) {
|
||||
$SNG_candidate = singleton($candidate);
|
||||
$candidateManyMany = $SNG_candidate->stat('belongs_many_many');
|
||||
|
||||
|
||||
// Find the relation given the class
|
||||
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
|
||||
if($relatedClass == $class) {
|
||||
@ -1867,7 +1932,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($relationName) {
|
||||
$extraFields = $SNG_candidate->stat('many_many_extraFields');
|
||||
if(isset($extraFields[$relationName])) {
|
||||
@ -1875,31 +1940,31 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$manyMany = $SNG_class->stat('belongs_many_many');
|
||||
$candidate = (isset($manyMany[$component])) ? $manyMany[$component] : null;
|
||||
if($candidate) {
|
||||
$SNG_candidate = singleton($candidate);
|
||||
$candidateManyMany = $SNG_candidate->stat('many_many');
|
||||
|
||||
|
||||
// Find the relation given the class
|
||||
if($candidateManyMany) foreach($candidateManyMany as $relation => $relatedClass) {
|
||||
if($relatedClass == $class) {
|
||||
$relationName = $relation;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$extraFields = $SNG_candidate->stat('many_many_extraFields');
|
||||
if(isset($extraFields[$relationName])) {
|
||||
return $extraFields[$relationName];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
// Find all the extra fields for all components
|
||||
$newItems = eval("return (array){$class}::\$many_many_extraFields;");
|
||||
|
||||
|
||||
foreach($newItems as $k => $v) {
|
||||
if(!is_array($v)) {
|
||||
user_error(
|
||||
@ -1910,12 +1975,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return isset($items) ? array_merge($newItems, $items) : $newItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return information about a many-to-many component.
|
||||
* The return value is an array of (parentclass, childclass). If $component is null, then all many-many
|
||||
@ -1969,18 +2034,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
|
||||
user_error("$class::\$many_many has a bad entry: "
|
||||
user_error("$class::\$many_many has a bad entry: "
|
||||
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
|
||||
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
|
||||
|
||||
|
||||
$newItems = (array)Config::inst()->get($class, 'belongs_many_many', Config::UNINHERITED);
|
||||
// Validate the data
|
||||
foreach($newItems as $k => $v) {
|
||||
if(!is_string($k) || is_numeric($k) || !is_string($v)) {
|
||||
user_error("$class::\$belongs_many_many has a bad entry: "
|
||||
user_error("$class::\$belongs_many_many has a bad entry: "
|
||||
. var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a"
|
||||
. " relationship name, and the map value should be the data class to join to.", E_USER_ERROR);
|
||||
}
|
||||
@ -1989,20 +2054,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$items = isset($items) ? array_merge($newItems, $items) : $newItems;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return isset($items) ? $items : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This returns an array (if it exists) describing the database extensions that are required, or false if none
|
||||
*
|
||||
*
|
||||
* This is experimental, and is currently only a Postgres-specific enhancement.
|
||||
*
|
||||
*
|
||||
* @return array or false
|
||||
*/
|
||||
public function database_extensions($class){
|
||||
$extensions = Config::inst()->get($class, 'database_extensions', Config::UNINHERITED);
|
||||
|
||||
|
||||
if($extensions)
|
||||
return $extensions;
|
||||
else
|
||||
@ -2017,12 +2082,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function getDefaultSearchContext() {
|
||||
return new SearchContext(
|
||||
$this->class,
|
||||
$this->scaffoldSearchFields(),
|
||||
$this->class,
|
||||
$this->scaffoldSearchFields(),
|
||||
$this->defaultSearchFilters()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine which properties on the DataObject are
|
||||
* searchable, and map them to their default {@link FormField}
|
||||
@ -2032,7 +2097,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* how generic or specific the field type is.
|
||||
*
|
||||
* Used by {@link SearchContext}.
|
||||
*
|
||||
*
|
||||
* @param array $_params
|
||||
* 'fieldClasses': Associative array of field names as keys and FormField classes as values
|
||||
* 'restrictFields': Numeric array of a field name whitelist
|
||||
@ -2049,7 +2114,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$fields = new FieldList();
|
||||
foreach($this->searchableFields() as $fieldName => $spec) {
|
||||
if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue;
|
||||
|
||||
|
||||
// If a custom fieldclass is provided as a string, use it
|
||||
if($params['fieldClasses'] && isset($params['fieldClasses'][$fieldName])) {
|
||||
$fieldClass = $params['fieldClasses'][$fieldName];
|
||||
@ -2060,17 +2125,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(is_string($spec['field'])) {
|
||||
$fieldClass = $spec['field'];
|
||||
$field = new $fieldClass($fieldName);
|
||||
|
||||
|
||||
// If it's a FormField object, then just use that object directly.
|
||||
} else if($spec['field'] instanceof FormField) {
|
||||
$field = $spec['field'];
|
||||
|
||||
|
||||
// Otherwise we have a bug
|
||||
} else {
|
||||
user_error("Bad value for searchable_fields, 'field' value: "
|
||||
. var_export($spec['field'], true), E_USER_WARNING);
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, use the database field's scaffolder
|
||||
} else {
|
||||
$field = $this->relObject($fieldName)->scaffoldSearchField();
|
||||
@ -2092,7 +2157,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
|
||||
*
|
||||
* @uses FormScaffolder
|
||||
*
|
||||
*
|
||||
* @param array $_params Associative array passing through properties to {@link FormScaffolder}.
|
||||
* @return FieldList
|
||||
*/
|
||||
@ -2107,27 +2172,27 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
),
|
||||
(array)$_params
|
||||
);
|
||||
|
||||
|
||||
$fs = new FormScaffolder($this);
|
||||
$fs->tabbed = $params['tabbed'];
|
||||
$fs->includeRelations = $params['includeRelations'];
|
||||
$fs->restrictFields = $params['restrictFields'];
|
||||
$fs->fieldClasses = $params['fieldClasses'];
|
||||
$fs->ajaxSafe = $params['ajaxSafe'];
|
||||
|
||||
|
||||
return $fs->getFieldList();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allows user code to hook into DataObject::getCMSFields prior to updateCMSFields
|
||||
* being called on extensions
|
||||
*
|
||||
*
|
||||
* @param callable $callback The callback to execute
|
||||
*/
|
||||
protected function beforeUpdateCMSFields($callback) {
|
||||
$this->beforeExtending('updateCMSFields', $callback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Centerpiece of every data administration interface in Silverstripe,
|
||||
* which returns a {@link FieldList} suitable for a {@link Form} object.
|
||||
@ -2158,16 +2223,16 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
'tabbed' => true,
|
||||
'ajaxSafe' => true
|
||||
));
|
||||
|
||||
|
||||
$this->extend('updateCMSFields', $tabbedFields);
|
||||
|
||||
|
||||
return $tabbedFields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* need to be overload by solid dataobject, so that the customised actions of that dataobject,
|
||||
* including that dataobject's extensions customised actions could be added to the EditForm.
|
||||
*
|
||||
*
|
||||
* @return an Empty FieldList(); need to be overload by solid subclass
|
||||
*/
|
||||
public function getCMSActions() {
|
||||
@ -2175,14 +2240,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$this->extend('updateCMSActions', $actions);
|
||||
return $actions;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Used for simple frontend forms without relation editing
|
||||
* or {@link TabSet} behaviour. Uses {@link scaffoldFormFields()}
|
||||
* by default. To customize, either overload this method in your
|
||||
* subclass, or extend it by {@link DataExtension->updateFrontEndFields()}.
|
||||
*
|
||||
*
|
||||
* @todo Decide on naming for "website|frontend|site|page" and stick with it in the API
|
||||
*
|
||||
* @param array $params See {@link scaffoldFormFields()}
|
||||
@ -2191,7 +2256,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public function getFrontEndFields($params = null) {
|
||||
$untabbedFields = $this->scaffoldFormFields($params);
|
||||
$this->extend('updateFrontEndFields', $untabbedFields);
|
||||
|
||||
|
||||
return $untabbedFields;
|
||||
}
|
||||
|
||||
@ -2230,7 +2295,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// or a valid record has been loaded from the database
|
||||
$value = (isset($this->record[$field])) ? $this->record[$field] : null;
|
||||
if($value || $this->exists()) $fieldObj->setValue($value, $this->record, false);
|
||||
|
||||
|
||||
$this->record[$field] = $fieldObj;
|
||||
|
||||
return $this->record[$field];
|
||||
@ -2260,7 +2325,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
$dataQuery = new DataQuery($tableClass);
|
||||
|
||||
|
||||
// Reset query parameter context to that of this DataObject
|
||||
if($params = $this->getSourceQueryParams()) {
|
||||
foreach($params as $key => $value) $dataQuery->setQueryParam($key, $value);
|
||||
@ -2279,7 +2344,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
// Add SQL for fields, both simple & multi-value
|
||||
// TODO: This is copy & pasted from buildSQL(), it could be moved into a method
|
||||
$databaseFields = self::database_fields($tableClass);
|
||||
$databaseFields = self::database_fields($tableClass, false);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if(!isset($this->record[$k]) || $this->record[$k] === null) {
|
||||
$columns[] = $k;
|
||||
@ -2317,7 +2382,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Return the fields that have changed.
|
||||
*
|
||||
*
|
||||
* The change level affects what the functions defines as "changed":
|
||||
* - Level CHANGE_STRICT (integer 1) will return strict changes, even !== ones.
|
||||
* - Level CHANGE_VALUE (integer 2) is more lenient, it will only return real data changes,
|
||||
@ -2336,14 +2401,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public function getChangedFields($databaseFieldsOnly = false, $changeLevel = self::CHANGE_STRICT) {
|
||||
$changedFields = array();
|
||||
|
||||
|
||||
// Update the changed array with references to changed obj-fields
|
||||
foreach($this->record as $k => $v) {
|
||||
if(is_object($v) && method_exists($v, 'isChanged') && $v->isChanged()) {
|
||||
$this->changed[$k] = self::CHANGE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($databaseFieldsOnly) {
|
||||
$databaseFields = $this->inheritedDatabaseFields();
|
||||
$databaseFields['ID'] = true;
|
||||
@ -2363,7 +2428,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($fields) foreach($fields as $name => $level) {
|
||||
$changedFields[$name] = array(
|
||||
'before' => array_key_exists($name, $this->original) ? $this->original[$name] : null,
|
||||
@ -2374,11 +2439,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
return $changedFields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uses {@link getChangedFields()} to determine if fields have been changed
|
||||
* since loading them from the database.
|
||||
*
|
||||
*
|
||||
* @param string $fieldName Name of the database field to check, will check for any if not given
|
||||
* @param int $changeLevel See {@link getChangedFields()}
|
||||
* @return boolean
|
||||
@ -2387,7 +2452,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$changed = $this->getChangedFields(false, $changeLevel);
|
||||
if(!isset($fieldName)) {
|
||||
return !empty($changed);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return array_key_exists($fieldName, $changed);
|
||||
}
|
||||
@ -2418,15 +2483,14 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if(is_object($val) && $this->db($fieldName)) {
|
||||
user_error('DataObject::setField: passed an object that is not a DBField', E_USER_WARNING);
|
||||
}
|
||||
|
||||
$defaults = $this->stat('defaults');
|
||||
|
||||
// if a field is not existing or has strictly changed
|
||||
if(!isset($this->record[$fieldName]) || $this->record[$fieldName] !== $val) {
|
||||
// TODO Add check for php-level defaults which are not set in the db
|
||||
// TODO Add check for hidden input-fields (readonly) which are not set in the db
|
||||
// At the very least, the type has changed
|
||||
$this->changed[$fieldName] = self::CHANGE_STRICT;
|
||||
|
||||
|
||||
if((!isset($this->record[$fieldName]) && $val) || (isset($this->record[$fieldName])
|
||||
&& $this->record[$fieldName] != $val)) {
|
||||
|
||||
@ -2472,8 +2536,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given field exists in a database column on any of
|
||||
* the objects tables and optionally look up a dynamic getter with
|
||||
* Returns true if the given field exists in a database column on any of
|
||||
* the objects tables and optionally look up a dynamic getter with
|
||||
* get<fieldName>().
|
||||
*
|
||||
* @param string $field Name of the field
|
||||
@ -2500,7 +2564,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
return array_key_exists($field, $this->inheritedDatabaseFields());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the field type of the given field, if it belongs to this class, and not a parent.
|
||||
* Note that the field type will not include constructor arguments in round brackets, only the classname.
|
||||
@ -2509,58 +2573,31 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return string The field type of the given field
|
||||
*/
|
||||
public function hasOwnTableDatabaseField($field) {
|
||||
// Add base fields which are not defined in static $db
|
||||
return self::has_own_table_database_field($this->class, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field type of the given field, if it belongs to this class, and not a parent.
|
||||
* Note that the field type will not include constructor arguments in round brackets, only the classname.
|
||||
*
|
||||
* @param string $class Class name to check
|
||||
* @param string $field Name of the field
|
||||
* @return string The field type of the given field
|
||||
*/
|
||||
public static function has_own_table_database_field($class, $field) {
|
||||
// Since database_fields omits 'ID'
|
||||
if($field == "ID") return "Int";
|
||||
if($field == "ClassName" && get_parent_class($this) == "DataObject") return "Enum";
|
||||
if($field == "LastEdited" && get_parent_class($this) == "DataObject") return "SS_Datetime";
|
||||
if($field == "Created" && get_parent_class($this) == "DataObject") return "SS_Datetime";
|
||||
|
||||
// Add fields from Versioned extension
|
||||
if($field == 'Version' && $this->hasExtension('Versioned')) {
|
||||
return 'Int';
|
||||
}
|
||||
// get cached fieldmap
|
||||
$fieldMap = isset(DataObject::$cache_has_own_table_field[$this->class])
|
||||
? DataObject::$cache_has_own_table_field[$this->class] : null;
|
||||
|
||||
// if no fieldmap is cached, get all fields
|
||||
if(!$fieldMap) {
|
||||
$fieldMap = Config::inst()->get($this->class, 'db', Config::UNINHERITED);
|
||||
|
||||
// all $db fields on this specific class (no parents)
|
||||
foreach(self::composite_fields($this->class, false) as $fieldname => $fieldtype) {
|
||||
$combined_db = singleton($fieldtype)->compositeDatabaseFields();
|
||||
foreach($combined_db as $name => $type){
|
||||
$fieldMap[$fieldname.$name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
// all has_one relations on this specific class,
|
||||
// add foreign key
|
||||
|
||||
$hasOne = Config::inst()->get($this->class, 'has_one', Config::UNINHERITED);
|
||||
if($hasOne) foreach($hasOne as $fieldName => $fieldSchema) {
|
||||
if($fieldSchema === 'DataObject') {
|
||||
// For polymorphic has_one relation break into individual subfields
|
||||
$fieldMap[$fieldName . 'ID'] = "Int";
|
||||
$fieldMap[$fieldName . 'Class'] = "Enum";
|
||||
$fieldMap[$fieldName] = "PolymorphicForeignKey";
|
||||
} else {
|
||||
$fieldMap[$fieldName . 'ID'] = "ForeignKey";
|
||||
}
|
||||
}
|
||||
|
||||
// set cached fieldmap
|
||||
DataObject::$cache_has_own_table_field[$this->class] = $fieldMap;
|
||||
}
|
||||
$fieldMap = self::database_fields($class, false);
|
||||
|
||||
// Remove string-based "constructor-arguments" from the DBField definition
|
||||
if(isset($fieldMap[$field])) {
|
||||
if(is_string($fieldMap[$field])) return strtok($fieldMap[$field],'(');
|
||||
else return $fieldMap[$field]['type'];
|
||||
$spec = $fieldMap[$field];
|
||||
if(is_string($spec)) return strtok($spec,'(');
|
||||
else return $spec['type'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if given class has its own table. Uses the rules for whether the table should exist rather than
|
||||
* actually looking in the database.
|
||||
@ -2570,19 +2607,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*/
|
||||
public static function has_own_table($dataClass) {
|
||||
if(!is_subclass_of($dataClass,'DataObject')) return false;
|
||||
|
||||
|
||||
if(!isset(DataObject::$cache_has_own_table[$dataClass])) {
|
||||
if(get_parent_class($dataClass) == 'DataObject') {
|
||||
DataObject::$cache_has_own_table[$dataClass] = true;
|
||||
} else {
|
||||
DataObject::$cache_has_own_table[$dataClass]
|
||||
= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
|
||||
DataObject::$cache_has_own_table[$dataClass]
|
||||
= Config::inst()->get($dataClass, 'db', Config::UNINHERITED)
|
||||
|| Config::inst()->get($dataClass, 'has_one', Config::UNINHERITED);
|
||||
}
|
||||
}
|
||||
return DataObject::$cache_has_own_table[$dataClass];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the member is allowed to do the given action.
|
||||
* See {@link extendedCan()} for a more versatile tri-state permission control.
|
||||
@ -2618,7 +2655,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$groupList = implode(', ', $groups->column("ID"));
|
||||
|
||||
// TODO Fix relation table hardcoding
|
||||
$query = new SQLQuery(
|
||||
$query = new SQLSelect(
|
||||
"\"Page_Can$perm\".PageID",
|
||||
array("\"Page_Can$perm\""),
|
||||
"GroupID IN ($groupList)");
|
||||
@ -2627,7 +2664,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
if($perm == "View") {
|
||||
// TODO Fix relation table hardcoding
|
||||
$query = new SQLQuery("\"SiteTree\".\"ID\"", array(
|
||||
$query = new SQLSelect("\"SiteTree\".\"ID\"", array(
|
||||
"\"SiteTree\"",
|
||||
"LEFT JOIN \"Page_CanView\" ON \"Page_CanView\".\"PageID\" = \"SiteTree\".\"ID\""
|
||||
), "\"Page_CanView\".\"PageID\" IS NULL");
|
||||
@ -2656,11 +2693,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Process tri-state responses from permission-alterting extensions. The extensions are
|
||||
* expected to return one of three values:
|
||||
*
|
||||
*
|
||||
* - false: Disallow this permission, regardless of what other extensions say
|
||||
* - true: Allow this permission, as long as no other extensions return false
|
||||
* - NULL: Don't affect the outcome
|
||||
*
|
||||
*
|
||||
* This method itself returns a tri-state value, and is designed to be used like this:
|
||||
*
|
||||
* <code>
|
||||
@ -2668,7 +2705,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* if($extended !== null) return $extended;
|
||||
* else return $normalValue;
|
||||
* </code>
|
||||
*
|
||||
*
|
||||
* @param String $methodName Method on the same object, e.g. {@link canEdit()}
|
||||
* @param Member|int $member
|
||||
* @return boolean|null
|
||||
@ -2679,12 +2716,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Remove NULLs
|
||||
$results = array_filter($results, function($v) {return !is_null($v);});
|
||||
// If there are any non-NULL responses, then return the lowest one of them.
|
||||
// If any explicitly deny the permission, then we don't get access
|
||||
// If any explicitly deny the permission, then we don't get access
|
||||
if($results) return min($results);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Member $member
|
||||
* @return boolean
|
||||
@ -2763,7 +2800,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// If we have a CompositeDBField object in $this->record, then return that
|
||||
if(isset($this->record[$fieldName]) && is_object($this->record[$fieldName])) {
|
||||
return $this->record[$fieldName];
|
||||
|
||||
|
||||
// Special case for ID field
|
||||
} else if($fieldName == 'ID') {
|
||||
return new PrimaryKey($fieldName, $this);
|
||||
@ -2771,22 +2808,22 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// Special case for ClassName
|
||||
} else if($fieldName == 'ClassName') {
|
||||
$val = get_class($this);
|
||||
return DBField::create_field('Varchar', $val, $fieldName, $this);
|
||||
return DBField::create_field('Varchar', $val, $fieldName);
|
||||
|
||||
} else if(array_key_exists($fieldName, self::$fixed_fields)) {
|
||||
return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName, $this);
|
||||
return DBField::create_field(self::$fixed_fields[$fieldName], $this->$fieldName, $fieldName);
|
||||
|
||||
// General casting information for items in $db
|
||||
} else if($helper = $this->db($fieldName)) {
|
||||
$obj = Object::create_from_string($helper, $fieldName);
|
||||
$obj->setValue($this->$fieldName, $this->record, false);
|
||||
return $obj;
|
||||
|
||||
|
||||
// Special case for has_one relationships
|
||||
} else if(preg_match('/ID$/', $fieldName) && $this->has_one(substr($fieldName,0,-2))) {
|
||||
$val = $this->$fieldName;
|
||||
return DBField::create_field('ForeignKey', $val, $fieldName, $this);
|
||||
|
||||
|
||||
// has_one for polymorphic relations do not end in ID
|
||||
} else if(($type = $this->has_one($fieldName)) && ($type === 'DataObject')) {
|
||||
$val = $this->$fieldName();
|
||||
@ -2798,7 +2835,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Traverses to a DBField referenced by relationships between data objects.
|
||||
*
|
||||
* The path to the related field is specified with dot separated syntax
|
||||
* The path to the related field is specified with dot separated syntax
|
||||
* (eg: Parent.Child.Child.FieldName).
|
||||
*
|
||||
* @param string $fieldPath
|
||||
@ -2860,7 +2897,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$component = $component->relation($relation);
|
||||
} elseif($component instanceof DataObject
|
||||
&& ($dbObject = $component->dbObject($relation))
|
||||
) {
|
||||
) {
|
||||
// Select db object
|
||||
$component = $dbObject;
|
||||
} else {
|
||||
@ -2868,7 +2905,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bail if the component is null
|
||||
if(!$component) {
|
||||
return null;
|
||||
@ -2882,7 +2919,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Temporary hack to return an association name, based on class, to get around the mangle
|
||||
* of having to deal with reverse lookup of relationships to determine autogenerated foreign keys.
|
||||
*
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
public function getReverseAssociation($className) {
|
||||
@ -2898,7 +2935,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$has_one = array_flip($this->has_one());
|
||||
if (array_key_exists($className, $has_one)) return $has_one[$className];
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -2907,14 +2944,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* sub-classes are automatically selected and included
|
||||
*
|
||||
* @param string $callerClass The class of objects to be returned
|
||||
* @param string $filter A filter to be inserted into the WHERE clause.
|
||||
* @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted,
|
||||
* self::$default_sort will be used.
|
||||
* @param string|array $filter A filter to be inserted into the WHERE clause.
|
||||
* Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
|
||||
* @param string|array $sort A sort expression to be inserted into the ORDER
|
||||
* BY clause. If omitted, self::$default_sort will be used.
|
||||
* @param string $join Deprecated 3.0 Join clause. Use leftJoin($table, $joinClause) instead.
|
||||
* @param string|array $limit A limit expression to be inserted into the LIMIT clause.
|
||||
* @param string $containerClass The container class to return the results in.
|
||||
*
|
||||
* @return DataList
|
||||
* @todo $containerClass is Ignored, why?
|
||||
*
|
||||
* @return DataList The objects matching the filter, in the class specified by $containerClass
|
||||
*/
|
||||
public static function get($callerClass = null, $filter = "", $sort = "", $join = "", $limit = null,
|
||||
$containerClass = 'DataList') {
|
||||
@ -2924,12 +2964,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if($callerClass == 'DataObject') {
|
||||
throw new \InvalidArgumentException('Call <classname>::get() instead of DataObject::get()');
|
||||
}
|
||||
|
||||
|
||||
if($filter || $sort || $join || $limit || ($containerClass != 'DataList')) {
|
||||
throw new \InvalidArgumentException('If calling <classname>::get() then you shouldn\'t pass any other'
|
||||
. ' arguments');
|
||||
}
|
||||
|
||||
|
||||
$result = DataList::create(get_called_class());
|
||||
$result->setDataModel(DataModel::inst());
|
||||
return $result;
|
||||
@ -2940,7 +2980,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
$result = DataList::create($callerClass)->where($filter)->sort($sort);
|
||||
|
||||
if($limit && strpos($limit, ',') !== false) {
|
||||
@ -2953,7 +2993,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
$result->setDataModel(DataModel::inst());
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated 3.1 Use DataList::create and DataList to do your querying
|
||||
*/
|
||||
@ -2989,7 +3029,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* All calls to get_one() are cached.
|
||||
*
|
||||
* @param string $callerClass The class of objects to be returned
|
||||
* @param string $filter A filter to be inserted into the WHERE clause
|
||||
* @param string|array $filter A filter to be inserted into the WHERE clause.
|
||||
* Supports parameterised queries. See SQLSelect::addWhere() for syntax examples.
|
||||
* @param boolean $cache Use caching
|
||||
* @param string $orderby A sort expression to be inserted into the ORDER BY clause.
|
||||
*
|
||||
@ -2998,21 +3039,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public static function get_one($callerClass, $filter = "", $cache = true, $orderby = "") {
|
||||
$SNG = singleton($callerClass);
|
||||
|
||||
$cacheKey = "{$filter}-{$orderby}";
|
||||
if($extra = $SNG->extend('cacheKeyComponent')) {
|
||||
$cacheKey .= '-' . implode("-", $extra);
|
||||
}
|
||||
$cacheKey = md5($cacheKey);
|
||||
|
||||
$cacheComponents = array($filter, $orderby, $SNG->extend('cacheKeyComponent'));
|
||||
$cacheKey = md5(var_export($cacheComponents, true));
|
||||
|
||||
// Flush destroyed items out of the cache
|
||||
if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])
|
||||
&& DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
|
||||
if($cache && isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])
|
||||
&& DataObject::$_cache_get_one[$callerClass][$cacheKey] instanceof DataObject
|
||||
&& DataObject::$_cache_get_one[$callerClass][$cacheKey]->destroyed) {
|
||||
|
||||
DataObject::$_cache_get_one[$callerClass][$cacheKey] = false;
|
||||
}
|
||||
if(!$cache || !isset(DataObject::$_cache_get_one[$callerClass][$cacheKey])) {
|
||||
$dl = $callerClass::get()->where($filter)->sort($orderby);
|
||||
$dl = DataObject::get($callerClass)->where($filter)->sort($orderby);
|
||||
$item = $dl->First();
|
||||
|
||||
if($cache) {
|
||||
@ -3030,12 +3068,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Also clears any cached aggregate data.
|
||||
*
|
||||
* @param boolean $persistent When true will also clear persistent data stored in the Cache system.
|
||||
* When false will just clear session-local cached data
|
||||
* When false will just clear session-local cached data
|
||||
* @return DataObject $this
|
||||
*/
|
||||
public function flushCache($persistent = true) {
|
||||
if($persistent) Aggregate::flushCache($this->class);
|
||||
|
||||
|
||||
if($this->class == 'DataObject') {
|
||||
DataObject::$_cache_get_one = array();
|
||||
return $this;
|
||||
@ -3045,9 +3083,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
foreach($classes as $class) {
|
||||
if(isset(DataObject::$_cache_get_one[$class])) unset(DataObject::$_cache_get_one[$class]);
|
||||
}
|
||||
|
||||
|
||||
$this->extend('flushCache');
|
||||
|
||||
|
||||
$this->components = array();
|
||||
return $this;
|
||||
}
|
||||
@ -3063,13 +3101,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
DataObject::$_cache_get_one = array();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset all global caches associated with DataObject.
|
||||
*/
|
||||
public static function reset() {
|
||||
self::clear_classname_spec_cache();
|
||||
DataObject::$cache_has_own_table = array();
|
||||
DataObject::$cache_has_own_table_field = array();
|
||||
DataObject::$_cache_db = array();
|
||||
DataObject::$_cache_get_one = array();
|
||||
DataObject::$_cache_composite_fields = array();
|
||||
@ -3088,18 +3126,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @return DataObject The element
|
||||
*/
|
||||
public static function get_by_id($callerClass, $id, $cache = true) {
|
||||
if(is_numeric($id)) {
|
||||
if(is_subclass_of($callerClass, 'DataObject')) {
|
||||
$baseClass = ClassInfo::baseDataClass($callerClass);
|
||||
return DataObject::get_one($callerClass,"\"$baseClass\".\"ID\" = $id", $cache);
|
||||
|
||||
// This simpler code will be used by non-DataObject classes that implement DataObjectInterface
|
||||
} else {
|
||||
return DataObject::get_one($callerClass,"\"ID\" = $id", $cache);
|
||||
}
|
||||
} else {
|
||||
if(!is_numeric($id)) {
|
||||
user_error("DataObject::get_by_id passed a non-numeric ID #$id", E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Check filter column
|
||||
if(is_subclass_of($callerClass, 'DataObject')) {
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3112,7 +3153,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* @var Array Parameters used in the query that built this object.
|
||||
* This can be used by decorators (e.g. lazy loading) to
|
||||
* This can be used by decorators (e.g. lazy loading) to
|
||||
* run additional queries using the same context.
|
||||
*/
|
||||
protected $sourceQueryParams;
|
||||
@ -3185,7 +3226,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
/**
|
||||
* Check the database schema and update it as necessary.
|
||||
*
|
||||
*
|
||||
* @uses DataExtension->augmentDatabase()
|
||||
*/
|
||||
public function requireTable() {
|
||||
@ -3197,10 +3238,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
if($fields) {
|
||||
$hasAutoIncPK = ($this->class == ClassInfo::baseDataClass($this->class));
|
||||
DB::requireTable($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
|
||||
DB::require_table($this->class, $fields, $indexes, $hasAutoIncPK, $this->stat('create_table_options'),
|
||||
$extensions);
|
||||
} else {
|
||||
DB::dontRequireTable($this->class);
|
||||
DB::dont_require_table($this->class);
|
||||
}
|
||||
|
||||
// Build any child tables for many_many items
|
||||
@ -3221,8 +3262,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
"{$this->class}ID" => true,
|
||||
(($this->class == $childClass) ? "ChildID" : "{$childClass}ID") => true,
|
||||
);
|
||||
|
||||
DB::requireTable("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
|
||||
|
||||
DB::require_table("{$this->class}_$relationship", $manymanyFields, $manymanyIndexes, true, null,
|
||||
$extensions);
|
||||
}
|
||||
}
|
||||
@ -3236,7 +3277,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* database is built, after the database tables have all been created. Overload
|
||||
* this to add default records when the database is built, but make sure you
|
||||
* call parent::requireDefaultRecords().
|
||||
*
|
||||
*
|
||||
* @uses DataExtension->requireDefaultRecords()
|
||||
*/
|
||||
public function requireDefaultRecords() {
|
||||
@ -3253,11 +3294,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
DB::alteration_message("Added default records to $className table","created");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Let any extentions make their own database default data
|
||||
$this->extend('requireDefaultRecords', $dummy);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns fields bu traversing the class heirachy in a bottom-up direction.
|
||||
*
|
||||
@ -3271,18 +3312,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public function inheritedDatabaseFields() {
|
||||
$fields = array();
|
||||
$currentObj = $this->class;
|
||||
|
||||
|
||||
while($currentObj != 'DataObject') {
|
||||
$fields = array_merge($fields, self::custom_database_fields($currentObj));
|
||||
$currentObj = get_parent_class($currentObj);
|
||||
}
|
||||
|
||||
|
||||
return (array) $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default searchable fields for this object, as defined in the
|
||||
* $searchable_fields list. If searchable fields are not defined on the
|
||||
* Get the default searchable fields for this object, as defined in the
|
||||
* $searchable_fields list. If searchable fields are not defined on the
|
||||
* data object, uses a default selection of summary fields.
|
||||
*
|
||||
* @return array
|
||||
@ -3291,7 +3332,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
// can have mixed format, need to make consistent in most verbose form
|
||||
$fields = $this->stat('searchable_fields');
|
||||
$labels = $this->fieldLabels();
|
||||
|
||||
|
||||
// fallback to summary fields (unless empty array is explicitly specified)
|
||||
if( ! $fields && ! is_array($fields)) {
|
||||
$summaryFields = array_keys($this->summaryFields());
|
||||
@ -3315,7 +3356,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we need to make sure the format is unified before
|
||||
// augmenting fields, so extensions can apply consistent checks
|
||||
// but also after augmenting fields, because the extension
|
||||
@ -3355,13 +3396,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
$fields = $rewrite;
|
||||
|
||||
|
||||
// apply DataExtensions if present
|
||||
$this->extend('updateSearchableFields', $fields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get any user defined searchable fields labels that
|
||||
* exist. Allows overriding of default field names in the form
|
||||
@ -3374,23 +3415,23 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* would generally only be set in the case of more complex relationships
|
||||
* between data object being required in the search interface.
|
||||
*
|
||||
* Generates labels based on name of the field itself, if no static property
|
||||
* Generates labels based on name of the field itself, if no static property
|
||||
* {@link self::field_labels} exists.
|
||||
*
|
||||
* @uses $field_labels
|
||||
* @uses FormField::name_to_label()
|
||||
*
|
||||
* @param boolean $includerelations a boolean value to indicate if the labels returned include relation fields
|
||||
*
|
||||
*
|
||||
* @return array|string Array of all element labels if no argument given, otherwise the label of the field
|
||||
*/
|
||||
public function fieldLabels($includerelations = true) {
|
||||
$cacheKey = $this->class . '_' . $includerelations;
|
||||
|
||||
|
||||
if(!isset(self::$_cache_field_labels[$cacheKey])) {
|
||||
$customLabels = $this->stat('field_labels');
|
||||
$autoLabels = array();
|
||||
|
||||
|
||||
// get all translated static properties as defined in i18nCollectStatics()
|
||||
$ancestry = ClassInfo::ancestry($this->class);
|
||||
$ancestry = array_reverse($ancestry);
|
||||
@ -3412,20 +3453,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
}
|
||||
|
||||
$labels = array_merge((array)$autoLabels, (array)$customLabels);
|
||||
$this->extend('updateFieldLabels', $labels);
|
||||
$this->extend('updateFieldLabels', $labels);
|
||||
self::$_cache_field_labels[$cacheKey] = $labels;
|
||||
}
|
||||
|
||||
|
||||
return self::$_cache_field_labels[$cacheKey];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a human-readable label for a single field,
|
||||
* see {@link fieldLabels()} for more details.
|
||||
*
|
||||
*
|
||||
* @uses fieldLabels()
|
||||
* @uses FormField::name_to_label()
|
||||
*
|
||||
*
|
||||
* @param string $name Name of the field
|
||||
* @return string Label of the field
|
||||
*/
|
||||
@ -3459,7 +3500,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
if ($this->hasField('FirstName')) $fields['FirstName'] = 'First Name';
|
||||
}
|
||||
$this->extend("updateSummaryFields", $fields);
|
||||
|
||||
|
||||
// Final fail-over, just list ID field
|
||||
if(!$fields) $fields['ID'] = 'ID';
|
||||
|
||||
@ -3467,7 +3508,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
foreach($this->fieldLabels(false) as $name => $label) {
|
||||
if(isset($fields[$name])) $fields[$name] = $label;
|
||||
}
|
||||
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
@ -3487,7 +3528,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
|
||||
foreach($this->searchableFields() as $name => $spec) {
|
||||
$filterClass = $spec['filter'];
|
||||
|
||||
|
||||
if($spec['filter'] instanceof SearchFilter) {
|
||||
$filters[$name] = $spec['filter'];
|
||||
} else {
|
||||
@ -3514,8 +3555,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/*
|
||||
* @ignore
|
||||
*/
|
||||
private static $subclass_access = true;
|
||||
|
||||
private static $subclass_access = true;
|
||||
|
||||
/**
|
||||
* Temporarily disable subclass access in data object qeur
|
||||
*/
|
||||
@ -3525,7 +3566,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
public static function enable_subclass_access() {
|
||||
self::$subclass_access = true;
|
||||
}
|
||||
|
||||
|
||||
//-------------------------------------------------------------------------------------------//
|
||||
|
||||
/**
|
||||
@ -3547,13 +3588,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
"Created" => "SS_Datetime",
|
||||
"Title" => 'Text',
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Specify custom options for a CREATE TABLE call.
|
||||
* Can be used to specify a custom storage engine for specific database table.
|
||||
* All options have to be keyed for a specific database implementation,
|
||||
* identified by their class name (extending from {@link SS_Database}).
|
||||
*
|
||||
*
|
||||
* <code>
|
||||
* array(
|
||||
* 'MySQLDatabase' => 'ENGINE=MyISAM'
|
||||
@ -3562,7 +3603,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
*
|
||||
* Caution: This API is experimental, and might not be
|
||||
* included in the next major release. Please use with care.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
* @config
|
||||
*/
|
||||
@ -3574,7 +3615,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* If a field is in this array, then create a database index
|
||||
* on that field. This is a map from fieldname to index type.
|
||||
* See {@link SS_Database->requireIndex()} and custom subclasses for details on the array notation.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
* @config
|
||||
*/
|
||||
@ -3584,11 +3625,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* Inserts standard column-values when a DataObject
|
||||
* is instanciated. Does not insert default records {@see $default_records}.
|
||||
* This is a map from fieldname to default value.
|
||||
*
|
||||
*
|
||||
* - If you would like to change a default value in a sub-class, just specify it.
|
||||
* - If you would like to disable the default value given by a parent class, set the default value to 0,'',
|
||||
* or false in your subclass. Setting it to null won't work.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
* @config
|
||||
*/
|
||||
@ -3621,7 +3662,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @config
|
||||
*/
|
||||
private static $has_one = null;
|
||||
|
||||
|
||||
/**
|
||||
* A meta-relationship that allows you to define the reverse side of a {@link DataObject::$has_one}.
|
||||
*
|
||||
@ -3635,7 +3676,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @config
|
||||
*/
|
||||
private static $belongs_to;
|
||||
|
||||
|
||||
/**
|
||||
* This defines a one-to-many relationship. It is a map of component name to the remote data class.
|
||||
*
|
||||
@ -3660,7 +3701,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
/**
|
||||
* Extra fields to include on the connecting many-many table.
|
||||
* This is a map from field name to field type.
|
||||
*
|
||||
*
|
||||
* Example code:
|
||||
* <code>
|
||||
* public static $many_many_extraFields = array(
|
||||
@ -3669,7 +3710,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* )
|
||||
* );
|
||||
* </code>
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
* @config
|
||||
*/
|
||||
@ -3701,7 +3742,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* "Name" => "PartialMatchFilter"
|
||||
* );
|
||||
* </code>
|
||||
*
|
||||
*
|
||||
* Overriding the default form fields, with a custom defined field.
|
||||
* The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
|
||||
* The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
|
||||
@ -3717,7 +3758,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* <code>
|
||||
* static $searchable_fields = array(
|
||||
* "Organisation.ZipCode" => array(
|
||||
* "field" => "TextField",
|
||||
* "field" => "TextField",
|
||||
* "filter" => "PartialMatchFilter",
|
||||
* "title" => 'Organisation ZIP'
|
||||
* )
|
||||
@ -3740,44 +3781,44 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
||||
* @config
|
||||
*/
|
||||
private static $summary_fields = null;
|
||||
|
||||
|
||||
/**
|
||||
* Provides a list of allowed methods that can be called via RESTful api.
|
||||
*/
|
||||
public static $allowed_actions = null;
|
||||
|
||||
|
||||
/**
|
||||
* Collect all static properties on the object
|
||||
* which contain natural language, and need to be translated.
|
||||
* The full entity name is composed from the class name and a custom identifier.
|
||||
*
|
||||
*
|
||||
* @return array A numerical array which contains one or more entities in array-form.
|
||||
* Each numeric entity array contains the "arguments" for a _t() call as array values:
|
||||
* $entity, $string, $priority, $context.
|
||||
*/
|
||||
public function provideI18nEntities() {
|
||||
$entities = array();
|
||||
|
||||
|
||||
$entities["{$this->class}.SINGULARNAME"] = array(
|
||||
$this->singular_name(),
|
||||
|
||||
|
||||
'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
|
||||
);
|
||||
|
||||
$entities["{$this->class}.PLURALNAME"] = array(
|
||||
$this->plural_name(),
|
||||
|
||||
|
||||
'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
|
||||
. ' interface'
|
||||
);
|
||||
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given method/parameter has a value
|
||||
* (Uses the DBField::hasValue if the parameter is a database field)
|
||||
*
|
||||
*
|
||||
* @param string $field The field name
|
||||
* @param array $arguments
|
||||
* @param bool $cache
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
/**
|
||||
* An object representing a query of data from the DataObject's supporting database.
|
||||
* Acts as a wrapper over {@link SQLQuery} and performs all of the query generation.
|
||||
* Acts as a wrapper over {@link SQLSelect} and performs all of the query generation.
|
||||
* Used extensively by {@link DataList}.
|
||||
*
|
||||
* Unlike DataList, modifiers on DataQuery modify the object rather than returning a clone.
|
||||
@ -19,7 +19,7 @@ class DataQuery {
|
||||
protected $dataClass;
|
||||
|
||||
/**
|
||||
* @var SQLQuery
|
||||
* @var SQLSelect
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
@ -65,7 +65,7 @@ class DataQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link SQLQuery} object that represents the current query; note that it will
|
||||
* Return the {@link SQLSelect} object that represents the current query; note that it will
|
||||
* be a clone of the object.
|
||||
*/
|
||||
public function query() {
|
||||
@ -75,15 +75,41 @@ class DataQuery {
|
||||
|
||||
/**
|
||||
* Remove a filter from the query
|
||||
*
|
||||
* @param string|array $fieldExpression The predicate of the condition to remove
|
||||
* (ignoring parameters). The expression will be considered a match if it's
|
||||
* contained within any other predicate.
|
||||
* @return DataQuery Self reference
|
||||
*/
|
||||
public function removeFilterOn($fieldExpression) {
|
||||
$matched = false;
|
||||
|
||||
// If given a parameterised condition extract only the condition
|
||||
if(is_array($fieldExpression)) {
|
||||
reset($fieldExpression);
|
||||
$fieldExpression = key($fieldExpression);
|
||||
}
|
||||
|
||||
$where = $this->query->getWhere();
|
||||
foreach($where as $i => $clause) {
|
||||
if(strpos($clause, $fieldExpression) !== false) {
|
||||
unset($where[$i]);
|
||||
$matched = true;
|
||||
// Iterate through each condition
|
||||
foreach($where as $i => $condition) {
|
||||
|
||||
// Rewrite condition groups as plain conditions before comparison
|
||||
if($condition instanceof SQLConditionGroup) {
|
||||
$predicate = $condition->conditionSQL($parameters);
|
||||
$condition = array($predicate => $parameters);
|
||||
}
|
||||
|
||||
// As each condition is a single length array, do a single
|
||||
// iteration to extract the predicate and parameters
|
||||
foreach($condition as $predicate => $parameters) {
|
||||
// @see SQLSelect::addWhere for why this is required here
|
||||
if(strpos($predicate, $fieldExpression) !== false) {
|
||||
unset($where[$i]);
|
||||
$matched = true;
|
||||
}
|
||||
// Enforce single-item condition predicate => parameters structure
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +146,7 @@ class DataQuery {
|
||||
$baseClass = array_shift($tableClasses);
|
||||
|
||||
// Build our intial query
|
||||
$this->query = new SQLQuery(array());
|
||||
$this->query = new SQLSelect(array());
|
||||
$this->query->setDistinct(true);
|
||||
|
||||
if($sort = singleton($this->dataClass)->stat('default_sort')) {
|
||||
@ -139,8 +165,9 @@ class DataQuery {
|
||||
|
||||
/**
|
||||
* Ensure that the query is ready to execute.
|
||||
*
|
||||
* @return SQLQuery
|
||||
*
|
||||
* @param array|null $queriedColumns Any columns to filter the query by
|
||||
* @return SQLSelect The finalised sql query
|
||||
*/
|
||||
public function getFinalisedQuery($queriedColumns = null) {
|
||||
if(!$queriedColumns) $queriedColumns = $this->queriedColumns;
|
||||
@ -157,8 +184,8 @@ class DataQuery {
|
||||
if($queriedColumns) {
|
||||
// Specifying certain columns allows joining of child tables
|
||||
$tableClasses = ClassInfo::dataClassesFor($this->dataClass);
|
||||
|
||||
foreach ($query->getWhere() as $where) {
|
||||
|
||||
foreach ($query->getWhereParameterised($parameters) as $where) {
|
||||
// Check for just the column, in the form '"Column" = ?' and the form '"Table"."Column"' = ?
|
||||
if (preg_match('/^"([^"]+)"/', $where, $matches) ||
|
||||
preg_match('/^"([^"]+)"\."[^"]+"/', $where, $matches)) {
|
||||
@ -180,7 +207,7 @@ class DataQuery {
|
||||
$selectColumns = null;
|
||||
if ($queriedColumns) {
|
||||
// Restrict queried columns to that on the selected table
|
||||
$tableFields = DataObject::database_fields($tableClass);
|
||||
$tableFields = DataObject::database_fields($tableClass, false);
|
||||
$selectColumns = array_intersect($queriedColumns, array_keys($tableFields));
|
||||
}
|
||||
|
||||
@ -206,7 +233,7 @@ class DataQuery {
|
||||
if(preg_match('/^"([^"]+)"/', $collision, $matches)) {
|
||||
$collisionBase = $matches[1];
|
||||
$collisionClasses = ClassInfo::subclassesFor($collisionBase);
|
||||
$collisionClasses = array_map(array(DB::getConn(), 'prepStringForDB'), $collisionClasses);
|
||||
$collisionClasses = Convert::raw2sql($collisionClasses, true);
|
||||
$caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ("
|
||||
. implode(", ", $collisionClasses) . ") THEN $collision";
|
||||
} else {
|
||||
@ -224,14 +251,19 @@ class DataQuery {
|
||||
// Get the ClassName values to filter to
|
||||
$classNames = ClassInfo::subclassesFor($this->dataClass);
|
||||
if(!$classNames) user_error("DataList::create() Can't find data sub-classes for '$callerClass'");
|
||||
$classNames = array_map(array(DB::getConn(), 'prepStringForDB'), $classNames);
|
||||
$query->addWhere("\"$baseClass\".\"ClassName\" IN (" . implode(",", $classNames) . ")");
|
||||
$classNamesPlaceholders = DB::placeholders($classNames);
|
||||
$query->addWhere(array(
|
||||
"\"$baseClass\".\"ClassName\" IN ($classNamesPlaceholders)" => $classNames
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$query->selectField("\"$baseClass\".\"ID\"", "ID");
|
||||
$query->selectField("CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\""
|
||||
. " ELSE ".DB::getConn()->prepStringForDB($baseClass)." END", "RecordClassName");
|
||||
$query->selectField("
|
||||
CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\"
|
||||
ELSE ".Convert::raw2sql($baseClass, true)." END",
|
||||
"RecordClassName"
|
||||
);
|
||||
|
||||
// TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses
|
||||
// of DataQuery
|
||||
@ -247,7 +279,7 @@ class DataQuery {
|
||||
/**
|
||||
* Ensure that if a query has an order by clause, those columns are present in the select.
|
||||
*
|
||||
* @param SQLQuery $query
|
||||
* @param SQLSelect $query
|
||||
* @return null
|
||||
*/
|
||||
protected function ensureSelectContainsOrderbyColumns($query, $originalSelect = array()) {
|
||||
@ -273,13 +305,8 @@ class DataQuery {
|
||||
}
|
||||
|
||||
if(count($parts) == 1) {
|
||||
$databaseFields = DataObject::database_fields($baseClass);
|
||||
|
||||
// database_fields() doesn't return ID, so we need to
|
||||
// manually add it here
|
||||
$databaseFields['ID'] = true;
|
||||
|
||||
if(isset($databaseFields[$parts[0]])) {
|
||||
|
||||
if(DataObject::has_own_table_database_field($baseClass, $parts[0])) {
|
||||
$qualCol = "\"$baseClass\".\"{$parts[0]}\"";
|
||||
} else {
|
||||
$qualCol = "\"$parts[0]\"";
|
||||
@ -290,7 +317,7 @@ class DataQuery {
|
||||
// add new columns sort
|
||||
$newOrderby[$qualCol] = $dir;
|
||||
|
||||
// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
|
||||
// To-do: Remove this if block once SQLSelect::$select has been refactored to store getSelect()
|
||||
// format internally; then this check can be part of selectField()
|
||||
$selects = $query->getSelect();
|
||||
if(!isset($selects[$col]) && !in_array($qualCol, $selects)) {
|
||||
@ -299,7 +326,7 @@ class DataQuery {
|
||||
} else {
|
||||
$qualCol = '"' . implode('"."', $parts) . '"';
|
||||
|
||||
// To-do: Remove this if block once SQLQuery::$select has been refactored to store getSelect()
|
||||
// To-do: Remove this if block once SQLSelect::$select has been refactored to store getSelect()
|
||||
// format internally; then this check can be part of selectField()
|
||||
if(!in_array($qualCol, $query->getSelect())) {
|
||||
$query->selectField($qualCol);
|
||||
@ -320,9 +347,18 @@ class DataQuery {
|
||||
|
||||
/**
|
||||
* Return this query's SQL
|
||||
*
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @return string The resulting SQL query (may be paramaterised)
|
||||
*/
|
||||
public function sql() {
|
||||
return $this->getFinalisedQuery()->sql();
|
||||
public function sql(&$parameters = array()) {
|
||||
if(func_num_args() == 0) {
|
||||
Deprecation::notice(
|
||||
'3.2',
|
||||
'DataQuery::sql() now may produce parameters which are necessary to execute this query'
|
||||
);
|
||||
}
|
||||
return $this->getFinalisedQuery()->sql($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -337,37 +373,41 @@ class DataQuery {
|
||||
/**
|
||||
* Return the maximum value of the given field in this DataList
|
||||
*
|
||||
* @param String $field Unquoted database column name (will be escaped automatically)
|
||||
* @param String $field Unquoted database column name. Will be ANSI quoted
|
||||
* automatically so must not contain double quotes.
|
||||
*/
|
||||
public function max($field) {
|
||||
return $this->aggregate(sprintf('MAX("%s")', Convert::raw2sql($field)));
|
||||
return $this->aggregate("MAX(\"$field\")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum value of the given field in this DataList
|
||||
*
|
||||
* @param String $field Unquoted database column name (will be escaped automatically)
|
||||
* @param String $field Unquoted database column name. Will be ANSI quoted
|
||||
* automatically so must not contain double quotes.
|
||||
*/
|
||||
public function min($field) {
|
||||
return $this->aggregate(sprintf('MIN("%s")', Convert::raw2sql($field)));
|
||||
return $this->aggregate("MIN(\"$field\")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average value of the given field in this DataList
|
||||
*
|
||||
* @param String $field Unquoted database column name (will be escaped automatically)
|
||||
* @param String $field Unquoted database column name. Will be ANSI quoted
|
||||
* automatically so must not contain double quotes.
|
||||
*/
|
||||
public function avg($field) {
|
||||
return $this->aggregate(sprintf('AVG("%s")', Convert::raw2sql($field)));
|
||||
return $this->aggregate("AVG(\"$field\")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sum of the values of the given field in this DataList
|
||||
*
|
||||
* @param String $field Unquoted database column name (will be escaped automatically)
|
||||
* @param String $field Unquoted database column name. Will be ANSI quoted
|
||||
* automatically so must not contain double quotes.
|
||||
*/
|
||||
public function sum($field) {
|
||||
return $this->aggregate(sprintf('SUM("%s")', Convert::raw2sql($field)));
|
||||
return $this->aggregate("SUM(\"$field\")");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -396,9 +436,9 @@ class DataQuery {
|
||||
/**
|
||||
* Update the SELECT clause of the query with the columns from the given table
|
||||
*/
|
||||
protected function selectColumnsFromTable(SQLQuery &$query, $tableClass, $columns = null) {
|
||||
protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) {
|
||||
// Add SQL for multi-value fields
|
||||
$databaseFields = DataObject::database_fields($tableClass);
|
||||
$databaseFields = DataObject::database_fields($tableClass, false);
|
||||
$compositeFields = DataObject::composite_fields($tableClass, false);
|
||||
if($databaseFields) foreach($databaseFields as $k => $v) {
|
||||
if((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) {
|
||||
@ -463,18 +503,14 @@ class DataQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a WHERE clause to this query.
|
||||
* There are two different ways of doing this:
|
||||
* Adds a WHERE clause.
|
||||
*
|
||||
* @see SQLSelect::addWhere() for syntax examples, although DataQuery
|
||||
* won't expand multiple arguments as SQLSelect does.
|
||||
*
|
||||
* <code>
|
||||
* // the entire predicate as a single string
|
||||
* $query->where("\"Column\" = 'Value'");
|
||||
*
|
||||
* // multiple predicates as an array
|
||||
* $query->where(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
||||
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
|
||||
* paramaterised queries
|
||||
* @return DataQuery
|
||||
*/
|
||||
public function where($filter) {
|
||||
if($filter) {
|
||||
@ -486,10 +522,11 @@ class DataQuery {
|
||||
/**
|
||||
* Append a WHERE with OR.
|
||||
*
|
||||
* @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'"));
|
||||
* @see where()
|
||||
* @see SQLSelect::addWhere() for syntax examples, although DataQuery
|
||||
* won't expand multiple method arguments as SQLSelect does.
|
||||
*
|
||||
* @param array $filter Escaped SQL statement.
|
||||
* @param string|array|SQLConditionGroup $filter Predicate(s) to set, as escaped SQL statements or
|
||||
* paramaterised queries
|
||||
* @return DataQuery
|
||||
*/
|
||||
public function whereAny($filter) {
|
||||
@ -502,7 +539,7 @@ class DataQuery {
|
||||
/**
|
||||
* Set the ORDER BY clause of this query
|
||||
*
|
||||
* @see SQLQuery::orderby()
|
||||
* @see SQLSelect::orderby()
|
||||
*
|
||||
* @param String $sort Column to sort on (escaped SQL statement)
|
||||
* @param String $direction Direction ("ASC" or "DESC", escaped SQL statement)
|
||||
@ -662,7 +699,8 @@ class DataQuery {
|
||||
$subSelect->setSelect(array());
|
||||
$subSelect->selectField($fieldExpression, $field);
|
||||
$subSelect->setOrderBy(null);
|
||||
$this->where($this->expressionForField($field).' NOT IN ('.$subSelect->sql().')');
|
||||
$subSelectSQL = $subSelect->sql($subSelectParameters);
|
||||
$this->where(array($this->expressionForField($field)." NOT IN ($subSelectSQL)" => $subSelectParameters));
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -674,9 +712,9 @@ class DataQuery {
|
||||
* @param Array $fields Database column names (will be escaped automatically)
|
||||
*/
|
||||
public function selectFromTable($table, $fields) {
|
||||
$table = Convert::raw2sql($table);
|
||||
$fieldExpressions = array_map(create_function('$item',
|
||||
"return '\"$table\".\"' . Convert::raw2sql(\$item) . '\"';"), $fields);
|
||||
$fieldExpressions = array_map(function($item) use($table) {
|
||||
return "\"$table\".\"$item\"";
|
||||
}, $fields);
|
||||
|
||||
$this->query->setSelect($fieldExpressions);
|
||||
|
||||
@ -702,7 +740,7 @@ class DataQuery {
|
||||
|
||||
/**
|
||||
* @param String $field Select statement identifier, either the unquoted column name,
|
||||
* the full composite SQL statement, or the alias set through {@link SQLQuery->selectField()}.
|
||||
* the full composite SQL statement, or the alias set through {@link SQLSelect->selectField()}.
|
||||
* @return String The expression used to query this field via this DataQuery
|
||||
*/
|
||||
protected function expressionForField($field) {
|
||||
@ -767,41 +805,25 @@ class DataQuery {
|
||||
/**
|
||||
* Represents a subgroup inside a WHERE clause in a {@link DataQuery}
|
||||
*
|
||||
* Stores the clauses for the subgroup inside a specific {@link SQLQuery}
|
||||
* object.
|
||||
*
|
||||
* Stores the clauses for the subgroup inside a specific {@link SQLSelect} object.
|
||||
* All non-where methods call their DataQuery versions, which uses the base
|
||||
* query object.
|
||||
*
|
||||
* @package framework
|
||||
*/
|
||||
class DataQuery_SubGroup extends DataQuery {
|
||||
class DataQuery_SubGroup extends DataQuery implements SQLConditionGroup {
|
||||
|
||||
protected $whereQuery;
|
||||
|
||||
public function __construct(DataQuery $base, $connective) {
|
||||
$this->dataClass = $base->dataClass;
|
||||
$this->query = $base->query;
|
||||
$this->whereQuery = new SQLQuery;
|
||||
$this->whereQuery = new SQLSelect();
|
||||
$this->whereQuery->setConnective($connective);
|
||||
|
||||
$base->where($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the WHERE clause of this query.
|
||||
* There are two different ways of doing this:
|
||||
*
|
||||
* <code>
|
||||
* // the entire predicate as a single string
|
||||
* $query->where("\"Column\" = 'Value'");
|
||||
*
|
||||
* // multiple predicates as an array
|
||||
* $query->where(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
||||
*/
|
||||
public function where($filter) {
|
||||
if($filter) {
|
||||
$this->whereQuery->addWhere($filter);
|
||||
@ -809,16 +831,7 @@ class DataQuery_SubGroup extends DataQuery {
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a WHERE with OR.
|
||||
*
|
||||
* @example $dataQuery->whereAny(array("\"Monkey\" = 'Chimp'", "\"Color\" = 'Brown'"));
|
||||
* @see where()
|
||||
*
|
||||
* @param array $filter Escaped SQL statement.
|
||||
* @return DataQuery
|
||||
*/
|
||||
|
||||
public function whereAny($filter) {
|
||||
if($filter) {
|
||||
$this->whereQuery->addWhereAny($filter);
|
||||
@ -827,19 +840,15 @@ class DataQuery_SubGroup extends DataQuery {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
if(!$this->whereQuery->getWhere()) {
|
||||
// We always need to have something so we don't end up with something like '... AND () AND ...'
|
||||
return '1=1';
|
||||
}
|
||||
|
||||
$sql = DB::getConn()->sqlWhereToString(
|
||||
$this->whereQuery->getWhere(),
|
||||
$this->whereQuery->getConnective()
|
||||
);
|
||||
public function conditionSQL(&$parameters) {
|
||||
$parameters = array();
|
||||
|
||||
$sql = preg_replace('[^\s*WHERE\s*]', '', $sql);
|
||||
|
||||
return $sql;
|
||||
// Ignore empty conditions
|
||||
$where = $this->whereQuery->getWhere();
|
||||
if(empty($where)) return null;
|
||||
|
||||
// Allow database to manage joining of conditions
|
||||
$sql = DB::get_conn()->getQueryBuilder()->buildWhereFragment($this->whereQuery, $parameters);
|
||||
return preg_replace('/^\s*WHERE\s*/i', '', $sql);
|
||||
}
|
||||
}
|
||||
|
1055
model/Database.php
1055
model/Database.php
@ -1,1055 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract database connectivity class.
|
||||
* Sub-classes of this implement the actual database connection libraries
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class SS_Database {
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var boolean Check tables when running /dev/build, and repair them if necessary.
|
||||
* In case of large databases or more fine-grained control on how to handle
|
||||
* data corruption in tables, you can disable this behaviour and handle it
|
||||
* outside of this class, e.g. through a nightly system task with extended logging capabilities.
|
||||
*/
|
||||
private static $check_and_repair_on_build = true;
|
||||
|
||||
/**
|
||||
* If this is false, then information about database operations
|
||||
* will be displayed, eg creation of tables.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
protected $supressOutput = false;
|
||||
|
||||
/**
|
||||
* Execute the given SQL query.
|
||||
* This abstract function must be defined by subclasses as part of the actual implementation.
|
||||
* It should return a subclass of SS_Query as the result.
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
abstract public function query($sql, $errorLevel = E_USER_ERROR);
|
||||
|
||||
/**
|
||||
* Get the autogenerated ID from the previous INSERT query.
|
||||
* @return int
|
||||
*/
|
||||
abstract public function getGeneratedID($table);
|
||||
|
||||
/**
|
||||
* Check if the connection to the database is active.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function isActive();
|
||||
|
||||
/**
|
||||
* Create the database and connect to it. This can be called if the
|
||||
* initial database connection is not successful because the database
|
||||
* does not exist.
|
||||
*
|
||||
* It takes no parameters, and should create the database from the information
|
||||
* specified in the constructor.
|
||||
*
|
||||
* @return boolean Returns true if successful
|
||||
*/
|
||||
abstract public function createDatabase();
|
||||
|
||||
/**
|
||||
* Build the connection string from input
|
||||
* @param array $parameters The connection details
|
||||
* @return string $connect The connection string
|
||||
**/
|
||||
abstract public function getConnect($parameters);
|
||||
|
||||
/**
|
||||
* Create a new table.
|
||||
* @param $tableName The name of the table
|
||||
* @param $fields A map of field names to field types
|
||||
* @param $indexes A map of indexes
|
||||
* @param $options An map of additional options. The available keys are as follows:
|
||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
||||
* - 'temporary' - If true, then a temporary table will be created
|
||||
* @return The table name generated. This may be different from the table name, for example with temporary tables.
|
||||
*/
|
||||
abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
|
||||
$advancedOptions = null);
|
||||
|
||||
/**
|
||||
* Alter a table's schema.
|
||||
*/
|
||||
abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
|
||||
$alteredIndexes = null, $alteredOptions=null, $advancedOptions=null);
|
||||
|
||||
/**
|
||||
* Rename a table.
|
||||
* @param string $oldTableName The old table name.
|
||||
* @param string $newTableName The new table name.
|
||||
*/
|
||||
abstract public function renameTable($oldTableName, $newTableName);
|
||||
|
||||
/**
|
||||
* Create a new field on a table.
|
||||
* @param string $table Name of the table.
|
||||
* @param string $field Name of the field to add.
|
||||
* @param string $spec The field specification, eg 'INTEGER NOT NULL'
|
||||
*/
|
||||
abstract public function createField($table, $field, $spec);
|
||||
|
||||
/**
|
||||
* Change the database column name of the given field.
|
||||
*
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $oldName The name of the field to change.
|
||||
* @param string $newName The new name of the field
|
||||
*/
|
||||
abstract public function renameField($tableName, $oldName, $newName);
|
||||
|
||||
/**
|
||||
* Get a list of all the fields for the given table.
|
||||
* Returns a map of field name => field spec.
|
||||
* @param string $table The table name.
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function fieldList($table);
|
||||
|
||||
/**
|
||||
* Returns a list of all tables in the database.
|
||||
* Keys are table names in lower case, values are table names in case that
|
||||
* database expects.
|
||||
* @return array
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* This is a stub function. Postgres caches the fieldlist results.
|
||||
*
|
||||
* @param string $tableName
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function clearCachedFieldlist($tableName=false){
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected function tableList();
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the given table exists in the database
|
||||
*/
|
||||
abstract public function hasTable($tableName);
|
||||
|
||||
/**
|
||||
* Returns the enum values available on the given field
|
||||
*/
|
||||
abstract public function enumValuesForField($tableName, $fieldName);
|
||||
|
||||
/**
|
||||
* Returns an escaped string.
|
||||
*
|
||||
* @param string
|
||||
* @return string - escaped string
|
||||
*/
|
||||
abstract public function addslashes($val);
|
||||
|
||||
/**
|
||||
* The table list, generated by the tableList() function.
|
||||
* Used by the requireTable() function.
|
||||
* @var array
|
||||
*/
|
||||
protected $tableList;
|
||||
|
||||
/**
|
||||
* The field list, generated by the fieldList() function.
|
||||
* An array of maps of field name => field spec, indexed
|
||||
* by table name.
|
||||
* @var array
|
||||
*/
|
||||
protected $fieldList;
|
||||
|
||||
/**
|
||||
* The index list for each table, generated by the indexList() function.
|
||||
* An map from table name to an array of index names.
|
||||
* @var array
|
||||
*/
|
||||
protected $indexList;
|
||||
|
||||
/**
|
||||
* Keeps track whether we are currently updating the schema.
|
||||
*/
|
||||
protected $schemaIsUpdating = false;
|
||||
|
||||
/**
|
||||
* Large array structure that represents a schema update transaction
|
||||
*/
|
||||
protected $schemaUpdateTransaction;
|
||||
|
||||
/**
|
||||
* Start a schema-updating transaction.
|
||||
* All calls to requireTable/Field/Index will keep track of the changes requested, but not actually do anything.
|
||||
* Once
|
||||
*/
|
||||
public function beginSchemaUpdate() {
|
||||
$this->schemaIsUpdating = true;
|
||||
$this->tableList = array();
|
||||
$tables = $this->tableList();
|
||||
foreach($tables as $table) $this->tableList[strtolower($table)] = $table;
|
||||
|
||||
$this->indexList = null;
|
||||
$this->fieldList = null;
|
||||
$this->schemaUpdateTransaction = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes a schema-updated transaction, executing all the schema chagnes.
|
||||
*/
|
||||
public function endSchemaUpdate() {
|
||||
foreach($this->schemaUpdateTransaction as $tableName => $changes) {
|
||||
switch($changes['command']) {
|
||||
case 'create':
|
||||
$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'], $changes['options'],
|
||||
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
|
||||
);
|
||||
break;
|
||||
|
||||
case 'alter':
|
||||
$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
|
||||
$changes['alteredFields'], $changes['alteredIndexes'], $changes['alteredOptions'],
|
||||
isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->schemaUpdateTransaction = null;
|
||||
$this->schemaIsUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the schema updates requested after a beginSchemaUpdate() call.
|
||||
*/
|
||||
public function cancelSchemaUpdate() {
|
||||
$this->schemaUpdateTransaction = null;
|
||||
$this->schemaIsUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are during a schema update.
|
||||
*/
|
||||
function isSchemaUpdating() {
|
||||
return $this->schemaIsUpdating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if schema modifications were requested after a beginSchemaUpdate() call.
|
||||
*/
|
||||
public function doesSchemaNeedUpdating() {
|
||||
return (bool)$this->schemaUpdateTransaction;
|
||||
}
|
||||
|
||||
// Transactional schema altering functions - they don't do anyhting except for update schemaUpdateTransaction
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param string $options
|
||||
*/
|
||||
public function transCreateTable($table, $options = null, $advanced_options = null) {
|
||||
$this->schemaUpdateTransaction[$table] = array(
|
||||
'command' => 'create',
|
||||
'newFields' => array(),
|
||||
'newIndexes' => array(),
|
||||
'options' => $options,
|
||||
'advancedOptions' => $advanced_options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param array $options
|
||||
*/
|
||||
public function transAlterTable($table, $options, $advanced_options) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
|
||||
$this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
|
||||
}
|
||||
|
||||
public function transCreateField($table, $field, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
|
||||
}
|
||||
public function transCreateIndex($table, $index, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
|
||||
}
|
||||
public function transAlterField($table, $field, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
|
||||
}
|
||||
public function transAlterIndex($table, $index, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the other transXXX methods - mark the given table as being altered
|
||||
* if it doesn't already exist
|
||||
*/
|
||||
protected function transInitTable($table) {
|
||||
if(!isset($this->schemaUpdateTransaction[$table])) {
|
||||
$this->schemaUpdateTransaction[$table] = array(
|
||||
'command' => 'alter',
|
||||
'newFields' => array(),
|
||||
'newIndexes' => array(),
|
||||
'alteredFields' => array(),
|
||||
'alteredIndexes' => array(),
|
||||
'alteredOptions' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate the following table in the database, modifying whatever already exists
|
||||
* as necessary.
|
||||
* @todo Change detection for CREATE TABLE $options other than "Engine"
|
||||
*
|
||||
* @param string $table The name of the table
|
||||
* @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
|
||||
* @param string $indexSchema A list of indexes to create. See {@link requireIndex()}
|
||||
* @param array $options
|
||||
*/
|
||||
public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK=true,
|
||||
$options = Array(), $extensions=false) {
|
||||
|
||||
if(!isset($this->tableList[strtolower($table)])) {
|
||||
$this->transCreateTable($table, $options, $extensions);
|
||||
$this->alterationMessage("Table $table: created","created");
|
||||
} else {
|
||||
if(Config::inst()->get('SS_Database', 'check_and_repair_on_build')) {
|
||||
$this->checkAndRepairTable($table, $options);
|
||||
}
|
||||
|
||||
// Check if options changed
|
||||
$tableOptionsChanged = false;
|
||||
if(isset($options[get_class($this)]) || true) {
|
||||
if(isset($options[get_class($this)])) {
|
||||
if(preg_match('/ENGINE=([^\s]*)/', $options[get_class($this)], $alteredEngineMatches)) {
|
||||
$alteredEngine = $alteredEngineMatches[1];
|
||||
$tableStatus = DB::query(sprintf(
|
||||
'SHOW TABLE STATUS LIKE \'%s\'',
|
||||
$table
|
||||
))->first();
|
||||
$tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($tableOptionsChanged || ($extensions && DB::getConn()->supportsExtensions()))
|
||||
$this->transAlterTable($table, $options, $extensions);
|
||||
|
||||
}
|
||||
|
||||
//DB ABSTRACTION: we need to convert this to a db-specific version:
|
||||
$this->requireField($table, 'ID', DB::getConn()->IdColumn(false, $hasAutoIncPK));
|
||||
|
||||
// Create custom fields
|
||||
if($fieldSchema) {
|
||||
foreach($fieldSchema as $fieldName => $fieldSpec) {
|
||||
|
||||
//Is this an array field?
|
||||
$arrayValue='';
|
||||
if(strpos($fieldSpec, '[')!==false){
|
||||
//If so, remove it and store that info separately
|
||||
$pos=strpos($fieldSpec, '[');
|
||||
$arrayValue=substr($fieldSpec, $pos);
|
||||
$fieldSpec=substr($fieldSpec, 0, $pos);
|
||||
}
|
||||
|
||||
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
|
||||
$fieldObj->arrayValue=$arrayValue;
|
||||
|
||||
$fieldObj->setTable($table);
|
||||
$fieldObj->requireField();
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom indexes
|
||||
if($indexSchema) {
|
||||
foreach($indexSchema as $indexName => $indexDetails) {
|
||||
$this->requireIndex($table, $indexName, $indexDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
|
||||
* @param string $table The table name.
|
||||
*/
|
||||
public function dontRequireTable($table) {
|
||||
if(isset($this->tableList[strtolower($table)])) {
|
||||
$suffix = '';
|
||||
while(isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
|
||||
$suffix = $suffix ? ($suffix+1) : 2;
|
||||
}
|
||||
$this->renameTable($table, "_obsolete_{$table}$suffix");
|
||||
$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix","obsolete");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given index in the database, modifying whatever already exists as necessary.
|
||||
*
|
||||
* The keys of the array are the names of the index.
|
||||
* The values of the array can be one of:
|
||||
* - true: Create a single column index on the field named the same as the index.
|
||||
* - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
|
||||
* control over the index.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $index The index name.
|
||||
* @param string|boolean $spec The specification of the index. See requireTable() for more information.
|
||||
*/
|
||||
public function requireIndex($table, $index, $spec) {
|
||||
$newTable = false;
|
||||
|
||||
//DB Abstraction: remove this ===true option as a possibility?
|
||||
if($spec === true) {
|
||||
$spec = "(\"$index\")";
|
||||
}
|
||||
|
||||
//Indexes specified as arrays cannot be checked with this line: (it flattens out the array)
|
||||
if(!is_array($spec)) {
|
||||
$spec = preg_replace('/\s*,\s*/', ',', $spec);
|
||||
}
|
||||
|
||||
if(!isset($this->tableList[strtolower($table)])) $newTable = true;
|
||||
|
||||
if(!$newTable && !isset($this->indexList[$table])) {
|
||||
$this->indexList[$table] = $this->indexList($table);
|
||||
}
|
||||
|
||||
//Fix up the index for database purposes
|
||||
$index=DB::getConn()->getDbSqlDefinition($table, $index, null, true);
|
||||
|
||||
//Fix the key for database purposes
|
||||
$index_alt=DB::getConn()->modifyIndex($index, $spec);
|
||||
|
||||
if(!$newTable) {
|
||||
if(isset($this->indexList[$table][$index_alt])) {
|
||||
if(is_array($this->indexList[$table][$index_alt])) {
|
||||
$array_spec = $this->indexList[$table][$index_alt]['spec'];
|
||||
} else {
|
||||
$array_spec = $this->indexList[$table][$index_alt];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($newTable || !isset($this->indexList[$table][$index_alt])) {
|
||||
$this->transCreateIndex($table, $index, $spec);
|
||||
$this->alterationMessage("Index $table.$index: created as "
|
||||
. DB::getConn()->convertIndexSpec($spec),"created");
|
||||
} else if($array_spec != DB::getConn()->convertIndexSpec($spec)) {
|
||||
$this->transAlterIndex($table, $index, $spec);
|
||||
$spec_msg=DB::getConn()->convertIndexSpec($spec);
|
||||
$this->alterationMessage("Index $table.$index: changed to $spec_msg"
|
||||
. " <i style=\"color: #AAA\">(from {$array_spec})</i>","changed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the table exists and already has a the field specified
|
||||
* @param string $tableName - The table to check
|
||||
* @param string $fieldName - The field to check
|
||||
* @return bool - True if the table exists and the field exists on the table
|
||||
*/
|
||||
public function hasField($tableName, $fieldName) {
|
||||
if (!$this->hasTable($tableName)) return false;
|
||||
$fields = $this->fieldList($tableName);
|
||||
return array_key_exists($fieldName, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given field on the table, modifying whatever already exists as necessary.
|
||||
* @param string $table The table name.
|
||||
* @param string $field The field name.
|
||||
* @param array|string $spec The field specification. If passed in array syntax, the specific database
|
||||
* driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
|
||||
* be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
|
||||
* need to take care of database abstraction in your DBField subclass.
|
||||
*/
|
||||
public function requireField($table, $field, $spec) {
|
||||
//TODO: this is starting to get extremely fragmented.
|
||||
//There are two different versions of $spec floating around, and their content changes depending
|
||||
//on how they are structured. This needs to be tidied up.
|
||||
$fieldValue = null;
|
||||
$newTable = false;
|
||||
|
||||
// backwards compatibility patch for pre 2.4 requireField() calls
|
||||
$spec_orig=$spec;
|
||||
|
||||
if(!is_string($spec)) {
|
||||
$spec['parts']['name'] = $field;
|
||||
$spec_orig['parts']['name'] = $field;
|
||||
//Convert the $spec array into a database-specific string
|
||||
$spec=DB::getConn()->$spec['type']($spec['parts'], true);
|
||||
}
|
||||
|
||||
// Collations didn't come in until MySQL 4.1. Anything earlier will throw a syntax error if you try and use
|
||||
// collations.
|
||||
// TODO: move this to the MySQLDatabase file, or drop it altogether?
|
||||
if(!$this->supportsCollations()) {
|
||||
$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
|
||||
}
|
||||
|
||||
if(!isset($this->tableList[strtolower($table)])) $newTable = true;
|
||||
|
||||
if(!$newTable && !isset($this->fieldList[$table])) {
|
||||
$this->fieldList[$table] = $this->fieldList($table);
|
||||
}
|
||||
|
||||
if(is_array($spec)) {
|
||||
$specValue = DB::getConn()->$spec_orig['type']($spec_orig['parts']);
|
||||
} else {
|
||||
$specValue = $spec;
|
||||
}
|
||||
|
||||
// We need to get db-specific versions of the ID column:
|
||||
if($spec_orig==DB::getConn()->IdColumn() || $spec_orig==DB::getConn()->IdColumn(true))
|
||||
$specValue=DB::getConn()->IdColumn(true);
|
||||
|
||||
if(!$newTable) {
|
||||
if(isset($this->fieldList[$table][$field])) {
|
||||
if(is_array($this->fieldList[$table][$field])) {
|
||||
$fieldValue = $this->fieldList[$table][$field]['data_type'];
|
||||
} else {
|
||||
$fieldValue = $this->fieldList[$table][$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the version of the field as we would create it. This is used for comparison purposes to see if the
|
||||
// existing field is different to what we now want
|
||||
if(is_array($spec_orig)) {
|
||||
$spec_orig=DB::getConn()->$spec_orig['type']($spec_orig['parts']);
|
||||
}
|
||||
|
||||
if($newTable || $fieldValue=='') {
|
||||
$this->transCreateField($table, $field, $spec_orig);
|
||||
$this->alterationMessage("Field $table.$field: created as $spec_orig","created");
|
||||
} else if($fieldValue != $specValue) {
|
||||
// If enums/sets are being modified, then we need to fix existing data in the table.
|
||||
// Update any records where the enum is set to a legacy value to be set to the default.
|
||||
// One hard-coded exception is SiteTree - the default for this is Page.
|
||||
foreach(array('enum','set') as $enumtype) {
|
||||
if(preg_match("/^$enumtype/i",$specValue)) {
|
||||
$newStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i","",$spec_orig);
|
||||
$new = preg_split("/'\s*,\s*'/", $newStr);
|
||||
|
||||
$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i","", $fieldValue);
|
||||
$old = preg_split("/'\s*,\s*'/", $newStr);
|
||||
|
||||
$holder = array();
|
||||
foreach($old as $check) {
|
||||
if(!in_array($check, $new)) {
|
||||
$holder[] = $check;
|
||||
}
|
||||
}
|
||||
if(count($holder)) {
|
||||
$default = explode('default ', $spec_orig);
|
||||
$default = $default[1];
|
||||
if($default == "'SiteTree'") $default = "'Page'";
|
||||
$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
|
||||
for($i=0;$i+1<count($holder);$i++) {
|
||||
$query .= "'{$holder[$i]}', ";
|
||||
}
|
||||
$query .= "'{$holder[$i]}')";
|
||||
DB::query($query);
|
||||
$amount = DB::affectedRows();
|
||||
$this->alterationMessage("Changed $amount rows to default value of field $field"
|
||||
. " (Value: $default)");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->transAlterField($table, $field, $spec_orig);
|
||||
$this->alterationMessage(
|
||||
"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
|
||||
"changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $fieldName
|
||||
*/
|
||||
public function dontRequireField($table, $fieldName) {
|
||||
$fieldList = $this->fieldList($table);
|
||||
if(array_key_exists($fieldName, $fieldList)) {
|
||||
$suffix = '';
|
||||
while(isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
|
||||
$suffix = $suffix ? ($suffix+1) : 2;
|
||||
}
|
||||
$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
|
||||
$this->alterationMessage("Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
|
||||
"obsolete");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a complex manipulation on the database.
|
||||
* A manipulation is an array of insert / or update sequences. The keys of the array are table names,
|
||||
* and the values are map containing 'command' and 'fields'. Command should be 'insert' or 'update',
|
||||
* and fields should be a map of field names to field values, including quotes. The field value can
|
||||
* also be a SQL function or similar.
|
||||
* @param array $manipulation
|
||||
*/
|
||||
public function manipulate($manipulation) {
|
||||
if($manipulation) foreach($manipulation as $table => $writeInfo) {
|
||||
|
||||
if(isset($writeInfo['fields']) && $writeInfo['fields']) {
|
||||
$fieldList = $columnList = $valueList = array();
|
||||
foreach($writeInfo['fields'] as $fieldName => $fieldVal) {
|
||||
$fieldList[] = "\"$fieldName\" = $fieldVal";
|
||||
$columnList[] = "\"$fieldName\"";
|
||||
|
||||
// Empty strings inserted as null in INSERTs. Replacement of SS_Database::replace_with_null().
|
||||
if($fieldVal === "''") $valueList[] = "null";
|
||||
else $valueList[] = $fieldVal;
|
||||
}
|
||||
|
||||
if(!isset($writeInfo['where']) && isset($writeInfo['id'])) {
|
||||
$writeInfo['where'] = "\"ID\" = " . (int)$writeInfo['id'];
|
||||
}
|
||||
|
||||
switch($writeInfo['command']) {
|
||||
case "update":
|
||||
// Test to see if this update query shouldn't, in fact, be an insert
|
||||
if($this->query("SELECT \"ID\" FROM \"$table\" WHERE $writeInfo[where]")->value()) {
|
||||
$fieldList = implode(", ", $fieldList);
|
||||
$sql = "UPDATE \"$table\" SET $fieldList where $writeInfo[where]";
|
||||
$this->query($sql);
|
||||
break;
|
||||
}
|
||||
|
||||
// ...if not, we'll skip on to the insert code
|
||||
|
||||
case "insert":
|
||||
if(!isset($writeInfo['fields']['ID']) && isset($writeInfo['id'])) {
|
||||
$columnList[] = "\"ID\"";
|
||||
$valueList[] = (int)$writeInfo['id'];
|
||||
}
|
||||
|
||||
$columnList = implode(", ", $columnList);
|
||||
$valueList = implode(", ", $valueList);
|
||||
$sql = "INSERT INTO \"$table\" ($columnList) VALUES ($valueList)";
|
||||
$this->query($sql);
|
||||
break;
|
||||
|
||||
default:
|
||||
$sql = null;
|
||||
user_error("SS_Database::manipulate() Can't recognise command '$writeInfo[command]'",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Replaces "\'\'" with "null", recursively walks through the given array.
|
||||
* @param string $array Array where the replacement should happen
|
||||
*/
|
||||
public static function replace_with_null(&$array) {
|
||||
$array = preg_replace('/= *\'\'/', '= null', $array);
|
||||
|
||||
if(is_array($array)) {
|
||||
foreach($array as $key => $value) {
|
||||
if(is_array($value)) {
|
||||
array_walk($array, array(SS_Database, 'replace_with_null'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler for database errors.
|
||||
* All database errors will call this function to report the error. It isn't a static function;
|
||||
* it will be called on the object itself and as such can be overridden in a subclass.
|
||||
* @todo hook this into a more well-structured error handling system.
|
||||
* @param string $msg The error message.
|
||||
* @param int $errorLevel The level of the error to throw.
|
||||
*/
|
||||
public function databaseError($msg, $errorLevel = E_USER_ERROR) {
|
||||
user_error($msg, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable supression of database messages.
|
||||
*/
|
||||
public function quiet() {
|
||||
$this->supressOutput = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a message about database alteration
|
||||
*
|
||||
* @param string message to display
|
||||
* @param string type one of [created|changed|repaired|obsolete|deleted|error]
|
||||
*/
|
||||
public function alterationMessage($message,$type=""){
|
||||
if(!$this->supressOutput) {
|
||||
if(Director::is_cli()) {
|
||||
switch ($type){
|
||||
case "created":
|
||||
case "changed":
|
||||
case "repaired":
|
||||
$sign = "+";
|
||||
break;
|
||||
case "obsolete":
|
||||
case "deleted":
|
||||
$sign = '-';
|
||||
break;
|
||||
case "error":
|
||||
$sign = "!";
|
||||
break;
|
||||
default:
|
||||
$sign=" ";
|
||||
}
|
||||
$message = strip_tags($message);
|
||||
echo " $sign $message\n";
|
||||
} else {
|
||||
switch ($type){
|
||||
case "created":
|
||||
$color = "green";
|
||||
break;
|
||||
case "obsolete":
|
||||
$color = "red";
|
||||
break;
|
||||
case "error":
|
||||
$color = "red";
|
||||
break;
|
||||
case "deleted":
|
||||
$color = "red";
|
||||
break;
|
||||
case "changed":
|
||||
$color = "blue";
|
||||
break;
|
||||
case "repaired":
|
||||
$color = "blue";
|
||||
break;
|
||||
default:
|
||||
$color="";
|
||||
}
|
||||
echo "<li style=\"color: $color\">$message</li>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SELECT clauses ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @param array $select Select columns
|
||||
* @param boolean $distinct Distinct select?
|
||||
* @return string
|
||||
*/
|
||||
public function sqlSelectToString($select, $distinct = false) {
|
||||
$clauses = array();
|
||||
|
||||
foreach($select as $alias => $field) {
|
||||
// Don't include redundant aliases.
|
||||
if($alias === $field || preg_match('/"' . preg_quote($alias) . '"$/', $field)) $clauses[] = $field;
|
||||
else $clauses[] = "$field AS \"$alias\"";
|
||||
}
|
||||
|
||||
$text = 'SELECT ';
|
||||
if($distinct) $text .= 'DISTINCT ';
|
||||
return $text .= implode(', ', $clauses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FROM clause ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlFromToString($from) {
|
||||
return ' FROM ' . implode(' ', $from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WHERE clauses ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlWhereToString($where, $connective) {
|
||||
return ' WHERE (' . implode(") {$connective} (" , $where) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ORDER BY clauses ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlOrderByToString($orderby) {
|
||||
$statements = array();
|
||||
|
||||
foreach($orderby as $clause => $dir) {
|
||||
$statements[] = trim($clause . ' ' . $dir);
|
||||
}
|
||||
|
||||
return ' ORDER BY ' . implode(', ', $statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GROUP BY clauses ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlGroupByToString($groupby) {
|
||||
return ' GROUP BY ' . implode(', ', $groupby);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HAVING clauses ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlHavingToString($having) {
|
||||
return ' HAVING ( ' . implode(' ) AND ( ', $having) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the LIMIT clause ready for inserting into a query.
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sqlLimitToString($limit) {
|
||||
$clause = '';
|
||||
|
||||
// Pass limit as array or SQL string value
|
||||
if(is_array($limit)) {
|
||||
if(!array_key_exists('limit', $limit)) {
|
||||
throw new InvalidArgumentException('Database::sqlLimitToString(): Wrong format for $limit: '
|
||||
. var_export($limit, true));
|
||||
}
|
||||
|
||||
if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit'])
|
||||
&& is_numeric($limit['limit'])) {
|
||||
|
||||
$combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]";
|
||||
} elseif(isset($limit['limit']) && is_numeric($limit['limit'])) {
|
||||
$combinedLimit = (int)$limit['limit'];
|
||||
} else {
|
||||
$combinedLimit = false;
|
||||
}
|
||||
if(!empty($combinedLimit)) $clause .= ' LIMIT ' . $combinedLimit;
|
||||
} else {
|
||||
$clause .= ' LIMIT ' . $limit;
|
||||
}
|
||||
|
||||
return $clause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a SQLQuery object into a SQL statement
|
||||
* Caution: Expects correctly quoted and escaped SQL fragments.
|
||||
*
|
||||
* @param $query SQLQuery
|
||||
*/
|
||||
public function sqlQueryToString(SQLQuery $query) {
|
||||
if($query->getDelete()) {
|
||||
$text = 'DELETE ';
|
||||
} else {
|
||||
$text = $this->sqlSelectToString($query->getSelect(), $query->getDistinct());
|
||||
}
|
||||
|
||||
if($query->getFrom()) $text .= $this->sqlFromToString($query->getFrom());
|
||||
if($query->getWhere()) $text .= $this->sqlWhereToString($query->getWhere(), $query->getConnective());
|
||||
|
||||
// these clauses only make sense in SELECT queries, not DELETE
|
||||
if(!$query->getDelete()) {
|
||||
if($query->getGroupBy()) $text .= $this->sqlGroupByToString($query->getGroupBy());
|
||||
if($query->getHaving()) $text .= $this->sqlHavingToString($query->getHaving());
|
||||
if($query->getOrderBy()) $text .= $this->sqlOrderByToString($query->getOrderBy());
|
||||
if($query->getLimit()) $text .= $this->sqlLimitToString($query->getLimit());
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string into DB-specific quotes. MySQL, PostgreSQL and SQLite3 only need single quotes around the string.
|
||||
* MSSQL will overload this and include it's own N prefix to mark the string as unicode, so characters like macrons
|
||||
* are saved correctly.
|
||||
*
|
||||
* @param string $string String to be prepared for database query
|
||||
* @return string Prepared string
|
||||
*/
|
||||
public function prepStringForDB($string) {
|
||||
return "'" . Convert::raw2sql($string) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a WHERE clause for text matching.
|
||||
*
|
||||
* @param String $field Quoted field name
|
||||
* @param String $value Escaped search. Can include percentage wildcards.
|
||||
* @param boolean $exact Exact matches or wildcard support.
|
||||
* @param boolean $negate Negate the clause.
|
||||
* @param boolean $caseSensitive Perform case sensitive search.
|
||||
* @return String SQL
|
||||
*/
|
||||
abstract public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = false);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime in a certain format
|
||||
* @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or
|
||||
* field name, e.g. '"SiteTree"."Created"'
|
||||
* @param string $format to be used, supported specifiers:
|
||||
* %Y = Year (four digits)
|
||||
* %m = Month (01..12)
|
||||
* %d = Day (01..31)
|
||||
* %H = Hour (00..23)
|
||||
* %i = Minutes (00..59)
|
||||
* %s = Seconds (00..59)
|
||||
* %U = unix timestamp, can only be used on it's own
|
||||
* @return string SQL datetime expression to query for a formatted datetime
|
||||
*/
|
||||
abstract public function formattedDatetimeClause($date, $format);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime addition
|
||||
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes,
|
||||
* +1 YEAR
|
||||
* supported qualifiers:
|
||||
* - years
|
||||
* - months
|
||||
* - days
|
||||
* - hours
|
||||
* - minutes
|
||||
* - seconds
|
||||
* This includes the singular forms as well
|
||||
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of
|
||||
* the addition
|
||||
*/
|
||||
abstract public function datetimeIntervalClause($date, $interval);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime substraction
|
||||
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $date2 to be substracted of $date1, can be either 'now', literal datetime
|
||||
* like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
||||
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which
|
||||
* is the result of the substraction
|
||||
*/
|
||||
abstract public function datetimeDifferenceClause($date1, $date2);
|
||||
|
||||
/**
|
||||
* Can the database override timezone as a connection setting,
|
||||
* or does it use the system timezone exclusively?
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
abstract public function supportsTimezoneOverride();
|
||||
|
||||
/*
|
||||
* Does this database support transactions?
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function supportsTransactions();
|
||||
|
||||
/*
|
||||
* Start a prepared transaction
|
||||
* See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on
|
||||
* transaction isolation options
|
||||
*/
|
||||
abstract public function transactionStart($transaction_mode=false, $session_characteristics=false);
|
||||
|
||||
/*
|
||||
* Create a savepoint that you can jump back to if you encounter problems
|
||||
*/
|
||||
abstract public function transactionSavepoint($savepoint);
|
||||
|
||||
/*
|
||||
* Rollback or revert to a savepoint if your queries encounter problems
|
||||
* If you encounter a problem at any point during a transaction, you may
|
||||
* need to rollback that particular query, or return to a savepoint
|
||||
*/
|
||||
abstract public function transactionRollback($savepoint=false);
|
||||
|
||||
/*
|
||||
* Commit everything inside this transaction so far
|
||||
*/
|
||||
abstract public function transactionEnd();
|
||||
|
||||
/**
|
||||
* Determines if the used database supports application-level locks,
|
||||
* which is different from table- or row-level locking.
|
||||
* See {@link getLock()} for details.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function supportsLocks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the lock is available.
|
||||
* See {@link supportsLocks()} to check if locking is generally supported.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public function canLock($name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an application-level lock so that no two processes can run at the same time,
|
||||
* also called a "cooperative advisory lock".
|
||||
*
|
||||
* Return FALSE if acquiring the lock fails; otherwise return TRUE, if lock was acquired successfully.
|
||||
* Lock is automatically released if connection to the database is broken (either normally or abnormally),
|
||||
* making it less prone to deadlocks than session- or file-based locks.
|
||||
* Should be accompanied by a {@link releaseLock()} call after the logic requiring the lock has completed.
|
||||
* Can be called multiple times, in which case locks "stack" (PostgreSQL, SQL Server),
|
||||
* or auto-releases the previous lock (MySQL).
|
||||
*
|
||||
* Note that this might trigger the database to wait for the lock to be released, delaying further execution.
|
||||
*
|
||||
* @param String
|
||||
* @param Int Timeout in seconds
|
||||
* @return Boolean
|
||||
*/
|
||||
public function getLock($name, $timeout = 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an application-level lock file to allow another process to run
|
||||
* (if the execution aborts (e.g. due to an error) all locks are automatically released).
|
||||
*
|
||||
* @param String
|
||||
* @return Boolean
|
||||
*/
|
||||
public function releaseLock($name) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -155,11 +155,11 @@ class DatabaseAdmin extends Controller {
|
||||
if($quiet) {
|
||||
DB::quiet();
|
||||
} else {
|
||||
$conn = DB::getConn();
|
||||
$conn = DB::get_conn();
|
||||
// Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database")
|
||||
$dbType = substr(get_class($conn), 0, -8);
|
||||
$dbVersion = $conn->getVersion();
|
||||
$databaseName = (method_exists($conn, 'currentDatabase')) ? $conn->currentDatabase() : "";
|
||||
$databaseName = (method_exists($conn, 'currentDatabase')) ? $conn->getSelectedDatabase() : "";
|
||||
|
||||
if(Director::is_cli()) {
|
||||
echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion);
|
||||
@ -169,23 +169,29 @@ class DatabaseAdmin extends Controller {
|
||||
}
|
||||
|
||||
// Set up the initial database
|
||||
if(!DB::isActive()) {
|
||||
if(!DB::is_active()) {
|
||||
if(!$quiet) {
|
||||
echo '<p><b>Creating database</b></p>';
|
||||
}
|
||||
|
||||
// Load parameters from existing configuration
|
||||
global $databaseConfig;
|
||||
$parameters = $databaseConfig ? $databaseConfig : $_REQUEST['db'];
|
||||
$connect = DB::getConnect($parameters);
|
||||
$username = $parameters['username'];
|
||||
$password = $parameters['password'];
|
||||
$database = $parameters['database'];
|
||||
|
||||
if(!$database) {
|
||||
user_error("No database name given; please give a value for \$databaseConfig['database']",
|
||||
E_USER_ERROR);
|
||||
if(empty($databaseConfig) && empty($_REQUEST['db'])) {
|
||||
user_error("No database configuration available", E_USER_ERROR);
|
||||
}
|
||||
|
||||
DB::createDatabase($connect, $username, $password, $database);
|
||||
$parameters = (!empty($databaseConfig)) ? $databaseConfig : $_REQUEST['db'];
|
||||
|
||||
// Check database name is given
|
||||
if(empty($parameters['database'])) {
|
||||
user_error("No database name given; please give a value for \$databaseConfig['database']",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
$database = $parameters['database'];
|
||||
|
||||
// Establish connection and create database in two steps
|
||||
unset($parameters['database']);
|
||||
DB::connect($parameters);
|
||||
DB::create_database($database);
|
||||
}
|
||||
|
||||
// Build the database. Most of the hard work is handled by DataObject
|
||||
@ -197,22 +203,27 @@ class DatabaseAdmin extends Controller {
|
||||
else echo "\n<p><b>Creating database tables</b></p>\n\n";
|
||||
}
|
||||
|
||||
$conn = DB::getConn();
|
||||
$conn->beginSchemaUpdate();
|
||||
foreach($dataClasses as $dataClass) {
|
||||
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
|
||||
if(class_exists($dataClass)) {
|
||||
// Initiate schema update
|
||||
$dbSchema = DB::get_schema();
|
||||
$dbSchema->schemaUpdate(function() use($dataClasses, $testMode, $quiet){
|
||||
foreach($dataClasses as $dataClass) {
|
||||
// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
|
||||
if(!class_exists($dataClass)) continue;
|
||||
|
||||
// Check if this class should be excluded as per testing conventions
|
||||
$SNG = singleton($dataClass);
|
||||
if($testMode || !($SNG instanceof TestOnly)) {
|
||||
if(!$quiet) {
|
||||
if(Director::is_cli()) echo " * $dataClass\n";
|
||||
else echo "<li>$dataClass</li>\n";
|
||||
}
|
||||
$SNG->requireTable();
|
||||
if(!$testMode && $SNG instanceof TestOnly) continue;
|
||||
|
||||
// Log data
|
||||
if(!$quiet) {
|
||||
if(Director::is_cli()) echo " * $dataClass\n";
|
||||
else echo "<li>$dataClass</li>\n";
|
||||
}
|
||||
|
||||
// Instruct the class to apply its schema to the database
|
||||
$SNG->requireTable();
|
||||
}
|
||||
}
|
||||
$conn->endSchemaUpdate();
|
||||
});
|
||||
ClassInfo::reset_db_cache();
|
||||
|
||||
if($populate) {
|
||||
@ -235,8 +246,10 @@ class DatabaseAdmin extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
touch(TEMP_FOLDER . '/database-last-generated-' .
|
||||
str_replace(array('\\', '/', ':'), '.', Director::baseFolder()));
|
||||
touch(TEMP_FOLDER
|
||||
. '/database-last-generated-'
|
||||
. str_replace(array('\\', '/', ':'), '.', Director::baseFolder())
|
||||
);
|
||||
|
||||
if(isset($_REQUEST['from_installer'])) {
|
||||
echo "OK";
|
||||
@ -251,14 +264,12 @@ class DatabaseAdmin extends Controller {
|
||||
|
||||
/**
|
||||
* Clear all data out of the database
|
||||
* @todo Move this code into SS_Database class, for DB abstraction
|
||||
*
|
||||
* @deprecated since version 3.2
|
||||
*/
|
||||
public function clearAllData() {
|
||||
$tables = DB::getConn()->tableList();
|
||||
foreach($tables as $table) {
|
||||
if(method_exists(DB::getConn(), 'clearTable')) DB::getConn()->clearTable($table);
|
||||
else DB::query("TRUNCATE \"$table\"");
|
||||
}
|
||||
Deprecation::notice('3.2', 'Use DB::get_conn()->clearAllData() instead');
|
||||
DB::get_conn()->clearAllData();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -278,7 +289,7 @@ class DatabaseAdmin extends Controller {
|
||||
$subclasses = ClassInfo::subclassesFor($baseClass);
|
||||
unset($subclasses[0]);
|
||||
foreach($subclasses as $k => $subclass) {
|
||||
if(DataObject::database_fields($subclass)) {
|
||||
if(DataObject::has_own_table($subclass)) {
|
||||
unset($subclasses[$k]);
|
||||
}
|
||||
}
|
||||
@ -309,5 +320,3 @@ class DatabaseAdmin extends Controller {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,12 +38,11 @@ class HasManyList extends RelationList {
|
||||
if ($id === null) $id = $this->getForeignID();
|
||||
|
||||
// Apply relation filter
|
||||
$key = "\"$this->foreignKey\"";
|
||||
if(is_array($id)) {
|
||||
return "\"$this->foreignKey\" IN ('" .
|
||||
implode("', '", array_map('Convert::raw2sql', $id)) . "')";
|
||||
return array("$key IN (".DB::placeholders($id).")" => $id);
|
||||
} else if($id !== null){
|
||||
return "\"$this->foreignKey\" = '" .
|
||||
Convert::raw2sql($id) . "'";
|
||||
return array($key => $id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,8 +72,8 @@ class HasManyList extends RelationList {
|
||||
return;
|
||||
}
|
||||
|
||||
$fk = $this->foreignKey;
|
||||
$item->$fk = $foreignID;
|
||||
$foreignKey = $this->foreignKey;
|
||||
$item->$foreignKey = $foreignID;
|
||||
|
||||
$item->write();
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ class Hierarchy extends DataExtension {
|
||||
*/
|
||||
private static $node_threshold_leaf = 250;
|
||||
|
||||
public function augmentSQL(SQLQuery &$query) {
|
||||
public function augmentSQL(SQLSelect $query) {
|
||||
}
|
||||
|
||||
public function augmentDatabase() {
|
||||
@ -658,12 +658,14 @@ class Hierarchy extends DataExtension {
|
||||
* Get the parent of this class.
|
||||
* @return DataObject
|
||||
*/
|
||||
public function getParent($filter = '') {
|
||||
public function getParent($filter = null) {
|
||||
if($p = $this->owner->__get("ParentID")) {
|
||||
$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
|
||||
$baseClass = array_shift($tableClasses);
|
||||
$filter .= ($filter) ? " AND " : ""."\"$baseClass\".\"ID\" = $p";
|
||||
return DataObject::get_one($this->owner->class, $filter);
|
||||
return DataObject::get_one($this->owner->class, array(
|
||||
array("\"$baseClass\".\"ID\"" => $p),
|
||||
$filter
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -777,4 +779,3 @@ class Hierarchy extends DataExtension {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -253,7 +253,7 @@ class Image extends File {
|
||||
* Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth.
|
||||
*
|
||||
* @param Image_Backend $backend
|
||||
* @param type $width The width to set
|
||||
* @param int $width The width to set
|
||||
* @return Image_Backend
|
||||
*/
|
||||
public function generateSetWidth(Image_Backend $backend, $width) {
|
||||
|
@ -50,23 +50,20 @@ class ManyManyList extends RelationList {
|
||||
if($extraFields) $this->dataQuery->selectFromTable($joinTable, array_keys($extraFields));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a filter expression for when getting the contents of the relationship for some foreign ID
|
||||
* @return string
|
||||
*/
|
||||
protected function foreignIDFilter($id = null) {
|
||||
if ($id === null) $id = $this->getForeignID();
|
||||
if ($id === null) {
|
||||
$id = $this->getForeignID();
|
||||
}
|
||||
|
||||
// Apply relation filter
|
||||
$key = "\"$this->joinTable\".\"$this->foreignKey\"";
|
||||
if(is_array($id)) {
|
||||
return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" .
|
||||
implode("', '", array_map('Convert::raw2sql', $id)) . "')";
|
||||
return array("$key IN (".DB::placeholders($id).")" => $id);
|
||||
} else if($id !== null){
|
||||
return "\"$this->joinTable\".\"$this->foreignKey\" = '" .
|
||||
Convert::raw2sql($id) . "'";
|
||||
return array($key => $id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a filter expression for the join table when writing to the join table
|
||||
*
|
||||
@ -74,7 +71,9 @@ class ManyManyList extends RelationList {
|
||||
* entries. However some subclasses of ManyManyList (Member_GroupSet) modify foreignIDFilter to
|
||||
* include additional calculated entries, so we need different filters when reading and when writing
|
||||
*
|
||||
* @return string
|
||||
* @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current ids
|
||||
* as per getForeignID
|
||||
* @return array Condition In array(SQL => parameters format)
|
||||
*/
|
||||
protected function foreignIDWriteFilter($id = null) {
|
||||
return $this->foreignIDFilter($id);
|
||||
@ -83,53 +82,68 @@ class ManyManyList extends RelationList {
|
||||
/**
|
||||
* Add an item to this many_many relationship
|
||||
* Does so by adding an entry to the joinTable.
|
||||
*
|
||||
* @param $extraFields A map of additional columns to insert into the joinTable
|
||||
*/
|
||||
public function add($item, $extraFields = null) {
|
||||
if(is_numeric($item)) $itemID = $item;
|
||||
else if($item instanceof $this->dataClass) $itemID = $item->ID;
|
||||
else {
|
||||
public function add($item, $extraFields = array()) {
|
||||
// Ensure nulls or empty strings are correctly treated as empty arrays
|
||||
if(empty($extraFields)) $extraFields = array();
|
||||
|
||||
// Determine ID of new record
|
||||
if(is_numeric($item)) {
|
||||
$itemID = $item;
|
||||
} elseif($item instanceof $this->dataClass) {
|
||||
$itemID = $item->ID;
|
||||
} else {
|
||||
throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
|
||||
$foreignIDs = $this->getForeignID();
|
||||
$foreignFilter = $this->foreignIDWriteFilter();
|
||||
|
||||
// Validate foreignID
|
||||
if(!$foreignIDs) {
|
||||
$foreignIDs = $this->getForeignID();
|
||||
if(empty($foreignIDs)) {
|
||||
throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING);
|
||||
}
|
||||
|
||||
if($foreignFilter) {
|
||||
$query = new SQLQuery("*", array("\"$this->joinTable\""));
|
||||
$query->setWhere($foreignFilter);
|
||||
$hasExisting = ($query->count() > 0);
|
||||
} else {
|
||||
$hasExisting = false;
|
||||
}
|
||||
|
||||
// Insert or update
|
||||
foreach((array)$foreignIDs as $foreignID) {
|
||||
$manipulation = array();
|
||||
if($hasExisting) {
|
||||
$manipulation[$this->joinTable]['command'] = 'update';
|
||||
$manipulation[$this->joinTable]['where'] = "\"$this->joinTable\".\"$this->foreignKey\" = " .
|
||||
"'" . Convert::raw2sql($foreignID) . "'" .
|
||||
" AND \"$this->localKey\" = {$itemID}";
|
||||
// Apply this item to each given foreign ID record
|
||||
if(!is_array($foreignIDs)) $foreignIDs = array($foreignIDs);
|
||||
$baseClass = ClassInfo::baseDataClass($this->dataClass);
|
||||
foreach($foreignIDs as $foreignID) {
|
||||
|
||||
// Check for existing records for this item
|
||||
if($foreignFilter = $this->foreignIDFilter($foreignID)) {
|
||||
// With the current query, simply add the foreign and local conditions
|
||||
// The query can be a bit odd, especially if custom relation classes
|
||||
// don't join expected tables (@see Member_GroupSet for example).
|
||||
$query = $this->dataQuery->query()->toSelect();
|
||||
$query->addWhere($foreignFilter);
|
||||
$query->addWhere(array(
|
||||
"\"$baseClass\".\"ID\"" => $itemID
|
||||
));
|
||||
$hasExisting = ($query->count() > 0);
|
||||
} else {
|
||||
$manipulation[$this->joinTable]['command'] = 'insert';
|
||||
$hasExisting = false;
|
||||
}
|
||||
|
||||
if($extraFields) foreach($extraFields as $k => $v) {
|
||||
if(is_null($v)) $manipulation[$this->joinTable]['fields'][$k] = 'NULL';
|
||||
else $manipulation[$this->joinTable]['fields'][$k] = "'" . Convert::raw2sql($v) . "'";
|
||||
|
||||
// Determine entry type
|
||||
if(!$hasExisting) {
|
||||
// Field values for new record
|
||||
$fieldValues = array_merge($extraFields, array(
|
||||
"\"{$this->foreignKey}\"" => $foreignID,
|
||||
"\"{$this->localKey}\"" => $itemID
|
||||
));
|
||||
// Create new record
|
||||
$insert = new SQLInsert("\"{$this->joinTable}\"", $fieldValues);
|
||||
$insert->execute();
|
||||
} elseif(!empty($extraFields)) {
|
||||
// For existing records, simply update any extra data supplied
|
||||
$foreignWriteFilter = $this->foreignIDWriteFilter($foreignID);
|
||||
$update = new SQLUpdate("\"{$this->joinTable}\"", $extraFields, $foreignWriteFilter);
|
||||
$update->addWhere(array(
|
||||
"\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID
|
||||
));
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
$manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID;
|
||||
$manipulation[$this->joinTable]['fields'][$this->foreignKey] = $foreignID;
|
||||
|
||||
DB::manipulate($manipulation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,8 +168,7 @@ class ManyManyList extends RelationList {
|
||||
public function removeByID($itemID) {
|
||||
if(!is_numeric($itemID)) throw new InvalidArgumentException("ManyManyList::removeById() expecting an ID");
|
||||
|
||||
$query = new SQLQuery("*", array("\"$this->joinTable\""));
|
||||
$query->setDelete(true);
|
||||
$query = new SQLDelete("\"$this->joinTable\"");
|
||||
|
||||
if($filter = $this->foreignIDWriteFilter($this->getForeignID())) {
|
||||
$query->setWhere($filter);
|
||||
@ -163,7 +176,7 @@ class ManyManyList extends RelationList {
|
||||
user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING);
|
||||
}
|
||||
|
||||
$query->addWhere("\"$this->localKey\" = {$itemID}");
|
||||
$query->addWhere(array("\"$this->localKey\"" => $itemID));
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
@ -175,24 +188,27 @@ class ManyManyList extends RelationList {
|
||||
|
||||
// Remove the join to the join table to avoid MySQL row locking issues.
|
||||
$query = $this->dataQuery();
|
||||
$query->removeFilterOn($query->getQueryParam('Foreign.Filter'));
|
||||
$foreignFilter = $query->getQueryParam('Foreign.Filter');
|
||||
$query->removeFilterOn($foreignFilter);
|
||||
|
||||
$query = $query->query();
|
||||
$query->setSelect("\"$base\".\"ID\"");
|
||||
$selectQuery = $query->query();
|
||||
$selectQuery->setSelect("\"$base\".\"ID\"");
|
||||
|
||||
$from = $query->getFrom();
|
||||
$from = $selectQuery->getFrom();
|
||||
unset($from[$this->joinTable]);
|
||||
$query->setFrom($from);
|
||||
$query->setDistinct(false);
|
||||
$query->setOrderBy(null, null); // ensure any default sorting is removed, ORDER BY can break DELETE clauses
|
||||
$selectQuery->setFrom($from);
|
||||
$selectQuery->setOrderBy(); // ORDER BY in subselects breaks MS SQL Server and is not necessary here
|
||||
$selectQuery->setDistinct(false);
|
||||
|
||||
// Use a sub-query as SQLite does not support setting delete targets in
|
||||
// joined queries.
|
||||
$delete = new SQLQuery();
|
||||
$delete->setDelete(true);
|
||||
$delete = new SQLDelete();
|
||||
$delete->setFrom("\"$this->joinTable\"");
|
||||
$delete->addWhere($this->foreignIDFilter());
|
||||
$delete->addWhere("\"$this->joinTable\".\"$this->localKey\" IN ({$query->sql()})");
|
||||
$subSelect = $selectQuery->sql($parameters);
|
||||
$delete->addWhere(array(
|
||||
"\"$this->joinTable\".\"$this->localKey\" IN ($subSelect)" => $parameters
|
||||
));
|
||||
$delete->execute();
|
||||
}
|
||||
|
||||
@ -216,7 +232,7 @@ class ManyManyList extends RelationList {
|
||||
// @todo Optimize into a single query instead of one per extra field
|
||||
if($this->extraFields) {
|
||||
foreach($this->extraFields as $fieldName => $dbFieldSpec) {
|
||||
$query = new SQLQuery("\"$fieldName\"", array("\"$this->joinTable\""));
|
||||
$query = new SQLSelect("\"$fieldName\"", "\"$this->joinTable\"");
|
||||
if($filter = $this->foreignIDWriteFilter($this->getForeignID())) {
|
||||
$query->setWhere($filter);
|
||||
} else {
|
||||
|
@ -1,1218 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* MySQL connector class.
|
||||
*
|
||||
* Supported indexes for {@link requireTable()}:
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLDatabase extends SS_Database {
|
||||
/**
|
||||
* Connection to the DBMS.
|
||||
* @var resource
|
||||
*/
|
||||
protected $dbConn;
|
||||
|
||||
/**
|
||||
* True if we are connected to a database.
|
||||
* @var boolean
|
||||
*/
|
||||
protected $active;
|
||||
|
||||
/**
|
||||
* The name of the database.
|
||||
* @var string
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* @config
|
||||
* @var String
|
||||
*/
|
||||
private static $connection_charset = null;
|
||||
|
||||
protected $supportsTransactions = true;
|
||||
|
||||
/**
|
||||
* Sets the character set for the MySQL database connection.
|
||||
*
|
||||
* The character set connection should be set to 'utf8' for SilverStripe version 2.4.0 and
|
||||
* later.
|
||||
*
|
||||
* However, sites created before version 2.4.0 should leave this unset or data that isn't 7-bit
|
||||
* safe will be corrupted. As such, the installer comes with this set in mysite/_config.php by
|
||||
* default in versions 2.4.0 and later.
|
||||
*
|
||||
* @deprecated 3.1 Use "MySQLDatabase.connection_charset" config setting instead
|
||||
*/
|
||||
public static function set_connection_charset($charset = 'utf8') {
|
||||
Deprecation::notice('3.2', 'Use "MySQLDatabase.connection_charset" config setting instead');
|
||||
Config::inst()->update('MySQLDatabase', 'connection_charset', $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a MySQL database.
|
||||
* @param array $parameters An map of parameters, which should include:
|
||||
* - server: The server, eg, localhost
|
||||
* - username: The username to log on with
|
||||
* - password: The password to log on with
|
||||
* - database: The database to connect to
|
||||
* - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
|
||||
*/
|
||||
public function __construct($parameters) {
|
||||
if(!empty($parameters['port'])) {
|
||||
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password'],
|
||||
'', $parameters['port']);
|
||||
} else {
|
||||
$this->dbConn = new MySQLi($parameters['server'], $parameters['username'], $parameters['password']);
|
||||
}
|
||||
|
||||
if($this->dbConn->connect_error) {
|
||||
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
|
||||
}
|
||||
|
||||
$this->query("SET sql_mode = 'ANSI'");
|
||||
|
||||
if(Config::inst()->get('MySQLDatabase', 'connection_charset')) {
|
||||
$this->dbConn->set_charset(Config::inst()->get('MySQLDatabase', 'connection_charset'));
|
||||
}
|
||||
|
||||
$this->active = $this->dbConn->select_db($parameters['database']);
|
||||
$this->database = $parameters['database'];
|
||||
|
||||
if(isset($parameters['timezone'])) {
|
||||
$this->query(sprintf("SET SESSION time_zone = '%s'", $parameters['timezone']));
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if($this->dbConn) {
|
||||
mysqli_close($this->dbConn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not implemented, needed for PDO
|
||||
*/
|
||||
public function getConnect($parameters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this database supports collations
|
||||
* @return boolean
|
||||
*/
|
||||
public function supportsCollations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsTimezoneOverride() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of MySQL.
|
||||
* @return string
|
||||
*/
|
||||
public function getVersion() {
|
||||
return $this->dbConn->server_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database server, namely mysql.
|
||||
* @return string
|
||||
*/
|
||||
public function getDatabaseServer() {
|
||||
return "mysql";
|
||||
}
|
||||
|
||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))),
|
||||
array('insert','update','delete','replace'))) {
|
||||
|
||||
Debug::message("Will execute: $sql");
|
||||
return;
|
||||
}
|
||||
|
||||
if(isset($_REQUEST['showqueries']) && Director::isDev(true)) {
|
||||
$starttime = microtime(true);
|
||||
}
|
||||
|
||||
$handle = $this->dbConn->query($sql);
|
||||
|
||||
if(isset($_REQUEST['showqueries']) && Director::isDev(true)) {
|
||||
$endtime = round(microtime(true) - $starttime,4);
|
||||
Debug::message("\n$sql\n{$endtime}s\n", false);
|
||||
}
|
||||
|
||||
if(!$handle && $errorLevel) {
|
||||
$this->databaseError("Couldn't run query: $sql | " . $this->dbConn->error, $errorLevel);
|
||||
}
|
||||
return new MySQLQuery($this, $handle);
|
||||
}
|
||||
|
||||
public function getGeneratedID($table) {
|
||||
return $this->dbConn->insert_id;
|
||||
}
|
||||
|
||||
public function isActive() {
|
||||
return $this->active ? true : false;
|
||||
}
|
||||
|
||||
public function createDatabase() {
|
||||
$this->query("CREATE DATABASE \"$this->database\" DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci");
|
||||
$this->query("USE \"$this->database\"");
|
||||
|
||||
$this->tableList = $this->fieldList = $this->indexList = null;
|
||||
|
||||
$this->active = $this->dbConn->select_db($this->database);
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the database that this object is currently connected to.
|
||||
* Use with caution.
|
||||
*/
|
||||
public function dropDatabase() {
|
||||
$this->dropDatabaseByName($this->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the database that this object is currently connected to.
|
||||
* Use with caution.
|
||||
*/
|
||||
public function dropDatabaseByName($dbName) {
|
||||
$this->query("DROP DATABASE \"$dbName\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the currently selected database
|
||||
*/
|
||||
public function currentDatabase() {
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the given database.
|
||||
* If the database doesn't exist, you should call createDatabase() after calling selectDatabase()
|
||||
*/
|
||||
public function selectDatabase($dbname) {
|
||||
$this->database = $dbname;
|
||||
$this->tableList = $this->fieldList = $this->indexList = null;
|
||||
$this->active = false;
|
||||
if($this->databaseExists($this->database)) {
|
||||
$this->active = $this->dbConn->select_db($this->database);
|
||||
}
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the named database exists.
|
||||
*/
|
||||
public function databaseExists($name) {
|
||||
$SQL_name = Convert::raw2sql($name);
|
||||
return $this->query("SHOW DATABASES LIKE '$SQL_name'")->value() ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a column
|
||||
*/
|
||||
public function allDatabaseNames() {
|
||||
return $this->query("SHOW DATABASES")->column();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new table.
|
||||
* @param $tableName The name of the table
|
||||
* @param $fields A map of field names to field types
|
||||
* @param $indexes A map of indexes
|
||||
* @param $options An map of additional options. The available keys are as follows:
|
||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
||||
* - 'temporary' - If true, then a temporary table will be created
|
||||
* @return The table name generated. This may be different from the table name, for example with temporary tables.
|
||||
*/
|
||||
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
||||
$fieldSchemas = $indexSchemas = "";
|
||||
|
||||
if(!empty($options[get_class($this)])) {
|
||||
$addOptions = $options[get_class($this)];
|
||||
} elseif(!empty($options[get_parent_class($this)])) {
|
||||
$addOptions = $options[get_parent_class($this)];
|
||||
} else {
|
||||
$addOptions = "ENGINE=InnoDB";
|
||||
}
|
||||
|
||||
if(!isset($fields['ID'])) $fields['ID'] = "int(11) not null auto_increment";
|
||||
if($fields) foreach($fields as $k => $v) $fieldSchemas .= "\"$k\" $v,\n";
|
||||
if($indexes) foreach($indexes as $k => $v) $indexSchemas .= $this->getIndexSqlDefinition($k, $v) . ",\n";
|
||||
|
||||
// Switch to "CREATE TEMPORARY TABLE" for temporary tables
|
||||
$temporary = empty($options['temporary']) ? "" : "TEMPORARY";
|
||||
|
||||
$this->query("CREATE $temporary TABLE \"$table\" (
|
||||
$fieldSchemas
|
||||
$indexSchemas
|
||||
primary key (ID)
|
||||
) {$addOptions}");
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter a table's schema.
|
||||
* @param $table The name of the table to alter
|
||||
* @param $newFields New fields, a map of field name => field schema
|
||||
* @param $newIndexes New indexes, a map of index name => index type
|
||||
* @param $alteredFields Updated fields, a map of field name => field schema
|
||||
* @param $alteredIndexes Updated indexes, a map of index name => index type
|
||||
* @param $alteredOptions
|
||||
*/
|
||||
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null,
|
||||
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null) {
|
||||
|
||||
if($this->isView($tableName)) {
|
||||
DB::alteration_message(
|
||||
sprintf("Table %s not changed as it is a view", $tableName),
|
||||
"changed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
$fieldSchemas = $indexSchemas = "";
|
||||
$alterList = array();
|
||||
|
||||
if($newFields) foreach($newFields as $k => $v) $alterList[] .= "ADD \"$k\" $v";
|
||||
if($newIndexes) foreach($newIndexes as $k => $v) $alterList[] .= "ADD " . $this->getIndexSqlDefinition($k, $v);
|
||||
if($alteredFields) foreach($alteredFields as $k => $v) $alterList[] .= "CHANGE \"$k\" \"$k\" $v";
|
||||
if($alteredIndexes) foreach($alteredIndexes as $k => $v) {
|
||||
$alterList[] .= "DROP INDEX \"$k\"";
|
||||
$alterList[] .= "ADD ". $this->getIndexSqlDefinition($k, $v);
|
||||
}
|
||||
|
||||
if($alteredOptions && isset($alteredOptions[get_class($this)])) {
|
||||
if(!isset($this->indexList[$tableName])) {
|
||||
$this->indexList[$tableName] = $this->indexList($tableName);
|
||||
}
|
||||
|
||||
$skip = false;
|
||||
foreach($this->indexList[$tableName] as $index) {
|
||||
if(strpos($index, 'fulltext ') === 0) {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if($skip) {
|
||||
DB::alteration_message(
|
||||
sprintf("Table %s options not changed to %s due to fulltextsearch index",
|
||||
$tableName, $alteredOptions[get_class($this)]),
|
||||
"changed"
|
||||
);
|
||||
} else {
|
||||
$this->query(sprintf("ALTER TABLE \"%s\" %s", $tableName, $alteredOptions[get_class($this)]));
|
||||
DB::alteration_message(
|
||||
sprintf("Table %s options changed: %s", $tableName, $alteredOptions[get_class($this)]),
|
||||
"changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$alterations = implode(",\n", $alterList);
|
||||
$this->query("ALTER TABLE \"$tableName\" $alterations");
|
||||
}
|
||||
|
||||
public function isView($tableName) {
|
||||
$info = $this->query("SHOW /*!50002 FULL*/ TABLES LIKE '$tableName'")->record();
|
||||
return $info && strtoupper($info['Table_type']) == 'VIEW';
|
||||
}
|
||||
|
||||
public function renameTable($oldTableName, $newTableName) {
|
||||
$this->query("ALTER TABLE \"$oldTableName\" RENAME \"$newTableName\"");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Checks a table's integrity and repairs it if necessary.
|
||||
* @var string $tableName The name of the table.
|
||||
* @return boolean Return true if the table has integrity after the method is complete.
|
||||
*/
|
||||
public function checkAndRepairTable($tableName) {
|
||||
if(!$this->runTableCheckCommand("CHECK TABLE \"$tableName\"")) {
|
||||
if($this->runTableCheckCommand("CHECK TABLE \"".strtolower($tableName)."\"")){
|
||||
DB::alteration_message("Table $tableName: renamed from lowercase","repaired");
|
||||
return $this->renameTable(strtolower($tableName),$tableName);
|
||||
}
|
||||
|
||||
DB::alteration_message("Table $tableName: repaired","repaired");
|
||||
return $this->runTableCheckCommand("REPAIR TABLE \"$tableName\" USE_FRM");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used by checkAndRepairTable.
|
||||
* @param string $sql Query to run.
|
||||
* @return boolean Returns if the query returns a successful result.
|
||||
*/
|
||||
protected function runTableCheckCommand($sql) {
|
||||
$testResults = $this->query($sql);
|
||||
foreach($testResults as $testRecord) {
|
||||
if(strtolower($testRecord['Msg_text']) != 'ok') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createField($tableName, $fieldName, $fieldSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the database type of the given field.
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $fieldName The name of the field to change.
|
||||
* @param string $fieldSpec The new field specification
|
||||
*/
|
||||
public function alterField($tableName, $fieldName, $fieldSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the database column name of the given field.
|
||||
*
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $oldName The name of the field to change.
|
||||
* @param string $newName The new name of the field
|
||||
*/
|
||||
public function renameField($tableName, $oldName, $newName) {
|
||||
$fieldList = $this->fieldList($tableName);
|
||||
if(array_key_exists($oldName, $fieldList)) {
|
||||
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$oldName\" \"$newName\" " . $fieldList[$oldName]);
|
||||
}
|
||||
}
|
||||
|
||||
private static $_cache_collation_info = array();
|
||||
|
||||
public function fieldList($table) {
|
||||
$fields = DB::query("SHOW FULL FIELDS IN \"$table\"");
|
||||
foreach($fields as $field) {
|
||||
|
||||
// ensure that '' is converted to \' in field specification (mostly for the benefit of ENUM values)
|
||||
$fieldSpec = str_replace('\'\'', '\\\'', $field['Type']);
|
||||
if(!$field['Null'] || $field['Null'] == 'NO') {
|
||||
$fieldSpec .= ' not null';
|
||||
}
|
||||
|
||||
if($field['Collation'] && $field['Collation'] != 'NULL') {
|
||||
// Cache collation info to cut down on database traffic
|
||||
if(!isset(self::$_cache_collation_info[$field['Collation']])) {
|
||||
self::$_cache_collation_info[$field['Collation']]
|
||||
= DB::query("SHOW COLLATION LIKE '$field[Collation]'")->record();
|
||||
}
|
||||
$collInfo = self::$_cache_collation_info[$field['Collation']];
|
||||
$fieldSpec .= " character set $collInfo[Charset] collate $field[Collation]";
|
||||
}
|
||||
|
||||
if($field['Default'] || $field['Default'] === "0") {
|
||||
if(is_numeric($field['Default']))
|
||||
$fieldSpec .= " default " . Convert::raw2sql($field['Default']);
|
||||
else
|
||||
$fieldSpec .= " default '" . Convert::raw2sql($field['Default']) . "'";
|
||||
}
|
||||
if($field['Extra']) $fieldSpec .= " $field[Extra]";
|
||||
|
||||
$fieldList[$field['Field']] = $fieldSpec;
|
||||
}
|
||||
return $fieldList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an index on a table.
|
||||
*
|
||||
* @param string $tableName The name of the table.
|
||||
* @param string $indexName The name of the index.
|
||||
* @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()} for more
|
||||
* details.
|
||||
*/
|
||||
public function createIndex($tableName, $indexName, $indexSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD " . $this->getIndexSqlDefinition($indexName, $indexSpec));
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
|
||||
* and turns it into a proper string.
|
||||
* Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
|
||||
* arrays to be created. See {@link requireTable()} for details on the index format.
|
||||
*
|
||||
* @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
|
||||
*
|
||||
* @param string|array $indexSpec
|
||||
* @return string MySQL compatible ALTER TABLE syntax
|
||||
*/
|
||||
public function convertIndexSpec($indexSpec){
|
||||
if(is_array($indexSpec)){
|
||||
//Here we create a db-specific version of whatever index we need to create.
|
||||
switch($indexSpec['type']){
|
||||
case 'fulltext':
|
||||
$indexSpec='fulltext (' . str_replace(' ', '', $indexSpec['value']) . ')';
|
||||
break;
|
||||
case 'unique':
|
||||
$indexSpec='unique (' . $indexSpec['value'] . ')';
|
||||
break;
|
||||
case 'btree':
|
||||
case 'index':
|
||||
$indexSpec='using btree (' . $indexSpec['value'] . ')';
|
||||
break;
|
||||
case 'hash':
|
||||
$indexSpec='using hash (' . $indexSpec['value'] . ')';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $indexSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $indexName
|
||||
* @param string|array $indexSpec See {@link requireTable()} for details
|
||||
* @return string MySQL compatible ALTER TABLE syntax
|
||||
*/
|
||||
protected function getIndexSqlDefinition($indexName, $indexSpec=null) {
|
||||
|
||||
$indexSpec=$this->convertIndexSpec($indexSpec);
|
||||
|
||||
$indexSpec = trim($indexSpec);
|
||||
if($indexSpec[0] != '(') list($indexType, $indexFields) = explode(' ',$indexSpec,2);
|
||||
else $indexFields = $indexSpec;
|
||||
|
||||
if(!isset($indexType))
|
||||
$indexType = "index";
|
||||
|
||||
if($indexType=='using')
|
||||
return "index \"$indexName\" using $indexFields";
|
||||
else {
|
||||
return "$indexType \"$indexName\" $indexFields";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL does not need any transformations done on the index that's created, so we can just return it as-is
|
||||
*/
|
||||
public function getDbSqlDefinition($tableName, $indexName, $indexSpec){
|
||||
return $indexName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter an index on a table.
|
||||
* @param string $tableName The name of the table.
|
||||
* @param string $indexName The name of the index.
|
||||
* @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
|
||||
* for more details.
|
||||
*/
|
||||
public function alterIndex($tableName, $indexName, $indexSpec) {
|
||||
|
||||
$indexSpec=$this->convertIndexSpec($indexSpec);
|
||||
|
||||
$indexSpec = trim($indexSpec);
|
||||
if($indexSpec[0] != '(') {
|
||||
list($indexType, $indexFields) = explode(' ',$indexSpec,2);
|
||||
} else {
|
||||
$indexFields = $indexSpec;
|
||||
}
|
||||
|
||||
if(!$indexType) {
|
||||
$indexType = "index";
|
||||
}
|
||||
|
||||
$this->query("ALTER TABLE \"$tableName\" DROP INDEX \"$indexName\"");
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of indexes in a table.
|
||||
* @param string $table The table name.
|
||||
* @return array
|
||||
*/
|
||||
public function indexList($table) {
|
||||
$indexes = DB::query("SHOW INDEXES IN \"$table\"");
|
||||
$groupedIndexes = array();
|
||||
$indexList = array();
|
||||
|
||||
foreach($indexes as $index) {
|
||||
$groupedIndexes[$index['Key_name']]['fields'][$index['Seq_in_index']] = $index['Column_name'];
|
||||
|
||||
if($index['Index_type'] == 'FULLTEXT') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'fulltext ';
|
||||
} else if(!$index['Non_unique']) {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'unique ';
|
||||
} else if($index['Index_type'] =='HASH') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'hash ';
|
||||
} else if($index['Index_type'] =='RTREE') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'rtree ';
|
||||
} else {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
if($groupedIndexes) {
|
||||
foreach($groupedIndexes as $index => $details) {
|
||||
ksort($details['fields']);
|
||||
$indexList[$index] = $details['type'] . '("' . implode('","',$details['fields']) . '")';
|
||||
}
|
||||
}
|
||||
|
||||
return $indexList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the tables in the database.
|
||||
* @return array
|
||||
*/
|
||||
public function tableList() {
|
||||
$tables = array();
|
||||
foreach($this->query("SHOW TABLES") as $record) {
|
||||
$table = reset($record);
|
||||
$tables[strtolower($table)] = $table;
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of rows affected by the previous operation.
|
||||
* @return int
|
||||
*/
|
||||
public function affectedRows() {
|
||||
return $this->dbConn->affected_rows;
|
||||
}
|
||||
|
||||
public function databaseError($msg, $errorLevel = E_USER_ERROR) {
|
||||
// try to extract and format query
|
||||
if(preg_match('/Couldn\'t run query: ([^\|]*)\|\s*(.*)/', $msg, $matches)) {
|
||||
$formatter = new SQLFormatter();
|
||||
$msg = "Couldn't run query: \n" . $formatter->formatPlain($matches[1]) . "\n\n" . $matches[2];
|
||||
}
|
||||
|
||||
user_error($msg, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function boolean($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'tinyint', 'precision'=>1, 'sign'=>'unsigned', 'null'=>'not null',
|
||||
//'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "tinyint(1) unsigned not null default
|
||||
//'{$this->defaultVal}'");
|
||||
|
||||
return 'tinyint(1) unsigned not null default ' . (int)$values['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a date type-formatted string
|
||||
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function date($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'date');
|
||||
//DB::requireField($this->tableName, $this->name, "date");
|
||||
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a decimal type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function decimal($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'decimal', 'precision'=>"$this->wholeSize,$this->decimalSize");
|
||||
//DB::requireField($this->tableName, $this->name, "decimal($this->wholeSize,$this->decimalSize)");
|
||||
|
||||
// Avoid empty strings being put in the db
|
||||
if($values['precision'] == '') {
|
||||
$precision = 1;
|
||||
} else {
|
||||
$precision = $values['precision'];
|
||||
}
|
||||
|
||||
$defaultValue = '';
|
||||
if(isset($values['default']) && is_numeric($values['default'])) {
|
||||
$decs = strpos($precision, ',') !== false ? (int)substr($precision, strpos($precision, ',')+1) : 0;
|
||||
$defaultValue = ' default ' . number_format($values['default'], $decs, '.', '');
|
||||
}
|
||||
|
||||
return 'decimal(' . $precision . ') not null' . $defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a enum type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function enum($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
||||
// 'utf8_general_ci', 'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
||||
// utf8 collate utf8_general_ci default '{$this->default}'");
|
||||
|
||||
return 'enum(\'' . implode('\',\'', $values['enums']) . '\')'
|
||||
. ' character set utf8 collate utf8_general_ci default \'' . $values['default'] . '\'';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a set type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function set($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
||||
// 'utf8_general_ci', 'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
||||
//utf8 collate utf8_general_ci default '{$this->default}'");
|
||||
$default = empty($values['default']) ? '' : " default '$values[default]'";
|
||||
return 'set(\'' . implode('\',\'', $values['enums']) . '\') character set utf8 collate utf8_general_ci'
|
||||
. $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a float type-formatted string
|
||||
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function float($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'float');
|
||||
//DB::requireField($this->tableName, $this->name, "float");
|
||||
|
||||
return 'float not null default ' . $values['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a int type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function int($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "int(11) not null default '{$this->defaultVal}'");
|
||||
|
||||
return 'int(11) not null default ' . (int)$values['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime type-formatted string
|
||||
* For MySQL, we simply return the word 'datetime', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function ss_datetime($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'datetime');
|
||||
//DB::requireField($this->tableName, $this->name, $values);
|
||||
|
||||
return 'datetime';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a text type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function text($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci');
|
||||
//DB::requireField($this->tableName, $this->name, "mediumtext character set utf8 collate utf8_general_ci");
|
||||
|
||||
return 'mediumtext character set utf8 collate utf8_general_ci';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a time type-formatted string
|
||||
* For MySQL, we simply return the word 'time', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function time($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'time');
|
||||
//DB::requireField($this->tableName, $this->name, "time");
|
||||
|
||||
return 'time';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a varchar type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function varchar($values){
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>
|
||||
//'utf8_general_ci');
|
||||
//DB::requireField($this->tableName, $this->name, "varchar($this->size) character set utf8 collate
|
||||
// utf8_general_ci");
|
||||
|
||||
return 'varchar(' . $values['precision'] . ') character set utf8 collate utf8_general_ci';
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the MySQL-proprietary 'Year' datatype
|
||||
*/
|
||||
public function year($values){
|
||||
return 'year(4)';
|
||||
}
|
||||
/**
|
||||
* This returns the column which is the primary key for each table
|
||||
* In Postgres, it is a SERIAL8, which is the equivalent of an auto_increment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function IdColumn(){
|
||||
return 'int(11) not null auto_increment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL command to get all the tables in this database
|
||||
*/
|
||||
public function allTablesSQL(){
|
||||
return "SHOW TABLES;";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given table is exists in the current database
|
||||
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
||||
*/
|
||||
public function hasTable($table) {
|
||||
$SQL_table = Convert::raw2sql($table);
|
||||
return (bool)($this->query("SHOW TABLES LIKE '$SQL_table'")->value());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values of the given enum field
|
||||
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
||||
*/
|
||||
public function enumValuesForField($tableName, $fieldName) {
|
||||
// Get the enum of all page types from the SiteTree table
|
||||
$classnameinfo = DB::query("DESCRIBE \"$tableName\" \"$fieldName\"")->first();
|
||||
preg_match_all("/'[^,]+'/", $classnameinfo["Type"], $matches);
|
||||
|
||||
$classes = array();
|
||||
foreach($matches[0] as $value) {
|
||||
$classes[] = stripslashes(trim($value, "'"));
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The core search engine, used by this class and its subclasses to do fun stuff.
|
||||
* Searches both SiteTree and File.
|
||||
*
|
||||
* @param string $keywords Keywords as a string.
|
||||
*/
|
||||
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
|
||||
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
|
||||
|
||||
if(!class_exists('SiteTree')) throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
|
||||
if(!class_exists('File')) throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
|
||||
|
||||
$fileFilter = '';
|
||||
$keywords = Convert::raw2sql($keywords);
|
||||
$htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
|
||||
|
||||
$extraFilters = array('SiteTree' => '', 'File' => '');
|
||||
|
||||
if($booleanSearch) $boolean = "IN BOOLEAN MODE";
|
||||
|
||||
if($extraFilter) {
|
||||
$extraFilters['SiteTree'] = " AND $extraFilter";
|
||||
|
||||
if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter";
|
||||
else $extraFilters['File'] = $extraFilters['SiteTree'];
|
||||
}
|
||||
|
||||
// Always ensure that only pages with ShowInSearch = 1 can be searched
|
||||
$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
|
||||
|
||||
// File.ShowInSearch was added later, keep the database driver backwards compatible
|
||||
// by checking for its existence first
|
||||
$fields = $this->fieldList('File');
|
||||
if(array_key_exists('ShowInSearch', $fields)) $extraFilters['File'] .= " AND ShowInSearch <> 0";
|
||||
|
||||
$limit = $start . ", " . (int) $pageLength;
|
||||
|
||||
$notMatch = $invertedMatch ? "NOT " : "";
|
||||
if($keywords) {
|
||||
$match['SiteTree'] = "
|
||||
MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$keywords' $boolean)
|
||||
+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityKeywords' $boolean)
|
||||
";
|
||||
$match['File'] = "MATCH (Filename, Title, Content) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
|
||||
|
||||
// We make the relevance search by converting a boolean mode search into a normal one
|
||||
$relevanceKeywords = str_replace(array('*','+','-'),'',$keywords);
|
||||
$htmlEntityRelevanceKeywords = str_replace(array('*','+','-'),'',$htmlEntityKeywords);
|
||||
$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaDescription) "
|
||||
. "AGAINST ('$relevanceKeywords') "
|
||||
. "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
|
||||
$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
|
||||
} else {
|
||||
$relevance['SiteTree'] = $relevance['File'] = 1;
|
||||
$match['SiteTree'] = $match['File'] = "1 = 1";
|
||||
}
|
||||
|
||||
// Generate initial DataLists and base table names
|
||||
$lists = array();
|
||||
$baseClasses = array('SiteTree' => '', 'File' => '');
|
||||
foreach($classesToSearch as $class) {
|
||||
$lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "");
|
||||
$baseClasses[$class] = '"'.$class.'"';
|
||||
}
|
||||
|
||||
// Make column selection lists
|
||||
$select = array(
|
||||
'SiteTree' => array(
|
||||
"ClassName", "$baseClasses[SiteTree].\"ID\"", "ParentID",
|
||||
"Title", "MenuTitle", "URLSegment", "Content",
|
||||
"LastEdited", "Created",
|
||||
"Filename" => "_utf8''", "Name" => "_utf8''",
|
||||
"Relevance" => $relevance['SiteTree'], "CanViewType"
|
||||
),
|
||||
'File' => array(
|
||||
"ClassName", "$baseClasses[File].\"ID\"", "ParentID" => "_utf8''",
|
||||
"Title", "MenuTitle" => "_utf8''", "URLSegment" => "_utf8''", "Content",
|
||||
"LastEdited", "Created",
|
||||
"Filename", "Name",
|
||||
"Relevance" => $relevance['File'], "CanViewType" => "NULL"
|
||||
),
|
||||
);
|
||||
|
||||
// Process and combine queries
|
||||
$querySQLs = array();
|
||||
$totalCount = 0;
|
||||
foreach($lists as $class => $list) {
|
||||
$query = $list->dataQuery()->query();
|
||||
|
||||
// There's no need to do all that joining
|
||||
$query->setFrom(array(str_replace(array('"','`'), '', $baseClasses[$class]) => $baseClasses[$class]));
|
||||
$query->setSelect($select[$class]);
|
||||
$query->setOrderBy(array());
|
||||
|
||||
$querySQLs[] = $query->sql();
|
||||
$totalCount += $query->unlimitedRowCount();
|
||||
}
|
||||
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
|
||||
|
||||
// Get records
|
||||
$records = DB::query($fullQuery);
|
||||
|
||||
$objects = array();
|
||||
|
||||
foreach($records as $record) {
|
||||
$objects[] = new $record['ClassName']($record);
|
||||
}
|
||||
|
||||
$list = new PaginatedList(new ArrayList($objects));
|
||||
$list->setPageStart($start);
|
||||
$list->setPageLength($pageLength);
|
||||
$list->setTotalItems($totalCount);
|
||||
|
||||
// The list has already been limited by the query above
|
||||
$list->setLimitItems(false);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL uses NOW() to return the current date/time.
|
||||
*/
|
||||
public function now(){
|
||||
return 'NOW()';
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the database-specific version of the random() function
|
||||
*/
|
||||
public function random(){
|
||||
return 'RAND()';
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a lookup table for data types.
|
||||
* For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
|
||||
* So this is a DB-specific list of equivilents.
|
||||
*/
|
||||
public function dbDataType($type){
|
||||
$values=Array(
|
||||
'unsigned integer'=>'UNSIGNED'
|
||||
);
|
||||
|
||||
if(isset($values[$type]))
|
||||
return $values[$type];
|
||||
else return '';
|
||||
}
|
||||
|
||||
/*
|
||||
* This will return text which has been escaped in a database-friendly manner.
|
||||
*/
|
||||
public function addslashes($value){
|
||||
return $this->dbConn->real_escape_string($value);
|
||||
}
|
||||
|
||||
/*
|
||||
* This changes the index name depending on database requirements.
|
||||
* MySQL doesn't need any changes.
|
||||
*/
|
||||
public function modifyIndex($index){
|
||||
return $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SQL fragment for querying a fulltext search index
|
||||
* @param $fields array The list of field names to search on
|
||||
* @param $keywords string The search query
|
||||
* @param $booleanSearch A MySQL-specific flag to switch to boolean search
|
||||
*/
|
||||
public function fullTextSearchSQL($fields, $keywords, $booleanSearch = false) {
|
||||
$boolean = $booleanSearch ? "IN BOOLEAN MODE" : "";
|
||||
$fieldNames = '"' . implode('", "', $fields) . '"';
|
||||
|
||||
$SQL_keywords = Convert::raw2sql($keywords);
|
||||
$SQL_htmlEntityKeywords = Convert::raw2sql(htmlentities($keywords, ENT_NOQUOTES, 'UTF-8'));
|
||||
|
||||
return "(MATCH ($fieldNames) AGAINST ('$SQL_keywords' $boolean) + MATCH ($fieldNames)"
|
||||
. " AGAINST ('$SQL_htmlEntityKeywords' $boolean))";
|
||||
}
|
||||
|
||||
/*
|
||||
* Does this database support transactions?
|
||||
*/
|
||||
public function supportsTransactions(){
|
||||
return $this->supportsTransactions;
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a quick lookup to discover if the database supports particular extensions
|
||||
* Currently, MySQL supports no extensions
|
||||
*/
|
||||
public function supportsExtensions($extensions=Array('partitions', 'tablespaces', 'clustering')){
|
||||
if(isset($extensions['partitions']))
|
||||
return false;
|
||||
elseif(isset($extensions['tablespaces']))
|
||||
return false;
|
||||
elseif(isset($extensions['clustering']))
|
||||
return false;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Start a prepared transaction
|
||||
* See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on transaction
|
||||
* isolation options
|
||||
*/
|
||||
public function transactionStart($transaction_mode=false, $session_characteristics=false){
|
||||
// This sets the isolation level for the NEXT transaction, not the current one.
|
||||
if($transaction_mode) {
|
||||
$this->query('SET TRANSACTION ' . $transaction_mode . ';');
|
||||
}
|
||||
|
||||
$this->query('START TRANSACTION;');
|
||||
|
||||
if($session_characteristics) {
|
||||
$this->query('SET SESSION TRANSACTION ' . $session_characteristics . ';');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a savepoint that you can jump back to if you encounter problems
|
||||
*/
|
||||
public function transactionSavepoint($savepoint){
|
||||
$this->query("SAVEPOINT $savepoint;");
|
||||
}
|
||||
|
||||
/*
|
||||
* Rollback or revert to a savepoint if your queries encounter problems
|
||||
* If you encounter a problem at any point during a transaction, you may
|
||||
* need to rollback that particular query, or return to a savepoint
|
||||
*/
|
||||
public function transactionRollback($savepoint = false){
|
||||
if($savepoint) {
|
||||
$this->query('ROLLBACK TO ' . $savepoint . ';');
|
||||
} else {
|
||||
$this->query('ROLLBACK');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Commit everything inside this transaction so far
|
||||
*/
|
||||
public function transactionEnd($chain = false){
|
||||
$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN;');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a WHERE clause for text matching.
|
||||
*
|
||||
* @param String $field Quoted field name
|
||||
* @param String $value Escaped search. Can include percentage wildcards.
|
||||
* @param boolean $exact Exact matches or wildcard support.
|
||||
* @param boolean $negate Negate the clause.
|
||||
* @param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE.
|
||||
* Stick with default collation if set to NULL.
|
||||
* @return String SQL
|
||||
*/
|
||||
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null) {
|
||||
if($exact && $caseSensitive === null) {
|
||||
$comp = ($negate) ? '!=' : '=';
|
||||
} else {
|
||||
$comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
|
||||
if($negate) $comp = 'NOT ' . $comp;
|
||||
}
|
||||
|
||||
return sprintf("%s %s '%s'", $field, $comp, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with MySQL
|
||||
* used for querying a datetime in a certain format
|
||||
* @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or
|
||||
* field name, e.g. '"SiteTree"."Created"'
|
||||
* @param string $format to be used, supported specifiers:
|
||||
* %Y = Year (four digits)
|
||||
* %m = Month (01..12)
|
||||
* %d = Day (01..31)
|
||||
* %H = Hour (00..23)
|
||||
* %i = Minutes (00..59)
|
||||
* %s = Seconds (00..59)
|
||||
* %U = unix timestamp, can only be used on it's own
|
||||
* @return string SQL datetime expression to query for a formatted datetime
|
||||
*/
|
||||
public function formattedDatetimeClause($date, $format) {
|
||||
|
||||
preg_match_all('/%(.)/', $format, $matches);
|
||||
foreach($matches[1] as $match) if(array_search($match, array('Y','m','d','H','i','s','U')) === false) {
|
||||
user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
||||
}
|
||||
|
||||
if(preg_match('/^now$/i', $date)) {
|
||||
$date = "NOW()";
|
||||
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
||||
$date = "'$date'";
|
||||
}
|
||||
|
||||
if($format == '%U') return "UNIX_TIMESTAMP($date)";
|
||||
|
||||
return "DATE_FORMAT($date, '$format')";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with MySQL
|
||||
* used for querying a datetime addition
|
||||
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $interval to be added, use the format [sign][integer] [qualifier],
|
||||
* e.g. -1 Day, +15 minutes, +1 YEAR
|
||||
* supported qualifiers:
|
||||
* - years
|
||||
* - months
|
||||
* - days
|
||||
* - hours
|
||||
* - minutes
|
||||
* - seconds
|
||||
* This includes the singular forms as well
|
||||
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of
|
||||
* the addition
|
||||
*/
|
||||
public function datetimeIntervalClause($date, $interval) {
|
||||
|
||||
$interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
|
||||
|
||||
if(preg_match('/^now$/i', $date)) {
|
||||
$date = "NOW()";
|
||||
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
||||
$date = "'$date'";
|
||||
}
|
||||
|
||||
return "$date + INTERVAL $interval";
|
||||
}
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with MySQL
|
||||
* used for querying a datetime substraction
|
||||
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like
|
||||
* '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
||||
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which
|
||||
* is the result of the substraction
|
||||
*/
|
||||
public function datetimeDifferenceClause($date1, $date2) {
|
||||
|
||||
if(preg_match('/^now$/i', $date1)) {
|
||||
$date1 = "NOW()";
|
||||
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
||||
$date1 = "'$date1'";
|
||||
}
|
||||
|
||||
if(preg_match('/^now$/i', $date2)) {
|
||||
$date2 = "NOW()";
|
||||
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
|
||||
$date2 = "'$date2'";
|
||||
}
|
||||
|
||||
return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
|
||||
}
|
||||
|
||||
public function supportsLocks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canLock($name) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
return (bool)DB::query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
|
||||
}
|
||||
|
||||
public function getLock($name, $timeout = 5) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
|
||||
// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
|
||||
// in contrast to PostgreSQL and SQL Server who stack the locks.
|
||||
|
||||
return (bool)DB::query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
|
||||
}
|
||||
|
||||
public function releaseLock($name) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
return (bool)DB::query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
|
||||
}
|
||||
|
||||
protected function getLockIdentifier($name) {
|
||||
// Prefix with database name
|
||||
return Convert::raw2sql($this->database . '_' . Convert::raw2sql($name));
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A result-set from a MySQL database.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLQuery extends SS_Query {
|
||||
|
||||
/**
|
||||
* The MySQLDatabase object that created this result set.
|
||||
* @var MySQLDatabase
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The internal MySQL handle that points to the result set.
|
||||
* @var resource
|
||||
*/
|
||||
protected $handle;
|
||||
|
||||
/**
|
||||
* Hook the result-set given into a Query class, suitable for use by
|
||||
* SilverStripe.
|
||||
*
|
||||
* @param database $database The database object that created this query.
|
||||
* @param handle $handle the internal mysql handle that is points to the resultset.
|
||||
*/
|
||||
public function __construct(MySQLDatabase $database, $handle) {
|
||||
$this->database = $database;
|
||||
$this->handle = $handle;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if(is_object($this->handle)) {
|
||||
$this->handle->free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function seek($row) {
|
||||
if(is_object($this->handle)) {
|
||||
return $this->handle->data_seek($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function numRecords() {
|
||||
if(is_object($this->handle)) {
|
||||
return $this->handle->num_rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function nextRecord() {
|
||||
if(is_object($this->handle) && ($data = $this->handle->fetch_assoc())) {
|
||||
return $data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -50,7 +50,10 @@ abstract class RelationList extends DataList {
|
||||
* Returns a where clause that filters the members of this relationship to
|
||||
* just the related items.
|
||||
*
|
||||
* @param $id (optional) An ID or an array of IDs - if not provided, will use the current ids as per getForeignID
|
||||
*
|
||||
* @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current ids as
|
||||
* per getForeignID
|
||||
* @return array Condition In array(SQL => parameters format)
|
||||
*/
|
||||
abstract protected function foreignIDFilter($id = null);
|
||||
}
|
||||
|
@ -9,16 +9,17 @@
|
||||
class SQLMap extends Object implements IteratorAggregate {
|
||||
/**
|
||||
* The query used to generate the map.
|
||||
* @var SQLQuery
|
||||
* @var SQLSelect
|
||||
*/
|
||||
protected $query;
|
||||
protected $keyField, $titleField;
|
||||
|
||||
/**
|
||||
* Construct a SQLMap.
|
||||
* @param SQLQuery $query The query to generate this map. THis isn't executed until it's needed.
|
||||
*
|
||||
* @param SQLSelect $query The query to generate this map. THis isn't executed until it's needed.
|
||||
*/
|
||||
public function __construct(SQLQuery $query, $keyField = "ID", $titleField = "Title") {
|
||||
public function __construct(SQLSelect $query, $keyField = "ID", $titleField = "Title") {
|
||||
Deprecation::notice('3.0', 'Use SS_Map or DataList::map() instead.', Deprecation::SCOPE_CLASS);
|
||||
|
||||
if(!$query) {
|
||||
@ -40,10 +41,12 @@ class SQLMap extends Object implements IteratorAggregate {
|
||||
public function getItem($id) {
|
||||
if($id) {
|
||||
$baseTable = reset($this->query->from);
|
||||
$where = "$baseTable.\"ID\" = $id";
|
||||
$this->query->where[sha1($where)] = $where;
|
||||
$oldWhere = $this->query->getWhere();
|
||||
$this->query->where(array(
|
||||
"\"$baseTable\".\"ID\" = ?" => $id
|
||||
));
|
||||
$record = $this->query->execute()->first();
|
||||
unset($this->query->where[sha1($where)]);
|
||||
$this->query->setWhere($oldWhere);
|
||||
if($record) {
|
||||
$className = $record['ClassName'];
|
||||
$obj = new $className($record);
|
||||
|
1200
model/SQLQuery.php
1200
model/SQLQuery.php
@ -1,1200 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Object representing a SQL query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* Caution: Only supports SELECT (default) and DELETE at the moment.
|
||||
*
|
||||
* @todo Add support for INSERT and UPDATE queries
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLQuery {
|
||||
|
||||
/**
|
||||
* An array of SELECT fields, keyed by an optional alias.
|
||||
* @var array
|
||||
*/
|
||||
protected $select = array();
|
||||
|
||||
/**
|
||||
* An array of FROM clauses. The first one is just the table name.
|
||||
* @var array
|
||||
*/
|
||||
protected $from = array();
|
||||
|
||||
/**
|
||||
* An array of WHERE clauses.
|
||||
* @var array
|
||||
*/
|
||||
protected $where = array();
|
||||
|
||||
/**
|
||||
* An array of ORDER BY clauses, functions. Stores as an associative
|
||||
* array of column / function to direction.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $orderby = array();
|
||||
|
||||
/**
|
||||
* An array of GROUP BY clauses.
|
||||
* @var array
|
||||
*/
|
||||
protected $groupby = array();
|
||||
|
||||
/**
|
||||
* An array of having clauses.
|
||||
* @var array
|
||||
*/
|
||||
protected $having = array();
|
||||
|
||||
/**
|
||||
* An array containing limit and offset keys for LIMIT clause.
|
||||
* @var array
|
||||
*/
|
||||
protected $limit = array();
|
||||
|
||||
/**
|
||||
* If this is true DISTINCT will be added to the SQL.
|
||||
* @var boolean
|
||||
*/
|
||||
protected $distinct = false;
|
||||
|
||||
/**
|
||||
* If this is true, this statement will delete rather than select.
|
||||
* @var boolean
|
||||
*/
|
||||
protected $delete = false;
|
||||
|
||||
/**
|
||||
* The logical connective used to join WHERE clauses. Defaults to AND.
|
||||
* @var string
|
||||
*/
|
||||
protected $connective = 'AND';
|
||||
|
||||
/**
|
||||
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
||||
* query SQL.
|
||||
* @var array
|
||||
*/
|
||||
protected $replacementsOld = array();
|
||||
|
||||
/**
|
||||
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
||||
* query SQL.
|
||||
* @var array
|
||||
*/
|
||||
protected $replacementsNew = array();
|
||||
|
||||
/**
|
||||
* Construct a new SQLQuery.
|
||||
*
|
||||
* @param array $select An array of SELECT fields.
|
||||
* @param array $from An array of FROM clauses. The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
* @param array $orderby An array ORDER BY clause.
|
||||
* @param array $groupby An array of GROUP BY clauses.
|
||||
* @param array $having An array of HAVING clauses.
|
||||
* @param array|string $limit A LIMIT clause or array with limit and offset keys
|
||||
*/
|
||||
public function __construct($select = "*", $from = array(), $where = array(), $orderby = array(),
|
||||
$groupby = array(), $having = array(), $limit = array()) {
|
||||
|
||||
$this->setSelect($select);
|
||||
$this->setFrom($from);
|
||||
$this->setWhere($where);
|
||||
$this->setOrderBy($orderby);
|
||||
$this->setGroupBy($groupby);
|
||||
$this->setHaving($having);
|
||||
$this->setLimit($limit);
|
||||
}
|
||||
|
||||
public function __get($field) {
|
||||
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use getSelect() instead');
|
||||
if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use getFrom() instead');
|
||||
if(strtolower($field) == 'groupby') Deprecation::notice('3.0', 'Please use getGroupBy() instead');
|
||||
if(strtolower($field) == 'orderby') Deprecation::notice('3.0', 'Please use getOrderBy() instead');
|
||||
if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use getHaving() instead');
|
||||
if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use getLimit() instead');
|
||||
if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use getDelete() instead');
|
||||
if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use getConnective() instead');
|
||||
if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use getDistinct() instead');
|
||||
|
||||
return $this->$field;
|
||||
}
|
||||
|
||||
public function __set($field, $value) {
|
||||
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead');
|
||||
if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead');
|
||||
if(strtolower($field) == 'groupby') {
|
||||
Deprecation::notice('3.0', 'Please use setGroupBy() or addGroupBy() instead');
|
||||
}
|
||||
if(strtolower($field) == 'orderby') {
|
||||
Deprecation::notice('3.0', 'Please use setOrderBy() or addOrderBy() instead');
|
||||
}
|
||||
if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead');
|
||||
if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use setLimit() instead');
|
||||
if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use setDelete() instead');
|
||||
if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use setConnective() instead');
|
||||
if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use setDistinct() instead');
|
||||
|
||||
return $this->$field = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of columns to be selected by the query.
|
||||
*
|
||||
* <code>
|
||||
* // pass fields to select as single parameter array
|
||||
* $query->setSelect(array("Col1","Col2"))->setFrom("MyTable");
|
||||
*
|
||||
* // pass fields to select as multiple parameters
|
||||
* $query->setSelect("Col1", "Col2")->setFrom("MyTable");
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @param boolean $clear Clear existing select fields?
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setSelect($fields) {
|
||||
$this->select = array();
|
||||
if (func_num_args() > 1) {
|
||||
$fields = func_get_args();
|
||||
} else if(!is_array($fields)) {
|
||||
$fields = array($fields);
|
||||
}
|
||||
return $this->addSelect($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the list of columns to be selected by the query.
|
||||
*
|
||||
* <code>
|
||||
* // pass fields to select as single parameter array
|
||||
* $query->addSelect(array("Col1","Col2"))->setFrom("MyTable");
|
||||
*
|
||||
* // pass fields to select as multiple parameters
|
||||
* $query->addSelect("Col1", "Col2")->setFrom("MyTable");
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @param boolean $clear Clear existing select fields?
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addSelect($fields) {
|
||||
if (func_num_args() > 1) {
|
||||
$fields = func_get_args();
|
||||
} else if(!is_array($fields)) {
|
||||
$fields = array($fields);
|
||||
}
|
||||
|
||||
foreach($fields as $idx => $field) {
|
||||
if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $field, $matches)) {
|
||||
Deprecation::notice("3.0", "Use selectField() to specify column aliases");
|
||||
$this->selectField($matches[1], $matches[2]);
|
||||
} else {
|
||||
$this->selectField($field, is_numeric($idx) ? null : $idx);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function select($fields) {
|
||||
Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead!');
|
||||
$this->setSelect($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an additional field.
|
||||
*
|
||||
* @param $field String The field to select (escaped SQL statement)
|
||||
* @param $alias String The alias of that field (escaped SQL statement).
|
||||
* Defaults to the unquoted column name of the $field parameter.
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function selectField($field, $alias = null) {
|
||||
if(!$alias) {
|
||||
if(preg_match('/"([^"]+)"$/', $field, $matches)) $alias = $matches[1];
|
||||
else $alias = $field;
|
||||
}
|
||||
$this->select[$alias] = $field;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SQL expression for the given field alias.
|
||||
* Returns null if the given alias doesn't exist.
|
||||
* See {@link selectField()} for details on alias generation.
|
||||
*
|
||||
* @param String $field
|
||||
* @return String
|
||||
*/
|
||||
public function expressionForField($field) {
|
||||
return isset($this->select[$field]) ? $this->select[$field] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set table for the SELECT clause.
|
||||
*
|
||||
* @example $query->setFrom("MyTable"); // SELECT * FROM MyTable
|
||||
*
|
||||
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setFrom($from) {
|
||||
$this->from = array();
|
||||
return $this->addFrom($from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a table to the SELECT clause.
|
||||
*
|
||||
* @example $query->addFrom("MyTable"); // SELECT * FROM MyTable
|
||||
*
|
||||
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addFrom($from) {
|
||||
if(is_array($from)) {
|
||||
$this->from = array_merge($this->from, $from);
|
||||
} elseif(!empty($from)) {
|
||||
$this->from[str_replace(array('"','`'), '', $from)] = $from;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function from($from) {
|
||||
Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead!');
|
||||
return $this->setFrom($from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT JOIN criteria to the FROM clause.
|
||||
*
|
||||
* @param string $table Unquoted table name
|
||||
* @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement, Needs to be valid
|
||||
* (quoted) SQL.
|
||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
||||
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||
* ORM have a value of 10.
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20) {
|
||||
if(!$tableAlias) {
|
||||
$tableAlias = $table;
|
||||
}
|
||||
$this->from[$tableAlias] = array(
|
||||
'type' => 'LEFT',
|
||||
'table' => $table,
|
||||
'filter' => array($onPredicate),
|
||||
'order' => $order
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function leftjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
Deprecation::notice('3.0', 'Please use addLeftJoin() instead!');
|
||||
$this->addLeftJoin($table, $onPredicate, $tableAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN criteria to the FROM clause.
|
||||
*
|
||||
* @param string $table Unquoted table name
|
||||
* @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. Needs to be
|
||||
* valid (quoted) SQL.
|
||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
||||
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||
* ORM have a value of 10.
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
if(!$tableAlias) $tableAlias = $table;
|
||||
$this->from[$tableAlias] = array(
|
||||
'type' => 'INNER',
|
||||
'table' => $table,
|
||||
'filter' => array($onPredicate),
|
||||
'order' => $order
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function innerjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
Deprecation::notice('3.0', 'Please use addInnerJoin() instead!');
|
||||
return $this->addInnerJoin($table, $onPredicate, $tableAlias, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an additional filter (part of the ON clause) on a join.
|
||||
*
|
||||
* @param string $table Table to join on from the original join
|
||||
* @param string $filter The "ON" SQL fragment (escaped)
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addFilterToJoin($table, $filter) {
|
||||
$this->from[$table]['filter'][] = $filter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter (part of the ON clause) on a join.
|
||||
*
|
||||
* @param string $table Table to join on from the original join
|
||||
* @param string $filter The "ON" SQL fragment (escaped)
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setJoinFilter($table, $filter) {
|
||||
$this->from[$table]['filter'] = array($filter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are already joining to the given table alias
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isJoinedTo($tableAlias) {
|
||||
return isset($this->from[$tableAlias]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of tables that this query is selecting from.
|
||||
*
|
||||
* @return array Unquoted table names
|
||||
*/
|
||||
public function queriedTables() {
|
||||
$tables = array();
|
||||
|
||||
foreach($this->from as $key => $tableClause) {
|
||||
if(is_array($tableClause)) {
|
||||
$table = '"'.$tableClause['table'].'"';
|
||||
} else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) {
|
||||
$table = $matches[1];
|
||||
} else {
|
||||
$table = $tableClause;
|
||||
}
|
||||
|
||||
// Handle string replacements
|
||||
if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table);
|
||||
|
||||
$tables[] = preg_replace('/^"|"$/','',$table);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set distinct property.
|
||||
* @param boolean $value
|
||||
*/
|
||||
public function setDistinct($value) {
|
||||
$this->distinct = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the distinct property.
|
||||
* @return boolean
|
||||
*/
|
||||
public function getDistinct() {
|
||||
return $this->distinct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the delete property.
|
||||
* @param boolean $value
|
||||
*/
|
||||
public function setDelete($value) {
|
||||
$this->delete = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delete property.
|
||||
* @return boolean
|
||||
*/
|
||||
public function getDelete() {
|
||||
return $this->delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connective property.
|
||||
* @param boolean $value
|
||||
*/
|
||||
public function setConnective($value) {
|
||||
$this->connective = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connective property.
|
||||
* @return string
|
||||
*/
|
||||
public function getConnective() {
|
||||
return $this->connective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limit property.
|
||||
* @return array
|
||||
*/
|
||||
public function getLimit() {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass LIMIT clause either as SQL snippet or in array format.
|
||||
* Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
|
||||
*
|
||||
* @param int|string|array $limit If passed as a string or array, assumes SQL escaped data.
|
||||
* Only applies for positive values, or if an $offset is set as well.
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return SQLQuery This instance
|
||||
*/
|
||||
public function setLimit($limit, $offset = 0) {
|
||||
if((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) {
|
||||
throw new InvalidArgumentException("SQLQuery::setLimit() only takes positive values");
|
||||
}
|
||||
|
||||
if(is_numeric($limit) && ($limit || $offset)) {
|
||||
$this->limit = array(
|
||||
'start' => $offset,
|
||||
'limit' => $limit,
|
||||
);
|
||||
} else if($limit && is_string($limit)) {
|
||||
if(strpos($limit, ',') !== false) {
|
||||
list($start, $innerLimit) = explode(',', $limit, 2);
|
||||
}
|
||||
else {
|
||||
list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
|
||||
}
|
||||
|
||||
$this->limit = array(
|
||||
'start' => trim($start),
|
||||
'limit' => trim($innerLimit),
|
||||
);
|
||||
} else {
|
||||
$this->limit = $limit;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function limit($limit, $offset = 0) {
|
||||
Deprecation::notice('3.0', 'Please use setLimit() instead!');
|
||||
return $this->setLimit($limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ORDER BY clause either as SQL snippet or in array format.
|
||||
*
|
||||
* @example $sql->setOrderBy("Column");
|
||||
* @example $sql->setOrderBy("Column DESC");
|
||||
* @example $sql->setOrderBy("Column DESC, ColumnTwo ASC");
|
||||
* @example $sql->setOrderBy("Column", "DESC");
|
||||
* @example $sql->setOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
||||
*
|
||||
* @param string|array $orderby Clauses to add (escaped SQL statement)
|
||||
* @param string $dir Sort direction, ASC or DESC
|
||||
*
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setOrderBy($clauses = null, $direction = null) {
|
||||
$this->orderby = array();
|
||||
return $this->addOrderBy($clauses, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ORDER BY clause either as SQL snippet or in array format.
|
||||
*
|
||||
* @example $sql->addOrderBy("Column");
|
||||
* @example $sql->addOrderBy("Column DESC");
|
||||
* @example $sql->addOrderBy("Column DESC, ColumnTwo ASC");
|
||||
* @example $sql->addOrderBy("Column", "DESC");
|
||||
* @example $sql->addOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
||||
*
|
||||
* @param string|array $clauses Clauses to add (escaped SQL statements)
|
||||
* @param string $dir Sort direction, ASC or DESC
|
||||
*
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addOrderBy($clauses = null, $direction = null) {
|
||||
if(!$clauses) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if(is_string($clauses)) {
|
||||
if(strpos($clauses, "(") !== false) {
|
||||
$sort = preg_split("/,(?![^()]*+\\))/", $clauses);
|
||||
} else {
|
||||
$sort = explode(",", $clauses);
|
||||
}
|
||||
|
||||
$clauses = array();
|
||||
|
||||
foreach($sort as $clause) {
|
||||
list($column, $direction) = $this->getDirectionFromString($clause, $direction);
|
||||
$clauses[$column] = $direction;
|
||||
}
|
||||
}
|
||||
|
||||
if(is_array($clauses)) {
|
||||
foreach($clauses as $key => $value) {
|
||||
if(!is_numeric($key)) {
|
||||
$column = trim($key);
|
||||
$columnDir = strtoupper(trim($value));
|
||||
} else {
|
||||
list($column, $columnDir) = $this->getDirectionFromString($value);
|
||||
}
|
||||
|
||||
$this->orderby[$column] = $columnDir;
|
||||
}
|
||||
} else {
|
||||
user_error('SQLQuery::orderby() incorrect format for $orderby', E_USER_WARNING);
|
||||
}
|
||||
|
||||
// If sort contains a public function call, let's move the sort clause into a
|
||||
// separate selected field.
|
||||
//
|
||||
// Some versions of MySQL choke if you have a group public function referenced
|
||||
// directly in the ORDER BY
|
||||
if($this->orderby) {
|
||||
$i = 0;
|
||||
$orderby = array();
|
||||
foreach($this->orderby as $clause => $dir) {
|
||||
|
||||
// public function calls and multi-word columns like "CASE WHEN ..."
|
||||
if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) {
|
||||
|
||||
// Move the clause to the select fragment, substituting a placeholder column in the sort fragment.
|
||||
$clause = trim($clause);
|
||||
$column = "_SortColumn{$i}";
|
||||
$this->selectField($clause, $column);
|
||||
$clause = '"' . $column . '"';
|
||||
$i++;
|
||||
}
|
||||
|
||||
$orderby[$clause] = $dir;
|
||||
}
|
||||
$this->orderby = $orderby;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function orderby($clauses = null, $direction = null) {
|
||||
Deprecation::notice('3.0', 'Please use setOrderBy() instead!');
|
||||
return $this->setOrderBy($clauses, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direction part of a single-column order by clause.
|
||||
*
|
||||
* @param String
|
||||
* @param String
|
||||
* @return Array A two element array: array($column, $direction)
|
||||
*/
|
||||
private function getDirectionFromString($value, $defaultDirection = null) {
|
||||
if(preg_match('/^(.*)(asc|desc)$/i', $value, $matches)) {
|
||||
$column = trim($matches[1]);
|
||||
$direction = strtoupper($matches[2]);
|
||||
} else {
|
||||
$column = $value;
|
||||
$direction = $defaultDirection ? $defaultDirection : "ASC";
|
||||
}
|
||||
return array($column, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current order by as array if not already. To handle legacy
|
||||
* statements which are stored as strings. Without clauses and directions,
|
||||
* convert the orderby clause to something readable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOrderBy() {
|
||||
$orderby = $this->orderby;
|
||||
if(!$orderby) $orderby = array();
|
||||
|
||||
if(!is_array($orderby)) {
|
||||
// spilt by any commas not within brackets
|
||||
$orderby = preg_split('/,(?![^()]*+\\))/', $orderby);
|
||||
}
|
||||
|
||||
foreach($orderby as $k => $v) {
|
||||
if(strpos($v, ' ') !== false) {
|
||||
unset($orderby[$k]);
|
||||
|
||||
$rule = explode(' ', trim($v));
|
||||
$clause = $rule[0];
|
||||
$dir = (isset($rule[1])) ? $rule[1] : 'ASC';
|
||||
|
||||
$orderby[$clause] = $dir;
|
||||
}
|
||||
}
|
||||
|
||||
return $orderby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the order by clause by replacing ASC or DESC references in the
|
||||
* current order by with it's corollary.
|
||||
*
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function reverseOrderBy() {
|
||||
$order = $this->getOrderBy();
|
||||
$this->orderby = array();
|
||||
|
||||
foreach($order as $clause => $dir) {
|
||||
$dir = (strtoupper($dir) == 'DESC') ? 'ASC' : 'DESC';
|
||||
$this->addOrderBy($clause, $dir);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a GROUP BY clause.
|
||||
*
|
||||
* @param string|array $groupby Escaped SQL statement
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setGroupBy($groupby) {
|
||||
$this->groupby = array();
|
||||
return $this->addGroupBy($groupby);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a GROUP BY clause.
|
||||
*
|
||||
* @param string|array $groupby Escaped SQL statement
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addGroupBy($groupby) {
|
||||
if(is_array($groupby)) {
|
||||
$this->groupby = array_merge($this->groupby, $groupby);
|
||||
} elseif(!empty($groupby)) {
|
||||
$this->groupby[] = $groupby;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function groupby($where) {
|
||||
Deprecation::notice('3.0', 'Please use setGroupBy() or addHaving() instead!');
|
||||
return $this->setGroupBy($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a HAVING clause.
|
||||
*
|
||||
* @param string|array $having
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setHaving($having) {
|
||||
$this->having = array();
|
||||
return $this->addHaving($having);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HAVING clause
|
||||
*
|
||||
* @param string|array $having Escaped SQL statement
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addHaving($having) {
|
||||
if(is_array($having)) {
|
||||
$this->having = array_merge($this->having, $having);
|
||||
} elseif(!empty($having)) {
|
||||
$this->having[] = $having;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function having($having) {
|
||||
Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead!');
|
||||
return $this->setHaving($having);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a WHERE clause.
|
||||
*
|
||||
* There are two different ways of doing this:
|
||||
*
|
||||
* <code>
|
||||
* // the entire predicate as a single string
|
||||
* $query->where("Column = 'Value'");
|
||||
*
|
||||
* // multiple predicates as an array
|
||||
* $query->where(array("Column = 'Value'", "Column != 'Value'"));
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function setWhere($where) {
|
||||
$this->where = array();
|
||||
return $this->addWhere($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a WHERE predicate.
|
||||
*
|
||||
* There are two different ways of doing this:
|
||||
*
|
||||
* <code>
|
||||
* // the entire predicate as a single string
|
||||
* $query->where("Column = 'Value'");
|
||||
*
|
||||
* // multiple predicates as an array
|
||||
* $query->where(array("Column = 'Value'", "Column != 'Value'"));
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
||||
* @return SQLQuery
|
||||
*/
|
||||
public function addWhere($where) {
|
||||
if(is_array($where)) {
|
||||
$this->where = array_merge($this->where, $where);
|
||||
} elseif(!empty($where)) {
|
||||
$this->where[] = $where;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function where($where) {
|
||||
Deprecation::notice('3.0', 'Please use setWhere() or addWhere() instead!');
|
||||
return $this->setWhere($where);
|
||||
}
|
||||
|
||||
public function whereAny($where) {
|
||||
Deprecation::notice('3.0', 'Please use setWhereAny() or setWhereAny() instead!');
|
||||
return $this->setWhereAny($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String|array $filters Predicate(s) to set, as escaped SQL statements.
|
||||
*/
|
||||
public function setWhereAny($filters) {
|
||||
if(is_string($filters)) $filters = func_get_args();
|
||||
$clause = implode(" OR ", $filters);
|
||||
return $this->setWhere($clause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param String|array $filters Predicate(s) to set, as escaped SQL statements.
|
||||
*/
|
||||
public function addWhereAny($filters) {
|
||||
if(is_string($filters)) $filters = func_get_args();
|
||||
$clause = implode(" OR ", $filters);
|
||||
return $this->addWhere($clause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
||||
*/
|
||||
public function useDisjunction() {
|
||||
$this->connective = 'OR';
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
|
||||
*/
|
||||
public function useConjunction() {
|
||||
$this->connective = 'AND';
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the use of one table with another.
|
||||
*
|
||||
* @param string $old Name of the old table (unquoted, escaped)
|
||||
* @param string $new Name of the new table (unquoted, escaped)
|
||||
*/
|
||||
public function renameTable($old, $new) {
|
||||
$this->replaceText("`$old`", "`$new`");
|
||||
$this->replaceText("\"$old\"", "\"$new\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap some text in the SQL query with another.
|
||||
*
|
||||
* @param string $old The old text (escaped)
|
||||
* @param string $new The new text (escaped)
|
||||
*/
|
||||
public function replaceText($old, $new) {
|
||||
$this->replacementsOld[] = $old;
|
||||
$this->replacementsNew[] = $new;
|
||||
}
|
||||
|
||||
public function getFilter() {
|
||||
Deprecation::notice('3.0', 'Please use itemized filters in getWhere() instead of getFilter()');
|
||||
return DB::getConn()->sqlWhereToString($this->getWhere(), $this->getConnective());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of FROM clauses used internally.
|
||||
* @return array
|
||||
*/
|
||||
public function getFrom() {
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of HAVING clauses used internally.
|
||||
* @return array
|
||||
*/
|
||||
public function getHaving() {
|
||||
return $this->having;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of GROUP BY clauses used internally.
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupBy() {
|
||||
return $this->groupby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of WHERE clauses used internally.
|
||||
* @return array
|
||||
*/
|
||||
public function getWhere() {
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an itemised select list as a map, where keys are the aliases, and values are the column sources.
|
||||
* Aliases will always be provided (if the alias is implicit, the alias value will be inferred), and won't be
|
||||
* quoted.
|
||||
* E.g., 'Title' => '"SiteTree"."Title"'.
|
||||
*/
|
||||
public function getSelect() {
|
||||
return $this->select;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the SQL statement for this query.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function sql() {
|
||||
// TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new
|
||||
// syntax
|
||||
$origFrom = $this->from;
|
||||
// Sort the joins
|
||||
$this->from = $this->getOrderedJoins($this->from);
|
||||
// Build from clauses
|
||||
foreach($this->from as $alias => $join) {
|
||||
// $join can be something like this array structure
|
||||
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
|
||||
// "Status = 'approved'", 'order' => 20))
|
||||
if(is_array($join)) {
|
||||
if(is_string($join['filter'])) $filter = $join['filter'];
|
||||
else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0];
|
||||
else $filter = "(" . implode(") AND (", $join['filter']) . ")";
|
||||
|
||||
$aliasClause = ($alias != $join['table']) ? " AS \"" . Convert::raw2sql($alias) . "\"" : "";
|
||||
$this->from[$alias] = strtoupper($join['type']) . " JOIN \""
|
||||
. $join['table'] . "\"$aliasClause ON $filter";
|
||||
}
|
||||
}
|
||||
|
||||
$sql = DB::getConn()->sqlQueryToString($this);
|
||||
|
||||
if($this->replacementsOld) {
|
||||
$sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
|
||||
}
|
||||
|
||||
$this->from = $origFrom;
|
||||
|
||||
// The query was most likely just created and then exectued.
|
||||
if(trim($sql) === 'SELECT * FROM') {
|
||||
return '';
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the generated SQL string for this query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
try {
|
||||
return $this->sql();
|
||||
} catch(Exception $e) {
|
||||
return "<sql query>";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this query.
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function execute() {
|
||||
return DB::query($this->sql(), E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this query is for a specific ID in a table
|
||||
*
|
||||
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function filtersOnID() {
|
||||
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/';
|
||||
|
||||
// Sometimes the ID filter will be the 2nd element, if there's a ClasssName filter first.
|
||||
if(isset($this->where[0]) && preg_match($regexp, $this->where[0])) return true;
|
||||
if(isset($this->where[1]) && preg_match($regexp, $this->where[1])) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this query is filtering on a foreign key, ie finding a has_many relationship
|
||||
*
|
||||
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5")
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function filtersOnFK() {
|
||||
return (
|
||||
$this->where
|
||||
&& preg_match('/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/', $this->where[0])
|
||||
);
|
||||
}
|
||||
|
||||
/// VARIOUS TRANSFORMATIONS BELOW
|
||||
|
||||
/**
|
||||
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
|
||||
* @return int
|
||||
*/
|
||||
public function unlimitedRowCount($column = null) {
|
||||
// we can't clear the select if we're relying on its output by a HAVING clause
|
||||
if(count($this->having)) {
|
||||
$records = $this->execute();
|
||||
return $records->numRecords();
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->limit = null;
|
||||
$clone->orderby = null;
|
||||
|
||||
// Choose a default column
|
||||
if($column == null) {
|
||||
if($this->groupby) {
|
||||
$countQuery = new SQLQuery();
|
||||
$countQuery->select("count(*)");
|
||||
$countQuery->from = array('(' . $clone->sql() . ') all_distinct');
|
||||
|
||||
return $countQuery->execute()->value();
|
||||
|
||||
} else {
|
||||
$clone->setSelect(array("count(*)"));
|
||||
}
|
||||
} else {
|
||||
$clone->setSelect(array("count($column)"));
|
||||
}
|
||||
|
||||
$clone->setGroupBy(array());;
|
||||
return $clone->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this query can be sorted by the given field.
|
||||
*/
|
||||
public function canSortBy($fieldName) {
|
||||
$fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
|
||||
|
||||
return isset($this->select[$fieldName]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
|
||||
*
|
||||
* @todo Respect HAVING and GROUPBY, which can affect the result-count
|
||||
*
|
||||
* @param String $column Quoted, escaped column name
|
||||
* @return int
|
||||
*/
|
||||
public function count( $column = null) {
|
||||
// Choose a default column
|
||||
if($column == null) {
|
||||
if($this->groupby) {
|
||||
$column = 'DISTINCT ' . implode(", ", $this->groupby);
|
||||
} else {
|
||||
$column = '*';
|
||||
}
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->select = array("count($column)");
|
||||
$clone->limit = null;
|
||||
$clone->orderby = null;
|
||||
$clone->groupby = null;
|
||||
|
||||
$count = $clone->execute()->value();
|
||||
// If there's a limit set, then that limit is going to heavily affect the count
|
||||
if($this->limit) {
|
||||
if($count >= ($this->limit['start'] + $this->limit['limit']))
|
||||
return $this->limit['limit'];
|
||||
else
|
||||
return max(0, $count - $this->limit['start']);
|
||||
|
||||
// Otherwise, the count is going to be the output of the SQL query
|
||||
} else {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new SQLQuery that calls the given aggregate functions on this data.
|
||||
*
|
||||
* @param $column An aggregate expression, such as 'MAX("Balance")', or a set of them (as an escaped SQL statement)
|
||||
* @param $alias An optional alias for the aggregate column.
|
||||
*/
|
||||
public function aggregate($column, $alias = null) {
|
||||
$clone = clone $this;
|
||||
|
||||
// don't set an ORDER BY clause if no limit has been set. It doesn't make
|
||||
// sense to add an ORDER BY if there is no limit, and it will break
|
||||
// queries to databases like MSSQL if you do so. Note that the reason
|
||||
// this came up is because DataQuery::initialiseQuery() introduces
|
||||
// a default sort.
|
||||
if($this->limit) {
|
||||
$clone->setLimit($this->limit);
|
||||
$clone->setOrderBy($this->orderby);
|
||||
} else {
|
||||
$clone->setOrderBy(array());
|
||||
}
|
||||
|
||||
$clone->setGroupBy($this->groupby);
|
||||
if($alias) {
|
||||
$clone->setSelect(array());
|
||||
$clone->selectField($column, $alias);
|
||||
} else {
|
||||
$clone->setSelect($column);
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query that returns only the first row of this query
|
||||
*/
|
||||
public function firstRow() {
|
||||
$query = clone $this;
|
||||
$offset = $this->limit ? $this->limit['start'] : 0;
|
||||
$query->setLimit(1, $offset);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query that returns only the last row of this query
|
||||
*/
|
||||
public function lastRow() {
|
||||
$query = clone $this;
|
||||
$offset = $this->limit ? $this->limit['start'] : 0;
|
||||
|
||||
// Limit index to start in case of empty results
|
||||
$index = max($this->count() + $offset - 1, 0);
|
||||
$query->setLimit(1, $index);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
|
||||
* This prevents issues where developer-initiated JOINs attempt to JOIN using relations that haven't actually
|
||||
* yet been scaffolded by the framework. Demonstrated by PostGres in errors like:
|
||||
*"...ERROR: missing FROM-clause..."
|
||||
*
|
||||
* @param $from array - in the format of $this->select
|
||||
* @return array - and reorderded list of selects
|
||||
*/
|
||||
protected function getOrderedJoins($from) {
|
||||
// shift the first FROM table out from so we only deal with the JOINs
|
||||
$baseFrom = array_shift($from);
|
||||
$this->mergesort($from, function($firstJoin, $secondJoin) {
|
||||
if(
|
||||
!is_array($firstJoin)
|
||||
|| !is_array($secondJoin)
|
||||
|| $firstJoin['order'] == $secondJoin['order']
|
||||
) {
|
||||
return 0;
|
||||
} else {
|
||||
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Put the first FROM table back into the results
|
||||
array_unshift($from, $baseFrom);
|
||||
return $from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since uasort don't preserve the order of an array if the comparison is equal
|
||||
* we have to resort to a merge sort. It's quick and stable: O(n*log(n)).
|
||||
*
|
||||
* @see http://stackoverflow.com/q/4353739/139301
|
||||
*
|
||||
* @param array &$array - the array to sort
|
||||
* @param callable $cmpFunction - the function to use for comparison
|
||||
*/
|
||||
protected function mergesort(&$array, $cmpFunction = 'strcmp') {
|
||||
// Arrays of size < 2 require no action.
|
||||
if (count($array) < 2) {
|
||||
return;
|
||||
}
|
||||
// Split the array in half
|
||||
$halfway = count($array) / 2;
|
||||
$array1 = array_slice($array, 0, $halfway);
|
||||
$array2 = array_slice($array, $halfway);
|
||||
// Recurse to sort the two halves
|
||||
$this->mergesort($array1, $cmpFunction);
|
||||
$this->mergesort($array2, $cmpFunction);
|
||||
// If all of $array1 is <= all of $array2, just append them.
|
||||
if(call_user_func($cmpFunction, end($array1), reset($array2)) < 1) {
|
||||
$array = array_merge($array1, $array2);
|
||||
return;
|
||||
}
|
||||
// Merge the two sorted arrays into a single sorted array
|
||||
$array = array();
|
||||
$val1 = reset($array1);
|
||||
$val2 = reset($array2);
|
||||
do {
|
||||
if (call_user_func($cmpFunction, $val1, $val2) < 1) {
|
||||
$array[key($array1)] = $val1;
|
||||
$val1 = next($array1);
|
||||
} else {
|
||||
$array[key($array2)] = $val2;
|
||||
$val2 = next($array2);
|
||||
}
|
||||
} while($val1 && $val2);
|
||||
|
||||
// Merge the remainder
|
||||
while($val1) {
|
||||
$array[key($array1)] = $val1;
|
||||
$val1 = next($array1);
|
||||
}
|
||||
while($val2) {
|
||||
$array[key($array2)] = $val2;
|
||||
$val2 = next($array2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -162,10 +162,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* Amend freshly created DataQuery objects with versioned-specific
|
||||
* information.
|
||||
*
|
||||
* @param SQLQuery
|
||||
* @param SQLSelect
|
||||
* @param DataQuery
|
||||
*/
|
||||
public function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) {
|
||||
public function augmentDataQueryCreation(SQLSelect &$query, DataQuery &$dataQuery) {
|
||||
$parts = explode('.', Versioned::get_reading_mode());
|
||||
|
||||
if($parts[0] == 'Archive') {
|
||||
@ -182,22 +182,22 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the the SQLQuery that is created by the DataQuery
|
||||
* Augment the the SQLSelect that is created by the DataQuery
|
||||
* @todo Should this all go into VersionedDataQuery?
|
||||
*/
|
||||
public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
|
||||
public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
|
||||
if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
|
||||
|
||||
|
||||
switch($dataQuery->getQueryParam('Versioned.mode')) {
|
||||
// Reading a specific data from the archive
|
||||
case 'archive':
|
||||
$date = $dataQuery->getQueryParam('Versioned.date');
|
||||
foreach($query->getFrom() as $table => $dummy) {
|
||||
if(!DB::getConn()->hasTable($table . '_versions')) {
|
||||
if(!DB::get_schema()->hasTable($table . '_versions')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -216,19 +216,19 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
}
|
||||
// Link to the version archived on that date
|
||||
$safeDate = Convert::raw2sql($date);
|
||||
$query->addWhere(
|
||||
"\"{$baseTable}_versions\".\"Version\" IN
|
||||
(SELECT LatestVersion FROM
|
||||
(SELECT
|
||||
\"{$baseTable}_versions\".\"RecordID\",
|
||||
MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
|
||||
FROM \"{$baseTable}_versions\"
|
||||
WHERE \"{$baseTable}_versions\".\"LastEdited\" <= '$safeDate'
|
||||
GROUP BY \"{$baseTable}_versions\".\"RecordID\"
|
||||
) AS \"{$baseTable}_versions_latest\"
|
||||
WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
|
||||
)");
|
||||
$query->addWhere(array(
|
||||
"\"{$baseTable}_versions\".\"Version\" IN
|
||||
(SELECT LatestVersion FROM
|
||||
(SELECT
|
||||
\"{$baseTable}_versions\".\"RecordID\",
|
||||
MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
|
||||
FROM \"{$baseTable}_versions\"
|
||||
WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
|
||||
GROUP BY \"{$baseTable}_versions\".\"RecordID\"
|
||||
) AS \"{$baseTable}_versions_latest\"
|
||||
WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
|
||||
)" => $date
|
||||
));
|
||||
break;
|
||||
|
||||
// Reading a specific stage (Stage or Live)
|
||||
@ -329,11 +329,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
/**
|
||||
* For lazy loaded fields requiring extra sql manipulation, ie versioning.
|
||||
*
|
||||
* @param SQLQuery $query
|
||||
* @param SQLSelect $query
|
||||
* @param DataQuery $dataQuery
|
||||
* @param DataObject $dataObject
|
||||
*/
|
||||
public function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) {
|
||||
public function augmentLoadLazyFields(SQLSelect &$query, DataQuery &$dataQuery = null, $dataObject) {
|
||||
// The VersionedMode local variable ensures that this decorator only applies to
|
||||
// queries that have originated from the Versioned object, and have the Versioned
|
||||
// metadata set on the query object. This prevents regular queries from
|
||||
@ -362,7 +362,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
*/
|
||||
public static function on_db_reset() {
|
||||
// Drop all temporary tables
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
foreach(self::$archive_tables as $tableName) {
|
||||
if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
|
||||
else $db->query("DROP TABLE \"$tableName\"");
|
||||
@ -417,7 +417,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
// otherwise.
|
||||
$indexes = $this->uniqueToIndex($indexes);
|
||||
if($stage != $this->defaultStage) {
|
||||
DB::requireTable("{$table}_$stage", $fields, $indexes, false, $options);
|
||||
DB::require_table("{$table}_$stage", $fields, $indexes, false, $options);
|
||||
}
|
||||
|
||||
// Version fields on each root table (including Stage)
|
||||
@ -464,7 +464,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
);
|
||||
}
|
||||
|
||||
if(DB::getConn()->hasTable("{$table}_versions")) {
|
||||
if(DB::get_schema()->hasTable("{$table}_versions")) {
|
||||
// Fix data that lacks the uniqueness constraint (since this was added later and
|
||||
// bugs meant that the constraint was validated)
|
||||
$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
|
||||
@ -474,8 +474,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
foreach($duplications as $dup) {
|
||||
DB::alteration_message("Removing {$table}_versions duplicate data for "
|
||||
."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
|
||||
DB::query("DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = {$dup['RecordID']}
|
||||
AND \"Version\" = {$dup['Version']} AND \"ID\" != {$dup['ID']}");
|
||||
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
|
||||
@ -496,7 +499,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
if($count > 0) {
|
||||
DB::alteration_message("Removing orphaned versioned records", "deleted");
|
||||
|
||||
$effectedIDs = DB::query("
|
||||
$affectedIDs = DB::query("
|
||||
SELECT \"{$table}_versions\".\"ID\" FROM \"{$table}_versions\"
|
||||
LEFT JOIN \"{$child}_versions\"
|
||||
ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
|
||||
@ -504,10 +507,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
WHERE \"{$child}_versions\".\"ID\" IS NULL
|
||||
")->column();
|
||||
|
||||
if(is_array($effectedIDs)) {
|
||||
foreach($effectedIDs as $key => $value) {
|
||||
DB::query("DELETE FROM \"{$table}_versions\""
|
||||
. " WHERE \"{$table}_versions\".\"ID\" = '$value'");
|
||||
if(is_array($affectedIDs)) {
|
||||
foreach($affectedIDs as $key => $value) {
|
||||
DB::prepared_query(
|
||||
"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
|
||||
array($value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -515,11 +520,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
}
|
||||
|
||||
DB::requireTable("{$table}_versions", $versionFields, $versionIndexes, true, $options);
|
||||
DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
|
||||
} else {
|
||||
DB::dontRequireTable("{$table}_versions");
|
||||
DB::dont_require_table("{$table}_versions");
|
||||
foreach($this->stages as $stage) {
|
||||
if($stage != $this->defaultStage) DB::dontrequireTable("{$table}_$stage");
|
||||
if($stage != $this->defaultStage) DB::dont_require_table("{$table}_$stage");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -554,11 +559,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment a write-record request.
|
||||
*
|
||||
* @param SQLQuery $manipulation Query to augment.
|
||||
*/
|
||||
public function augmentWrite(&$manipulation) {
|
||||
$tables = array_keys($manipulation);
|
||||
$version_table = array();
|
||||
@ -590,10 +590,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
// Otherwise, we're just copying a version to another table
|
||||
if(!isset($manipulation[$table]['fields']['Version'])) {
|
||||
// Add any extra, unchanged fields to the version record.
|
||||
$data = DB::query("SELECT * FROM \"$table\" WHERE \"ID\" = $id")->record();
|
||||
$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($id))->record();
|
||||
if($data) foreach($data as $k => $v) {
|
||||
if (!isset($newManipulation['fields'][$k])) {
|
||||
$newManipulation['fields'][$k] = "'" . Convert::raw2sql($v) . "'";
|
||||
$newManipulation['fields'][$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
@ -606,8 +606,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
else unset($nextVersion);
|
||||
|
||||
if($rid && !isset($nextVersion)) {
|
||||
$nextVersion = DB::query("SELECT MAX(\"Version\") + 1 FROM \"{$baseDataClass}_versions\""
|
||||
. " WHERE \"RecordID\" = $rid")->value();
|
||||
$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
|
||||
FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
|
||||
array($rid)
|
||||
)->value();
|
||||
}
|
||||
|
||||
$newManipulation['fields']['Version'] = $nextVersion ? $nextVersion : 1;
|
||||
@ -646,7 +648,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
) {
|
||||
// If the record has already been inserted in the (table), get rid of it.
|
||||
if($manipulation[$table]['command']=='insert') {
|
||||
DB::query("DELETE FROM \"{$table}\" WHERE \"ID\"='$id'");
|
||||
DB::prepared_query(
|
||||
"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
|
||||
array($id)
|
||||
);
|
||||
}
|
||||
|
||||
$newTable = $table . '_' . Versioned::current_stage();
|
||||
@ -758,9 +763,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
|
||||
$table2 = $table1 . "_$this->liveStage";
|
||||
|
||||
return DB::query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\""
|
||||
. " INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\""
|
||||
. " WHERE \"$table1\".\"ID\" = ". $this->owner->ID)->value();
|
||||
return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
|
||||
INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
|
||||
WHERE \"$table1\".\"ID\" = ?",
|
||||
array($this->owner->ID)
|
||||
)->value();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -796,13 +803,16 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
// Mark this version as having been published at some stage
|
||||
DB::query("UPDATE \"{$extTable}_versions\" SET \"WasPublished\" = '1', \"PublisherID\" = $publisherID"
|
||||
. " WHERE \"RecordID\" = $from->ID AND \"Version\" = $from->Version");
|
||||
DB::prepared_query("UPDATE \"{$extTable}_versions\"
|
||||
SET \"WasPublished\" = ?, \"PublisherID\" = ?
|
||||
WHERE \"RecordID\" = ? AND \"Version\" = ?",
|
||||
array(1, $publisherID, $from->ID, $from->Version)
|
||||
);
|
||||
|
||||
$oldMode = Versioned::get_reading_mode();
|
||||
Versioned::reading_stage($toStage);
|
||||
|
||||
$conn = DB::getConn();
|
||||
$conn = DB::get_conn();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, true);
|
||||
$from->write();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, false);
|
||||
@ -844,9 +854,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
// will be false.
|
||||
|
||||
// TODO: DB Abstraction: if statement here:
|
||||
$stagesAreEqual = DB::query("SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\""
|
||||
. " THEN 1 ELSE 0 END FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\""
|
||||
. " AND \"$table1\".\"ID\" = {$this->owner->ID}")->value();
|
||||
$stagesAreEqual = DB::prepared_query(
|
||||
"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
|
||||
FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
|
||||
AND \"$table1\".\"ID\" = ?",
|
||||
array($this->owner->ID)
|
||||
)->value();
|
||||
|
||||
return !$stagesAreEqual;
|
||||
}
|
||||
@ -898,7 +911,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
|
||||
}
|
||||
|
||||
$query->addWhere("\"{$baseTable}_versions\".\"RecordID\" = '{$this->owner->ID}'");
|
||||
$query->addWhere(array(
|
||||
"\"{$baseTable}_versions\".\"RecordID\" = ?" => $this->owner->ID
|
||||
));
|
||||
$query->setOrderBy(($sort) ? $sort
|
||||
: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
|
||||
|
||||
@ -1120,7 +1135,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
// get version as performance-optimized SQL query (gets called for each page in the sitetree)
|
||||
$version = DB::query("SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = $id")->value();
|
||||
$version = DB::prepared_query(
|
||||
"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
|
||||
array($id)
|
||||
)->value();
|
||||
|
||||
// cache value (if required)
|
||||
if($cache) {
|
||||
@ -1152,7 +1170,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
return;
|
||||
}
|
||||
$filter = "";
|
||||
|
||||
$parameters = array();
|
||||
if($idList) {
|
||||
// Validate the ID list
|
||||
foreach($idList as $id) {
|
||||
@ -1161,14 +1179,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
$filter = "WHERE \"ID\" IN(" .implode(", ", $idList) . ")";
|
||||
$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
|
||||
$parameters = $idList;
|
||||
}
|
||||
|
||||
$baseClass = ClassInfo::baseDataClass($class);
|
||||
$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
|
||||
|
||||
$versions = DB::query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter")->map();
|
||||
$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
|
||||
|
||||
foreach($versions as $id => $version) {
|
||||
self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
|
||||
@ -1186,7 +1204,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
||||
* @param int $limit A limit on the number of records returned from the database.
|
||||
* @param string $containerClass The container class for the result set (default is DataList)
|
||||
*
|
||||
* @return SS_List
|
||||
* @return DataList A modified DataList designated to the specified stage
|
||||
*/
|
||||
public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '',
|
||||
$containerClass = 'DataList') {
|
||||
|
248
model/connect/DBConnector.php
Normal file
248
model/connect/DBConnector.php
Normal file
@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents an object responsible for wrapping DB connector api
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class DBConnector {
|
||||
|
||||
/**
|
||||
* List of operations to treat as write
|
||||
*
|
||||
* @config
|
||||
* @var array
|
||||
*/
|
||||
private static $write_operations = array('insert', 'update', 'delete', 'replace', 'alter', 'drop');
|
||||
|
||||
/**
|
||||
* Error handler for database errors.
|
||||
* All database errors will call this function to report the error. It isn't a static function;
|
||||
* it will be called on the object itself and as such can be overridden in a subclass.
|
||||
* Subclasses should run all errors through this function.
|
||||
*
|
||||
* @todo hook this into a more well-structured error handling system.
|
||||
* @param string $msg The error message.
|
||||
* @param integer $errorLevel The level of the error to throw.
|
||||
* @param string $sql The SQL related to this query
|
||||
* @param array $parameters Parameters passed to the query
|
||||
*/
|
||||
protected function databaseError($msg, $errorLevel = E_USER_ERROR, $sql = null, $parameters = array()) {
|
||||
// Prevent errors when error checking is set at zero level
|
||||
if(empty($errorLevel)) return;
|
||||
|
||||
// Format query if given
|
||||
if (!empty($sql)) {
|
||||
$formatter = new SQLFormatter();
|
||||
$formattedSQL = $formatter->formatPlain($sql);
|
||||
$msg = "Couldn't run query:\n\n{$formattedSQL}\n\n{$msg}";
|
||||
}
|
||||
|
||||
if($errorLevel === E_USER_ERROR) {
|
||||
// Treating errors as exceptions better allows for responding to errors
|
||||
// in code, such as credential checking during installation
|
||||
throw new SS_DatabaseException($msg, 0, null, $sql, $parameters);
|
||||
} else {
|
||||
user_error($msg, $errorLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the query should be previewed, and thus interrupted silently.
|
||||
* If so, this function also displays the query via the debuging system.
|
||||
* Subclasess should respect the results of this call for each query, and not
|
||||
* execute any queries that generate a true response.
|
||||
*
|
||||
* @param string $sql The query to be executed
|
||||
* @return boolean Flag indicating that the query was previewed
|
||||
*/
|
||||
protected function previewWrite($sql) {
|
||||
// Break if not requested
|
||||
if (!isset($_REQUEST['previewwrite'])) return false;
|
||||
|
||||
// Break if non-write operation
|
||||
$operation = strtolower(substr($sql, 0, strpos($sql, ' ')));
|
||||
$writeOperations = Config::inst()->get(get_class($this), 'write_operations');
|
||||
if (!in_array($operation, $writeOperations)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// output preview message
|
||||
Debug::message("Will execute: $sql");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the display and benchmarking of queries as they are being run
|
||||
*
|
||||
* @param string $sql Query to run, and single parameter to callback
|
||||
* @param callable $callback Callback to execute code
|
||||
* @return mixed Result of query
|
||||
*/
|
||||
protected function benchmarkQuery($sql, $callback) {
|
||||
if (isset($_REQUEST['showqueries']) && Director::isDev(true)) {
|
||||
$starttime = microtime(true);
|
||||
$result = $callback($sql);
|
||||
$endtime = round(microtime(true) - $starttime, 4);
|
||||
Debug::message("\n$sql\n{$endtime}ms\n", false);
|
||||
return $result;
|
||||
} else {
|
||||
return $callback($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts only the parameter values for error reporting
|
||||
*
|
||||
* @param array $parameters
|
||||
* @return array List of parameter values
|
||||
*/
|
||||
protected function parameterValues($parameters) {
|
||||
$values = array();
|
||||
foreach($parameters as $value) {
|
||||
$values[] = is_array($value) ? $value['value'] : $value;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link this connector to the database given the specified parameters
|
||||
* Will throw an exception rather than return a success state.
|
||||
* The connector should not select the database once connected until
|
||||
* explicitly called by selectDatabase()
|
||||
*
|
||||
* @param array $parameters List of parameters such as
|
||||
* <ul>
|
||||
* <li>type</li>
|
||||
* <li>server</li>
|
||||
* <li>username</li>
|
||||
* <li>password</li>
|
||||
* <li>database</li>
|
||||
* <li>path</li>
|
||||
* </ul>
|
||||
* @param boolean $selectDB By default database selection should be
|
||||
* handled by the database controller (to enable database creation on the
|
||||
* fly if necessary), but some interfaces require that the database is
|
||||
* specified during connection (SQLite, Azure, etc).
|
||||
*/
|
||||
abstract public function connect($parameters, $selectDB = false);
|
||||
|
||||
/**
|
||||
* Query for the version of the currently connected database
|
||||
*
|
||||
* @return string Version of this database
|
||||
*/
|
||||
abstract public function getVersion();
|
||||
|
||||
/**
|
||||
* Given a value escape this for use in a query for the current database
|
||||
* connector. Note that this does not quote the value.
|
||||
*
|
||||
* @param string $value The value to be escaped
|
||||
* @return string The appropritaely escaped string for value
|
||||
*/
|
||||
abstract public function escapeString($value);
|
||||
|
||||
/**
|
||||
* Given a value escape and quote this appropriately for the current
|
||||
* database connector.
|
||||
*
|
||||
* @param string $value The value to be injected into a query
|
||||
* @return string The appropriately escaped and quoted string for $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.
|
||||
* Implementations of this function should respect previewWrite and benchmarkQuery
|
||||
*
|
||||
* @see http://php.net/manual/en/errorfunc.constants.php
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param integer $errorLevel For errors to this query, raise PHP errors
|
||||
* using this error level.
|
||||
*/
|
||||
abstract public function query($sql, $errorLevel = E_USER_ERROR);
|
||||
|
||||
/**
|
||||
* Execute the given SQL parameterised query with the specified arguments
|
||||
*
|
||||
* @param string $sql The SQL query to execute. The ? character will denote parameters.
|
||||
* @param array $parameters An ordered list of arguments.
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
abstract public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR);
|
||||
|
||||
/**
|
||||
* Select a database by name
|
||||
*
|
||||
* @param string $name Name of database
|
||||
* @return boolean Flag indicating success
|
||||
*/
|
||||
abstract public function selectDatabase($name);
|
||||
|
||||
/**
|
||||
* Retrieves the name of the currently selected database
|
||||
*
|
||||
* @return string Name of the database, or null if none selected
|
||||
*/
|
||||
abstract public function getSelectedDatabase();
|
||||
|
||||
/**
|
||||
* De-selects the currently selected database
|
||||
*/
|
||||
abstract public function unloadDatabase();
|
||||
|
||||
/**
|
||||
* Retrieves the last error generated from the database connection
|
||||
*
|
||||
* @return string The error message
|
||||
*/
|
||||
abstract public function getLastError();
|
||||
|
||||
/**
|
||||
* Determines the last ID generated from the specified table.
|
||||
* Note that some connectors may not be able to return $table specific responses,
|
||||
* and this parameter may be ignored.
|
||||
*
|
||||
* @param string $table The target table to return the last generated ID for
|
||||
* @return integer ID value
|
||||
*/
|
||||
abstract public function getGeneratedID($table);
|
||||
|
||||
/**
|
||||
* Determines the number of affected rows from the last SQL query
|
||||
*
|
||||
* @return integer Number of affected rows
|
||||
*/
|
||||
abstract public function affectedRows();
|
||||
|
||||
/**
|
||||
* Determines if we are connected to a server AND have a valid database
|
||||
* selected.
|
||||
*
|
||||
* @return boolean Flag indicating that a valid database is connected
|
||||
*/
|
||||
abstract public function isActive();
|
||||
}
|
332
model/connect/DBQueryBuilder.php
Normal file
332
model/connect/DBQueryBuilder.php
Normal file
@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Builds a SQL query string from a SQLExpression object
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class DBQueryBuilder {
|
||||
|
||||
/**
|
||||
* Determines the line separator to use.
|
||||
*
|
||||
* @return string Non-empty whitespace character
|
||||
*/
|
||||
public function getSeparator() {
|
||||
return "\n ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a sql query with the specified connection
|
||||
*
|
||||
* @param SQLExpression $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string The resulting SQL as a string
|
||||
*/
|
||||
public function buildSQL(SQLExpression $query, &$parameters) {
|
||||
$sql = null;
|
||||
$parameters = array();
|
||||
|
||||
// Ignore null queries
|
||||
if($query->isEmpty()) return null;
|
||||
|
||||
if($query instanceof SQLSelect) {
|
||||
$sql = $this->buildSelectQuery($query, $parameters);
|
||||
} elseif($query instanceof SQLDelete) {
|
||||
$sql = $this->buildDeleteQuery($query, $parameters);
|
||||
} elseif($query instanceof SQLInsert) {
|
||||
$sql = $this->buildInsertQuery($query, $parameters);
|
||||
} elseif($query instanceof SQLUpdate) {
|
||||
$sql = $this->buildUpdateQuery($query, $parameters);
|
||||
} else {
|
||||
user_error("Not implemented: query generation for type " . $query->getType());
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query from a SQLSelect expression
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed SQL string
|
||||
*/
|
||||
protected function buildSelectQuery(SQLSelect $query, array &$parameters) {
|
||||
$sql = $this->buildSelectFragment($query, $parameters);
|
||||
$sql .= $this->buildFromFragment($query, $parameters);
|
||||
$sql .= $this->buildWhereFragment($query, $parameters);
|
||||
$sql .= $this->buildGroupByFragment($query, $parameters);
|
||||
$sql .= $this->buildHavingFragment($query, $parameters);
|
||||
$sql .= $this->buildOrderByFragment($query, $parameters);
|
||||
$sql .= $this->buildLimitFragment($query, $parameters);
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query from a SQLDelete expression
|
||||
*
|
||||
* @param SQLDelete $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed SQL string
|
||||
*/
|
||||
protected function buildDeleteQuery(SQLDelete $query, array &$parameters) {
|
||||
$sql = $this->buildDeleteFragment($query, $parameters);
|
||||
$sql .= $this->buildFromFragment($query, $parameters);
|
||||
$sql .= $this->buildWhereFragment($query, $parameters);
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query from a SQLInsert expression
|
||||
*
|
||||
* @param SQLInsert $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed SQL string
|
||||
*/
|
||||
protected function buildInsertQuery(SQLInsert $query, array &$parameters) {
|
||||
$nl = $this->getSeparator();
|
||||
$into = $query->getInto();
|
||||
|
||||
// Column identifiers
|
||||
$columns = $query->getColumns();
|
||||
$sql = "INSERT INTO {$into}{$nl}(" . implode(', ', $columns) . ")";
|
||||
|
||||
// Values
|
||||
$sql .= "{$nl}VALUES";
|
||||
|
||||
// Build all rows
|
||||
$rowParts = array();
|
||||
foreach($query->getRows() as $row) {
|
||||
// Build all columns in this row
|
||||
$assignments = $row->getAssignments();
|
||||
// Join SET components together, considering parameters
|
||||
$parts = array();
|
||||
foreach($columns as $column) {
|
||||
// Check if this column has a value for this row
|
||||
if(isset($assignments[$column])) {
|
||||
// Assigment is a single item array, expand with a loop here
|
||||
foreach($assignments[$column] as $assignmentSQL => $assignmentParameters) {
|
||||
$parts[] = $assignmentSQL;
|
||||
$parameters = array_merge($parameters, $assignmentParameters);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// This row is missing a value for a column used by another row
|
||||
$parts[] = '?';
|
||||
$parameters[] = null;
|
||||
}
|
||||
}
|
||||
$rowParts[] = '(' . implode(', ', $parts) . ')';
|
||||
}
|
||||
$sql .= $nl . implode(",$nl", $rowParts);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a query from a SQLUpdate expression
|
||||
*
|
||||
* @param SQLUpdate $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed SQL string
|
||||
*/
|
||||
protected function buildUpdateQuery(SQLUpdate $query, array &$parameters) {
|
||||
$sql = $this->buildUpdateFragment($query, $parameters);
|
||||
$sql .= $this->buildWhereFragment($query, $parameters);
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SELECT clauses ready for inserting into a query.
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed select part of statement
|
||||
*/
|
||||
protected function buildSelectFragment(SQLSelect $query, array &$parameters) {
|
||||
$distinct = $query->getDistinct();
|
||||
$select = $query->getSelect();
|
||||
$clauses = array();
|
||||
|
||||
foreach ($select as $alias => $field) {
|
||||
// Don't include redundant aliases.
|
||||
$fieldAlias = "\"{$alias}\"";
|
||||
if ($alias === $field || substr($field, -strlen($fieldAlias)) === $fieldAlias) {
|
||||
$clauses[] = $field;
|
||||
} else {
|
||||
$clauses[] = "$field AS $fieldAlias";
|
||||
}
|
||||
}
|
||||
|
||||
$text = 'SELECT ';
|
||||
if ($distinct) $text .= 'DISTINCT ';
|
||||
return $text .= implode(', ', $clauses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the DELETE clause ready for inserting into a query.
|
||||
*
|
||||
* @param SQLExpression $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed delete part of statement
|
||||
*/
|
||||
public function buildDeleteFragment(SQLDelete $query, array &$parameters) {
|
||||
$text = 'DELETE';
|
||||
|
||||
// If doing a multiple table delete then list the target deletion tables here
|
||||
// Note that some schemas don't support multiple table deletion
|
||||
$delete = $query->getDelete();
|
||||
if(!empty($delete)) {
|
||||
$text .= ' ' . implode(', ', $delete);
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the UPDATE clause ready for inserting into a query.
|
||||
*
|
||||
* @param SQLExpression $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed from part of statement
|
||||
*/
|
||||
public function buildUpdateFragment(SQLUpdate $query, array &$parameters) {
|
||||
$table = $query->getTable();
|
||||
$text = "UPDATE $table";
|
||||
|
||||
// Join SET components together, considering parameters
|
||||
$parts = array();
|
||||
foreach($query->getAssignments() as $column => $assignment) {
|
||||
// Assigment is a single item array, expand with a loop here
|
||||
foreach($assignment as $assignmentSQL => $assignmentParameters) {
|
||||
$parts[] = "$column = $assignmentSQL";
|
||||
$parameters = array_merge($parameters, $assignmentParameters);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$nl = $this->getSeparator();
|
||||
$text .= "{$nl}SET " . implode(', ', $parts);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FROM clause ready for inserting into a query.
|
||||
*
|
||||
* @param SQLExpression $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed from part of statement
|
||||
*/
|
||||
public function buildFromFragment(SQLExpression $query, array &$parameters) {
|
||||
$from = $query->getJoins();
|
||||
$nl = $this->getSeparator();
|
||||
return "{$nl}FROM " . implode(' ', $from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the WHERE clauses ready for inserting into a query.
|
||||
*
|
||||
* @param SQLExpression $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed where condition
|
||||
*/
|
||||
public function buildWhereFragment(SQLExpression $query, array &$parameters) {
|
||||
|
||||
// Get parameterised elements
|
||||
$where = $query->getWhereParameterised($whereParameters);
|
||||
if(empty($where)) return '';
|
||||
|
||||
// Join conditions
|
||||
$connective = $query->getConnective();
|
||||
$parameters = array_merge($parameters, $whereParameters);
|
||||
$nl = $this->getSeparator();
|
||||
return "{$nl}WHERE (" . implode("){$nl}{$connective} (", $where) . ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ORDER BY clauses ready for inserting into a query.
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed order by part of statement
|
||||
*/
|
||||
public function buildOrderByFragment(SQLSelect $query, array &$parameters) {
|
||||
$orderBy = $query->getOrderBy();
|
||||
if(empty($orderBy)) return '';
|
||||
|
||||
// Build orders, each with direction considered
|
||||
$statements = array();
|
||||
foreach ($orderBy as $clause => $dir) {
|
||||
$statements[] = trim("$clause $dir");
|
||||
}
|
||||
|
||||
$nl = $this->getSeparator();
|
||||
return "{$nl}ORDER BY " . implode(', ', $statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GROUP BY clauses ready for inserting into a query.
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string Completed group part of statement
|
||||
*/
|
||||
public function buildGroupByFragment(SQLSelect $query, array &$parameters) {
|
||||
$groupBy = $query->getGroupBy();
|
||||
if(empty($groupBy)) return '';
|
||||
|
||||
$nl = $this->getSeparator();
|
||||
return "{$nl}GROUP BY " . implode(', ', $groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HAVING clauses ready for inserting into a query.
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string
|
||||
*/
|
||||
public function buildHavingFragment(SQLSelect $query, array &$parameters) {
|
||||
$having = $query->getHavingParameterised($havingParameters);
|
||||
if(empty($having)) return '';
|
||||
|
||||
// Generate having, considering parameters present
|
||||
$connective = $query->getConnective();
|
||||
$parameters = array_merge($parameters, $havingParameters);
|
||||
$nl = $this->getSeparator();
|
||||
return "{$nl}HAVING (" . implode("){$nl}{$connective} (", $having) . ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the LIMIT clause ready for inserting into a query.
|
||||
*
|
||||
* @param SQLSelect $query The expression object to build from
|
||||
* @param array $parameters Out parameter for the resulting query parameters
|
||||
* @return string The finalised limit SQL fragment
|
||||
*/
|
||||
public function buildLimitFragment(SQLSelect $query, array &$parameters) {
|
||||
$nl = $this->getSeparator();
|
||||
|
||||
// Ensure limit is given
|
||||
$limit = $query->getLimit();
|
||||
if(empty($limit)) return '';
|
||||
|
||||
// For literal values return this as the limit SQL
|
||||
if (!is_array($limit)) {
|
||||
return "{$nl}LIMIT $limit";
|
||||
}
|
||||
|
||||
// Assert that the array version provides the 'limit' key
|
||||
if (!isset($limit['limit']) || !is_numeric($limit['limit'])) {
|
||||
throw new InvalidArgumentException(
|
||||
'DBQueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true)
|
||||
);
|
||||
}
|
||||
|
||||
// Format the array limit, given an optional start key
|
||||
$clause = "{$nl}LIMIT {$limit['limit']}";
|
||||
if(isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) {
|
||||
$clause .= " OFFSET {$limit['start']}";
|
||||
}
|
||||
return $clause;
|
||||
}
|
||||
}
|
938
model/connect/DBSchemaManager.php
Normal file
938
model/connect/DBSchemaManager.php
Normal file
@ -0,0 +1,938 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents and handles all schema management for a database
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class DBSchemaManager {
|
||||
|
||||
/**
|
||||
*
|
||||
* @config
|
||||
* Check tables when running /dev/build, and repair them if necessary.
|
||||
* In case of large databases or more fine-grained control on how to handle
|
||||
* data corruption in tables, you can disable this behaviour and handle it
|
||||
* outside of this class, e.g. through a nightly system task with extended logging capabilities.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $check_and_repair_on_build = true;
|
||||
|
||||
/**
|
||||
* Instance of the database controller this schema belongs to
|
||||
*
|
||||
* @var SS_Database
|
||||
*/
|
||||
protected $database = null;
|
||||
|
||||
/**
|
||||
* If this is false, then information about database operations
|
||||
* will be displayed, eg creation of tables.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $supressOutput = false;
|
||||
|
||||
/**
|
||||
* Injector injection point for database controller
|
||||
*
|
||||
* @param SS_Database $connector
|
||||
*/
|
||||
public function setDatabase(SS_Database $database) {
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* The table list, generated by the tableList() function.
|
||||
* Used by the requireTable() function.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $tableList;
|
||||
|
||||
/**
|
||||
* Keeps track whether we are currently updating the schema.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $schemaIsUpdating = false;
|
||||
|
||||
/**
|
||||
* Large array structure that represents a schema update transaction
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $schemaUpdateTransaction;
|
||||
|
||||
/**
|
||||
* Enable supression of database messages.
|
||||
*/
|
||||
public function quiet() {
|
||||
$this->supressOutput = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given SQL query.
|
||||
* This abstract function must be defined by subclasses as part of the actual implementation.
|
||||
* It should return a subclass of SS_Query as the result.
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
return $this->database->query($sql, $errorLevel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the given SQL parameterised query with the specified arguments
|
||||
*
|
||||
* @param string $sql The SQL query to execute. The ? character will denote parameters.
|
||||
* @param array $parameters An ordered list of arguments.
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||
return $this->database->preparedQuery($sql, $parameters, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a schema update within a single callback
|
||||
*
|
||||
* @var callable $callback
|
||||
* @throws Exception
|
||||
*/
|
||||
public function schemaUpdate($callback) {
|
||||
// Begin schema update
|
||||
$this->schemaIsUpdating = true;
|
||||
|
||||
// Update table list
|
||||
$this->tableList = array();
|
||||
$tables = $this->tableList();
|
||||
foreach ($tables as $table) {
|
||||
$this->tableList[strtolower($table)] = $table;
|
||||
}
|
||||
|
||||
// Clear update list for client code to mess around with
|
||||
$this->schemaUpdateTransaction = array();
|
||||
|
||||
$error = null;
|
||||
try {
|
||||
|
||||
// Yield control to client code
|
||||
$callback();
|
||||
|
||||
// If the client code has cancelled the update then abort
|
||||
if(!$this->isSchemaUpdating()) return;
|
||||
|
||||
// End schema update
|
||||
foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
|
||||
switch ($changes['command']) {
|
||||
case 'create':
|
||||
$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'],
|
||||
$changes['options'], @$changes['advancedOptions']);
|
||||
break;
|
||||
|
||||
case 'alter':
|
||||
$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
|
||||
$changes['alteredFields'], $changes['alteredIndexes'],
|
||||
$changes['alteredOptions'], @$changes['advancedOptions']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch(Exception $ex) {
|
||||
$error = $ex;
|
||||
}
|
||||
// finally {
|
||||
$this->schemaUpdateTransaction = null;
|
||||
$this->schemaIsUpdating = false;
|
||||
// }
|
||||
|
||||
if($error) throw $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the schema updates requested during (but not after) schemaUpdate() call.
|
||||
*/
|
||||
public function cancelSchemaUpdate() {
|
||||
$this->schemaUpdateTransaction = null;
|
||||
$this->schemaIsUpdating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are during a schema update.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function isSchemaUpdating() {
|
||||
return $this->schemaIsUpdating;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function doesSchemaNeedUpdating() {
|
||||
return (bool) $this->schemaUpdateTransaction;
|
||||
}
|
||||
|
||||
// Transactional schema altering functions - they don't do anyhting except for update schemaUpdateTransaction
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record a table creation to later execute
|
||||
*
|
||||
* @param string $table Name of the table
|
||||
* @param array $options Create table options (ENGINE, etc.)
|
||||
* @param array $advanced_options Advanced table creation options
|
||||
*/
|
||||
public function transCreateTable($table, $options = null, $advanced_options = null) {
|
||||
$this->schemaUpdateTransaction[$table] = array(
|
||||
'command' => 'create',
|
||||
'newFields' => array(),
|
||||
'newIndexes' => array(),
|
||||
'options' => $options,
|
||||
'advancedOptions' => $advanced_options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record a table alteration to later execute
|
||||
*
|
||||
* @param string $table Name of the table
|
||||
* @param array $options Create table options (ENGINE, etc.)
|
||||
* @param array $advanced_options Advanced table creation options
|
||||
*/
|
||||
public function transAlterTable($table, $options, $advanced_options) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
|
||||
$this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record a field to be later created
|
||||
*
|
||||
* @param string $table Name of the table to hold this field
|
||||
* @param string $field Name of the field to create
|
||||
* @param string $schema Field specification as a string
|
||||
*/
|
||||
public function transCreateField($table, $field, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record an index to be later created
|
||||
*
|
||||
* @param string $table Name of the table to hold this index
|
||||
* @param string $index Name of the index to create
|
||||
* @param array $schema Already parsed index specification
|
||||
*/
|
||||
public function transCreateIndex($table, $index, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record a field to be later updated
|
||||
*
|
||||
* @param string $table Name of the table to hold this field
|
||||
* @param string $field Name of the field to update
|
||||
* @param string $schema Field specification as a string
|
||||
*/
|
||||
public function transAlterField($table, $field, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the schema manager to record an index to be later updated
|
||||
*
|
||||
* @param string $table Name of the table to hold this index
|
||||
* @param string $index Name of the index to update
|
||||
* @param array $schema Already parsed index specification
|
||||
*/
|
||||
public function transAlterIndex($table, $index, $schema) {
|
||||
$this->transInitTable($table);
|
||||
$this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the other transXXX methods - mark the given table as being altered
|
||||
* if it doesn't already exist
|
||||
*
|
||||
* @param string $table Name of the table to initialise
|
||||
*/
|
||||
protected function transInitTable($table) {
|
||||
if (!isset($this->schemaUpdateTransaction[$table])) {
|
||||
$this->schemaUpdateTransaction[$table] = array(
|
||||
'command' => 'alter',
|
||||
'newFields' => array(),
|
||||
'newIndexes' => array(),
|
||||
'alteredFields' => array(),
|
||||
'alteredIndexes' => array(),
|
||||
'alteredOptions' => ''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the following table in the database, modifying whatever already exists
|
||||
* as necessary.
|
||||
*
|
||||
* @todo Change detection for CREATE TABLE $options other than "Engine"
|
||||
*
|
||||
* @param string $table The name of the table
|
||||
* @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
|
||||
* @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
|
||||
* The values of the array can be one of:
|
||||
* - true: Create a single column index on the field named the same as the index.
|
||||
* - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
|
||||
* control over the index.
|
||||
* @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
|
||||
* @param string $options SQL statement to append to the CREATE TABLE call.
|
||||
* @param array $extensions List of extensions
|
||||
*/
|
||||
public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
|
||||
$options = array(), $extensions = false
|
||||
) {
|
||||
if (!isset($this->tableList[strtolower($table)])) {
|
||||
$this->transCreateTable($table, $options, $extensions);
|
||||
$this->alterationMessage("Table $table: created", "created");
|
||||
} else {
|
||||
if (Config::inst()->get('DBSchemaManager', 'check_and_repair_on_build')) {
|
||||
$this->checkAndRepairTable($table, $options);
|
||||
}
|
||||
|
||||
// Check if options changed
|
||||
$tableOptionsChanged = false;
|
||||
if (isset($options[get_class($this)]) || true) {
|
||||
if (isset($options[get_class($this)])) {
|
||||
if (preg_match('/ENGINE=([^\s]*)/', $options[get_class($this)], $alteredEngineMatches)) {
|
||||
$alteredEngine = $alteredEngineMatches[1];
|
||||
$tableStatus = $this->query(sprintf(
|
||||
'SHOW TABLE STATUS LIKE \'%s\'', $table
|
||||
))->first();
|
||||
$tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
|
||||
$this->transAlterTable($table, $options, $extensions);
|
||||
}
|
||||
}
|
||||
|
||||
//DB ABSTRACTION: we need to convert this to a db-specific version:
|
||||
$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
|
||||
|
||||
// Create custom fields
|
||||
if ($fieldSchema) {
|
||||
foreach ($fieldSchema as $fieldName => $fieldSpec) {
|
||||
|
||||
//Is this an array field?
|
||||
$arrayValue = '';
|
||||
if (strpos($fieldSpec, '[') !== false) {
|
||||
//If so, remove it and store that info separately
|
||||
$pos = strpos($fieldSpec, '[');
|
||||
$arrayValue = substr($fieldSpec, $pos);
|
||||
$fieldSpec = substr($fieldSpec, 0, $pos);
|
||||
}
|
||||
|
||||
$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
|
||||
$fieldObj->arrayValue = $arrayValue;
|
||||
|
||||
$fieldObj->setTable($table);
|
||||
$fieldObj->requireField();
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom indexes
|
||||
if ($indexSchema) {
|
||||
foreach ($indexSchema as $indexName => $indexDetails) {
|
||||
$this->requireIndex($table, $indexName, $indexDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
|
||||
* @param string $table The table name.
|
||||
*/
|
||||
public function dontRequireTable($table) {
|
||||
if (isset($this->tableList[strtolower($table)])) {
|
||||
$suffix = '';
|
||||
while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
|
||||
$suffix = $suffix
|
||||
? ($suffix + 1)
|
||||
: 2;
|
||||
}
|
||||
$this->renameTable($table, "_obsolete_{$table}$suffix");
|
||||
$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given index in the database, modifying whatever already exists as necessary.
|
||||
*
|
||||
* The keys of the array are the names of the index.
|
||||
* The values of the array can be one of:
|
||||
* - true: Create a single column index on the field named the same as the index.
|
||||
* - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
|
||||
* control over the index.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $index The index name.
|
||||
* @param string|array|boolean $spec The specification of the index in any
|
||||
* loose format. See requireTable() for more information.
|
||||
*/
|
||||
public function requireIndex($table, $index, $spec) {
|
||||
// Detect if adding to a new table
|
||||
$newTable = !isset($this->tableList[strtolower($table)]);
|
||||
|
||||
// Force spec into standard array format
|
||||
$spec = $this->parseIndexSpec($index, $spec);
|
||||
$specString = $this->convertIndexSpec($spec);
|
||||
|
||||
// Check existing index
|
||||
if (!$newTable) {
|
||||
$indexKey = $this->indexKey($table, $index, $spec);
|
||||
$indexList = $this->indexList($table);
|
||||
if (isset($indexList[$indexKey])) {
|
||||
|
||||
// $oldSpec should be in standard array format
|
||||
$oldSpec = $indexList[$indexKey];
|
||||
$oldSpecString = $this->convertIndexSpec($oldSpec);
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate either generation or modification of index
|
||||
if ($newTable || !isset($indexList[$indexKey])) {
|
||||
// New index
|
||||
$this->transCreateIndex($table, $index, $spec);
|
||||
$this->alterationMessage("Index $table.$index: created as $specString", "created");
|
||||
} else if ($oldSpecString != $specString) {
|
||||
// Updated index
|
||||
$this->transAlterIndex($table, $index, $spec);
|
||||
$this->alterationMessage(
|
||||
"Index $table.$index: changed to $specString <i style=\"color: #AAA\">(from $oldSpecString)</i>",
|
||||
"changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a spec string safely, considering quoted columns, whitespace,
|
||||
* and cleaning brackets
|
||||
*
|
||||
* @param string $spec The input index specification string
|
||||
* @return array List of columns in the spec
|
||||
*/
|
||||
protected function explodeColumnString($spec) {
|
||||
// Remove any leading/trailing brackets and outlying modifiers
|
||||
// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
|
||||
$containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
|
||||
|
||||
// Split potentially quoted modifiers
|
||||
// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
|
||||
return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a properly quoted column list from an array
|
||||
*
|
||||
* @param array $columns List of columns to implode
|
||||
* @return string A properly quoted list of column names
|
||||
*/
|
||||
protected function implodeColumnList($columns) {
|
||||
if(empty($columns)) return '';
|
||||
return '"' . implode('","', $columns) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index specification in the form of a string ensure that each
|
||||
* column name is property quoted, stripping brackets and modifiers.
|
||||
* This index may also be in the form of a "CREATE INDEX..." sql fragment
|
||||
*
|
||||
* @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
|
||||
* @return string The properly quoted column list. E.g. '"Column1", "Column2"'
|
||||
*/
|
||||
protected function quoteColumnSpecString($spec) {
|
||||
$bits = $this->explodeColumnString($spec);
|
||||
return $this->implodeColumnList($bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index spec determines the index type
|
||||
*
|
||||
* @param array|string $spec
|
||||
* @return string
|
||||
*/
|
||||
protected function determineIndexType($spec) {
|
||||
// check array spec
|
||||
if(is_array($spec) && isset($spec['type'])) {
|
||||
return $spec['type'];
|
||||
} elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
|
||||
return strtolower($matchType['type']);
|
||||
} else {
|
||||
return 'index';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array or string index spec into a universally useful array
|
||||
*
|
||||
* @see convertIndexSpec() for approximate inverse
|
||||
* @param string|array $spec
|
||||
* @return array The resulting spec array with the required fields name, type, and value
|
||||
*/
|
||||
protected function parseIndexSpec($name, $spec) {
|
||||
// Support $indexes = array('ColumnName' => true) for quick indexes
|
||||
if ($spec === true) {
|
||||
return array(
|
||||
'name' => $name,
|
||||
'value' => $this->quoteColumnSpecString($name),
|
||||
'type' => 'index'
|
||||
);
|
||||
}
|
||||
|
||||
// Do minimal cleanup on any already parsed spec
|
||||
if(is_array($spec)) {
|
||||
$spec['value'] = $this->quoteColumnSpecString($spec['value']);
|
||||
$spec['type'] = empty($spec['type']) ? 'index' : trim($spec['type']);
|
||||
return $spec;
|
||||
}
|
||||
|
||||
// Nicely formatted spec!
|
||||
return array(
|
||||
'name' => $name,
|
||||
'value' => $this->quoteColumnSpecString($spec),
|
||||
'type' => $this->determineIndexType($spec)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
|
||||
* and turns it into a proper string.
|
||||
* Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
|
||||
* arrays to be created. See {@link requireTable()} for details on the index format.
|
||||
*
|
||||
* @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
|
||||
* @see parseIndexSpec() for approximate inverse
|
||||
*
|
||||
* @param string|array $indexSpec
|
||||
*/
|
||||
protected function convertIndexSpec($indexSpec) {
|
||||
// Return already converted spec
|
||||
if (!is_array($indexSpec)) return $indexSpec;
|
||||
|
||||
// Combine elements into standard string format
|
||||
return "{$indexSpec['type']} ({$indexSpec['value']})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given table is exists in the current database
|
||||
*
|
||||
* @param string $table Name of table to check
|
||||
* @return boolean Flag indicating existence of table
|
||||
*/
|
||||
abstract public function hasTable($tableName);
|
||||
|
||||
/**
|
||||
* Return true if the table exists and already has a the field specified
|
||||
*
|
||||
* @param string $tableName - The table to check
|
||||
* @param string $fieldName - The field to check
|
||||
* @return bool - True if the table exists and the field exists on the table
|
||||
*/
|
||||
public function hasField($tableName, $fieldName) {
|
||||
if (!$this->hasTable($tableName)) return false;
|
||||
$fields = $this->fieldList($tableName);
|
||||
return array_key_exists($fieldName, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the given field on the table, modifying whatever already exists as necessary.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @param string $field The field name.
|
||||
* @param array|string $spec The field specification. If passed in array syntax, the specific database
|
||||
* driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
|
||||
* be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
|
||||
* need to take care of database abstraction in your DBField subclass.
|
||||
*/
|
||||
public function requireField($table, $field, $spec) {
|
||||
//TODO: this is starting to get extremely fragmented.
|
||||
//There are two different versions of $spec floating around, and their content changes depending
|
||||
//on how they are structured. This needs to be tidied up.
|
||||
$fieldValue = null;
|
||||
$newTable = false;
|
||||
|
||||
// backwards compatibility patch for pre 2.4 requireField() calls
|
||||
$spec_orig = $spec;
|
||||
|
||||
if (!is_string($spec)) {
|
||||
$spec['parts']['name'] = $field;
|
||||
$spec_orig['parts']['name'] = $field;
|
||||
//Convert the $spec array into a database-specific string
|
||||
$spec = $this->$spec['type']($spec['parts'], true);
|
||||
}
|
||||
|
||||
// Collations didn't come in until MySQL 4.1. Anything earlier will throw a syntax error if you try and use
|
||||
// collations.
|
||||
// TODO: move this to the MySQLDatabase file, or drop it altogether?
|
||||
if (!$this->database->supportsCollations()) {
|
||||
$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
|
||||
}
|
||||
|
||||
if (!isset($this->tableList[strtolower($table)])) $newTable = true;
|
||||
|
||||
if (is_array($spec)) {
|
||||
$specValue = $this->$spec_orig['type']($spec_orig['parts']);
|
||||
} else {
|
||||
$specValue = $spec;
|
||||
}
|
||||
|
||||
// We need to get db-specific versions of the ID column:
|
||||
if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
|
||||
$specValue = $this->IdColumn(true);
|
||||
}
|
||||
|
||||
if (!$newTable) {
|
||||
$fieldList = $this->fieldList($table);
|
||||
if (isset($fieldList[$field])) {
|
||||
if (is_array($fieldList[$field])) {
|
||||
$fieldValue = $fieldList[$field]['data_type'];
|
||||
} else {
|
||||
$fieldValue = $fieldList[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the version of the field as we would create it. This is used for comparison purposes to see if the
|
||||
// existing field is different to what we now want
|
||||
if (is_array($spec_orig)) {
|
||||
$spec_orig = $this->$spec_orig['type']($spec_orig['parts']);
|
||||
}
|
||||
|
||||
if ($newTable || $fieldValue == '') {
|
||||
$this->transCreateField($table, $field, $spec_orig);
|
||||
$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
|
||||
} else if ($fieldValue != $specValue) {
|
||||
// If enums/sets are being modified, then we need to fix existing data in the table.
|
||||
// Update any records where the enum is set to a legacy value to be set to the default.
|
||||
// One hard-coded exception is SiteTree - the default for this is Page.
|
||||
foreach (array('enum', 'set') as $enumtype) {
|
||||
if (preg_match("/^$enumtype/i", $specValue)) {
|
||||
$newStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $spec_orig);
|
||||
$new = preg_split("/'\s*,\s*'/", $newStr);
|
||||
|
||||
$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue);
|
||||
$old = preg_split("/'\s*,\s*'/", $newStr);
|
||||
|
||||
$holder = array();
|
||||
foreach ($old as $check) {
|
||||
if (!in_array($check, $new)) {
|
||||
$holder[] = $check;
|
||||
}
|
||||
}
|
||||
if (count($holder)) {
|
||||
$default = explode('default ', $spec_orig);
|
||||
$default = $default[1];
|
||||
if ($default == "'SiteTree'") $default = "'Page'";
|
||||
$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
|
||||
for ($i = 0; $i + 1 < count($holder); $i++) {
|
||||
$query .= "'{$holder[$i]}', ";
|
||||
}
|
||||
$query .= "'{$holder[$i]}')";
|
||||
$this->query($query);
|
||||
$amount = $this->database->affectedRows();
|
||||
$this->alterationMessage("Changed $amount rows to default value of field $field"
|
||||
. " (Value: $default)");
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->transAlterField($table, $field, $spec_orig);
|
||||
$this->alterationMessage(
|
||||
"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
|
||||
"changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $fieldName
|
||||
*/
|
||||
public function dontRequireField($table, $fieldName) {
|
||||
$fieldList = $this->fieldList($table);
|
||||
if (array_key_exists($fieldName, $fieldList)) {
|
||||
$suffix = '';
|
||||
while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
|
||||
$suffix = $suffix
|
||||
? ($suffix + 1)
|
||||
: 2;
|
||||
}
|
||||
$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
|
||||
$this->alterationMessage(
|
||||
"Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
|
||||
"obsolete"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a message about database alteration
|
||||
*
|
||||
* @param string $message to display
|
||||
* @param string $type one of [created|changed|repaired|obsolete|deleted|error]
|
||||
*/
|
||||
public function alterationMessage($message, $type = "") {
|
||||
if (!$this->supressOutput) {
|
||||
if (Director::is_cli()) {
|
||||
switch ($type) {
|
||||
case "created":
|
||||
case "changed":
|
||||
case "repaired":
|
||||
$sign = "+";
|
||||
break;
|
||||
case "obsolete":
|
||||
case "deleted":
|
||||
$sign = '-';
|
||||
break;
|
||||
case "notice":
|
||||
$sign = '*';
|
||||
break;
|
||||
case "error":
|
||||
$sign = "!";
|
||||
break;
|
||||
default:
|
||||
$sign = " ";
|
||||
}
|
||||
$message = strip_tags($message);
|
||||
echo " $sign $message\n";
|
||||
} else {
|
||||
switch ($type) {
|
||||
case "created":
|
||||
$color = "green";
|
||||
break;
|
||||
case "obsolete":
|
||||
$color = "red";
|
||||
break;
|
||||
case "notice":
|
||||
$color = "orange";
|
||||
break;
|
||||
case "error":
|
||||
$color = "red";
|
||||
break;
|
||||
case "deleted":
|
||||
$color = "red";
|
||||
break;
|
||||
case "changed":
|
||||
$color = "blue";
|
||||
break;
|
||||
case "repaired":
|
||||
$color = "blue";
|
||||
break;
|
||||
default:
|
||||
$color = "";
|
||||
}
|
||||
echo "<li style=\"color: $color\">$message</li>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the data type for the id column which is the primary key for each table
|
||||
*
|
||||
* @param boolean $asDbValue
|
||||
* @param boolean $hasAutoIncPK
|
||||
* @return string
|
||||
*/
|
||||
abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
|
||||
|
||||
/**
|
||||
* Checks a table's integrity and repairs it if necessary.
|
||||
*
|
||||
* @param string $tableName The name of the table.
|
||||
* @return boolean Return true if the table has integrity after the method is complete.
|
||||
*/
|
||||
abstract public function checkAndRepairTable($tableName);
|
||||
|
||||
/**
|
||||
* Returns the values of the given enum field
|
||||
*
|
||||
* @param string $tableName Name of table to check
|
||||
* @param string $fieldName name of enum field to check
|
||||
* @return array List of enum values
|
||||
*/
|
||||
abstract public function enumValuesForField($tableName, $fieldName);
|
||||
|
||||
|
||||
/*
|
||||
* This is a lookup table for data types.
|
||||
* For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
|
||||
* So this is a DB-specific list of equivilents.
|
||||
*
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
abstract public function dbDataType($type);
|
||||
|
||||
/**
|
||||
* Retrieves the list of all databases the user has access to
|
||||
*
|
||||
* @return array List of database names
|
||||
*/
|
||||
abstract public function databaseList();
|
||||
|
||||
/**
|
||||
* Determine if the database with the specified name exists
|
||||
*
|
||||
* @param string $name Name of the database to check for
|
||||
* @return boolean Flag indicating whether this database exists
|
||||
*/
|
||||
abstract public function databaseExists($name);
|
||||
|
||||
/**
|
||||
* Create a database with the specified name
|
||||
*
|
||||
* @param string $name Name of the database to create
|
||||
* @return boolean True if successful
|
||||
*/
|
||||
abstract public function createDatabase($name);
|
||||
|
||||
/**
|
||||
* Drops a database with the specified name
|
||||
*
|
||||
* @param string $name Name of the database to drop
|
||||
*/
|
||||
abstract public function dropDatabase($name);
|
||||
|
||||
/**
|
||||
* Alter an index on a table.
|
||||
*
|
||||
* @param string $tableName The name of the table.
|
||||
* @param string $indexName The name of the index.
|
||||
* @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
|
||||
* for more details.
|
||||
* @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
|
||||
*/
|
||||
abstract public function alterIndex($tableName, $indexName, $indexSpec);
|
||||
|
||||
/**
|
||||
* Determines the key that should be used to identify this index
|
||||
* when retrieved from DBSchemaManager->indexList.
|
||||
* In some connectors this is the database-visible name, in others the
|
||||
* usercode-visible name.
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $index
|
||||
* @param array $spec
|
||||
* @return string Key for this index
|
||||
*/
|
||||
abstract protected function indexKey($table, $index, $spec);
|
||||
|
||||
/**
|
||||
* Return the list of indexes in a table.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @return array[array] List of current indexes in the table, each in standard
|
||||
* array form. The key for this array should be predictable using the indexKey
|
||||
* method
|
||||
*/
|
||||
abstract public function indexList($table);
|
||||
|
||||
/**
|
||||
* Returns a list of all tables in the database.
|
||||
* Keys are table names in lower case, values are table names in case that
|
||||
* database expects.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function tableList();
|
||||
|
||||
/**
|
||||
* Create a new table.
|
||||
*
|
||||
* @param string $table The name of the table
|
||||
* @param array $fields A map of field names to field types
|
||||
* @param array $indexes A map of indexes
|
||||
* @param array $options An map of additional options. The available keys are as follows:
|
||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
||||
* - 'temporary' - If true, then a temporary table will be created
|
||||
* @param $advancedOptions Advanced creation options
|
||||
* @return string The table name generated. This may be different from the table name, for example with temporary
|
||||
* tables.
|
||||
*/
|
||||
abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
|
||||
$advancedOptions = null);
|
||||
|
||||
/**
|
||||
* Alter a table's schema.
|
||||
*
|
||||
* @param string $table The name of the table to alter
|
||||
* @param array $newFields New fields, a map of field name => field schema
|
||||
* @param array $newIndexes New indexes, a map of index name => index type
|
||||
* @param array $alteredFields Updated fields, a map of field name => field schema
|
||||
* @param array $alteredIndexes Updated indexes, a map of index name => index type
|
||||
* @param array $alteredOptions
|
||||
* @param array $advancedOptions
|
||||
*/
|
||||
abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
|
||||
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null);
|
||||
|
||||
/**
|
||||
* Rename a table.
|
||||
*
|
||||
* @param string $oldTableName The old table name.
|
||||
* @param string $newTableName The new table name.
|
||||
*/
|
||||
abstract public function renameTable($oldTableName, $newTableName);
|
||||
|
||||
/**
|
||||
* Create a new field on a table.
|
||||
*
|
||||
* @param string $table Name of the table.
|
||||
* @param string $field Name of the field to add.
|
||||
* @param string $spec The field specification, eg 'INTEGER NOT NULL'
|
||||
*/
|
||||
abstract public function createField($table, $field, $spec);
|
||||
|
||||
/**
|
||||
* Change the database column name of the given field.
|
||||
*
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $oldName The name of the field to change.
|
||||
* @param string $newName The new name of the field
|
||||
*/
|
||||
abstract public function renameField($tableName, $oldName, $newName);
|
||||
|
||||
/**
|
||||
* Get a list of all the fields for the given table.
|
||||
* Returns a map of field name => field spec.
|
||||
*
|
||||
* @param string $table The table name.
|
||||
* @return array
|
||||
*/
|
||||
abstract public function fieldList($table);
|
||||
|
||||
/**
|
||||
*
|
||||
* This allows the cached values for a table's field list to be erased.
|
||||
* If $tablename is empty, then the whole cache is erased.
|
||||
*
|
||||
* @param string $tableName
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function clearCachedFieldlist($tableName = false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
908
model/connect/Database.php
Normal file
908
model/connect/Database.php
Normal file
@ -0,0 +1,908 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract database connectivity class.
|
||||
* Sub-classes of this implement the actual database connection libraries
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class SS_Database {
|
||||
|
||||
/**
|
||||
* Database connector object
|
||||
*
|
||||
* @var DBConnector
|
||||
*/
|
||||
protected $connector = null;
|
||||
|
||||
/**
|
||||
* Get the current connector
|
||||
*
|
||||
* @return DBConnector
|
||||
*/
|
||||
public function getConnector() {
|
||||
return $this->connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injector injection point for connector dependency
|
||||
*
|
||||
* @param DBConnector $connector
|
||||
*/
|
||||
public function setConnector(DBConnector $connector) {
|
||||
$this->connector = $connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database schema manager object
|
||||
*
|
||||
* @var DBSchemaManager
|
||||
*/
|
||||
protected $schemaManager = null;
|
||||
|
||||
/**
|
||||
* Returns the current schema manager
|
||||
*
|
||||
* @return DBSchemaManager
|
||||
*/
|
||||
public function getSchemaManager() {
|
||||
return $this->schemaManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injector injection point for schema manager
|
||||
*
|
||||
* @param DBSchemaManager $schemaManager
|
||||
*/
|
||||
public function setSchemaManager(DBSchemaManager $schemaManager) {
|
||||
$this->schemaManager = $schemaManager;
|
||||
|
||||
if ($this->schemaManager) {
|
||||
$this->schemaManager->setDatabase($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query builder object
|
||||
*
|
||||
* @var DBQueryBuilder
|
||||
*/
|
||||
protected $queryBuilder = null;
|
||||
|
||||
/**
|
||||
* Returns the current query builder
|
||||
*
|
||||
* @return DBQueryBuilder
|
||||
*/
|
||||
public function getQueryBuilder() {
|
||||
return $this->queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injector injection point for schema manager
|
||||
*
|
||||
* @param DBQueryBuilder $queryBuilder
|
||||
*/
|
||||
public function setQueryBuilder(DBQueryBuilder $queryBuilder) {
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given SQL query.
|
||||
*
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
return $this->connector->query($sql, $errorLevel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the given SQL parameterised query with the specified arguments
|
||||
*
|
||||
* @param string $sql The SQL query to execute. The ? character will denote parameters.
|
||||
* @param array $parameters An ordered list of arguments.
|
||||
* @param int $errorLevel The level of error reporting to enable for the query
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||
return $this->connector->preparedQuery($sql, $parameters, $errorLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the autogenerated ID from the previous INSERT query.
|
||||
*
|
||||
* @param string $table The name of the table to get the generated ID for
|
||||
* @return integer the most recently generated ID for the specified table
|
||||
*/
|
||||
public function getGeneratedID($table) {
|
||||
return $this->connector->getGeneratedID($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if we are connected to a server AND have a valid database
|
||||
* selected.
|
||||
*
|
||||
* @return boolean Flag indicating that a valid database is connected
|
||||
*/
|
||||
public function isActive() {
|
||||
return $this->connector->isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an escaped string. This string won't be quoted, so would be suitable
|
||||
* for appending to other quoted strings.
|
||||
*
|
||||
* @param mixed $value Value to be prepared for database query
|
||||
* @return string Prepared string
|
||||
*/
|
||||
public function escapeString($value) {
|
||||
return $this->connector->escapeString($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string into DB-specific quotes.
|
||||
*
|
||||
* @param mixed $value Value to be prepared for database query
|
||||
* @return string Prepared string
|
||||
*/
|
||||
public function quoteString($value) {
|
||||
return $this->connector->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 = '.') {
|
||||
return $this->connector->escapeIdentifier($value, $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes unquoted columns keys in an associative array
|
||||
*
|
||||
* @param array $fieldValues
|
||||
* @return array List of field values with the keys as escaped column names
|
||||
*/
|
||||
protected function escapeColumnKeys($fieldValues) {
|
||||
$out = array();
|
||||
foreach($fieldValues as $field => $value) {
|
||||
$out[$this->escapeIdentifier($field)] = $value;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a complex manipulation on the database.
|
||||
* A manipulation is an array of insert / or update sequences. The keys of the array are table names,
|
||||
* and the values are map containing 'command' and 'fields'. Command should be 'insert' or 'update',
|
||||
* and fields should be a map of field names to field values, NOT including quotes.
|
||||
*
|
||||
* The field values could also be in paramaterised format, such as
|
||||
* array('MAX(?,?)' => array(42, 69)), allowing the use of raw SQL values such as
|
||||
* array('NOW()' => array()).
|
||||
*
|
||||
* @see SQLWriteExpression::addAssignments for syntax examples
|
||||
*
|
||||
* @param array $manipulation
|
||||
*/
|
||||
public function manipulate($manipulation) {
|
||||
if (empty($manipulation)) return;
|
||||
|
||||
foreach ($manipulation as $table => $writeInfo) {
|
||||
if(empty($writeInfo['fields'])) continue;
|
||||
// Note: keys of $fieldValues are not escaped
|
||||
$fieldValues = $writeInfo['fields'];
|
||||
|
||||
// Switch command type
|
||||
switch ($writeInfo['command']) {
|
||||
case "update":
|
||||
|
||||
// Build update
|
||||
$query = new SQLUpdate("\"$table\"", $this->escapeColumnKeys($fieldValues));
|
||||
|
||||
// Set best condition to use
|
||||
if(!empty($writeInfo['where'])) {
|
||||
$query->addWhere($writeInfo['where']);
|
||||
} elseif(!empty($writeInfo['id'])) {
|
||||
$query->addWhere(array('"ID"' => $writeInfo['id']));
|
||||
}
|
||||
|
||||
// Test to see if this update query shouldn't, in fact, be an insert
|
||||
if($query->toSelect()->count()) {
|
||||
$query->execute();
|
||||
break;
|
||||
}
|
||||
// ...if not, we'll skip on to the insert code
|
||||
|
||||
case "insert":
|
||||
// Ensure that the ID clause is given if possible
|
||||
if (!isset($fieldValues['ID']) && isset($writeInfo['id'])) {
|
||||
$fieldValues['ID'] = $writeInfo['id'];
|
||||
}
|
||||
|
||||
// Build insert
|
||||
$query = new SQLInsert("\"$table\"", $this->escapeColumnKeys($fieldValues));
|
||||
|
||||
$query->execute();
|
||||
break;
|
||||
|
||||
default:
|
||||
user_error("SS_Database::manipulate() Can't recognise command '{$writeInfo['command']}'",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable supression of database messages.
|
||||
*/
|
||||
public function quiet() {
|
||||
$this->schemaManager->quiet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data out of the database
|
||||
*/
|
||||
public function clearAllData() {
|
||||
$tables = $this->getSchemaManager()->tableList();
|
||||
foreach ($tables as $table) {
|
||||
$this->clearTable($table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all data in a given table
|
||||
*
|
||||
* @param string $table Name of table
|
||||
*/
|
||||
public function clearTable($table) {
|
||||
$this->query("TRUNCATE \"$table\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a WHERE clause for text matching.
|
||||
*
|
||||
* @param String $field Quoted field name
|
||||
* @param String $value Escaped search. Can include percentage wildcards.
|
||||
* Ignored if $parameterised is true.
|
||||
* @param boolean $exact Exact matches or wildcard support.
|
||||
* @param boolean $negate Negate the clause.
|
||||
* @param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE.
|
||||
* Fallback to default collation if set to NULL.
|
||||
* @param boolean $parameterised Insert the ? placeholder rather than the
|
||||
* given value. If this is true then $value is ignored.
|
||||
* @return String SQL
|
||||
*/
|
||||
abstract public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
|
||||
$parameterised = false);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime in a certain format
|
||||
*
|
||||
* @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or
|
||||
* field name, e.g. '"SiteTree"."Created"'
|
||||
* @param string $format to be used, supported specifiers:
|
||||
* %Y = Year (four digits)
|
||||
* %m = Month (01..12)
|
||||
* %d = Day (01..31)
|
||||
* %H = Hour (00..23)
|
||||
* %i = Minutes (00..59)
|
||||
* %s = Seconds (00..59)
|
||||
* %U = unix timestamp, can only be used on it's own
|
||||
* @return string SQL datetime expression to query for a formatted datetime
|
||||
*/
|
||||
abstract public function formattedDatetimeClause($date, $format);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime addition
|
||||
*
|
||||
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes,
|
||||
* +1 YEAR
|
||||
* supported qualifiers:
|
||||
* - years
|
||||
* - months
|
||||
* - days
|
||||
* - hours
|
||||
* - minutes
|
||||
* - seconds
|
||||
* This includes the singular forms as well
|
||||
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of
|
||||
* the addition
|
||||
*/
|
||||
abstract public function datetimeIntervalClause($date, $interval);
|
||||
|
||||
/**
|
||||
* function to return an SQL datetime expression that can be used with the adapter in use
|
||||
* used for querying a datetime substraction
|
||||
*
|
||||
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name
|
||||
* e.g. '"SiteTree"."Created"'
|
||||
* @param string $date2 to be substracted of $date1, can be either 'now', literal datetime
|
||||
* like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
||||
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which
|
||||
* is the result of the substraction
|
||||
*/
|
||||
abstract public function datetimeDifferenceClause($date1, $date2);
|
||||
|
||||
/**
|
||||
* Returns true if this database supports collations
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function supportsCollations();
|
||||
|
||||
/**
|
||||
* Can the database override timezone as a connection setting,
|
||||
* or does it use the system timezone exclusively?
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
abstract public function supportsTimezoneOverride();
|
||||
|
||||
/**
|
||||
* Query for the version of the currently connected database
|
||||
* @return string Version of this database
|
||||
*/
|
||||
public function getVersion() {
|
||||
return $this->connector->getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database server type (e.g. mysql, postgresql).
|
||||
* This value is passed to the connector as the 'driver' argument when
|
||||
* initiating a database connection
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getDatabaseServer();
|
||||
|
||||
/**
|
||||
* Return the number of rows affected by the previous operation.
|
||||
* @return int
|
||||
*/
|
||||
public function affectedRows() {
|
||||
return $this->connector->affectedRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* The core search engine, used by this class and its subclasses to do fun stuff.
|
||||
* Searches both SiteTree and File.
|
||||
*
|
||||
* @param array $classesToSearch List of classes to search
|
||||
* @param string $keywords Keywords as a string.
|
||||
* @param integer $start Item to start returning results from
|
||||
* @param integer $pageLength Number of items per page
|
||||
* @param string $sortBy Sort order expression
|
||||
* @param string $extraFilter Additional filter
|
||||
* @param boolean $booleanSearch Flag for boolean search mode
|
||||
* @param string $alternativeFileFilter
|
||||
* @param boolean $invertedMatch
|
||||
* @return PaginatedList Search results
|
||||
*/
|
||||
abstract public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
|
||||
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false);
|
||||
|
||||
/**
|
||||
* Determines if this database supports transactions
|
||||
*
|
||||
* @return boolean Flag indicating support for transactions
|
||||
*/
|
||||
abstract public function supportsTransactions();
|
||||
|
||||
/*
|
||||
* Determines if the current database connection supports a given list of extensions
|
||||
*
|
||||
* @param array $extensions List of extensions to check for support of. The key of this array
|
||||
* will be an extension name, and the value the configuration for that extension. This
|
||||
* could be one of partitions, tablespaces, or clustering
|
||||
* @return boolean Flag indicating support for all of the above
|
||||
* @todo Write test cases
|
||||
*/
|
||||
protected function supportsExtensions($extensions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a prepared transaction
|
||||
* See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on
|
||||
* transaction isolation options
|
||||
*
|
||||
* @param string|boolean $transactionMode Transaction mode, or false to ignore
|
||||
* @param string|boolean $sessionCharacteristics Session characteristics, or false to ignore
|
||||
*/
|
||||
abstract public function transactionStart($transactionMode = false, $sessionCharacteristics = false);
|
||||
|
||||
/**
|
||||
* Create a savepoint that you can jump back to if you encounter problems
|
||||
*
|
||||
* @param string $savepoint Name of savepoint
|
||||
*/
|
||||
abstract public function transactionSavepoint($savepoint);
|
||||
|
||||
/**
|
||||
* Rollback or revert to a savepoint if your queries encounter problems
|
||||
* If you encounter a problem at any point during a transaction, you may
|
||||
* need to rollback that particular query, or return to a savepoint
|
||||
*
|
||||
* @param string|boolean $savepoint Name of savepoint, or leave empty to rollback
|
||||
* to last savepoint
|
||||
*/
|
||||
abstract public function transactionRollback($savepoint = false);
|
||||
|
||||
/**
|
||||
* Commit everything inside this transaction so far
|
||||
*
|
||||
* @param boolean $chain
|
||||
*/
|
||||
abstract public function transactionEnd($chain = false);
|
||||
|
||||
/**
|
||||
* Determines if the used database supports application-level locks,
|
||||
* which is different from table- or row-level locking.
|
||||
* See {@link getLock()} for details.
|
||||
*
|
||||
* @return boolean Flag indicating that locking is available
|
||||
*/
|
||||
public function supportsLocks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the lock is available.
|
||||
* See {@link supportsLocks()} to check if locking is generally supported.
|
||||
*
|
||||
* @param string $name Name of the lock
|
||||
* @return boolean
|
||||
*/
|
||||
public function canLock($name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an application-level lock so that no two processes can run at the same time,
|
||||
* also called a "cooperative advisory lock".
|
||||
*
|
||||
* Return FALSE if acquiring the lock fails; otherwise return TRUE, if lock was acquired successfully.
|
||||
* Lock is automatically released if connection to the database is broken (either normally or abnormally),
|
||||
* making it less prone to deadlocks than session- or file-based locks.
|
||||
* Should be accompanied by a {@link releaseLock()} call after the logic requiring the lock has completed.
|
||||
* Can be called multiple times, in which case locks "stack" (PostgreSQL, SQL Server),
|
||||
* or auto-releases the previous lock (MySQL).
|
||||
*
|
||||
* Note that this might trigger the database to wait for the lock to be released, delaying further execution.
|
||||
*
|
||||
* @param string $name Name of lock
|
||||
* @param integer $timeout Timeout in seconds
|
||||
* @return boolean
|
||||
*/
|
||||
public function getLock($name, $timeout = 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an application-level lock file to allow another process to run
|
||||
* (if the execution aborts (e.g. due to an error) all locks are automatically released).
|
||||
*
|
||||
* @param string $name Name of the lock
|
||||
* @return boolean Flag indicating whether the lock was successfully released
|
||||
*/
|
||||
public function releaseLock($name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the database to generate a live connection
|
||||
*
|
||||
* @param array $parameters An map of parameters, which should include:
|
||||
* - server: The server, eg, localhost
|
||||
* - username: The username to log on with
|
||||
* - password: The password to log on with
|
||||
* - database: The database to connect to
|
||||
* - charset: The character set to use. Defaults to utf8
|
||||
* - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
|
||||
* - driver: (optional) Driver name
|
||||
*/
|
||||
public function connect($parameters) {
|
||||
// Ensure that driver is available (required by PDO)
|
||||
if(empty($parameters['driver'])) {
|
||||
$parameters['driver'] = $this->getDatabaseServer();
|
||||
}
|
||||
|
||||
// Notify connector of parameters
|
||||
$this->connector->connect($parameters);
|
||||
|
||||
// SS_Database subclass maintains responsibility for selecting database
|
||||
// once connected in order to correctly handle schema queries about
|
||||
// existence of database, error handling at the correct level, etc
|
||||
if (!empty($parameters['database'])) {
|
||||
$this->selectDatabase($parameters['database'], false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the database with the specified name exists
|
||||
*
|
||||
* @param string $name Name of the database to check for
|
||||
* @return boolean Flag indicating whether this database exists
|
||||
*/
|
||||
public function databaseExists($name) {
|
||||
return $this->schemaManager->databaseExists($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of all databases the user has access to
|
||||
*
|
||||
* @return array List of database names
|
||||
*/
|
||||
public function databaseList() {
|
||||
return $this->schemaManager->databaseList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the connection to the specified database, optionally creating the
|
||||
* database if it doesn't exist in the current schema.
|
||||
*
|
||||
* @param string $name Name of the database
|
||||
* @param boolean $create Flag indicating whether the database should be created
|
||||
* if it doesn't exist. If $create is false and the database doesn't exist
|
||||
* then an error will be raised
|
||||
* @param int|boolean $errorLevel The level of error reporting to enable for the query, or false if no error
|
||||
* should be raised
|
||||
* @return boolean Flag indicating success
|
||||
*/
|
||||
public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) {
|
||||
if (!$this->schemaManager->databaseExists($name)) {
|
||||
// Check DB creation permisson
|
||||
if (!$create) {
|
||||
if ($errorLevel !== false) {
|
||||
user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
|
||||
}
|
||||
// Unselect database
|
||||
$this->connector->unloadDatabase();
|
||||
return false;
|
||||
}
|
||||
$this->schemaManager->createDatabase($name);
|
||||
}
|
||||
return $this->connector->selectDatabase($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the database that this object is currently connected to.
|
||||
* Use with caution.
|
||||
*/
|
||||
public function dropSelectedDatabase() {
|
||||
$databaseName = $this->connector->getSelectedDatabase();
|
||||
if ($databaseName) {
|
||||
$this->connector->unloadDatabase();
|
||||
$this->schemaManager->dropDatabase($databaseName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the currently selected database
|
||||
*
|
||||
* @return string|null Name of the selected database, or null if none selected
|
||||
*/
|
||||
public function getSelectedDatabase() {
|
||||
return $this->connector->getSelectedDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SQL expression used to represent the current date/time
|
||||
*
|
||||
* @return string Expression for the current date/time
|
||||
*/
|
||||
abstract public function now();
|
||||
|
||||
/**
|
||||
* Returns the database-specific version of the random() function
|
||||
*
|
||||
* @return string Expression for a random value
|
||||
*/
|
||||
abstract public function random();
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->dbDataType($type) instead
|
||||
*/
|
||||
public function dbDataType($type){
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->dbDataType($type) instead');
|
||||
return $this->getSchemaManager()->dbDataType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use selectDatabase('dbname', true) instead
|
||||
*/
|
||||
public function createDatabase() {
|
||||
Deprecation::notice('3.3', 'Use selectDatabase(\'dbname\',true) instead');
|
||||
$database = $this->connector->getSelectedDatabase();
|
||||
$this->selectDatabase($database, true);
|
||||
return $this->isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 SS_Database::getConnect was never implemented and is obsolete
|
||||
*/
|
||||
public function getConnect($parameters) {
|
||||
Deprecation::notice('3.3', 'SS_Database::getConnect was never implemented and is obsolete');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use Convert::raw2sql($string, true) instead
|
||||
*/
|
||||
public function prepStringForDB($string) {
|
||||
Deprecation::notice('3.3', 'Use Convert::raw2sql($string, true) instead');
|
||||
return $this->quoteString($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use dropSelectedDatabase instead
|
||||
*/
|
||||
public function dropDatabase() {
|
||||
Deprecation::notice('3.3', 'Use dropSelectedDatabase instead');
|
||||
$this->dropSelectedDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use databaseList instead
|
||||
*/
|
||||
public function allDatabaseNames() {
|
||||
Deprecation::notice('3.3', 'Use databaseList instead');
|
||||
return $this->databaseList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::create_table instead
|
||||
*/
|
||||
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
||||
Deprecation::notice('3.3', 'Use DB::create_table instead');
|
||||
return $this->getSchemaManager()->createTable($table, $fields, $indexes, $options, $advancedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->alterTable() instead
|
||||
*/
|
||||
public function alterTable($table, $newFields = null, $newIndexes = null,
|
||||
$alteredFields = null, $alteredIndexes = null, $alteredOptions = null,
|
||||
$advancedOptions = null
|
||||
) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->alterTable() instead');
|
||||
return $this->getSchemaManager()->alterTable(
|
||||
$table, $newFields, $newIndexes, $alteredFields,
|
||||
$alteredIndexes, $alteredOptions, $advancedOptions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->renameTable() instead
|
||||
*/
|
||||
public function renameTable($oldTableName, $newTableName) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->renameTable() instead');
|
||||
$this->getSchemaManager()->renameTable($oldTableName, $newTableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::create_field() instead
|
||||
*/
|
||||
public function createField($table, $field, $spec) {
|
||||
Deprecation::notice('3.3', 'Use DB::create_field() instead');
|
||||
$this->getSchemaManager()->createField($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->renameField() instead
|
||||
*/
|
||||
public function renameField($tableName, $oldName, $newName) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->renameField() instead');
|
||||
$this->getSchemaManager()->renameField($tableName, $oldName, $newName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use getSelectedDatabase instead
|
||||
*/
|
||||
public function currentDatabase() {
|
||||
Deprecation::notice('3.3', 'Use getSelectedDatabase instead');
|
||||
return $this->getSelectedDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::field_list instead
|
||||
*/
|
||||
public function fieldList($table) {
|
||||
Deprecation::notice('3.3', 'Use DB::field_list instead');
|
||||
return $this->getSchemaManager()->fieldList($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::table_list instead
|
||||
*/
|
||||
public function tableList() {
|
||||
Deprecation::notice('3.3', 'Use DB::table_list instead');
|
||||
return $this->getSchemaManager()->tableList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->hasTable() instead
|
||||
*/
|
||||
public function hasTable($tableName) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->hasTable() instead');
|
||||
return $this->getSchemaManager()->hasTable($tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->enumValuesForField() instead
|
||||
*/
|
||||
public function enumValuesForField($tableName, $fieldName) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->enumValuesForField() instead');
|
||||
return $this->getSchemaManager()->enumValuesForField($tableName, $fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use Convert::raw2sql instead
|
||||
*/
|
||||
public function addslashes($value) {
|
||||
Deprecation::notice('3.3', 'Use Convert::raw2sql instead');
|
||||
return $this->escapeString($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2 Use DB::get_schema()->schemaUpdate with a callback instead
|
||||
*/
|
||||
public function beginSchemaUpdate() {
|
||||
Deprecation::notice('3.2', 'Use DB::get_schema()->schemaUpdate with a callback instead');
|
||||
// Unable to recover so throw appropriate exception
|
||||
throw new BadMethodCallException('Use DB::get_schema()->schemaUpdate with a callback instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2 Use DB::get_schema()->schemaUpdate with a callback instead
|
||||
*/
|
||||
public function endSchemaUpdate() {
|
||||
Deprecation::notice('3.2', 'Use DB::get_schema()->schemaUpdate with a callback instead');
|
||||
// Unable to recover so throw appropriate exception
|
||||
throw new BadMethodCallException('Use DB::get_schema()->schemaUpdate with a callback instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->cancelSchemaUpdate instead
|
||||
*/
|
||||
public function cancelSchemaUpdate() {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->cancelSchemaUpdate instead');
|
||||
$this->getSchemaManager()->cancelSchemaUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->isSchemaUpdating() instead
|
||||
*/
|
||||
public function isSchemaUpdating() {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->isSchemaUpdating() instead');
|
||||
return $this->getSchemaManager()->isSchemaUpdating();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->doesSchemaNeedUpdating() instead
|
||||
*/
|
||||
public function doesSchemaNeedUpdating() {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->doesSchemaNeedUpdating() instead');
|
||||
return $this->getSchemaManager()->doesSchemaNeedUpdating();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transCreateTable() instead
|
||||
*/
|
||||
public function transCreateTable($table, $options = null, $advanced_options = null) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transCreateTable() instead');
|
||||
$this->getSchemaManager()->transCreateTable($table, $options, $advanced_options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transAlterTable() instead
|
||||
*/
|
||||
public function transAlterTable($table, $options, $advanced_options) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transAlterTable() instead');
|
||||
$this->getSchemaManager()->transAlterTable($table, $index, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transCreateField() instead
|
||||
*/
|
||||
public function transCreateField($table, $field, $schema) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transCreateField() instead');
|
||||
$this->getSchemaManager()->transCreateField($table, $index, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transCreateIndex() instead
|
||||
*/
|
||||
public function transCreateIndex($table, $index, $schema) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transCreateIndex() instead');
|
||||
$this->getSchemaManager()->transCreateIndex($table, $index, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transAlterField() instead
|
||||
*/
|
||||
public function transAlterField($table, $field, $schema) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transAlterField() instead');
|
||||
$this->getSchemaManager()->transAlterField($table, $index, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->transAlterIndex() instead
|
||||
*/
|
||||
public function transAlterIndex($table, $index, $schema) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->transAlterIndex() instead');
|
||||
$this->getSchemaManager()->transAlterIndex($table, $index, $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_table() instead
|
||||
*/
|
||||
public function requireTable($table, $fieldSchema = null, $indexSchema = null,
|
||||
$hasAutoIncPK = true, $options = array(), $extensions = false
|
||||
) {
|
||||
Deprecation::notice('3.3', 'Use DB::require_table() instead');
|
||||
return $this->getSchemaManager()->requireTable(
|
||||
$table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::dont_require_table() instead
|
||||
*/
|
||||
public function dontRequireTable($table) {
|
||||
Deprecation::notice('3.3', 'Use DB::dont_require_table() instead');
|
||||
$this->getSchemaManager()->dontRequireTable($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_index() instead
|
||||
*/
|
||||
public function requireIndex($table, $index, $spec) {
|
||||
Deprecation::notice('3.3', 'Use DB::require_index() instead');
|
||||
$this->getSchemaManager()->requireIndex($table, $index, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::get_schema()->hasField() instead
|
||||
*/
|
||||
public function hasField($tableName, $fieldName) {
|
||||
Deprecation::notice('3.3', 'Use DB::get_schema()->hasField() instead');
|
||||
return $this->getSchemaManager()->hasField($tableName, $fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::require_field() instead
|
||||
*/
|
||||
public function requireField($table, $field, $spec) {
|
||||
Deprecation::notice('3.3', 'Use DB::require_field() instead');
|
||||
$this->getSchemaManager()->requireField($table, $field, $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::dont_require_field() instead
|
||||
*/
|
||||
public function dontRequireField($table, $fieldName) {
|
||||
Deprecation::notice('3.3', 'Use DB::dont_require_field() instead');
|
||||
$this->getSchemaManager()->dontRequireField($table, $fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3 Use DB::build_sql() instead
|
||||
*/
|
||||
public function sqlQueryToString(SQLExpression $query, &$parameters = array()) {
|
||||
Deprecation::notice('3.3', 'Use DB::build_sql() instead');
|
||||
return $this->getQueryBuilder()->buildSQL($query, $parameters);
|
||||
}
|
||||
}
|
57
model/connect/DatabaseException.php
Normal file
57
model/connect/DatabaseException.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Error class for database exceptions
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SS_DatabaseException extends Exception {
|
||||
|
||||
/**
|
||||
* The SQL that generated this error
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $sql = null;
|
||||
|
||||
/**
|
||||
* The parameters given for this query, if any
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $parameters = array();
|
||||
|
||||
/**
|
||||
* Returns the SQL that generated this error
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSQL() {
|
||||
return $this->sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters given for this query, if any
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParameters() {
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the database exception
|
||||
*
|
||||
* @param string $message The Exception message to throw.
|
||||
* @param integer $code The Exception code.
|
||||
* @param Exception $previous The previous exception used for the exception chaining.
|
||||
* @param string $sql The SQL executed for this query
|
||||
* @param array $parameters The parameters given for this query, if any
|
||||
*/
|
||||
function __construct($message = '', $code = 0, $previous = null, $sql = null, $parameters = array()) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->sql = $sql;
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
}
|
362
model/connect/MySQLDatabase.php
Normal file
362
model/connect/MySQLDatabase.php
Normal file
@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MySQL connector class.
|
||||
*
|
||||
* Supported indexes for {@link requireTable()}:
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLDatabase extends SS_Database {
|
||||
|
||||
/**
|
||||
* Default connection charset (may be overridden in $databaseConfig)
|
||||
*
|
||||
* @config
|
||||
* @var String
|
||||
*/
|
||||
private static $connection_charset = null;
|
||||
|
||||
public function connect($parameters) {
|
||||
// Ensure that driver is available (required by PDO)
|
||||
if(empty($parameters['driver'])) {
|
||||
$parameters['driver'] = $this->getDatabaseServer();
|
||||
}
|
||||
|
||||
// Set charset
|
||||
if( empty($parameters['charset'])
|
||||
&& ($charset = Config::inst()->get('MySQLDatabase', 'connection_charset'))
|
||||
) {
|
||||
$parameters['charset'] = $charset;
|
||||
}
|
||||
|
||||
// Notify connector of parameters
|
||||
$this->connector->connect($parameters);
|
||||
|
||||
// This is important!
|
||||
$this->setSQLMode('ANSI');
|
||||
|
||||
if (isset($parameters['timezone'])) {
|
||||
$this->selectTimezone($parameters['timezone']);
|
||||
}
|
||||
|
||||
// SS_Database subclass maintains responsibility for selecting database
|
||||
// once connected in order to correctly handle schema queries about
|
||||
// existence of database, error handling at the correct level, etc
|
||||
if (!empty($parameters['database'])) {
|
||||
$this->selectDatabase($parameters['database'], false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the character set for the MySQL database connection.
|
||||
*
|
||||
* The character set connection should be set to 'utf8' for SilverStripe version 2.4.0 and
|
||||
* later.
|
||||
*
|
||||
* However, sites created before version 2.4.0 should leave this unset or data that isn't 7-bit
|
||||
* safe will be corrupted. As such, the installer comes with this set in mysite/_config.php by
|
||||
* default in versions 2.4.0 and later.
|
||||
*
|
||||
* @deprecated 3.2 Use "MySQLDatabase.connection_charset" config setting instead
|
||||
*/
|
||||
public static function set_connection_charset($charset = 'utf8') {
|
||||
Deprecation::notice('3.1', 'Use "MySQLDatabase.connection_charset" config setting instead');
|
||||
Config::inst()->update('MySQLDatabase', 'connection_charset', $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SQL mode
|
||||
*
|
||||
* @param string $mode Connection mode
|
||||
*/
|
||||
public function setSQLMode($mode) {
|
||||
if (empty($mode)) return;
|
||||
$this->preparedQuery("SET sql_mode = ?", array($mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system timezone for the database connection
|
||||
*
|
||||
* @param string $timezone
|
||||
*/
|
||||
public function selectTimezone($timezone) {
|
||||
if (empty($timezone)) return;
|
||||
$this->preparedQuery("SET SESSION time_zone = ?", array($timezone));
|
||||
}
|
||||
|
||||
public function supportsCollations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function supportsTimezoneOverride() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getDatabaseServer() {
|
||||
return "mysql";
|
||||
}
|
||||
|
||||
/**
|
||||
* The core search engine, used by this class and its subclasses to do fun stuff.
|
||||
* Searches both SiteTree and File.
|
||||
*
|
||||
* @param string $keywords Keywords as a string.
|
||||
*/
|
||||
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
|
||||
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
|
||||
) {
|
||||
if (!class_exists('SiteTree'))
|
||||
throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
|
||||
if (!class_exists('File'))
|
||||
throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
|
||||
|
||||
$fileFilter = '';
|
||||
$keywords = $this->escapeString($keywords);
|
||||
$htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
|
||||
|
||||
$extraFilters = array('SiteTree' => '', 'File' => '');
|
||||
|
||||
if ($booleanSearch) $boolean = "IN BOOLEAN MODE";
|
||||
|
||||
if ($extraFilter) {
|
||||
$extraFilters['SiteTree'] = " AND $extraFilter";
|
||||
|
||||
if ($alternativeFileFilter)
|
||||
$extraFilters['File'] = " AND $alternativeFileFilter";
|
||||
else $extraFilters['File'] = $extraFilters['SiteTree'];
|
||||
}
|
||||
|
||||
// Always ensure that only pages with ShowInSearch = 1 can be searched
|
||||
$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
|
||||
|
||||
// File.ShowInSearch was added later, keep the database driver backwards compatible
|
||||
// by checking for its existence first
|
||||
$fields = $this->fieldList('File');
|
||||
if (array_key_exists('ShowInSearch', $fields))
|
||||
$extraFilters['File'] .= " AND ShowInSearch <> 0";
|
||||
|
||||
$limit = $start . ", " . (int) $pageLength;
|
||||
|
||||
$notMatch = $invertedMatch
|
||||
? "NOT "
|
||||
: "";
|
||||
if ($keywords) {
|
||||
$match['SiteTree'] = "
|
||||
MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$keywords' $boolean)
|
||||
+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityKeywords' $boolean)
|
||||
";
|
||||
$match['File'] = "MATCH (Filename, Title, Content) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
|
||||
|
||||
// We make the relevance search by converting a boolean mode search into a normal one
|
||||
$relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords);
|
||||
$htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords);
|
||||
$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaDescription) "
|
||||
. "AGAINST ('$relevanceKeywords') "
|
||||
. "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
|
||||
$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
|
||||
} else {
|
||||
$relevance['SiteTree'] = $relevance['File'] = 1;
|
||||
$match['SiteTree'] = $match['File'] = "1 = 1";
|
||||
}
|
||||
|
||||
// Generate initial DataLists and base table names
|
||||
$lists = array();
|
||||
$baseClasses = array('SiteTree' => '', 'File' => '');
|
||||
foreach ($classesToSearch as $class) {
|
||||
$lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "");
|
||||
$baseClasses[$class] = '"' . $class . '"';
|
||||
}
|
||||
|
||||
// Make column selection lists
|
||||
$select = array(
|
||||
'SiteTree' => array(
|
||||
"ClassName", "$baseClasses[SiteTree].\"ID\"", "ParentID",
|
||||
"Title", "MenuTitle", "URLSegment", "Content",
|
||||
"LastEdited", "Created",
|
||||
"Filename" => "_utf8''", "Name" => "_utf8''",
|
||||
"Relevance" => $relevance['SiteTree'], "CanViewType"
|
||||
),
|
||||
'File' => array(
|
||||
"ClassName", "$baseClasses[File].\"ID\"", "ParentID" => "_utf8''",
|
||||
"Title", "MenuTitle" => "_utf8''", "URLSegment" => "_utf8''", "Content",
|
||||
"LastEdited", "Created",
|
||||
"Filename", "Name",
|
||||
"Relevance" => $relevance['File'], "CanViewType" => "NULL"
|
||||
),
|
||||
);
|
||||
|
||||
// Process and combine queries
|
||||
$querySQLs = array();
|
||||
$queryParameters = array();
|
||||
$totalCount = 0;
|
||||
foreach ($lists as $class => $list) {
|
||||
$query = $list->dataQuery()->query();
|
||||
|
||||
// There's no need to do all that joining
|
||||
$query->setFrom(array(str_replace(array('"', '`'), '', $baseClasses[$class]) => $baseClasses[$class]));
|
||||
$query->setSelect($select[$class]);
|
||||
$query->setOrderBy(array());
|
||||
|
||||
$querySQLs[] = $query->sql($parameters);
|
||||
$queryParameters = array_merge($queryParameters, $parameters);
|
||||
|
||||
$totalCount += $query->unlimitedRowCount();
|
||||
}
|
||||
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
|
||||
|
||||
// Get records
|
||||
$records = $this->preparedQuery($fullQuery, $queryParameters);
|
||||
|
||||
$objects = array();
|
||||
|
||||
foreach ($records as $record) {
|
||||
$objects[] = new $record['ClassName']($record);
|
||||
}
|
||||
|
||||
$list = new PaginatedList(new ArrayList($objects));
|
||||
$list->setPageStart($start);
|
||||
$list->setPageLength($pageLength);
|
||||
$list->setTotalItems($totalCount);
|
||||
|
||||
// The list has already been limited by the query above
|
||||
$list->setLimitItems(false);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function supportsTransactions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function transactionStart($transactionMode = false, $sessionCharacteristics = false) {
|
||||
// This sets the isolation level for the NEXT transaction, not the current one.
|
||||
if ($transactionMode) {
|
||||
$this->query('SET TRANSACTION ' . $transactionMode . ';');
|
||||
}
|
||||
|
||||
$this->query('START TRANSACTION;');
|
||||
|
||||
if ($sessionCharacteristics) {
|
||||
$this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics . ';');
|
||||
}
|
||||
}
|
||||
|
||||
public function transactionSavepoint($savepoint) {
|
||||
$this->query("SAVEPOINT $savepoint;");
|
||||
}
|
||||
|
||||
public function transactionRollback($savepoint = false) {
|
||||
if ($savepoint) {
|
||||
$this->query('ROLLBACK TO ' . $savepoint . ';');
|
||||
} else {
|
||||
$this->query('ROLLBACK');
|
||||
}
|
||||
}
|
||||
|
||||
public function transactionEnd($chain = false) {
|
||||
$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN;');
|
||||
}
|
||||
|
||||
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
|
||||
$parameterised = false
|
||||
) {
|
||||
if ($exact && $caseSensitive === null) {
|
||||
$comp = ($negate) ? '!=' : '=';
|
||||
} else {
|
||||
$comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
|
||||
if ($negate) $comp = 'NOT ' . $comp;
|
||||
}
|
||||
|
||||
if($parameterised) {
|
||||
return sprintf("%s %s ?", $field, $comp);
|
||||
} else {
|
||||
return sprintf("%s %s '%s'", $field, $comp, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function formattedDatetimeClause($date, $format) {
|
||||
preg_match_all('/%(.)/', $format, $matches);
|
||||
foreach ($matches[1] as $match)
|
||||
if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
|
||||
user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
||||
}
|
||||
|
||||
if (preg_match('/^now$/i', $date)) {
|
||||
$date = "NOW()";
|
||||
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
||||
$date = "'$date'";
|
||||
}
|
||||
|
||||
if ($format == '%U') return "UNIX_TIMESTAMP($date)";
|
||||
|
||||
return "DATE_FORMAT($date, '$format')";
|
||||
}
|
||||
|
||||
public function datetimeIntervalClause($date, $interval) {
|
||||
$interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
|
||||
|
||||
if (preg_match('/^now$/i', $date)) {
|
||||
$date = "NOW()";
|
||||
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
||||
$date = "'$date'";
|
||||
}
|
||||
|
||||
return "$date + INTERVAL $interval";
|
||||
}
|
||||
|
||||
public function datetimeDifferenceClause($date1, $date2) {
|
||||
// First date format
|
||||
if (preg_match('/^now$/i', $date1)) {
|
||||
$date1 = "NOW()";
|
||||
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
||||
$date1 = "'$date1'";
|
||||
}
|
||||
// Second date format
|
||||
if (preg_match('/^now$/i', $date2)) {
|
||||
$date2 = "NOW()";
|
||||
} else if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
|
||||
$date2 = "'$date2'";
|
||||
}
|
||||
|
||||
return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
|
||||
}
|
||||
|
||||
public function supportsLocks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canLock($name) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
return (bool) $this->query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
|
||||
}
|
||||
|
||||
public function getLock($name, $timeout = 5) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
|
||||
// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
|
||||
// in contrast to PostgreSQL and SQL Server who stack the locks.
|
||||
return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
|
||||
}
|
||||
|
||||
public function releaseLock($name) {
|
||||
$id = $this->getLockIdentifier($name);
|
||||
return (bool) $this->query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
|
||||
}
|
||||
|
||||
protected function getLockIdentifier($name) {
|
||||
// Prefix with database name
|
||||
$dbName = $this->connector->getSelectedDatabase() ;
|
||||
return $this->escapeString("{$dbName}_{$name}");
|
||||
}
|
||||
|
||||
public function now() {
|
||||
// MySQL uses NOW() to return the current date/time.
|
||||
return 'NOW()';
|
||||
}
|
||||
|
||||
public function random() {
|
||||
return 'RAND()';
|
||||
}
|
||||
}
|
66
model/connect/MySQLQuery.php
Normal file
66
model/connect/MySQLQuery.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A result-set from a MySQL database (using MySQLiConnector)
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLQuery extends SS_Query {
|
||||
|
||||
/**
|
||||
* The MySQLiConnector object that created this result set.
|
||||
*
|
||||
* @var MySQLiConnector
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The internal MySQL handle that points to the result set.
|
||||
*
|
||||
* @var mysqli_result
|
||||
*/
|
||||
protected $handle;
|
||||
|
||||
/**
|
||||
* The related mysqli statement object if generated using a prepared query
|
||||
*
|
||||
* @var mysqli_stmt
|
||||
*/
|
||||
protected $statement;
|
||||
|
||||
/**
|
||||
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
||||
* @param MySQLDatabase $database The database object that created this query.
|
||||
* @param mysqli_result $handle the internal mysql handle that is points to the resultset.
|
||||
* @param mysqli_stmt $statement The related statement, if present
|
||||
*/
|
||||
public function __construct(MySQLiConnector $database, $handle = null, $statement = null) {
|
||||
$this->database = $database;
|
||||
$this->handle = $handle;
|
||||
$this->statement = $statement;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if (is_object($this->handle)) $this->handle->free();
|
||||
// Don't close statement as these may be re-used across the life of this request
|
||||
// if (is_object($this->statement)) $this->statement->close();
|
||||
}
|
||||
|
||||
public function seek($row) {
|
||||
if (is_object($this->handle)) return $this->handle->data_seek($row);
|
||||
}
|
||||
|
||||
public function numRecords() {
|
||||
if (is_object($this->handle)) return $this->handle->num_rows;
|
||||
}
|
||||
|
||||
public function nextRecord() {
|
||||
if (is_object($this->handle) && ($data = $this->handle->fetch_assoc())) {
|
||||
return $data;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
569
model/connect/MySQLSchemaManager.php
Normal file
569
model/connect/MySQLSchemaManager.php
Normal file
@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents schema management object for MySQL
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLSchemaManager extends DBSchemaManager {
|
||||
|
||||
/**
|
||||
* Identifier for this schema, used for configuring schema-specific table
|
||||
* creation options
|
||||
*/
|
||||
const ID = 'MySQL';
|
||||
|
||||
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
||||
$fieldSchemas = $indexSchemas = "";
|
||||
|
||||
if (!empty($options[self::ID])) {
|
||||
$addOptions = $options[self::ID];
|
||||
} elseif (!empty($options[get_class($this)])) {
|
||||
Deprecation::notice(
|
||||
'3.2',
|
||||
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
|
||||
);
|
||||
$addOptions = $options[get_class($this)];
|
||||
} elseif (!empty($options[get_parent_class($this)])) {
|
||||
Deprecation::notice(
|
||||
'3.2',
|
||||
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
|
||||
);
|
||||
$addOptions = $options[get_parent_class($this)];
|
||||
} else {
|
||||
$addOptions = "ENGINE=InnoDB";
|
||||
}
|
||||
|
||||
if (!isset($fields['ID'])) {
|
||||
$fields['ID'] = "int(11) not null auto_increment";
|
||||
}
|
||||
if ($fields) {
|
||||
foreach ($fields as $k => $v)
|
||||
$fieldSchemas .= "\"$k\" $v,\n";
|
||||
}
|
||||
if ($indexes) {
|
||||
foreach ($indexes as $k => $v) {
|
||||
$indexSchemas .= $this->getIndexSqlDefinition($k, $v) . ",\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to "CREATE TEMPORARY TABLE" for temporary tables
|
||||
$temporary = empty($options['temporary'])
|
||||
? ""
|
||||
: "TEMPORARY";
|
||||
|
||||
$this->query("CREATE $temporary TABLE \"$table\" (
|
||||
$fieldSchemas
|
||||
$indexSchemas
|
||||
primary key (ID)
|
||||
) {$addOptions}");
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null,
|
||||
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null
|
||||
) {
|
||||
if ($this->isView($tableName)) {
|
||||
$this->alterationMessage(
|
||||
sprintf("Table %s not changed as it is a view", $tableName),
|
||||
"changed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
$alterList = array();
|
||||
|
||||
if ($newFields) {
|
||||
foreach ($newFields as $k => $v) {
|
||||
$alterList[] .= "ADD \"$k\" $v";
|
||||
}
|
||||
}
|
||||
if ($newIndexes) {
|
||||
foreach ($newIndexes as $k => $v) {
|
||||
$alterList[] .= "ADD " . $this->getIndexSqlDefinition($k, $v);
|
||||
}
|
||||
}
|
||||
if ($alteredFields) {
|
||||
foreach ($alteredFields as $k => $v) {
|
||||
$alterList[] .= "CHANGE \"$k\" \"$k\" $v";
|
||||
}
|
||||
}
|
||||
if ($alteredIndexes) {
|
||||
foreach ($alteredIndexes as $k => $v) {
|
||||
$alterList[] .= "DROP INDEX \"$k\"";
|
||||
$alterList[] .= "ADD " . $this->getIndexSqlDefinition($k, $v);
|
||||
}
|
||||
}
|
||||
|
||||
if ($alteredOptions && isset($alteredOptions[get_class($this)])) {
|
||||
$indexList = $this->indexList($tableName);
|
||||
$skip = false;
|
||||
foreach ($indexList as $index) {
|
||||
if ($index['type'] === 'fulltext') {
|
||||
$skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skip) {
|
||||
$this->alterationMessage(
|
||||
sprintf(
|
||||
"Table %s options not changed to %s due to fulltextsearch index",
|
||||
$tableName,
|
||||
$alteredOptions[get_class($this)]
|
||||
),
|
||||
"changed"
|
||||
);
|
||||
} else {
|
||||
$this->query(sprintf("ALTER TABLE \"%s\" %s", $tableName, $alteredOptions[get_class($this)]));
|
||||
$this->alterationMessage(
|
||||
sprintf("Table %s options changed: %s", $tableName, $alteredOptions[get_class($this)]),
|
||||
"changed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$alterations = implode(",\n", $alterList);
|
||||
$this->query("ALTER TABLE \"$tableName\" $alterations");
|
||||
}
|
||||
|
||||
public function isView($tableName) {
|
||||
$info = $this->query("SHOW /*!50002 FULL*/ TABLES LIKE '$tableName'")->record();
|
||||
return $info && strtoupper($info['Table_type']) == 'VIEW';
|
||||
}
|
||||
|
||||
public function renameTable($oldTableName, $newTableName) {
|
||||
$this->query("ALTER TABLE \"$oldTableName\" RENAME \"$newTableName\"");
|
||||
}
|
||||
|
||||
public function checkAndRepairTable($tableName) {
|
||||
// If running PDO and not in emulated mode, check table will fail
|
||||
if($this->database->getConnector() instanceof PDOConnector && !PDOConnector::is_emulate_prepare()) {
|
||||
$this->alterationMessage('CHECK TABLE command disabled for PDO in native mode', 'notice');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perform check
|
||||
if (!$this->runTableCheckCommand("CHECK TABLE \"$tableName\"")) {
|
||||
if ($this->runTableCheckCommand("CHECK TABLE \"" . strtolower($tableName) . "\"")) {
|
||||
$this->alterationMessage(
|
||||
"Table $tableName: renamed from lowercase",
|
||||
"repaired"
|
||||
);
|
||||
return $this->renameTable(strtolower($tableName), $tableName);
|
||||
}
|
||||
|
||||
$this->alterationMessage(
|
||||
"Table $tableName: repaired",
|
||||
"repaired"
|
||||
);
|
||||
return $this->runTableCheckCommand("REPAIR TABLE \"$tableName\" USE_FRM");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used by checkAndRepairTable.
|
||||
* @param string $sql Query to run.
|
||||
* @return boolean Returns if the query returns a successful result.
|
||||
*/
|
||||
protected function runTableCheckCommand($sql) {
|
||||
$testResults = $this->query($sql);
|
||||
foreach ($testResults as $testRecord) {
|
||||
if (strtolower($testRecord['Msg_text']) != 'ok') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasTable($table) {
|
||||
// MySQLi doesn't like parameterised queries for some queries
|
||||
$sqlTable = $this->database->quoteString($table);
|
||||
return (bool) ($this->query("SHOW TABLES LIKE $sqlTable")->value());
|
||||
}
|
||||
|
||||
public function createField($tableName, $fieldName, $fieldSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
||||
}
|
||||
|
||||
public function databaseList() {
|
||||
return $this->query("SHOW DATABASES")->column();
|
||||
}
|
||||
|
||||
public function databaseExists($name) {
|
||||
// MySQLi doesn't like parameterised queries for some queries
|
||||
$sqlName = $this->database->quoteString($name);
|
||||
return !!($this->query("SHOW DATABASES LIKE $sqlName")->value());
|
||||
}
|
||||
|
||||
public function createDatabase($name) {
|
||||
$this->query("CREATE DATABASE \"$name\" DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci");
|
||||
}
|
||||
|
||||
public function dropDatabase($name) {
|
||||
$this->query("DROP DATABASE \"$name\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the database type of the given field.
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $fieldName The name of the field to change.
|
||||
* @param string $fieldSpec The new field specification
|
||||
*/
|
||||
public function alterField($tableName, $fieldName, $fieldSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the database column name of the given field.
|
||||
*
|
||||
* @param string $tableName The name of the tbale the field is in.
|
||||
* @param string $oldName The name of the field to change.
|
||||
* @param string $newName The new name of the field
|
||||
*/
|
||||
public function renameField($tableName, $oldName, $newName) {
|
||||
$fieldList = $this->fieldList($tableName);
|
||||
if (array_key_exists($oldName, $fieldList)) {
|
||||
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$oldName\" \"$newName\" " . $fieldList[$oldName]);
|
||||
}
|
||||
}
|
||||
|
||||
protected static $_cache_collation_info = array();
|
||||
|
||||
public function fieldList($table) {
|
||||
$fields = $this->query("SHOW FULL FIELDS IN \"$table\"");
|
||||
foreach ($fields as $field) {
|
||||
|
||||
// ensure that '' is converted to \' in field specification (mostly for the benefit of ENUM values)
|
||||
$fieldSpec = str_replace('\'\'', '\\\'', $field['Type']);
|
||||
if (!$field['Null'] || $field['Null'] == 'NO') {
|
||||
$fieldSpec .= ' not null';
|
||||
}
|
||||
|
||||
if ($field['Collation'] && $field['Collation'] != 'NULL') {
|
||||
// Cache collation info to cut down on database traffic
|
||||
if (!isset(self::$_cache_collation_info[$field['Collation']])) {
|
||||
self::$_cache_collation_info[$field['Collation']]
|
||||
= $this->query("SHOW COLLATION LIKE '{$field['Collation']}'")->record();
|
||||
}
|
||||
$collInfo = self::$_cache_collation_info[$field['Collation']];
|
||||
$fieldSpec .= " character set $collInfo[Charset] collate $field[Collation]";
|
||||
}
|
||||
|
||||
if ($field['Default'] || $field['Default'] === "0") {
|
||||
$fieldSpec .= " default " . $this->database->quoteString($field['Default']);
|
||||
}
|
||||
if ($field['Extra']) $fieldSpec .= " " . $field['Extra'];
|
||||
|
||||
$fieldList[$field['Field']] = $fieldSpec;
|
||||
}
|
||||
return $fieldList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an index on a table.
|
||||
*
|
||||
* @param string $tableName The name of the table.
|
||||
* @param string $indexName The name of the index.
|
||||
* @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()} for more
|
||||
* details.
|
||||
*/
|
||||
public function createIndex($tableName, $indexName, $indexSpec) {
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD " . $this->getIndexSqlDefinition($indexName, $indexSpec));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL suitable for creating this index
|
||||
*
|
||||
* @param string $indexName
|
||||
* @param string|array $indexSpec See {@link requireTable()} for details
|
||||
* @return string MySQL compatible ALTER TABLE syntax
|
||||
*/
|
||||
protected function getIndexSqlDefinition($indexName, $indexSpec) {
|
||||
$indexSpec = $this->parseIndexSpec($indexName, $indexSpec);
|
||||
if ($indexSpec['type'] == 'using') {
|
||||
return "index \"$indexName\" using ({$indexSpec['value']})";
|
||||
} else {
|
||||
return "{$indexSpec['type']} \"$indexName\" ({$indexSpec['value']})";
|
||||
}
|
||||
}
|
||||
|
||||
public function alterIndex($tableName, $indexName, $indexSpec) {
|
||||
$indexSpec = $this->parseIndexSpec($indexName, $indexSpec);
|
||||
$this->query("ALTER TABLE \"$tableName\" DROP INDEX \"$indexName\"");
|
||||
$this->query("ALTER TABLE \"$tableName\" ADD {$indexSpec['type']} \"$indexName\" {$indexSpec['value']}");
|
||||
}
|
||||
|
||||
protected function indexKey($table, $index, $spec) {
|
||||
// MySQL simply uses the same index name as SilverStripe does internally
|
||||
return $index;
|
||||
}
|
||||
|
||||
public function indexList($table) {
|
||||
$indexes = $this->query("SHOW INDEXES IN \"$table\"");
|
||||
$groupedIndexes = array();
|
||||
$indexList = array();
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
$groupedIndexes[$index['Key_name']]['fields'][$index['Seq_in_index']] = $index['Column_name'];
|
||||
|
||||
if ($index['Index_type'] == 'FULLTEXT') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'fulltext';
|
||||
} else if (!$index['Non_unique']) {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'unique';
|
||||
} else if ($index['Index_type'] == 'HASH') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'hash';
|
||||
} else if ($index['Index_type'] == 'RTREE') {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'rtree';
|
||||
} else {
|
||||
$groupedIndexes[$index['Key_name']]['type'] = 'index';
|
||||
}
|
||||
}
|
||||
|
||||
if ($groupedIndexes) {
|
||||
foreach ($groupedIndexes as $index => $details) {
|
||||
ksort($details['fields']);
|
||||
$indexList[$index] = $this->parseIndexSpec($index, array(
|
||||
'name' => $index,
|
||||
'value' => $this->implodeColumnList($details['fields']),
|
||||
'type' => $details['type']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $indexList;
|
||||
}
|
||||
|
||||
public function tableList() {
|
||||
$tables = array();
|
||||
foreach ($this->query("SHOW TABLES") as $record) {
|
||||
$table = reset($record);
|
||||
$tables[strtolower($table)] = $table;
|
||||
}
|
||||
return $tables;
|
||||
}
|
||||
|
||||
public function enumValuesForField($tableName, $fieldName) {
|
||||
// Get the enum of all page types from the SiteTree table
|
||||
$classnameinfo = DB::query("DESCRIBE \"$tableName\" \"$fieldName\"")->first();
|
||||
preg_match_all("/'[^,]+'/", $classnameinfo["Type"], $matches);
|
||||
|
||||
$classes = array();
|
||||
foreach ($matches[0] as $value) {
|
||||
$classes[] = stripslashes(trim($value, "'"));
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
||||
public function dbDataType($type) {
|
||||
$values = Array(
|
||||
'unsigned integer' => 'UNSIGNED'
|
||||
);
|
||||
|
||||
if (isset($values[$type])) return $values[$type];
|
||||
else return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a boolean type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function boolean($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'tinyint', 'precision'=>1, 'sign'=>'unsigned', 'null'=>'not null',
|
||||
//'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "tinyint(1) unsigned not null default
|
||||
//'{$this->defaultVal}'");
|
||||
return 'tinyint(1) unsigned not null' . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a date type-formatted string
|
||||
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function date($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'date');
|
||||
//DB::requireField($this->tableName, $this->name, "date");
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a decimal type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function decimal($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'decimal', 'precision'=>"$this->wholeSize,$this->decimalSize");
|
||||
//DB::requireField($this->tableName, $this->name, "decimal($this->wholeSize,$this->decimalSize)");
|
||||
// Avoid empty strings being put in the db
|
||||
if ($values['precision'] == '') {
|
||||
$precision = 1;
|
||||
} else {
|
||||
$precision = $values['precision'];
|
||||
}
|
||||
|
||||
$defaultValue = '';
|
||||
if (isset($values['default']) && is_numeric($values['default'])) {
|
||||
$decs = strpos($precision, ',') !== false
|
||||
? (int) substr($precision, strpos($precision, ',') + 1)
|
||||
: 0;
|
||||
$defaultValue = ' default ' . number_format($values['default'], $decs, '.', '');
|
||||
}
|
||||
|
||||
return "decimal($precision) not null $defaultValue";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a enum type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function enum($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
||||
// 'utf8_general_ci', 'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
||||
// utf8 collate utf8_general_ci default '{$this->default}'");
|
||||
$valuesString = implode(",", Convert::raw2sql($values['enums'], true));
|
||||
return "enum($valuesString) character set utf8 collate utf8_general_ci" . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a set type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function set($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
||||
// 'utf8_general_ci', 'default'=>$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
||||
//utf8 collate utf8_general_ci default '{$this->default}'");
|
||||
$valuesString = implode(",", Convert::raw2sql($values['enums'], true));
|
||||
return "set($valuesString) character set utf8 collate utf8_general_ci" . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a float type-formatted string
|
||||
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function float($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'float');
|
||||
//DB::requireField($this->tableName, $this->name, "float");
|
||||
return "float not null" . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a int type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function int($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)$this->default);
|
||||
//DB::requireField($this->tableName, $this->name, "int(11) not null default '{$this->defaultVal}'");
|
||||
return "int(11) not null" . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a datetime type-formatted string
|
||||
* For MySQL, we simply return the word 'datetime', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function ss_datetime($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'datetime');
|
||||
//DB::requireField($this->tableName, $this->name, $values);
|
||||
return 'datetime';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a text type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function text($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci');
|
||||
//DB::requireField($this->tableName, $this->name, "mediumtext character set utf8 collate utf8_general_ci");
|
||||
return 'mediumtext character set utf8 collate utf8_general_ci' . $this->defaultClause($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a time type-formatted string
|
||||
* For MySQL, we simply return the word 'time', no other parameters are necessary
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function time($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'time');
|
||||
//DB::requireField($this->tableName, $this->name, "time");
|
||||
return 'time';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a varchar type-formatted string
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function varchar($values) {
|
||||
//For reference, this is what typically gets passed to this function:
|
||||
//$parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>
|
||||
//'utf8_general_ci');
|
||||
//DB::requireField($this->tableName, $this->name, "varchar($this->size) character set utf8 collate
|
||||
// utf8_general_ci");
|
||||
$default = $this->defaultClause($values);
|
||||
return "varchar({$values['precision']}) character set utf8 collate utf8_general_ci$default";
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the MySQL-proprietary 'Year' datatype
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string
|
||||
*/
|
||||
public function year($values) {
|
||||
return 'year(4)';
|
||||
}
|
||||
|
||||
public function IdColumn($asDbValue = false, $hasAutoIncPK = true) {
|
||||
return 'int(11) not null auto_increment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and escapes the default values for a specification
|
||||
*
|
||||
* @param array $values Contains a tokenised list of info about this data type
|
||||
* @return string Default clause
|
||||
*/
|
||||
protected function defaultClause($values) {
|
||||
if(isset($values['default'])) {
|
||||
return ' default ' . $this->database->quoteString($values['default']);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
317
model/connect/MySQLiConnector.php
Normal file
317
model/connect/MySQLiConnector.php
Normal file
@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Connector for MySQL using the MySQLi method
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class MySQLiConnector extends DBConnector {
|
||||
|
||||
/**
|
||||
* Connection to the MySQL database
|
||||
*
|
||||
* @var MySQLi
|
||||
*/
|
||||
protected $dbConn = null;
|
||||
|
||||
/**
|
||||
* Name of the currently selected database
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $databaseName = null;
|
||||
|
||||
/**
|
||||
* The most recent statement returned from MySQLiConnector->preparedQuery
|
||||
*
|
||||
* @var mysqli_stmt
|
||||
*/
|
||||
protected $lastStatement = null;
|
||||
|
||||
/**
|
||||
* Store the most recent statement for later use
|
||||
*
|
||||
* @param mysqli_stmt $statement
|
||||
*/
|
||||
public function setLastStatement($statement) {
|
||||
$this->lastStatement = $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of prepared statements, cached by SQL string
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cachedStatements = array();
|
||||
|
||||
/**
|
||||
* Flush all prepared statements
|
||||
*/
|
||||
public function flushStatements() {
|
||||
$this->cachedStatements = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a prepared statement for a given SQL string, or return an already prepared version if
|
||||
* one exists for the given query
|
||||
*
|
||||
* @param string $sql
|
||||
* @param boolean &$success
|
||||
* @return mysqli_stmt
|
||||
*/
|
||||
public function getOrPrepareStatement($sql, &$success) {
|
||||
// Check for cached statement
|
||||
if(!empty($this->cachedStatements[$sql])) {
|
||||
$success = true;
|
||||
return $this->cachedStatements[$sql];
|
||||
}
|
||||
|
||||
// Prepare statement with arguments
|
||||
$statement = $this->dbConn->stmt_init();
|
||||
if($success = $statement->prepare($sql)) {
|
||||
// Only cache prepared statement on success
|
||||
$this->cachedStatements[$sql] = $statement;
|
||||
}
|
||||
|
||||
return $statement;
|
||||
}
|
||||
|
||||
public function connect($parameters, $selectDB = false) {
|
||||
$this->flushStatements();
|
||||
|
||||
// Normally $selectDB is set to false by the MySQLDatabase controller, as per convention
|
||||
$selectedDB = ($selectDB && !empty($parameters['database'])) ? $parameters['database'] : null;
|
||||
|
||||
if(!empty($parameters['port'])) {
|
||||
$this->dbConn = new MySQLi(
|
||||
$parameters['server'],
|
||||
$parameters['username'],
|
||||
$parameters['password'],
|
||||
$selectedDB,
|
||||
$parameters['port']
|
||||
);
|
||||
} else {
|
||||
$this->dbConn = new MySQLi(
|
||||
$parameters['server'],
|
||||
$parameters['username'],
|
||||
$parameters['password'],
|
||||
$selectedDB
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->dbConn->connect_error) {
|
||||
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);
|
||||
}
|
||||
|
||||
// Set charset if given and not null. Can explicitly set to empty string to omit
|
||||
$charset = isset($parameters['charset'])
|
||||
? $parameters['charset']
|
||||
: 'utf8';
|
||||
if (!empty($charset)) $this->dbConn->set_charset($charset);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ($this->dbConn) {
|
||||
mysqli_close($this->dbConn);
|
||||
$this->dbConn = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function escapeString($value) {
|
||||
return $this->dbConn->real_escape_string($value);
|
||||
}
|
||||
|
||||
public function quoteString($value) {
|
||||
$value = $this->escapeString($value);
|
||||
return "'$value'";
|
||||
}
|
||||
|
||||
public function getVersion() {
|
||||
return $this->dbConn->server_info;
|
||||
}
|
||||
|
||||
protected function benchmarkQuery($sql, $callback) {
|
||||
// Clear the last statement
|
||||
$this->setLastStatement(null);
|
||||
return parent::benchmarkQuery($sql, $callback);
|
||||
}
|
||||
|
||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
// Check if we should only preview this query
|
||||
if ($this->previewWrite($sql)) return;
|
||||
|
||||
// Benchmark query
|
||||
$conn = $this->dbConn;
|
||||
$handle = $this->benchmarkQuery($sql, function($sql) use($conn) {
|
||||
return $conn->query($sql);
|
||||
});
|
||||
|
||||
if (!$handle || $this->dbConn->error) {
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||
return null;
|
||||
}
|
||||
|
||||
if($handle !== true) {
|
||||
// Some non-select queries return true on success
|
||||
return new MySQLQuery($this, $handle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
|
||||
*
|
||||
* @param array $parameters List of parameters
|
||||
* @param array &$blobs Out parameter for list of blobs to bind separately
|
||||
* @return array List of parameters appropriate for mysqli_stmt_bind_param function
|
||||
*/
|
||||
public function parsePreparedParameters($parameters, &$blobs) {
|
||||
$types = '';
|
||||
$values = array();
|
||||
$blobs = array();
|
||||
for($index = 0; $index < count($parameters); $index++) {
|
||||
$value = $parameters[$index];
|
||||
$phpType = gettype($value);
|
||||
|
||||
// Allow overriding of parameter type using an associative array
|
||||
if($phpType === 'array') {
|
||||
$phpType = $value['type'];
|
||||
$value = $value['value'];
|
||||
}
|
||||
|
||||
// Convert php variable type to one that makes mysqli_stmt_bind_param happy
|
||||
// @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
|
||||
switch($phpType) {
|
||||
case 'boolean':
|
||||
case 'integer':
|
||||
$types .= 'i';
|
||||
break;
|
||||
case 'float': // Not actually returnable from gettype
|
||||
case 'double':
|
||||
$types .= 'd';
|
||||
break;
|
||||
case 'object': // Allowed if the object or resource has a __toString method
|
||||
case 'resource':
|
||||
case 'string':
|
||||
case 'NULL': // Take care that a where clause should use "where XX is null" not "where XX = null"
|
||||
$types .= 's';
|
||||
break;
|
||||
case 'blob':
|
||||
$types .= 'b';
|
||||
// Blobs must be sent via send_long_data and set to null here
|
||||
$blobs[] = array(
|
||||
'index' => $index,
|
||||
'value' => $value
|
||||
);
|
||||
$value = null;
|
||||
break;
|
||||
case 'array':
|
||||
case 'unknown type':
|
||||
default:
|
||||
user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)",
|
||||
E_USER_ERROR);
|
||||
break;
|
||||
}
|
||||
$values[] = $value;
|
||||
}
|
||||
return array_merge(array($types), $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a list of parameters to a statement
|
||||
*
|
||||
* @param mysqli_stmt $statement MySQLi statement
|
||||
* @param array $parameters List of parameters to pass to bind_param
|
||||
*/
|
||||
public function bindParameters(mysqli_stmt $statement, array $parameters) {
|
||||
// Because mysqli_stmt::bind_param arguments must be passed by reference
|
||||
// we need to do a bit of hackery
|
||||
for ($i = 0; $i < count($parameters); $i++)
|
||||
{
|
||||
$boundName = "param$i";
|
||||
$$boundName = $parameters[$i];
|
||||
$boundNames[] = &$$boundName;
|
||||
}
|
||||
call_user_func_array( array($statement, 'bind_param'), $boundNames);
|
||||
}
|
||||
|
||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||
// Shortcut to basic query when not given parameters
|
||||
if(empty($parameters)) return $this->query($sql, $errorLevel);
|
||||
|
||||
// Check if we should only preview this query
|
||||
if ($this->previewWrite($sql)) return;
|
||||
|
||||
// Type check, identify, and prepare parameters for passing to the statement bind function
|
||||
$parsedParameters = $this->parsePreparedParameters($parameters, $blobs);
|
||||
|
||||
// Benchmark query
|
||||
$self = $this;
|
||||
$lastStatement = $this->benchmarkQuery($sql, function($sql) use($parsedParameters, $blobs, $self) {
|
||||
|
||||
$statement = $self->getOrPrepareStatement($sql, $success);
|
||||
if(!$success) return $statement;
|
||||
|
||||
$self->bindParameters($statement, $parsedParameters);
|
||||
|
||||
// Bind any blobs given
|
||||
foreach($blobs as $blob) {
|
||||
$statement->send_long_data($blob['index'], $blob['value']);
|
||||
}
|
||||
|
||||
// Safely execute the statement
|
||||
$statement->execute();
|
||||
return $statement;
|
||||
});
|
||||
|
||||
// check result
|
||||
$this->setLastStatement($lastStatement);
|
||||
if (!$lastStatement || $lastStatement->error) {
|
||||
$values = $this->parameterValues($parameters);
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
||||
return null;
|
||||
}
|
||||
|
||||
// May not return result for non-select statements
|
||||
if($result = $lastStatement->get_result()) {
|
||||
return new MySQLQuery($this, $result, $lastStatement);
|
||||
}
|
||||
}
|
||||
|
||||
public function selectDatabase($name) {
|
||||
if ($this->dbConn->select_db($name)) {
|
||||
$this->databaseName = $name;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSelectedDatabase() {
|
||||
return $this->databaseName;
|
||||
}
|
||||
|
||||
public function unloadDatabase() {
|
||||
$this->databaseName = null;
|
||||
}
|
||||
|
||||
public function isActive() {
|
||||
return $this->databaseName && $this->dbConn && empty($this->dbConn->connect_error);
|
||||
}
|
||||
|
||||
public function affectedRows() {
|
||||
return $this->dbConn->affected_rows;
|
||||
}
|
||||
|
||||
public function getGeneratedID($table) {
|
||||
return $this->dbConn->insert_id;
|
||||
}
|
||||
|
||||
public function getLastError() {
|
||||
// Check if a statement was used for the most recent query
|
||||
if($this->lastStatement && $this->lastStatement->error) {
|
||||
return $this->lastStatement->error;
|
||||
}
|
||||
return $this->dbConn->error;
|
||||
}
|
||||
|
||||
}
|
359
model/connect/PDOConnector.php
Normal file
359
model/connect/PDOConnector.php
Normal file
@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PDO driver database connector
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class PDOConnector extends DBConnector {
|
||||
|
||||
/**
|
||||
* Should ATTR_EMULATE_PREPARES flag be used to emulate prepared statements?
|
||||
*
|
||||
* @config
|
||||
* @var boolean
|
||||
*/
|
||||
private static $emulate_prepare = false;
|
||||
|
||||
/**
|
||||
* The PDO connection instance
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
protected $pdoConnection = null;
|
||||
|
||||
/**
|
||||
* Name of the currently selected database
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $databaseName = null;
|
||||
|
||||
/**
|
||||
* The most recent statement returned from PDODatabase->query
|
||||
*
|
||||
* @var PDOStatement
|
||||
*/
|
||||
protected $lastStatement = null;
|
||||
|
||||
/**
|
||||
* List of prepared statements, cached by SQL string
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cachedStatements = array();
|
||||
|
||||
/**
|
||||
* Flush all prepared statements
|
||||
*/
|
||||
public function flushStatements() {
|
||||
$this->cachedStatements = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a prepared statement for a given SQL string, or return an already prepared version if
|
||||
* one exists for the given query
|
||||
*
|
||||
* @param string $sql
|
||||
* @return PDOStatement
|
||||
*/
|
||||
public function getOrPrepareStatement($sql) {
|
||||
if(empty($this->cachedStatements[$sql])) {
|
||||
$this->cachedStatements[$sql] = $this->pdoConnection->prepare(
|
||||
$sql,
|
||||
array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)
|
||||
);
|
||||
}
|
||||
return $this->cachedStatements[$sql];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is PDO running in emulated mode
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_emulate_prepare() {
|
||||
return Config::inst()->get('PDOConnector', 'emulate_prepare');
|
||||
}
|
||||
|
||||
public function connect($parameters, $selectDB = false) {
|
||||
$this->flushStatements();
|
||||
|
||||
// Build DSN string
|
||||
// Note that we don't select the database here until explicitly
|
||||
// requested via selectDatabase
|
||||
$driver = $parameters['driver'] . ":";
|
||||
$dsn = array();
|
||||
|
||||
// Typically this is false, but some drivers will request this
|
||||
if($selectDB) {
|
||||
// Specify complete file path immediately following driver (SQLLite3)
|
||||
if(!empty($parameters['filepath'])) {
|
||||
$dsn[] = $parameters['filepath'];
|
||||
} elseif(!empty($parameters['database'])) {
|
||||
// Some databases require a selected database at connection (SQLite3, Azure)
|
||||
if($parameters['driver'] === 'sqlsrv') {
|
||||
$dsn[] = "Database={$parameters['database']}";
|
||||
} else {
|
||||
$dsn[] = "dbname={$parameters['database']}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax for sql server is slightly different
|
||||
if($parameters['driver'] === 'sqlsrv') {
|
||||
$server = $parameters['server'];
|
||||
if (!empty($parameters['port'])) {
|
||||
$server .= ",{$parameters['port']}";
|
||||
}
|
||||
$dsn[] = "Server=$server";
|
||||
} else {
|
||||
if (!empty($parameters['server'])) {
|
||||
// Use Server instead of host for sqlsrv
|
||||
$dsn[] = "host={$parameters['server']}";
|
||||
}
|
||||
|
||||
if (!empty($parameters['port'])) {
|
||||
$dsn[] = "port={$parameters['port']}";
|
||||
}
|
||||
}
|
||||
|
||||
// Set charset if given and not null. Can explicitly set to empty string to omit
|
||||
if($parameters['driver'] !== 'sqlsrv') {
|
||||
$charset = isset($parameters['charset'])
|
||||
? $parameters['charset']
|
||||
: 'utf8';
|
||||
if (!empty($charset)) $dsn[] = "charset=$charset";
|
||||
}
|
||||
|
||||
// Connection commands to be run on every re-connection
|
||||
$options = array(
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
PDO::ATTR_EMULATE_PREPARES => self::is_emulate_prepare()
|
||||
);
|
||||
|
||||
// May throw a PDOException if fails
|
||||
if(empty($parameters['username']) || empty($parameters['password'])) {
|
||||
$this->pdoConnection = new PDO($driver.implode(';', $dsn));
|
||||
} else {
|
||||
$this->pdoConnection = new PDO($driver.implode(';', $dsn), $parameters['username'],
|
||||
$parameters['password'], $options);
|
||||
}
|
||||
|
||||
// Show selected DB if requested
|
||||
if($this->pdoConnection && $selectDB && !empty($parameters['database'])) {
|
||||
$this->databaseName = $parameters['database'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getVersion() {
|
||||
return $this->pdoConnection->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
public function escapeString($value) {
|
||||
$value = $this->quoteString($value);
|
||||
|
||||
// Since the PDO library quotes the value, we should remove this to maintain
|
||||
// consistency with MySQLDatabase::escapeString
|
||||
if (preg_match('/^\'(?<value>.*)\'$/', $value, $matches)) {
|
||||
$value = $matches['value'];
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function quoteString($value) {
|
||||
return $this->pdoConnection->quote($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query that doesn't return a resultset
|
||||
*
|
||||
* @param string $sql
|
||||
* @param string $sql The SQL query to execute
|
||||
* @param integer $errorLevel For errors to this query, raise PHP errors
|
||||
* using this error level.
|
||||
*/
|
||||
public function exec($sql, $errorLevel = E_USER_ERROR) {
|
||||
// Check if we should only preview this query
|
||||
if ($this->previewWrite($sql)) return;
|
||||
|
||||
// Reset last statement to prevent interference in case of error
|
||||
$this->lastStatement = null;
|
||||
|
||||
// Benchmark query
|
||||
$pdo = $this->pdoConnection;
|
||||
$result = $this->benchmarkQuery($sql, function($sql) use($pdo) {
|
||||
return $pdo->exec($sql);
|
||||
});
|
||||
|
||||
// Check for errors
|
||||
if ($result === false) {
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
||||
// Check if we should only preview this query
|
||||
if ($this->previewWrite($sql)) return;
|
||||
|
||||
// Benchmark query
|
||||
$pdo = $this->pdoConnection;
|
||||
$this->lastStatement = $this->benchmarkQuery($sql, function($sql) use($pdo) {
|
||||
return $pdo->query($sql);
|
||||
});
|
||||
|
||||
// Check for errors
|
||||
if (!$this->lastStatement || $this->hasError($this->lastStatement)) {
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PDOQuery($this->lastStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the PDO::PARAM_* type for a given PHP type string
|
||||
* @param string $phpType Type of object in PHP
|
||||
* @return integer PDO Parameter constant value
|
||||
*/
|
||||
public function getPDOParamType($phpType) {
|
||||
switch($phpType) {
|
||||
case 'boolean':
|
||||
return PDO::PARAM_BOOL;
|
||||
case 'NULL':
|
||||
return PDO::PARAM_NULL;
|
||||
case 'integer':
|
||||
return PDO::PARAM_INT;
|
||||
case 'object': // Allowed if the object or resource has a __toString method
|
||||
case 'resource':
|
||||
case 'float': // Not actually returnable from get_type
|
||||
case 'double':
|
||||
case 'string':
|
||||
return PDO::PARAM_STR;
|
||||
case 'blob':
|
||||
return PDO::PARAM_LOB;
|
||||
case 'array':
|
||||
case 'unknown type':
|
||||
default:
|
||||
user_error("Cannot bind parameter as it is an unsupported type ($phpType)", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind all parameters to a PDOStatement
|
||||
*
|
||||
* @param PDOStatement $statement
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function bindParameters(PDOStatement $statement, $parameters) {
|
||||
// Bind all parameters
|
||||
for($index = 0; $index < count($parameters); $index++) {
|
||||
$value = $parameters[$index];
|
||||
$phpType = gettype($value);
|
||||
|
||||
// Allow overriding of parameter type using an associative array
|
||||
if($phpType === 'array') {
|
||||
$phpType = $value['type'];
|
||||
$value = $value['value'];
|
||||
}
|
||||
|
||||
// Check type of parameter
|
||||
$type = $this->getPDOParamType($phpType);
|
||||
if($type === PDO::PARAM_STR) $value = strval($value);
|
||||
|
||||
// Bind this value
|
||||
$statement->bindValue($index+1, $value, $type);
|
||||
}
|
||||
}
|
||||
|
||||
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
|
||||
// Check if we should only preview this query
|
||||
if ($this->previewWrite($sql)) return;
|
||||
|
||||
// Benchmark query
|
||||
$self = $this;
|
||||
$this->lastStatement = $this->benchmarkQuery($sql, function($sql) use($parameters, $self) {
|
||||
|
||||
// Prepare statement
|
||||
$statement = $self->getOrPrepareStatement($sql);
|
||||
if(!$statement) return null;
|
||||
|
||||
// Inject parameters
|
||||
$self->bindParameters($statement, $parameters);
|
||||
|
||||
// Safely execute the statement
|
||||
$statement->execute($parameters);
|
||||
return $statement;
|
||||
});
|
||||
|
||||
// Check for errors
|
||||
if (!$this->lastStatement || $this->hasError($this->lastStatement)) {
|
||||
$values = $this->parameterValues($parameters);
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PDOQuery($this->lastStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a resource has an attached error
|
||||
*
|
||||
* @param PDOStatement|PDO $resource the resource to check
|
||||
* @return boolean Flag indicating true if the resource has an error
|
||||
*/
|
||||
protected function hasError($resource) {
|
||||
// No error if no resource
|
||||
if(empty($resource)) return false;
|
||||
|
||||
// If the error code is empty the statement / connection has not been run yet
|
||||
$code = $resource->errorCode();
|
||||
if(empty($code)) return false;
|
||||
|
||||
// Skip 'ok' and undefined 'warning' types.
|
||||
// @see http://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm
|
||||
return $code !== '00000' && $code !== '01000';
|
||||
}
|
||||
|
||||
public function getLastError() {
|
||||
if ($this->hasError($this->lastStatement)) {
|
||||
$error = $this->lastStatement->errorInfo();
|
||||
} elseif($this->hasError($this->pdoConnection)) {
|
||||
$error = $this->pdoConnection->errorInfo();
|
||||
}
|
||||
if (isset($error)) {
|
||||
return sprintf("%s-%s: %s", $error[0], $error[1], $error[2]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getGeneratedID($table) {
|
||||
return $this->pdoConnection->lastInsertId();
|
||||
}
|
||||
|
||||
public function affectedRows() {
|
||||
if (empty($this->lastStatement)) return 0;
|
||||
return $this->lastStatement->rowCount();
|
||||
}
|
||||
|
||||
public function selectDatabase($name) {
|
||||
$this->exec("USE \"{$name}\"");
|
||||
$this->databaseName = $name;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getSelectedDatabase() {
|
||||
return $this->databaseName;
|
||||
}
|
||||
|
||||
public function unloadDatabase() {
|
||||
$this->databaseName = null;
|
||||
}
|
||||
|
||||
public function isActive() {
|
||||
return $this->databaseName && $this->pdoConnection;
|
||||
}
|
||||
|
||||
}
|
53
model/connect/PDOQuery.php
Normal file
53
model/connect/PDOQuery.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A result-set from a PDO database.
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class PDOQuery extends SS_Query {
|
||||
/**
|
||||
* The internal MySQL handle that points to the result set.
|
||||
* @var PDOStatement
|
||||
*/
|
||||
protected $statement = null;
|
||||
|
||||
protected $results = null;
|
||||
|
||||
/**
|
||||
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
||||
* @param PDOStatement $statement The internal PDOStatement containing the results
|
||||
*/
|
||||
public function __construct(PDOStatement $statement) {
|
||||
$this->statement = $statement;
|
||||
// Since no more than one PDOStatement for any one connection can be safely
|
||||
// traversed, each statement simply requests all rows at once for safety.
|
||||
// This could be re-engineered to call fetchAll on an as-needed basis
|
||||
$this->results = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
$statement->closeCursor();
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->statement->closeCursor();
|
||||
}
|
||||
|
||||
public function seek($row) {
|
||||
$this->rowNum = $row - 1;
|
||||
return $this->nextRecord();
|
||||
}
|
||||
|
||||
public function numRecords() {
|
||||
return count($this->results);
|
||||
}
|
||||
|
||||
public function nextRecord() {
|
||||
$index = $this->rowNum + 1;
|
||||
|
||||
if (isset($this->results[$index])) {
|
||||
return $this->results[$index];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,57 +2,51 @@
|
||||
|
||||
/**
|
||||
* Abstract query-result class.
|
||||
* Once again, this should be subclassed by an actual database implementation. It will only
|
||||
* ever be constructed by a subclass of SS_Database. The result of a database query - an iteratable object
|
||||
* that's returned by DB::SS_Query
|
||||
*
|
||||
* Once again, this should be subclassed by an actual database implementation
|
||||
* such as {@link MySQLQuery}.
|
||||
*
|
||||
* It will only ever be constructed by a subclass of {@link SS_Database} and
|
||||
* contain the result of a database query as an iteratable object.
|
||||
*
|
||||
* Primarily, the SS_Query class takes care of the iterator plumbing, letting
|
||||
* the subclasses focusing on providing the specific data-access methods that
|
||||
* are required: {@link nextRecord()}, {@link numRecords()} and {@link seek()}
|
||||
*
|
||||
* Primarily, the SS_Query class takes care of the iterator plumbing, letting the subclasses focusing
|
||||
* on providing the specific data-access methods that are required: {@link nextRecord()}, {@link numRecords()}
|
||||
* and {@link seek()}
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class SS_Query implements Iterator {
|
||||
|
||||
|
||||
/**
|
||||
* The current record in the interator.
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $currentRecord = null;
|
||||
|
||||
/**
|
||||
* The number of the current row in the interator.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $rowNum = -1;
|
||||
|
||||
/**
|
||||
* Flag to keep track of whether iteration has begun, to prevent unnecessary
|
||||
* seeks.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $queryHasBegun = false;
|
||||
protected $currentRecord = null;
|
||||
|
||||
/**
|
||||
* Return an array containing all the values from a specific column. If no
|
||||
* column is set, then the first will be returned.
|
||||
* The number of the current row in the interator.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $rowNum = -1;
|
||||
|
||||
/**
|
||||
* Flag to keep track of whether iteration has begun, to prevent unnecessary seeks
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $queryHasBegun = false;
|
||||
|
||||
/**
|
||||
* Return an array containing all the values from a specific column. If no column is set, then the first will be
|
||||
* returned
|
||||
*
|
||||
* @param string $column
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function column($column = null) {
|
||||
$result = array();
|
||||
|
||||
while($record = $this->next()) {
|
||||
if($column) {
|
||||
|
||||
while ($record = $this->next()) {
|
||||
if ($column) {
|
||||
$result[] = $record[$column];
|
||||
} else {
|
||||
$result[] = $record[key($record)];
|
||||
@ -63,42 +57,38 @@ abstract class SS_Query implements Iterator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array containing all values in the leftmost column, where the
|
||||
* keys are the same as the values.
|
||||
*
|
||||
* Return an array containing all values in the leftmost column, where the keys are the
|
||||
* same as the values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keyedColumn() {
|
||||
$column = array();
|
||||
|
||||
foreach($this as $record) {
|
||||
foreach ($this as $record) {
|
||||
$val = $record[key($record)];
|
||||
$column[$val] = $val;
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map from the first column to the second column.
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function map() {
|
||||
$column = array();
|
||||
|
||||
foreach($this as $record) {
|
||||
foreach ($this as $record) {
|
||||
$key = reset($record);
|
||||
$val = next($record);
|
||||
$column[$key] = $val;
|
||||
}
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next record in the iterator.
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function record() {
|
||||
@ -107,72 +97,66 @@ abstract class SS_Query implements Iterator {
|
||||
|
||||
/**
|
||||
* Returns the first column of the first record.
|
||||
*
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function value() {
|
||||
$record = $this->next();
|
||||
|
||||
if($record) {
|
||||
return $record[key($record)];
|
||||
}
|
||||
if ($record) return $record[key($record)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an HTML table containing the full result-set.
|
||||
* Return an HTML table containing the full result-set
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function table() {
|
||||
$first = true;
|
||||
$result = "<table>\n";
|
||||
|
||||
foreach($this as $record) {
|
||||
if($first) {
|
||||
|
||||
foreach ($this as $record) {
|
||||
if ($first) {
|
||||
$result .= "<tr>";
|
||||
foreach($record as $k => $v) {
|
||||
foreach ($record as $k => $v) {
|
||||
$result .= "<th>" . Convert::raw2xml($k) . "</th> ";
|
||||
}
|
||||
$result .= "</tr> \n";
|
||||
}
|
||||
|
||||
$result .= "<tr>";
|
||||
foreach($record as $k => $v) {
|
||||
foreach ($record as $k => $v) {
|
||||
$result .= "<td>" . Convert::raw2xml($v) . "</td> ";
|
||||
}
|
||||
$result .= "</tr> \n";
|
||||
|
||||
|
||||
$first = false;
|
||||
}
|
||||
$result .= "</table>\n";
|
||||
|
||||
if($first) return "No records found";
|
||||
|
||||
if ($first) return "No records found";
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Rewind the iterator to the first item
|
||||
* and return it.
|
||||
*
|
||||
* Makes use of {@link seek()} and {@link numRecords()}, takes care of the
|
||||
* plumbing.
|
||||
*
|
||||
* Iterator function implementation. Rewind the iterator to the first item and return it.
|
||||
* Makes use of {@link seek()} and {@link numRecords()}, takes care of the plumbing.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rewind() {
|
||||
if($this->queryHasBegun && $this->numRecords() > 0) {
|
||||
if ($this->queryHasBegun && $this->numRecords() > 0) {
|
||||
$this->queryHasBegun = false;
|
||||
|
||||
return $this->seek(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Return the current item of the
|
||||
* iterator.
|
||||
*
|
||||
* Iterator function implementation. Return the current item of the iterator.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function current() {
|
||||
if(!$this->currentRecord) {
|
||||
if (!$this->currentRecord) {
|
||||
return $this->next();
|
||||
} else {
|
||||
return $this->currentRecord;
|
||||
@ -181,18 +165,17 @@ abstract class SS_Query implements Iterator {
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Return the first item of this iterator.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function first() {
|
||||
$this->rewind();
|
||||
|
||||
return $this->current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Return the row number of the current
|
||||
* item.
|
||||
*
|
||||
* Iterator function implementation. Return the row number of the current item.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function key() {
|
||||
@ -201,9 +184,8 @@ abstract class SS_Query implements Iterator {
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Return the next record in the iterator.
|
||||
*
|
||||
* Makes use of {@link nextRecord()}, takes care of the plumbing.
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function next() {
|
||||
@ -214,38 +196,33 @@ abstract class SS_Query implements Iterator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator function implementation. Check if the iterator is pointing to a
|
||||
* valid item.
|
||||
*
|
||||
* @return boolean
|
||||
* Iterator function implementation. Check if the iterator is pointing to a valid item.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid() {
|
||||
if(!$this->queryHasBegun) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
if (!$this->queryHasBegun) $this->next();
|
||||
return $this->currentRecord !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next record in the query result.
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function nextRecord();
|
||||
|
||||
/**
|
||||
* Return the total number of items in the query result.
|
||||
*
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract public function numRecords();
|
||||
|
||||
/**
|
||||
* Go to a specific row number in the query result and return the record.
|
||||
*
|
||||
* @param int $rowNum Tow number to go to.
|
||||
*
|
||||
*
|
||||
* @param int $rowNum Row number to go to.
|
||||
* @return array
|
||||
*/
|
||||
abstract public function seek($rowNum);
|
@ -23,7 +23,7 @@ class Boolean extends DBField {
|
||||
'arrayValue'=>$this->arrayValue
|
||||
);
|
||||
$values=Array('type'=>'boolean', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
public function Nice() {
|
||||
@ -62,26 +62,27 @@ class Boolean extends DBField {
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an encoding of the given value suitable for inclusion in a SQL statement.
|
||||
* If necessary, this should include quotes.
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if(strpos($value, '[')!==false)
|
||||
return Convert::raw2sql($value);
|
||||
else {
|
||||
if($value && strtolower($value) != 'f') {
|
||||
return "'1'";
|
||||
} else {
|
||||
return "'0'";
|
||||
}
|
||||
}
|
||||
public function nullValue() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function nullValue() {
|
||||
return "'0'";
|
||||
public function prepValueForDB($value) {
|
||||
if(is_bool($value)) {
|
||||
return $value ? 1 : 0;
|
||||
} else if(empty($value)) {
|
||||
return 0;
|
||||
} else if(is_string($value)){
|
||||
switch(strtolower($value)) {
|
||||
case 'false':
|
||||
case 'f':
|
||||
return 0;
|
||||
case 'true':
|
||||
case 't':
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return $value ? 1 : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -150,10 +150,10 @@ interface CompositeDBField {
|
||||
/**
|
||||
* Add all columns which are defined through {@link requireField()}
|
||||
* and {@link $composite_db}, or any additional SQL that is required
|
||||
* to get to these columns. Will mostly just write to the {@link SQLQuery->select}
|
||||
* to get to these columns. Will mostly just write to the {@link SQLSelect->select}
|
||||
* array.
|
||||
*
|
||||
* @param SQLQuery $query
|
||||
* @param SQLSelect $query
|
||||
*/
|
||||
public function addToQuery(&$query);
|
||||
|
||||
|
@ -69,6 +69,12 @@ abstract class DBField extends ViewableData {
|
||||
/**
|
||||
* Create a DBField object that's not bound to any particular field.
|
||||
* Useful for accessing the classes behaviour for other parts of your code.
|
||||
*
|
||||
* @param string $className class of field to construct
|
||||
* @param mixed $value value of field
|
||||
* @param string $name Name of field
|
||||
* @param mixed $object Additional parameter to pass to field constructor
|
||||
* @return DBField
|
||||
*/
|
||||
public static function create_field($className, $value, $name = null, $object = null) {
|
||||
$dbField = Object::create($className, $name, $object);
|
||||
@ -130,18 +136,24 @@ abstract class DBField extends ViewableData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an encoding of the given value suitable
|
||||
* for inclusion in a SQL statement. If necessary,
|
||||
* this should include quotes.
|
||||
* Return the transformed value ready to be sent to the database. This value
|
||||
* will be escaped automatically by the prepared query processor, so it
|
||||
* should not be escaped or quoted at all.
|
||||
*
|
||||
* The field values could also be in paramaterised format, such as
|
||||
* array('MAX(?,?)' => array(42, 69)), allowing the use of raw SQL values such as
|
||||
* array('NOW()' => array()).
|
||||
*
|
||||
* @see SQLWriteExpression::addAssignments for syntax examples
|
||||
*
|
||||
* @param $value mixed The value to check
|
||||
* @return string The encoded value
|
||||
* @return mixed The raw value, or escaped parameterised details
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if($value === null || $value === "" || $value === false) {
|
||||
return "null";
|
||||
return null;
|
||||
} else {
|
||||
return DB::getConn()->prepStringForDB($value);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,9 +235,11 @@ abstract class DBField extends ViewableData {
|
||||
/**
|
||||
* Returns the value to be set in the database to blank this field.
|
||||
* Usually it's a choice between null, 0, and ''
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function nullValue() {
|
||||
return "null";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -325,7 +325,7 @@ class Date extends DBField {
|
||||
public function requireField() {
|
||||
$parts=Array('datatype'=>'date', 'arrayValue'=>$this->arrayValue);
|
||||
$values=Array('type'=>'date', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -119,7 +119,7 @@ class SS_Datetime extends Date implements TemplateGlobalProvider {
|
||||
public function requireField() {
|
||||
$parts=Array('datatype'=>'datetime', 'arrayValue'=>$this->arrayValue);
|
||||
$values=Array('type'=>'SS_Datetime', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,7 +53,7 @@ class Decimal extends DBField {
|
||||
'parts' => $parts
|
||||
);
|
||||
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,28 +83,16 @@ class Decimal extends DBField {
|
||||
* @return float
|
||||
*/
|
||||
public function nullValue() {
|
||||
return "0.00";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an encoding of the given value suitable for inclusion in a SQL
|
||||
* statement. If necessary, this should include quotes.
|
||||
*
|
||||
* @param float $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if($value === true) {
|
||||
return 1;
|
||||
} if(!$value || !is_numeric($value)) {
|
||||
if(strpos($value, '[') === false) {
|
||||
return '0';
|
||||
} else {
|
||||
return Convert::raw2sql($value);
|
||||
}
|
||||
} else {
|
||||
return Convert::raw2sql($value);
|
||||
} elseif(empty($value) || !is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -9,10 +9,10 @@ class Double extends Float {
|
||||
public function requireField() {
|
||||
// HACK: MSSQL does not support double so we're using float instead
|
||||
// @todo This should go into MSSQLDatabase ideally somehow
|
||||
if(DB::getConn() instanceof MySQLDatabase) {
|
||||
DB::requireField($this->tableName, $this->name, "double");
|
||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
||||
DB::require_field($this->tableName, $this->name, "double");
|
||||
} else {
|
||||
DB::requireField($this->tableName, $this->name, "float");
|
||||
DB::require_field($this->tableName, $this->name, "float");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,10 +67,10 @@ class Enum extends StringField {
|
||||
public function requireField() {
|
||||
$parts = array(
|
||||
'datatype' => 'enum',
|
||||
'enums' => Convert::raw2sql($this->enum),
|
||||
'enums' => $this->enum,
|
||||
'character set' => 'utf8',
|
||||
'collate' => 'utf8_general_ci',
|
||||
'default' => Convert::raw2sql($this->default),
|
||||
'default' => $this->default,
|
||||
'table' => $this->tableName,
|
||||
'arrayValue' => $this->arrayValue
|
||||
);
|
||||
@ -80,7 +80,7 @@ class Enum extends StringField {
|
||||
'parts' => $parts
|
||||
);
|
||||
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,14 +14,14 @@ class Float extends DBField {
|
||||
}
|
||||
|
||||
public function requireField() {
|
||||
$parts=Array(
|
||||
$parts = Array(
|
||||
'datatype'=>'float',
|
||||
'null'=>'not null',
|
||||
'default'=>$this->defaultVal,
|
||||
'arrayValue'=>$this->arrayValue);
|
||||
|
||||
$values=Array('type'=>'float', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
'arrayValue'=>$this->arrayValue
|
||||
);
|
||||
$values = Array('type'=>'float', 'parts'=>$parts);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,31 +45,18 @@ class Float extends DBField {
|
||||
return new NumericField($this->name, $title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to be set in the database to blank this field.
|
||||
* Usually it's a choice between null, 0, and ''
|
||||
*/
|
||||
public function nullValue() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an encoding of the given value suitable for inclusion in a SQL statement.
|
||||
* If necessary, this should include quotes.
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if($value === true) {
|
||||
return 1;
|
||||
} elseif(empty($value) || !is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
if(!$value || !is_numeric($value)) {
|
||||
if(strpos($value, '[') === false) {
|
||||
return '0';
|
||||
} else {
|
||||
return Convert::raw2sql($value);
|
||||
}
|
||||
} else {
|
||||
return Convert::raw2sql($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,10 +20,6 @@ class Int extends DBField {
|
||||
return number_format($this->value);
|
||||
}
|
||||
|
||||
public function nullValue() {
|
||||
return "0";
|
||||
}
|
||||
|
||||
public function requireField() {
|
||||
$parts=Array(
|
||||
'datatype'=>'int',
|
||||
@ -33,7 +29,7 @@ class Int extends DBField {
|
||||
'arrayValue'=>$this->arrayValue);
|
||||
|
||||
$values=Array('type'=>'int', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
public function Times() {
|
||||
@ -51,22 +47,19 @@ class Int extends DBField {
|
||||
public function scaffoldFormField($title = null, $params = null) {
|
||||
return new NumericField($this->name, $title);
|
||||
}
|
||||
|
||||
public function nullValue() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an encoding of the given value suitable for inclusion in a SQL statement.
|
||||
* If necessary, this should include quotes.
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if($value === true) {
|
||||
return 1;
|
||||
} if(!$value || !is_numeric($value)) {
|
||||
if(strpos($value, '[')===false)
|
||||
return '0';
|
||||
else
|
||||
return Convert::raw2sql($value);
|
||||
} else {
|
||||
return Convert::raw2sql($value);
|
||||
} elseif(empty($value) || !is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ class Money extends DBField implements CompositeDBField {
|
||||
public function requireField() {
|
||||
$fields = $this->compositeDatabaseFields();
|
||||
if($fields) foreach($fields as $name => $type){
|
||||
DB::requireField($this->tableName, $this->name.$name, $type);
|
||||
DB::require_field($this->tableName, $this->name.$name, $type);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,13 +38,13 @@ class MultiEnum extends Enum {
|
||||
'enums'=>$this->enum,
|
||||
'character set'=>'utf8',
|
||||
'collate'=> 'utf8_general_ci',
|
||||
'default'=>Convert::raw2sql($this->default),
|
||||
'default'=> $this->default,
|
||||
'table'=>$this->tableName,
|
||||
'arrayValue'=>$this->arrayValue
|
||||
)
|
||||
);
|
||||
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
|
||||
}
|
||||
|
||||
|
@ -167,9 +167,9 @@ class PolymorphicForeignKey extends ForeignKey implements CompositeDBField {
|
||||
$classNames = ClassInfo::subclassesFor('DataObject');
|
||||
unset($classNames['DataObject']);
|
||||
|
||||
$db = DB::getConn();
|
||||
if($db->hasField($this->tableName, "{$this->name}Class")) {
|
||||
$existing = $db->query("SELECT DISTINCT \"{$this->name}Class\" FROM \"{$this->tableName}\"")->column();
|
||||
$schema = DB::get_schema();
|
||||
if($schema->hasField($this->tableName, "{$this->name}Class")) {
|
||||
$existing = DB::query("SELECT DISTINCT \"{$this->name}Class\" FROM \"{$this->tableName}\"")->column();
|
||||
$classNames = array_unique(array_merge($classNames, $existing));
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ abstract class StringField extends DBField {
|
||||
*/
|
||||
public function prepValueForDB($value) {
|
||||
if(!$this->nullifyEmpty && $value === '') {
|
||||
return DB::getConn()->prepStringForDB($value);
|
||||
return $value;
|
||||
} else {
|
||||
return parent::prepValueForDB($value);
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class Text extends StringField {
|
||||
'parts' => $parts
|
||||
);
|
||||
|
||||
DB::requireField($this->tableName, $this->name, $values, $this->default);
|
||||
DB::require_field($this->tableName, $this->name, $values, $this->default);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,7 +67,7 @@ class Time extends DBField {
|
||||
public function requireField() {
|
||||
$parts=Array('datatype'=>'time', 'arrayValue'=>$this->arrayValue);
|
||||
$values=Array('type'=>'time', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
public function scaffoldFormField($title = null, $params = null) {
|
||||
|
@ -63,7 +63,7 @@ class Varchar extends StringField {
|
||||
'parts' => $parts
|
||||
);
|
||||
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,7 +15,7 @@ class Year extends DBField {
|
||||
public function requireField() {
|
||||
$parts=Array('datatype'=>'year', 'precision'=>4, 'arrayValue'=>$this->arrayValue);
|
||||
$values=Array('type'=>'year', 'parts'=>$parts);
|
||||
DB::requireField($this->tableName, $this->name, $values);
|
||||
DB::require_field($this->tableName, $this->name, $values);
|
||||
}
|
||||
|
||||
public function scaffoldFormField($title = null, $params = null) {
|
||||
|
220
model/queries/SQLAssignmentRow.php
Normal file
220
model/queries/SQLAssignmentRow.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a list of updates / inserts made to a single row in a table
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLAssignmentRow {
|
||||
|
||||
/**
|
||||
* List of field values to store for this query
|
||||
*
|
||||
* Each item in this array will be in the form of a single-length array
|
||||
* in the format array('sql' => array($parameters)).
|
||||
* The field name is stored as the key
|
||||
*
|
||||
* E.g.
|
||||
*
|
||||
* <code>$assignments['ID'] = array('?' => array(1));</code>
|
||||
*
|
||||
* This allows for complex, parameterised updates, or explict field values set
|
||||
* without any prameters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $assignments = array();
|
||||
|
||||
/**
|
||||
* Instantiate a new SQLAssignmentRow object with the given values
|
||||
*
|
||||
* @param array $values
|
||||
*/
|
||||
function __construct(array $values = array()) {
|
||||
$this->setAssignments($values);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a key / value pair, extract the predicate and any potential paramaters
|
||||
* in a format suitable for storing internally as a list of paramaterised conditions.
|
||||
*
|
||||
* @param mixed $value Either a literal field value, or an array with
|
||||
* placeholder => parameter(s) as a pair
|
||||
* @return array A single item array in the format array($sql => array($parameters))
|
||||
*/
|
||||
protected function parseAssignment($value) {
|
||||
// Assume string values (or values saved as customised array objects)
|
||||
// represent simple assignment
|
||||
if(!is_array($value) || isset($value['type'])) {
|
||||
return array('?' => array($value));
|
||||
}
|
||||
|
||||
// If given as array then extract and check both the SQL as well as the parameter(s)
|
||||
// Note that there could be multiple parameters, e.g.
|
||||
// array('MAX(?,?)' => array(1,2)) although the container should
|
||||
// have a single item
|
||||
if(count($value) == 1) {
|
||||
foreach($value as $sql => $parameters) {
|
||||
if(!is_string($sql)) continue;
|
||||
if(!is_array($parameters)) $parameters = array($parameters);
|
||||
|
||||
// @todo Some input sanitisation checking the key contains the
|
||||
// correct number of ? placeholders as the number of parameters
|
||||
return array($sql => $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
user_error("Nested field assignments should be given as a single parameterised item array in "
|
||||
. "array('?' => array('value')) format)", E_USER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of assignments in any user-acceptible format, normalise the
|
||||
* value to a common array('SQL' => array(parameters)) format
|
||||
*
|
||||
* @param array $predicates List of assignments.
|
||||
* The key of this array should be the field name, and the value the assigned
|
||||
* literal value, or an array with parameterised information.
|
||||
* @return array List of normalised assignments
|
||||
*/
|
||||
protected function normaliseAssignments(array $assignments) {
|
||||
$normalised = array();
|
||||
foreach($assignments as $field => $value) {
|
||||
$normalised[$field] = $this->parseAssignment($value);
|
||||
}
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds assignments for a list of several fields
|
||||
*
|
||||
* Note that field values must not be escaped, as these will be internally
|
||||
* parameterised by the database engine.
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* // Basic assignments
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Title"' => 'Bob',
|
||||
* '"Object"."Description"' => 'Bob was here'
|
||||
* ))
|
||||
*
|
||||
* // Parameterised assignments
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Title"' => array('?' => 'Bob')),
|
||||
* '"Object"."Description"' => array('?' => null))
|
||||
* ))
|
||||
*
|
||||
* // Complex parameters
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Score"' => array('MAX(?,?)' => array(1, 3))
|
||||
* ));
|
||||
*
|
||||
* // Assigment of literal SQL for a field. The empty array is
|
||||
* // important to denote the zero-number paramater list
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Score"' => array('NOW()' => array())
|
||||
* ));
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param array $assignments The list of fields to assign
|
||||
* @return self The self reference to this row
|
||||
*/
|
||||
public function addAssignments(array $assignments) {
|
||||
$assignments = $this->normaliseAssignments($assignments);
|
||||
$this->assignments = array_merge($this->assignments, $assignments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of assignments to the given list
|
||||
*
|
||||
* @see SQLWriteExpression::addAssignments() for syntax examples
|
||||
*
|
||||
* @param array $assignments
|
||||
* @return self The self reference to this row
|
||||
*/
|
||||
public function setAssignments(array $assignments) {
|
||||
return $this->clear()->addAssignments($assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of assignments in parameterised format
|
||||
*
|
||||
* @return array List of assigments. The key of this array will be the
|
||||
* column to assign, and the value a parameterised array in the format
|
||||
* array('SQL' => array(parameters));
|
||||
*/
|
||||
public function getAssignments() {
|
||||
return $this->assignments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a single field
|
||||
*
|
||||
* E.g.
|
||||
* <code>
|
||||
*
|
||||
* // Literal assignment
|
||||
* $query->assign('"Object"."Description"', 'lorum ipsum');
|
||||
*
|
||||
* // Single parameter
|
||||
* $query->assign('"Object"."Title"', array('?' => 'Bob'));
|
||||
*
|
||||
* // Complex parameters
|
||||
* $query->assign('"Object"."Score"', array('MAX(?,?)' => array(1, 3));
|
||||
* </code>
|
||||
*
|
||||
* @param string $field The field name to update
|
||||
* @param mixed $value The value to assign to this field. This could be an
|
||||
* array containing a parameterised SQL query of any number of parameters,
|
||||
* or a single literal value.
|
||||
* @return self The self reference to this row
|
||||
*/
|
||||
public function assign($field, $value) {
|
||||
return $this->addAssignments(array($field => $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a value to a field using the literal SQL expression, rather than
|
||||
* a value to be escaped
|
||||
*
|
||||
* @param string $field The field name to update
|
||||
* @param string $sql The SQL to use for this update. E.g. "NOW()"
|
||||
* @return self The self reference to this row
|
||||
*/
|
||||
public function assignSQL($field, $sql) {
|
||||
return $this->assign($field, array($sql => array()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this assignment is empty
|
||||
*
|
||||
* @return boolean Flag indicating that this assignment is empty
|
||||
*/
|
||||
public function isEmpty() {
|
||||
return empty($this->assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of columns updated
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns() {
|
||||
return array_keys($this->assignments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all assignment values
|
||||
*
|
||||
* @return self The self reference to this row
|
||||
*/
|
||||
public function clear() {
|
||||
$this->assignments = array();
|
||||
return $this;
|
||||
}
|
||||
}
|
20
model/queries/SQLConditionGroup.php
Normal file
20
model/queries/SQLConditionGroup.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a where condition that is dynamically generated. Maybe be stored
|
||||
* within a list of conditions, altered, and be allowed to affect the result
|
||||
* of the parent sql query during future execution.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
interface SQLConditionGroup {
|
||||
|
||||
/**
|
||||
* Determines the resulting SQL along with parameters for the group
|
||||
*
|
||||
* @param array $parameters Out list of parameters
|
||||
* @return string The complete SQL string for this item
|
||||
*/
|
||||
function conditionSQL(&$parameters);
|
||||
}
|
735
model/queries/SQLConditionalExpression.php
Normal file
735
model/queries/SQLConditionalExpression.php
Normal file
@ -0,0 +1,735 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a SQL query for an expression which interacts with existing rows
|
||||
* (SELECT / DELETE / UPDATE) with a WHERE clause
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class SQLConditionalExpression extends SQLExpression {
|
||||
|
||||
/**
|
||||
* An array of WHERE clauses.
|
||||
*
|
||||
* Each item in this array will be in the form of a single-length array
|
||||
* in the format array('predicate' => array($parameters))
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $where = array();
|
||||
|
||||
/**
|
||||
* The logical connective used to join WHERE clauses. Defaults to AND.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $connective = 'AND';
|
||||
|
||||
/**
|
||||
* An array of tables. The first one is just the table name.
|
||||
* Used as the FROM in DELETE/SELECT statements, the INTO in INSERT statements,
|
||||
* and the target table in UPDATE statements
|
||||
*
|
||||
* The keys of this array are the aliases of the tables (unquoted), where the
|
||||
* values are either the literal table names, or an array with join details.
|
||||
*
|
||||
* @see SQLConditionalExpression::addLeftJoin()
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $from = array();
|
||||
|
||||
/**
|
||||
* Construct a new SQLInteractExpression.
|
||||
*
|
||||
* @param array|string $from An array of Tables (FROM clauses). The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
*/
|
||||
function __construct($from = array(), $where = array()) {
|
||||
$this->setFrom($from);
|
||||
$this->setWhere($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of tables to query from or update
|
||||
*
|
||||
* @example $query->setFrom("MyTable"); // SELECT * FROM MyTable
|
||||
*
|
||||
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
||||
* @return SQLSelect
|
||||
*/
|
||||
public function setFrom($from) {
|
||||
$this->from = array();
|
||||
return $this->addFrom($from);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function from($from) {
|
||||
Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead!');
|
||||
return $this->setFrom($from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a table to include in the query or update
|
||||
*
|
||||
* @example $query->addFrom("MyTable"); // UPDATE MyTable
|
||||
*
|
||||
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addFrom($from) {
|
||||
if(is_array($from)) {
|
||||
$this->from = array_merge($this->from, $from);
|
||||
} elseif(!empty($from)) {
|
||||
$this->from[str_replace(array('"','`'), '', $from)] = $from;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the connective property.
|
||||
*
|
||||
* @param string $value either 'AND' or 'OR'
|
||||
*/
|
||||
public function setConnective($value) {
|
||||
$this->connective = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connective property.
|
||||
*
|
||||
* @return string 'AND' or 'OR'
|
||||
*/
|
||||
public function getConnective() {
|
||||
return $this->connective;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
||||
*/
|
||||
public function useDisjunction() {
|
||||
$this->setConnective('OR');
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
|
||||
*/
|
||||
public function useConjunction() {
|
||||
$this->setConnective('AND');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT JOIN criteria to the tables list.
|
||||
*
|
||||
* @param string $table Unquoted table name
|
||||
* @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement, Needs to be valid
|
||||
* (quoted) SQL.
|
||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
||||
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||
* ORM have a value of 10.
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20) {
|
||||
if(!$tableAlias) {
|
||||
$tableAlias = $table;
|
||||
}
|
||||
$this->from[$tableAlias] = array(
|
||||
'type' => 'LEFT',
|
||||
'table' => $table,
|
||||
'filter' => array($onPredicate),
|
||||
'order' => $order
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function leftjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
Deprecation::notice('3.0', 'Please use addLeftJoin() instead!');
|
||||
$this->addLeftJoin($table, $onPredicate, $tableAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN criteria
|
||||
*
|
||||
* @param string $table Unquoted table name
|
||||
* @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. Needs to be
|
||||
* valid (quoted) SQL.
|
||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||
* @param int $order A numerical index to control the order that joins are added to the query; lower order
|
||||
* values will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||
* ORM have a value of 10.
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
if(!$tableAlias) $tableAlias = $table;
|
||||
$this->from[$tableAlias] = array(
|
||||
'type' => 'INNER',
|
||||
'table' => $table,
|
||||
'filter' => array($onPredicate),
|
||||
'order' => $order
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function innerjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||
Deprecation::notice('3.0', 'Please use addInnerJoin() instead!');
|
||||
return $this->addInnerJoin($table, $onPredicate, $tableAlias, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an additional filter (part of the ON clause) on a join.
|
||||
*
|
||||
* @param string $table Table to join on from the original join
|
||||
* @param string $filter The "ON" SQL fragment (escaped)
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addFilterToJoin($table, $filter) {
|
||||
$this->from[$table]['filter'][] = $filter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter (part of the ON clause) on a join.
|
||||
*
|
||||
* @param string $table Table to join on from the original join
|
||||
* @param string $filter The "ON" SQL fragment (escaped)
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setJoinFilter($table, $filter) {
|
||||
$this->from[$table]['filter'] = array($filter);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we are already joining to the given table alias
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isJoinedTo($tableAlias) {
|
||||
return isset($this->from[$tableAlias]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of tables that this query is selecting from.
|
||||
*
|
||||
* @return array Unquoted table names
|
||||
*/
|
||||
public function queriedTables() {
|
||||
$tables = array();
|
||||
|
||||
foreach($this->from as $key => $tableClause) {
|
||||
if(is_array($tableClause)) {
|
||||
$table = '"'.$tableClause['table'].'"';
|
||||
} else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) {
|
||||
$table = $matches[1];
|
||||
} else {
|
||||
$table = $tableClause;
|
||||
}
|
||||
|
||||
// Handle string replacements
|
||||
if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table);
|
||||
|
||||
$tables[] = preg_replace('/^"|"$/','',$table);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of tables queried
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFrom() {
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the finalised list of joins
|
||||
*
|
||||
* @todo This part of the code could be simplified
|
||||
*
|
||||
* @return array List of joins as a mapping from array('Alias' => 'Join Expression')
|
||||
*/
|
||||
public function getJoins() {
|
||||
// Sort the joins
|
||||
$joins = $this->getOrderedJoins($this->from);
|
||||
|
||||
// Build from clauses
|
||||
foreach($joins as $alias => $join) {
|
||||
// $join can be something like this array structure
|
||||
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
|
||||
// "Status = 'approved'", 'order' => 20))
|
||||
if(is_array($join)) {
|
||||
if(is_string($join['filter'])) {
|
||||
$filter = $join['filter'];
|
||||
} elseif(sizeof($join['filter']) == 1) {
|
||||
$filter = $join['filter'][0];
|
||||
} else {
|
||||
$filter = "(" . implode(") AND (", $join['filter']) . ")";
|
||||
}
|
||||
|
||||
$aliasClause = ($alias != $join['table']) ? " AS \"$alias\"" : "";
|
||||
$joins[$alias] = strtoupper($join['type']) . ' JOIN "' . $join['table'] . "\"$aliasClause ON $filter";
|
||||
}
|
||||
}
|
||||
|
||||
return $joins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
|
||||
* This prevents issues where developer-initiated JOINs attempt to JOIN using relations that haven't actually
|
||||
* yet been scaffolded by the framework. Demonstrated by PostGres in errors like:
|
||||
*"...ERROR: missing FROM-clause..."
|
||||
*
|
||||
* @param $from array - in the format of $this->from
|
||||
* @return array - and reorderded list of selects
|
||||
*/
|
||||
protected function getOrderedJoins($from) {
|
||||
// shift the first FROM table out from so we only deal with the JOINs
|
||||
$baseFrom = array_shift($from);
|
||||
$this->mergesort($from, function($firstJoin, $secondJoin) {
|
||||
if(
|
||||
!is_array($firstJoin)
|
||||
|| !is_array($secondJoin)
|
||||
|| $firstJoin['order'] == $secondJoin['order']
|
||||
) {
|
||||
return 0;
|
||||
} else {
|
||||
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Put the first FROM table back into the results
|
||||
array_unshift($from, $baseFrom);
|
||||
return $from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since uasort don't preserve the order of an array if the comparison is equal
|
||||
* we have to resort to a merge sort. It's quick and stable: O(n*log(n)).
|
||||
*
|
||||
* @see http://stackoverflow.com/q/4353739/139301
|
||||
*
|
||||
* @param array &$array - the array to sort
|
||||
* @param callable $cmpFunction - the function to use for comparison
|
||||
*/
|
||||
protected function mergesort(&$array, $cmpFunction = 'strcmp') {
|
||||
// Arrays of size < 2 require no action.
|
||||
if (count($array) < 2) {
|
||||
return;
|
||||
}
|
||||
// Split the array in half
|
||||
$halfway = count($array) / 2;
|
||||
$array1 = array_slice($array, 0, $halfway);
|
||||
$array2 = array_slice($array, $halfway);
|
||||
// Recurse to sort the two halves
|
||||
$this->mergesort($array1, $cmpFunction);
|
||||
$this->mergesort($array2, $cmpFunction);
|
||||
// If all of $array1 is <= all of $array2, just append them.
|
||||
if(call_user_func($cmpFunction, end($array1), reset($array2)) < 1) {
|
||||
$array = array_merge($array1, $array2);
|
||||
return;
|
||||
}
|
||||
// Merge the two sorted arrays into a single sorted array
|
||||
$array = array();
|
||||
$val1 = reset($array1);
|
||||
$val2 = reset($array2);
|
||||
do {
|
||||
if (call_user_func($cmpFunction, $val1, $val2) < 1) {
|
||||
$array[key($array1)] = $val1;
|
||||
$val1 = next($array1);
|
||||
} else {
|
||||
$array[key($array2)] = $val2;
|
||||
$val2 = next($array2);
|
||||
}
|
||||
} while($val1 && $val2);
|
||||
|
||||
// Merge the remainder
|
||||
while($val1) {
|
||||
$array[key($array1)] = $val1;
|
||||
$val1 = next($array1);
|
||||
}
|
||||
while($val2) {
|
||||
$array[key($array2)] = $val2;
|
||||
$val2 = next($array2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a WHERE clause.
|
||||
*
|
||||
* @see SQLSelect::addWhere() for syntax examples
|
||||
*
|
||||
* @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $where,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setWhere($where) {
|
||||
$where = func_num_args() > 1 ? func_get_args() : $where;
|
||||
$this->where = array();
|
||||
return $this->addWhere($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a WHERE clause.
|
||||
*
|
||||
* Note that the database will execute any parameterised queries using
|
||||
* prepared statements whenever available.
|
||||
*
|
||||
* There are several different ways of doing this.
|
||||
*
|
||||
* <code>
|
||||
* // the entire predicate as a single string
|
||||
* $query->addWhere("\"Column\" = 'Value'");
|
||||
*
|
||||
* // multiple predicates as an array
|
||||
* $query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
||||
*
|
||||
* // Shorthand for the above using argument expansion
|
||||
* $query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
|
||||
*
|
||||
* // multiple predicates with parameters
|
||||
* $query->addWhere(array('"Column" = ?' => $column, '"Name" = ?' => $value)));
|
||||
*
|
||||
* // Shorthand for simple column comparison (as above), omitting the '?'
|
||||
* $query->addWhere(array('"Column"' => $column, '"Name"' => $value));
|
||||
*
|
||||
* // Multiple predicates, each with multiple parameters.
|
||||
* $query->addWhere(array(
|
||||
* '"ColumnOne" = ? OR "ColumnTwo" != ?' => array(1, 4),
|
||||
* '"ID" != ?' => $value
|
||||
* ));
|
||||
*
|
||||
* // Using a dynamically generated condition (any object that implements SQLConditionGroup)
|
||||
* $condition = new ObjectThatImplements_SQLConditionGroup();
|
||||
* $query->addWhere($condition);
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* Note that if giving multiple parameters for a single predicate the array
|
||||
* of values must be given as an indexed array, not an associative array.
|
||||
*
|
||||
* Also should be noted is that any null values for parameters may give unexpected
|
||||
* behaviour. array('Column' => NULL) is shorthand for array('Column = ?', NULL), and
|
||||
* will not match null values for that column, as 'Column IS NULL' is the correct syntax.
|
||||
*
|
||||
* Additionally, be careful of key conflicts. Adding two predicates with the same
|
||||
* condition but different parameters can cause a key conflict if added in the same array.
|
||||
* This can be solved by wrapping each individual condition in an array. E.g.
|
||||
*
|
||||
* <code>
|
||||
* // Multiple predicates with duplicate conditions
|
||||
* $query->addWhere(array(
|
||||
* array('ID != ?' => 5),
|
||||
* array('ID != ?' => 6)
|
||||
* ));
|
||||
*
|
||||
* // Alternatively this can be added in two separate calls to addWhere
|
||||
* $query->addWhere(array('ID != ?' => 5));
|
||||
* $query->addWhere(array('ID != ?' => 6));
|
||||
*
|
||||
* // Or simply omit the outer array
|
||||
* $query->addWhere(array('ID != ?' => 5), array('ID != ?' => 6));
|
||||
* </code>
|
||||
*
|
||||
* If it's necessary to force the parameter to be considered as a specific data type
|
||||
* by the database connector's prepared query processor any parameter can be cast
|
||||
* to that type by using the following format.
|
||||
*
|
||||
* <code>
|
||||
* // Treat this value as a double type, regardless of its type within PHP
|
||||
* $query->addWhere(array(
|
||||
* 'Column' => array(
|
||||
* 'value' => $variable,
|
||||
* 'type' => 'double'
|
||||
* )
|
||||
* ));
|
||||
* </code>
|
||||
*
|
||||
* @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $where,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addWhere($where) {
|
||||
$where = $this->normalisePredicates(func_get_args());
|
||||
|
||||
// If the function is called with an array of items
|
||||
$this->where = array_merge($this->where, $where);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function where($where) {
|
||||
Deprecation::notice('3.0', 'Please use setWhere() or addWhere() instead!');
|
||||
return $this->setWhere($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function whereAny($where) {
|
||||
Deprecation::notice('3.0', 'Please use setWhereAny() or setWhereAny() instead!');
|
||||
return $this->setWhereAny($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SQLSelect::addWhere()
|
||||
*
|
||||
* @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $filters,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setWhereAny($filters) {
|
||||
$filters = func_num_args() > 1 ? func_get_args() : $filters;
|
||||
return $this
|
||||
->setWhere(array())
|
||||
->addWhereAny($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SQLSelect::addWhere()
|
||||
*
|
||||
* @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $filters,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addWhereAny($filters) {
|
||||
// Parse and split predicates along with any parameters
|
||||
$filters = $this->normalisePredicates(func_get_args());
|
||||
$this->splitQueryParameters($filters, $predicates, $parameters);
|
||||
|
||||
$clause = "(".implode(") OR (", $predicates).")";
|
||||
return $this->addWhere(array($clause => $parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of WHERE clauses used internally.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWhere() {
|
||||
return $this->where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of WHERE clauses used internally.
|
||||
*
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @return array
|
||||
*/
|
||||
public function getWhereParameterised(&$parameters) {
|
||||
$this->splitQueryParameters($this->where, $predicates, $parameters);
|
||||
return $predicates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a key / value pair, extract the predicate and any potential paramaters
|
||||
* in a format suitable for storing internally as a list of paramaterised conditions.
|
||||
*
|
||||
* @param string|integer $key The left hand (key index) of this condition.
|
||||
* Could be the predicate or an integer index.
|
||||
* @param mixed $value The The right hand (array value) of this condition.
|
||||
* Could be the predicate (if non-paramaterised), or the parameter(s). Could also be
|
||||
* an array containing a nested condition in the similar format this function outputs.
|
||||
* @return array|SQLConditionGroup A single item array in the format
|
||||
* array($predicate => array($parameters)), unless it's a SQLConditionGroup
|
||||
*/
|
||||
protected function parsePredicate($key, $value) {
|
||||
// If a string key is given then presume this is a paramaterised condition
|
||||
if($value instanceof SQLConditionGroup) {
|
||||
return $value;
|
||||
} elseif(is_string($key)) {
|
||||
|
||||
// Extract the parameter(s) from the value
|
||||
if(!is_array($value) || isset($value['type'])) {
|
||||
$parameters = array($value);
|
||||
} else {
|
||||
$parameters = array_values($value);
|
||||
}
|
||||
|
||||
// Append '= ?' if not present, parameters are given, and we have exactly one parameter
|
||||
if(strpos($key, '?') === FALSE) {
|
||||
$parameterCount = count($parameters);
|
||||
if($parameterCount === 1) {
|
||||
$key .= " = ?";
|
||||
} elseif($parameterCount > 1) {
|
||||
user_error("Incorrect number of '?' in predicate $key. Expected $parameterCount but none given.",
|
||||
E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
return array($key => $parameters);
|
||||
} elseif(is_array($value)) {
|
||||
|
||||
// If predicates are nested one per array (as per the internal format)
|
||||
// then run a quick check over the contents and recursively parse
|
||||
if(count($value) != 1) {
|
||||
user_error('Nested predicates should be given as a single item array in '
|
||||
. 'array($predicate => array($prameters)) format)', E_USER_ERROR);
|
||||
}
|
||||
foreach($value as $key => $value) {
|
||||
return $this->parsePredicate($key, $value);
|
||||
}
|
||||
} else {
|
||||
// Non-paramaterised condition
|
||||
return array($value => array());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of conditions in any user-acceptable format, convert this
|
||||
* to an array of paramaterised predicates suitable for merging with $this->where.
|
||||
*
|
||||
* Normalised predicates are in the below format, in order to avoid key collisions.
|
||||
*
|
||||
* <code>
|
||||
* array(
|
||||
* array('Condition != ?' => array('parameter')),
|
||||
* array('Condition != ?' => array('otherparameter')),
|
||||
* array('Condition = 3' => array()),
|
||||
* array('Condition = ? OR Condition = ?' => array('parameter1', 'parameter2))
|
||||
* )
|
||||
* </code>
|
||||
*
|
||||
* @param array $predicates List of predicates. These should be wrapped in an array
|
||||
* one level more than for addWhere, as query expansion is not supported here.
|
||||
* @return array List of normalised predicates
|
||||
*/
|
||||
protected function normalisePredicates(array $predicates) {
|
||||
// Since this function is called with func_get_args we should un-nest the single first parameter
|
||||
if(count($predicates) == 1) $predicates = array_shift($predicates);
|
||||
|
||||
// Ensure single predicates are iterable
|
||||
if(!is_array($predicates)) $predicates = array($predicates);
|
||||
|
||||
$normalised = array();
|
||||
foreach($predicates as $key => $value) {
|
||||
if(empty($value) && (empty($key) || is_numeric($key))) continue; // Ignore empty conditions
|
||||
$normalised[] = $this->parsePredicate($key, $value);
|
||||
}
|
||||
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of conditions as per the format of $this->where, split
|
||||
* this into an array of predicates, and a separate array of ordered parameters
|
||||
*
|
||||
* Note, that any SQLConditionGroup objects will be evaluated here.
|
||||
* @see SQLConditionGroup
|
||||
*
|
||||
* @param array $conditions List of Conditions including parameters
|
||||
* @param array $predicates Out parameter for the list of string predicates
|
||||
* @param array $parameters Out parameter for the list of parameters
|
||||
*/
|
||||
public function splitQueryParameters($conditions, &$predicates, &$parameters) {
|
||||
// Merge all filters with paramaterised queries
|
||||
$predicates = array();
|
||||
$parameters = array();
|
||||
foreach($conditions as $condition) {
|
||||
// Evaluate the result of SQLConditionGroup here
|
||||
if($condition instanceof SQLConditionGroup) {
|
||||
$conditionSQL = $condition->conditionSQL($conditionParameters);
|
||||
if(!empty($conditionSQL)) {
|
||||
$predicates[] = $conditionSQL;
|
||||
$parameters = array_merge($parameters, $conditionParameters);
|
||||
}
|
||||
} else {
|
||||
foreach($condition as $key => $value) {
|
||||
$predicates[] = $key;
|
||||
$parameters = array_merge($parameters, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this query is for a specific ID in a table
|
||||
*
|
||||
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function filtersOnID() {
|
||||
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/';
|
||||
|
||||
// @todo - Test this works with paramaterised queries
|
||||
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this query is filtering on a foreign key, ie finding a has_many relationship
|
||||
*
|
||||
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5")
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function filtersOnFK() {
|
||||
$regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/';
|
||||
|
||||
// @todo - Test this works with paramaterised queries
|
||||
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
||||
if(preg_match($regexp, $predicate)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return empty($this->from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQLDelete object using the currently specified parameters
|
||||
*
|
||||
* @return SQLDelete
|
||||
*/
|
||||
public function toDelete() {
|
||||
$delete = new SQLDelete();
|
||||
$this->copyTo($delete);
|
||||
return $delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQLSelect object using the currently specified parameters.
|
||||
*
|
||||
* @return SQLSelect
|
||||
*/
|
||||
public function toSelect() {
|
||||
$select = new SQLSelect();
|
||||
$this->copyTo($select);
|
||||
return $select;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQLUpdate object using the currently specified parameters.
|
||||
* No fields will have any assigned values for the newly generated SQLUpdate
|
||||
* object.
|
||||
*
|
||||
* @return SQLUpdate
|
||||
*/
|
||||
public function toUpdate() {
|
||||
$update = new SQLUpdate();
|
||||
$this->copyTo($update);
|
||||
return $update;
|
||||
}
|
||||
}
|
82
model/queries/SQLDelete.php
Normal file
82
model/queries/SQLDelete.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Object representing a SQL DELETE query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLDelete extends SQLConditionalExpression {
|
||||
|
||||
/**
|
||||
* List of tables to limit the delete to, if multiple tables
|
||||
* are specified in the condition clause
|
||||
*
|
||||
* @see http://dev.mysql.com/doc/refman/5.0/en/delete.html
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $delete = array();
|
||||
|
||||
/**
|
||||
* Construct a new SQLDelete.
|
||||
*
|
||||
* @param array|string $from An array of Tables (FROM clauses). The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
* @param array|string $delete The table(s) to delete, if multiple tables are queried from
|
||||
* @return self Self reference
|
||||
*/
|
||||
public static function create($from = array(), $where = array(), $delete = array()) {
|
||||
return Injector::inst()->createWithArgs(__CLASS__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new SQLDelete.
|
||||
*
|
||||
* @param array|string $from An array of Tables (FROM clauses). The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
* @param array|string $delete The table(s) to delete, if multiple tables are queried from
|
||||
*/
|
||||
function __construct($from = array(), $where = array(), $delete = array()) {
|
||||
parent::__construct($from, $where);
|
||||
$this->setDelete($delete);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of tables to limit the delete to, if multiple tables
|
||||
* are specified in the condition clause
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDelete() {
|
||||
return $this->delete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of tables to limit the delete to, if multiple tables
|
||||
* are specified in the condition clause
|
||||
*
|
||||
* @param string|array $tables Escaped SQL statement, usually an unquoted table name
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setDelete($tables) {
|
||||
$this->delete = array();
|
||||
return $this->addDelete($tables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of tables to limit the delete to, if multiple tables
|
||||
* are specified in the condition clause
|
||||
*
|
||||
* @param string|array $tables Escaped SQL statement, usually an unquoted table name
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addDelete($tables) {
|
||||
if(is_array($tables)) {
|
||||
$this->delete = array_merge($this->delete, $tables);
|
||||
} elseif(!empty($tables)) {
|
||||
$this->delete[str_replace(array('"','`'), '', $tables)] = $tables;
|
||||
}
|
||||
}
|
||||
}
|
145
model/queries/SQLExpression.php
Normal file
145
model/queries/SQLExpression.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Abstract base class for an object representing an SQL query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
abstract class SQLExpression {
|
||||
|
||||
/**
|
||||
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
||||
* query SQL.
|
||||
* @var array
|
||||
*/
|
||||
protected $replacementsOld = array();
|
||||
|
||||
/**
|
||||
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
||||
* query SQL.
|
||||
* @var array
|
||||
*/
|
||||
protected $replacementsNew = array();
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2
|
||||
*/
|
||||
public function __get($field) {
|
||||
Deprecation::notice('3.2', 'use get{Field} to get the necessary protected field\'s value');
|
||||
return $this->$field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2
|
||||
*/
|
||||
public function __set($field, $value) {
|
||||
Deprecation::notice('3.2', 'use set{Field} to set the necessary protected field\'s value');
|
||||
return $this->$field = $value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Swap some text in the SQL query with another.
|
||||
*
|
||||
* Note that values in parameters will not be replaced
|
||||
*
|
||||
* @param string $old The old text (escaped)
|
||||
* @param string $new The new text (escaped)
|
||||
*/
|
||||
public function replaceText($old, $new) {
|
||||
$this->replacementsOld[] = $old;
|
||||
$this->replacementsNew[] = $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the generated SQL string for this query
|
||||
*
|
||||
* @todo Is it ok for this to consider parameters? Test cases here!
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
try {
|
||||
$sql = $this->sql($parameters);
|
||||
if(!empty($parameters)) {
|
||||
$sql .= " <" . var_export($parameters, true) . ">";
|
||||
}
|
||||
return $sql;
|
||||
} catch(Exception $e) {
|
||||
return "<sql query>";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the use of one table with another.
|
||||
*
|
||||
* @param string $old Name of the old table (unquoted, escaped)
|
||||
* @param string $new Name of the new table (unquoted, escaped)
|
||||
*/
|
||||
public function renameTable($old, $new) {
|
||||
$this->replaceText("`$old`", "`$new`");
|
||||
$this->replaceText("\"$old\"", "\"$new\"");
|
||||
$this->replaceText(Convert::symbol2sql($old), Convert::symbol2sql($new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this query is empty, and thus cannot be executed
|
||||
*
|
||||
* @return bool Flag indicating that this query is empty
|
||||
*/
|
||||
abstract public function isEmpty();
|
||||
|
||||
/**
|
||||
* Generate the SQL statement for this query.
|
||||
*
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @return string The completed SQL query
|
||||
*/
|
||||
public function sql(&$parameters = array()) {
|
||||
if(func_num_args() == 0) {
|
||||
Deprecation::notice(
|
||||
'3.2',
|
||||
'SQLExpression::sql() now may produce parameters which are necessary to execute this query'
|
||||
);
|
||||
}
|
||||
|
||||
// Build each component as needed
|
||||
$sql = DB::build_sql($this, $parameters);
|
||||
|
||||
if(empty($sql)) return null;
|
||||
|
||||
if($this->replacementsOld) {
|
||||
$sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this query.
|
||||
*
|
||||
* @return SS_Query
|
||||
*/
|
||||
public function execute() {
|
||||
$sql = $this->sql($parameters);
|
||||
return DB::prepared_query($sql, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the query parameters contained in this object to another
|
||||
* SQLExpression
|
||||
*
|
||||
* @param SQLExpression $expression The object to copy properties to
|
||||
*/
|
||||
protected function copyTo(SQLExpression $object) {
|
||||
$target = array_keys(get_object_vars($object));
|
||||
foreach(get_object_vars($this) as $variable => $value) {
|
||||
if(in_array($variable, $target)) {
|
||||
$object->$variable = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
model/queries/SQLInsert.php
Normal file
195
model/queries/SQLInsert.php
Normal file
@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Object representing a SQL INSERT query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLInsert extends SQLExpression implements SQLWriteExpression {
|
||||
|
||||
/**
|
||||
* List of rows to be inserted
|
||||
*
|
||||
* @var array[SQLAssignmentRow]
|
||||
*/
|
||||
protected $rows = array();
|
||||
|
||||
/**
|
||||
* The table name to insert into
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $into = null;
|
||||
|
||||
/**
|
||||
* Construct a new SQLInsert object
|
||||
*
|
||||
* @param string $into Table name to insert into
|
||||
* @param array $assignments List of column assignments
|
||||
* @return static
|
||||
*/
|
||||
public static function create($into = null, $assignments = array()) {
|
||||
return Injector::inst()->createWithArgs(__CLASS__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new SQLInsert object
|
||||
*
|
||||
* @param string $into Table name to insert into
|
||||
* @param array $assignments List of column assignments
|
||||
*/
|
||||
function __construct($into = null, $assignments = array()) {
|
||||
$this->setInto($into);
|
||||
if(!empty($assignments)) {
|
||||
$this->setAssignments($assignments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the table name to insert into
|
||||
*
|
||||
* @param string $into Single table name
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function setInto($into) {
|
||||
$this->into = $into;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table name to insert into
|
||||
*
|
||||
* @return string Single table name
|
||||
*/
|
||||
public function getInto() {
|
||||
return $this->into;
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return empty($this->into) || empty($this->rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a new row to insert
|
||||
*
|
||||
* @param array|SQLAssignmentRow $data A list of data to include for this row
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function addRow($data = null) {
|
||||
// Clear existing empty row
|
||||
if(($current = $this->currentRow()) && $current->isEmpty()) {
|
||||
array_pop($this->rows);
|
||||
}
|
||||
|
||||
// Append data
|
||||
if($data instanceof SQLAssignmentRow) {
|
||||
$this->rows[] = $data;
|
||||
} else {
|
||||
$this->rows[] = new SQLAssignmentRow($data);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current list of rows
|
||||
*
|
||||
* @return array[SQLAssignmentRow]
|
||||
*/
|
||||
public function getRows() {
|
||||
return $this->rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of distinct column names used in this insert
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns() {
|
||||
$columns = array();
|
||||
foreach($this->getRows() as $row) {
|
||||
$columns = array_merge($columns, $row->getColumns());
|
||||
}
|
||||
return array_unique($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all rows to the given array
|
||||
*
|
||||
* @param array $rows the list of rows
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function setRows(array $rows) {
|
||||
return $this->clear()->addRows($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the list of rows to the array
|
||||
*
|
||||
* @param array $rows the list of rows
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function addRows(array $rows) {
|
||||
foreach($rows as $row) $this->addRow($row);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently set row
|
||||
*
|
||||
* @param boolean $create Flag to indicate if a row should be created if none exists
|
||||
* @return SQLAssignmentRow|false The row, or false if none exists
|
||||
*/
|
||||
public function currentRow($create = false) {
|
||||
$current = end($this->rows);
|
||||
if($create && !$current) {
|
||||
$this->rows[] = $current = new SQLAssignmentRow();
|
||||
}
|
||||
return $current;
|
||||
}
|
||||
|
||||
public function addAssignments(array $assignments) {
|
||||
$this->currentRow(true)->addAssignments($assignments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAssignments(array $assignments) {
|
||||
$this->currentRow(true)->setAssignments($assignments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAssignments() {
|
||||
return $this->currentRow(true)->getAssignments();
|
||||
}
|
||||
|
||||
public function assign($field, $value) {
|
||||
$this->currentRow(true)->assign($field, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assignSQL($field, $sql) {
|
||||
$this->currentRow(true)->assignSQL($field, $sql);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all currently set assigment values on the current row
|
||||
*
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function clearRow() {
|
||||
$this->currentRow(true)->clear();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all rows
|
||||
*
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function clear() {
|
||||
$this->rows = array();
|
||||
return $this;
|
||||
}
|
||||
}
|
39
model/queries/SQLQuery.php
Normal file
39
model/queries/SQLQuery.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Object representing a SQL SELECT query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
* @deprecated since version 3.3
|
||||
*/
|
||||
class SQLQuery extends SQLSelect {
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.3
|
||||
*/
|
||||
public function __construct($select = "*", $from = array(), $where = array(), $orderby = array(),
|
||||
$groupby = array(), $having = array(), $limit = array()
|
||||
) {
|
||||
parent::__construct($select, $from, $where, $orderby, $groupby, $having, $limit);
|
||||
Deprecation::notice('3.3', 'Use SQLSelect instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2
|
||||
*/
|
||||
public function setDelete($value) {
|
||||
$message = 'SQLQuery->setDelete no longer works. Create a SQLDelete object instead, or use toDelete()';
|
||||
Deprecation::notice('3.2', $message);
|
||||
throw new BadMethodCallException($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.2
|
||||
*/
|
||||
public function getDelete() {
|
||||
Deprecation::notice('3.2', 'Use SQLDelete object instead');
|
||||
return false;
|
||||
}
|
||||
}
|
709
model/queries/SQLSelect.php
Normal file
709
model/queries/SQLSelect.php
Normal file
@ -0,0 +1,709 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Object representing a SQL SELECT query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLSelect extends SQLConditionalExpression {
|
||||
|
||||
/**
|
||||
* An array of SELECT fields, keyed by an optional alias.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $select = array();
|
||||
|
||||
/**
|
||||
* An array of GROUP BY clauses.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $groupby = array();
|
||||
|
||||
/**
|
||||
* An array of having clauses.
|
||||
* Each item in this array will be in the form of a single-length array
|
||||
* in the format array('predicate' => array($parameters))
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $having = array();
|
||||
|
||||
/**
|
||||
* If this is true DISTINCT will be added to the SQL.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $distinct = false;
|
||||
|
||||
/**
|
||||
* An array of ORDER BY clauses, functions. Stores as an associative
|
||||
* array of column / function to direction.
|
||||
*
|
||||
* May be used on SELECT or single table DELETE queries in some adapters
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $orderby = array();
|
||||
|
||||
/**
|
||||
* An array containing limit and offset keys for LIMIT clause.
|
||||
*
|
||||
* May be used on SELECT or single table DELETE queries in some adapters
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $limit = array();
|
||||
|
||||
/**
|
||||
* Construct a new SQLSelect.
|
||||
*
|
||||
* @param array $select An array of SELECT fields.
|
||||
* @param array|string $from An array of FROM clauses. The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
* @param array $orderby An array ORDER BY clause.
|
||||
* @param array $groupby An array of GROUP BY clauses.
|
||||
* @param array $having An array of HAVING clauses.
|
||||
* @param array|string $limit A LIMIT clause or array with limit and offset keys
|
||||
* @return static
|
||||
*/
|
||||
public static function create($select = "*", $from = array(), $where = array(), $orderby = array(),
|
||||
$groupby = array(), $having = array(), $limit = array()) {
|
||||
return Injector::inst()->createWithArgs(__CLASS__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new SQLSelect.
|
||||
*
|
||||
* @param array $select An array of SELECT fields.
|
||||
* @param array|string $from An array of FROM clauses. The first one should be just the table name.
|
||||
* @param array $where An array of WHERE clauses.
|
||||
* @param array $orderby An array ORDER BY clause.
|
||||
* @param array $groupby An array of GROUP BY clauses.
|
||||
* @param array $having An array of HAVING clauses.
|
||||
* @param array|string $limit A LIMIT clause or array with limit and offset keys
|
||||
*/
|
||||
public function __construct($select = "*", $from = array(), $where = array(), $orderby = array(),
|
||||
$groupby = array(), $having = array(), $limit = array()) {
|
||||
|
||||
parent::__construct($from, $where);
|
||||
|
||||
$this->setSelect($select);
|
||||
$this->setOrderBy($orderby);
|
||||
$this->setGroupBy($groupby);
|
||||
$this->setHaving($having);
|
||||
$this->setLimit($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of columns to be selected by the query.
|
||||
*
|
||||
* <code>
|
||||
* // pass fields to select as single parameter array
|
||||
* $query->setSelect(array("Col1","Col2"))->setFrom("MyTable");
|
||||
*
|
||||
* // pass fields to select as multiple parameters
|
||||
* $query->setSelect("Col1", "Col2")->setFrom("MyTable");
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @param bool $clear Clear existing select fields?
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setSelect($fields) {
|
||||
$this->select = array();
|
||||
if (func_num_args() > 1) {
|
||||
$fields = func_get_args();
|
||||
} else if(!is_array($fields)) {
|
||||
$fields = array($fields);
|
||||
}
|
||||
return $this->addSelect($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the list of columns to be selected by the query.
|
||||
*
|
||||
* <code>
|
||||
* // pass fields to select as single parameter array
|
||||
* $query->addSelect(array("Col1","Col2"))->setFrom("MyTable");
|
||||
*
|
||||
* // pass fields to select as multiple parameters
|
||||
* $query->addSelect("Col1", "Col2")->setFrom("MyTable");
|
||||
* </code>
|
||||
*
|
||||
* @param string|array $fields
|
||||
* @param bool $clear Clear existing select fields?
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addSelect($fields) {
|
||||
if (func_num_args() > 1) {
|
||||
$fields = func_get_args();
|
||||
} else if(!is_array($fields)) {
|
||||
$fields = array($fields);
|
||||
}
|
||||
|
||||
foreach($fields as $idx => $field) {
|
||||
if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $field, $matches)) {
|
||||
Deprecation::notice("3.0", "Use selectField() to specify column aliases");
|
||||
$this->selectField($matches[1], $matches[2]);
|
||||
} else {
|
||||
$this->selectField($field, is_numeric($idx) ? null : $idx);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function select($fields) {
|
||||
Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead!');
|
||||
$this->setSelect($fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an additional field.
|
||||
*
|
||||
* @param $field string The field to select (escaped SQL statement)
|
||||
* @param $alias string The alias of that field (escaped SQL statement).
|
||||
* Defaults to the unquoted column name of the $field parameter.
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function selectField($field, $alias = null) {
|
||||
if(!$alias) {
|
||||
if(preg_match('/"([^"]+)"$/', $field, $matches)) $alias = $matches[1];
|
||||
else $alias = $field;
|
||||
}
|
||||
$this->select[$alias] = $field;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SQL expression for the given field alias.
|
||||
* Returns null if the given alias doesn't exist.
|
||||
* See {@link selectField()} for details on alias generation.
|
||||
*
|
||||
* @param string $field
|
||||
* @return string
|
||||
*/
|
||||
public function expressionForField($field) {
|
||||
return isset($this->select[$field]) ? $this->select[$field] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set distinct property.
|
||||
*
|
||||
* @param bool $value
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setDistinct($value) {
|
||||
$this->distinct = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the distinct property.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getDistinct() {
|
||||
return $this->distinct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limit property.
|
||||
* @return array
|
||||
*/
|
||||
public function getLimit() {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass LIMIT clause either as SQL snippet or in array format.
|
||||
* Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
|
||||
*
|
||||
* @param int|string|array $limit If passed as a string or array, assumes SQL escaped data.
|
||||
* Only applies for positive values, or if an $offset is set as well.
|
||||
* @param int $offset
|
||||
* @throws InvalidArgumentException
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setLimit($limit, $offset = 0) {
|
||||
if((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) {
|
||||
throw new InvalidArgumentException("SQLSelect::setLimit() only takes positive values");
|
||||
}
|
||||
|
||||
if(is_numeric($limit) && ($limit || $offset)) {
|
||||
$this->limit = array(
|
||||
'start' => $offset,
|
||||
'limit' => $limit,
|
||||
);
|
||||
} else if($limit && is_string($limit)) {
|
||||
if(strpos($limit, ',') !== false) {
|
||||
list($start, $innerLimit) = explode(',', $limit, 2);
|
||||
}
|
||||
else {
|
||||
list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2);
|
||||
}
|
||||
|
||||
$this->limit = array(
|
||||
'start' => trim($start),
|
||||
'limit' => trim($innerLimit),
|
||||
);
|
||||
} else {
|
||||
$this->limit = $limit;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function limit($limit, $offset = 0) {
|
||||
Deprecation::notice('3.0', 'Please use setLimit() instead!');
|
||||
return $this->setLimit($limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ORDER BY clause either as SQL snippet or in array format.
|
||||
*
|
||||
* @example $sql->setOrderBy("Column");
|
||||
* @example $sql->setOrderBy("Column DESC");
|
||||
* @example $sql->setOrderBy("Column DESC, ColumnTwo ASC");
|
||||
* @example $sql->setOrderBy("Column", "DESC");
|
||||
* @example $sql->setOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
||||
*
|
||||
* @param string|array $clauses Clauses to add (escaped SQL statement)
|
||||
* @param string $dir Sort direction, ASC or DESC
|
||||
*
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setOrderBy($clauses = null, $direction = null) {
|
||||
$this->orderby = array();
|
||||
return $this->addOrderBy($clauses, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ORDER BY clause either as SQL snippet or in array format.
|
||||
*
|
||||
* @example $sql->addOrderBy("Column");
|
||||
* @example $sql->addOrderBy("Column DESC");
|
||||
* @example $sql->addOrderBy("Column DESC, ColumnTwo ASC");
|
||||
* @example $sql->addOrderBy("Column", "DESC");
|
||||
* @example $sql->addOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
||||
*
|
||||
* @param string|array $clauses Clauses to add (escaped SQL statements)
|
||||
* @param string $direction Sort direction, ASC or DESC
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addOrderBy($clauses = null, $direction = null) {
|
||||
if(empty($clauses)) return $this;
|
||||
|
||||
if(is_string($clauses)) {
|
||||
if(strpos($clauses, "(") !== false) {
|
||||
$sort = preg_split("/,(?![^()]*+\\))/", $clauses);
|
||||
} else {
|
||||
$sort = explode(",", $clauses);
|
||||
}
|
||||
|
||||
$clauses = array();
|
||||
|
||||
foreach($sort as $clause) {
|
||||
list($column, $direction) = $this->getDirectionFromString($clause, $direction);
|
||||
$clauses[$column] = $direction;
|
||||
}
|
||||
}
|
||||
|
||||
if(is_array($clauses)) {
|
||||
foreach($clauses as $key => $value) {
|
||||
if(!is_numeric($key)) {
|
||||
$column = trim($key);
|
||||
$columnDir = strtoupper(trim($value));
|
||||
} else {
|
||||
list($column, $columnDir) = $this->getDirectionFromString($value);
|
||||
}
|
||||
|
||||
$this->orderby[$column] = $columnDir;
|
||||
}
|
||||
} else {
|
||||
user_error('SQLSelect::orderby() incorrect format for $orderby', E_USER_WARNING);
|
||||
}
|
||||
|
||||
// If sort contains a public function call, let's move the sort clause into a
|
||||
// separate selected field.
|
||||
//
|
||||
// Some versions of MySQL choke if you have a group public function referenced
|
||||
// directly in the ORDER BY
|
||||
if($this->orderby) {
|
||||
$i = 0;
|
||||
$orderby = array();
|
||||
foreach($this->orderby as $clause => $dir) {
|
||||
|
||||
// public function calls and multi-word columns like "CASE WHEN ..."
|
||||
if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) {
|
||||
|
||||
// Move the clause to the select fragment, substituting a placeholder column in the sort fragment.
|
||||
$clause = trim($clause);
|
||||
$column = "_SortColumn{$i}";
|
||||
$this->selectField($clause, $column);
|
||||
$clause = '"' . $column . '"';
|
||||
$i++;
|
||||
}
|
||||
$orderby[$clause] = $dir;
|
||||
}
|
||||
$this->orderby = $orderby;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function orderby($clauses = null, $direction = null) {
|
||||
Deprecation::notice('3.0', 'Please use setOrderBy() instead!');
|
||||
return $this->setOrderBy($clauses, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the direction part of a single-column order by clause.
|
||||
*
|
||||
* @param String
|
||||
* @param String
|
||||
* @return Array A two element array: array($column, $direction)
|
||||
*/
|
||||
private function getDirectionFromString($value, $defaultDirection = null) {
|
||||
if(preg_match('/^(.*)(asc|desc)$/i', $value, $matches)) {
|
||||
$column = trim($matches[1]);
|
||||
$direction = strtoupper($matches[2]);
|
||||
} else {
|
||||
$column = $value;
|
||||
$direction = $defaultDirection ? $defaultDirection : "ASC";
|
||||
}
|
||||
return array($column, $direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current order by as array if not already. To handle legacy
|
||||
* statements which are stored as strings. Without clauses and directions,
|
||||
* convert the orderby clause to something readable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOrderBy() {
|
||||
$orderby = $this->orderby;
|
||||
if(!$orderby) $orderby = array();
|
||||
|
||||
if(!is_array($orderby)) {
|
||||
// spilt by any commas not within brackets
|
||||
$orderby = preg_split('/,(?![^()]*+\\))/', $orderby);
|
||||
}
|
||||
|
||||
foreach($orderby as $k => $v) {
|
||||
if(strpos($v, ' ') !== false) {
|
||||
unset($orderby[$k]);
|
||||
|
||||
$rule = explode(' ', trim($v));
|
||||
$clause = $rule[0];
|
||||
$dir = (isset($rule[1])) ? $rule[1] : 'ASC';
|
||||
|
||||
$orderby[$clause] = $dir;
|
||||
}
|
||||
}
|
||||
|
||||
return $orderby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the order by clause by replacing ASC or DESC references in the
|
||||
* current order by with it's corollary.
|
||||
*
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function reverseOrderBy() {
|
||||
$order = $this->getOrderBy();
|
||||
$this->orderby = array();
|
||||
|
||||
foreach($order as $clause => $dir) {
|
||||
$dir = (strtoupper($dir) == 'DESC') ? 'ASC' : 'DESC';
|
||||
$this->addOrderBy($clause, $dir);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a GROUP BY clause.
|
||||
*
|
||||
* @param string|array $groupby Escaped SQL statement
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setGroupBy($groupby) {
|
||||
$this->groupby = array();
|
||||
return $this->addGroupBy($groupby);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a GROUP BY clause.
|
||||
*
|
||||
* @param string|array $groupby Escaped SQL statement
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addGroupBy($groupby) {
|
||||
if(is_array($groupby)) {
|
||||
$this->groupby = array_merge($this->groupby, $groupby);
|
||||
} elseif(!empty($groupby)) {
|
||||
$this->groupby[] = $groupby;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function groupby($where) {
|
||||
Deprecation::notice('3.0', 'Please use setGroupBy() or addHaving() instead!');
|
||||
return $this->setGroupBy($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a HAVING clause.
|
||||
*
|
||||
* @see SQLSelect::addWhere() for syntax examples
|
||||
*
|
||||
* @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $having,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setHaving($having) {
|
||||
$having = func_num_args() > 1 ? func_get_args() : $having;
|
||||
$this->having = array();
|
||||
return $this->addHaving($having);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HAVING clause
|
||||
*
|
||||
* @see SQLSelect::addWhere() for syntax examples
|
||||
*
|
||||
* @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
||||
* @param mixed $having,... Unlimited additional predicates
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addHaving($having) {
|
||||
$having = $this->normalisePredicates(func_get_args());
|
||||
|
||||
// If the function is called with an array of items
|
||||
$this->having = array_merge($this->having, $having);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since version 3.0
|
||||
*/
|
||||
public function having($having) {
|
||||
Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead!');
|
||||
return $this->setHaving($having);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of HAVING clauses used internally.
|
||||
* @return array
|
||||
*/
|
||||
public function getHaving() {
|
||||
return $this->having;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of HAVING clauses used internally.
|
||||
*
|
||||
* @param array $parameters Out variable for parameters required for this query
|
||||
* @return array
|
||||
*/
|
||||
public function getHavingParameterised(&$parameters) {
|
||||
$this->splitQueryParameters($this->having, $conditions, $parameters);
|
||||
return $conditions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of GROUP BY clauses used internally.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getGroupBy() {
|
||||
return $this->groupby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an itemised select list as a map, where keys are the aliases, and values are the column sources.
|
||||
* Aliases will always be provided (if the alias is implicit, the alias value will be inferred), and won't be
|
||||
* quoted.
|
||||
* E.g., 'Title' => '"SiteTree"."Title"'.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSelect() {
|
||||
return $this->select;
|
||||
}
|
||||
|
||||
/// VARIOUS TRANSFORMATIONS BELOW
|
||||
|
||||
/**
|
||||
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
|
||||
*
|
||||
* @param string $column
|
||||
* @return int
|
||||
*/
|
||||
public function unlimitedRowCount($column = null) {
|
||||
// we can't clear the select if we're relying on its output by a HAVING clause
|
||||
if(count($this->having)) {
|
||||
$records = $this->execute();
|
||||
return $records->numRecords();
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->limit = null;
|
||||
$clone->orderby = null;
|
||||
|
||||
// Choose a default column
|
||||
if($column == null) {
|
||||
if($this->groupby) {
|
||||
// @todo Test case required here
|
||||
$countQuery = new SQLSelect();
|
||||
$countQuery->select("count(*)");
|
||||
$countQuery->addFrom(array('(' . $clone->sql($innerParameters) . ') all_distinct'));
|
||||
$sql = $countQuery->sql($parameters); // $parameters should be empty
|
||||
$result = DB::prepared_query($sql, $innerParameters);
|
||||
return $result->value();
|
||||
} else {
|
||||
$clone->setSelect(array("count(*)"));
|
||||
}
|
||||
} else {
|
||||
$clone->setSelect(array("count($column)"));
|
||||
}
|
||||
|
||||
$clone->setGroupBy(array());;
|
||||
return $clone->execute()->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this query can be sorted by the given field.
|
||||
*
|
||||
* @param string $fieldName
|
||||
* @return bool
|
||||
*/
|
||||
public function canSortBy($fieldName) {
|
||||
$fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
|
||||
|
||||
return isset($this->select[$fieldName]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
|
||||
*
|
||||
* @todo Respect HAVING and GROUPBY, which can affect the result-count
|
||||
*
|
||||
* @param string $column Quoted, escaped column name
|
||||
* @return int
|
||||
*/
|
||||
public function count( $column = null) {
|
||||
// Choose a default column
|
||||
if($column == null) {
|
||||
if($this->groupby) {
|
||||
$column = 'DISTINCT ' . implode(", ", $this->groupby);
|
||||
} else {
|
||||
$column = '*';
|
||||
}
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->select = array('Count' => "count($column)");
|
||||
$clone->limit = null;
|
||||
$clone->orderby = null;
|
||||
$clone->groupby = null;
|
||||
|
||||
$count = $clone->execute()->value();
|
||||
// If there's a limit set, then that limit is going to heavily affect the count
|
||||
if($this->limit) {
|
||||
if($count >= ($this->limit['start'] + $this->limit['limit'])) {
|
||||
return $this->limit['limit'];
|
||||
} else {
|
||||
return max(0, $count - $this->limit['start']);
|
||||
}
|
||||
|
||||
// Otherwise, the count is going to be the output of the SQL query
|
||||
} else {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new SQLSelect that calls the given aggregate functions on this data.
|
||||
*
|
||||
* @param string $column An aggregate expression, such as 'MAX("Balance")', or a set of them
|
||||
* (as an escaped SQL statement)
|
||||
* @param string $alias An optional alias for the aggregate column.
|
||||
* @return SQLSelect A clone of this object with the given aggregate function
|
||||
*/
|
||||
public function aggregate($column, $alias = null) {
|
||||
|
||||
$clone = clone $this;
|
||||
|
||||
// don't set an ORDER BY clause if no limit has been set. It doesn't make
|
||||
// sense to add an ORDER BY if there is no limit, and it will break
|
||||
// queries to databases like MSSQL if you do so. Note that the reason
|
||||
// this came up is because DataQuery::initialiseQuery() introduces
|
||||
// a default sort.
|
||||
if($this->limit) {
|
||||
$clone->setLimit($this->limit);
|
||||
$clone->setOrderBy($this->orderby);
|
||||
} else {
|
||||
$clone->setOrderBy(array());
|
||||
}
|
||||
|
||||
$clone->setGroupBy($this->groupby);
|
||||
if($alias) {
|
||||
$clone->setSelect(array());
|
||||
$clone->selectField($column, $alias);
|
||||
} else {
|
||||
$clone->setSelect($column);
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query that returns only the first row of this query
|
||||
*
|
||||
* @return SQLSelect A clone of this object with the first row only
|
||||
*/
|
||||
public function firstRow() {
|
||||
$query = clone $this;
|
||||
$offset = $this->limit ? $this->limit['start'] : 0;
|
||||
$query->setLimit(1, $offset);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query that returns only the last row of this query
|
||||
*
|
||||
* @return SQLSelect A clone of this object with the last row only
|
||||
*/
|
||||
public function lastRow() {
|
||||
$query = clone $this;
|
||||
$offset = $this->limit ? $this->limit['start'] : 0;
|
||||
|
||||
// Limit index to start in case of empty results
|
||||
$index = max($this->count() + $offset - 1, 0);
|
||||
$query->setLimit(1, $index);
|
||||
return $query;
|
||||
}
|
||||
}
|
101
model/queries/SQLUpdate.php
Normal file
101
model/queries/SQLUpdate.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Object representing a SQL UPDATE query.
|
||||
* The various parts of the SQL query can be manipulated individually.
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
class SQLUpdate extends SQLConditionalExpression implements SQLWriteExpression {
|
||||
|
||||
/**
|
||||
* The assignment to create for this update
|
||||
*
|
||||
* @var SQLAssignmentRow
|
||||
*/
|
||||
protected $assignment = null;
|
||||
|
||||
/**
|
||||
* Construct a new SQLUpdate object
|
||||
*
|
||||
* @param string $table Table name to update
|
||||
* @param array $assignment List of column assignments
|
||||
* @param array $where List of where clauses
|
||||
* @return static
|
||||
*/
|
||||
public static function create($table = null, $assignment = array(), $where = array()) {
|
||||
return Injector::inst()->createWithArgs(__CLASS__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new SQLUpdate object
|
||||
*
|
||||
* @param string $table Table name to update
|
||||
* @param array $assignment List of column assignments
|
||||
* @param array $where List of where clauses
|
||||
*/
|
||||
function __construct($table = null, $assignment = array(), $where = array()) {
|
||||
parent::__construct(null, $where);
|
||||
$this->assignment = new SQLAssignmentRow();
|
||||
$this->setTable($table);
|
||||
$this->setAssignments($assignment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the table name to update
|
||||
*
|
||||
* @param string $table
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setTable($table) {
|
||||
return $this->setFrom($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table name to update
|
||||
*
|
||||
* @return string Name of the table
|
||||
*/
|
||||
public function getTable() {
|
||||
return reset($this->from);
|
||||
}
|
||||
|
||||
public function addAssignments(array $assignments) {
|
||||
$this->assignment->addAssignments($assignments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAssignments(array $assignments) {
|
||||
$this->assignment->setAssignments($assignments);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAssignments() {
|
||||
return $this->assignment->getAssignments();
|
||||
}
|
||||
|
||||
public function assign($field, $value) {
|
||||
$this->assignment->assign($field, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assignSQL($field, $sql) {
|
||||
$this->assignment->assignSQL($field, $sql);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all currently set assigment values
|
||||
*
|
||||
* @return self The self reference to this query
|
||||
*/
|
||||
public function clear() {
|
||||
$this->assignment->clear();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return empty($this->assignment) || $this->assignment->isEmpty() || parent::isEmpty();
|
||||
}
|
||||
}
|
112
model/queries/SQLWriteExpression.php
Normal file
112
model/queries/SQLWriteExpression.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a SQL expression which may have field values assigned
|
||||
* (UPDATE/INSERT Expressions)
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage model
|
||||
*/
|
||||
interface SQLWriteExpression {
|
||||
|
||||
/**
|
||||
* Adds assignments for a list of several fields.
|
||||
*
|
||||
* For multi-row objects this applies this to the current row.
|
||||
*
|
||||
* Note that field values must not be escaped, as these will be internally
|
||||
* parameterised by the database engine.
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* // Basic assignments
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Title"' => 'Bob',
|
||||
* '"Object"."Description"' => 'Bob was here'
|
||||
* ))
|
||||
*
|
||||
* // Parameterised assignments
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Title"' => array('?' => 'Bob')),
|
||||
* '"Object"."Description"' => array('?' => null))
|
||||
* ))
|
||||
*
|
||||
* // Complex parameters
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Score"' => array('MAX(?,?)' => array(1, 3))
|
||||
* ));
|
||||
*
|
||||
* // Assigment of literal SQL for a field. The empty array is
|
||||
* // important to denote the zero-number paramater list
|
||||
* $query->addAssignments(array(
|
||||
* '"Object"."Score"' => array('NOW()' => array())
|
||||
* ));
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param array $assignments The list of fields to assign
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function addAssignments(array $assignments);
|
||||
|
||||
/**
|
||||
* Sets the list of assignments to the given list
|
||||
*
|
||||
* For multi-row objects this applies this to the current row.
|
||||
*
|
||||
* @see SQLWriteExpression::addAssignments() for syntax examples
|
||||
*
|
||||
* @param array $assignments
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function setAssignments(array $assignments);
|
||||
|
||||
/**
|
||||
* Retrieves the list of assignments in parameterised format
|
||||
*
|
||||
* For multi-row objects returns assignments for the current row.
|
||||
*
|
||||
* @return array List of assigments. The key of this array will be the
|
||||
* column to assign, and the value a parameterised array in the format
|
||||
* array('SQL' => array(parameters));
|
||||
*/
|
||||
public function getAssignments();
|
||||
|
||||
/**
|
||||
* Set the value for a single field
|
||||
*
|
||||
* For multi-row objects this applies this to the current row.
|
||||
*
|
||||
* E.g.
|
||||
* <code>
|
||||
*
|
||||
* // Literal assignment
|
||||
* $query->assign('"Object"."Description"', 'lorum ipsum'));
|
||||
*
|
||||
* // Single parameter
|
||||
* $query->assign('"Object"."Title"', array('?' => 'Bob'));
|
||||
*
|
||||
* // Complex parameters
|
||||
* $query->assign('"Object"."Score"', array('MAX(?,?)' => array(1, 3));
|
||||
* </code>
|
||||
*
|
||||
* @param string $field The field name to update
|
||||
* @param mixed $value The value to assign to this field. This could be an
|
||||
* array containing a parameterised SQL query of any number of parameters,
|
||||
* or a single literal value.
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function assign($field, $value);
|
||||
|
||||
/**
|
||||
* Assigns a value to a field using the literal SQL expression, rather than
|
||||
* a value to be escaped
|
||||
*
|
||||
* For multi-row objects this applies this to the current row.
|
||||
*
|
||||
* @param string $field The field name to update
|
||||
* @param string $sql The SQL to use for this update. E.g. "NOW()"
|
||||
* @return self Self reference
|
||||
*/
|
||||
public function assignSQL($field, $sql);
|
||||
}
|
@ -44,7 +44,7 @@ class FulltextSearchable extends DataExtension {
|
||||
public static function enable($searchableClasses = array('SiteTree', 'File')) {
|
||||
$defaultColumns = array(
|
||||
'SiteTree' => '"Title","MenuTitle","Content","MetaDescription"',
|
||||
'File' => '"Filename","Title","Content"'
|
||||
'File' => '"Title","Filename","Content"'
|
||||
);
|
||||
|
||||
if(!is_array($searchableClasses)) $searchableClasses = array($searchableClasses);
|
||||
@ -52,11 +52,14 @@ class FulltextSearchable extends DataExtension {
|
||||
if(!class_exists($class)) continue;
|
||||
|
||||
if(isset($defaultColumns[$class])) {
|
||||
Config::inst()->update($class, 'create_table_options', array('MySQLDatabase' => 'ENGINE=MyISAM'));
|
||||
Config::inst()->update(
|
||||
$class, 'create_table_options', array(MySQLSchemaManager::ID => 'ENGINE=MyISAM')
|
||||
);
|
||||
$class::add_extension("FulltextSearchable('{$defaultColumns[$class]}')");
|
||||
} else {
|
||||
throw new Exception(
|
||||
"FulltextSearchable::enable() I don't know the default search columns for class '$class'");
|
||||
"FulltextSearchable::enable() I don't know the default search columns for class '$class'"
|
||||
);
|
||||
}
|
||||
}
|
||||
self::$searchable_classes = $searchableClasses;
|
||||
|
@ -5,7 +5,7 @@
|
||||
* SearchContext is intentionally decoupled from any controller-logic,
|
||||
* it just receives a set of search parameters and an object class it acts on.
|
||||
*
|
||||
* The default output of a SearchContext is either a {@link SQLQuery} object
|
||||
* The default output of a SearchContext is either a {@link SQLSelect} object
|
||||
* for further refinement, or a {@link SS_List} that can be used to display
|
||||
* search results, e.g. in a {@link TableListField} instance.
|
||||
*
|
||||
@ -88,7 +88,7 @@ class SearchContext extends Object {
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo move to SQLQuery
|
||||
* @todo move to SQLSelect
|
||||
* @todo fix hack
|
||||
*/
|
||||
protected function applyBaseTableFields() {
|
||||
|
@ -38,17 +38,11 @@ abstract class ComparisonFilter extends SearchFilter {
|
||||
*/
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$value = $this->getDbFormattedValue();
|
||||
|
||||
if(is_numeric($value)) {
|
||||
$filter = sprintf("%s %s %s",
|
||||
$this->getDbName(), $this->getOperator(), Convert::raw2sql($value));
|
||||
} else {
|
||||
$filter = sprintf("%s %s '%s'",
|
||||
$this->getDbName(), $this->getOperator(), Convert::raw2sql($value));
|
||||
}
|
||||
|
||||
return $query->where($filter);
|
||||
$predicate = sprintf("%s %s ?", $this->getDbName(), $this->getOperator());
|
||||
return $query->where(array(
|
||||
$predicate => $this->getDbFormattedValue()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,17 +54,11 @@ abstract class ComparisonFilter extends SearchFilter {
|
||||
*/
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$value = $this->getDbFormattedValue();
|
||||
|
||||
if(is_numeric($value)) {
|
||||
$filter = sprintf("%s %s %s",
|
||||
$this->getDbName(), $this->getInverseOperator(), Convert::raw2sql($value));
|
||||
} else {
|
||||
$filter = sprintf("%s %s '%s'",
|
||||
$this->getDbName(), $this->getInverseOperator(), Convert::raw2sql($value));
|
||||
}
|
||||
|
||||
return $query->where($filter);
|
||||
$predicate = sprintf("%s %s ?", $this->getDbName(), $this->getInverseOperator());
|
||||
return $query->where(array(
|
||||
$predicate => $this->getDbFormattedValue()
|
||||
));
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
|
@ -16,100 +16,9 @@
|
||||
* @package framework
|
||||
* @subpackage search
|
||||
*/
|
||||
class EndsWithFilter extends SearchFilter {
|
||||
class EndsWithFilter extends PartialMatchFilter {
|
||||
|
||||
public function setModifiers(array $modifiers) {
|
||||
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
|
||||
throw new InvalidArgumentException(
|
||||
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
|
||||
}
|
||||
|
||||
parent::setModifiers($modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a match on the trailing characters of a field value.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($this->getValue()),
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
return $query->where($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a match on the trailing characters of a field value.
|
||||
* Matches against one of the many values.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function applyMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$connectives = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$connectives[] = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($value),
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
}
|
||||
$whereClause = implode(' OR ', $connectives);
|
||||
return $query->where($whereClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes a match on the trailing characters of a field value.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($this->getValue()),
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
return $query->where($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes a match on the trailing characters of a field value.
|
||||
* Excludes a field if it matches any of the values.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function excludeMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$connectives = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$connectives[] = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($value),
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
}
|
||||
$whereClause = implode(' AND ', $connectives);
|
||||
return $query->where($whereClause);
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
|
||||
protected function getMatchPattern($value) {
|
||||
return "%$value";
|
||||
}
|
||||
}
|
||||
|
@ -31,15 +31,15 @@ class ExactMatchFilter extends SearchFilter {
|
||||
*/
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$where = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue()),
|
||||
null,
|
||||
true, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
return $query->where($where);
|
||||
return $query->where(array($where => $this->getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,30 +50,30 @@ class ExactMatchFilter extends SearchFilter {
|
||||
*/
|
||||
protected function applyMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$values = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$values[] = Convert::raw2sql($value);
|
||||
}
|
||||
if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) {
|
||||
$valueStr = "'" . implode("', '", $values) . "'";
|
||||
return $query->where(sprintf(
|
||||
'%s IN (%s)',
|
||||
$this->getDbName(),
|
||||
$valueStr
|
||||
$caseSensitive = $this->getCaseSensitive();
|
||||
$values = $this->getValue();
|
||||
if($caseSensitive === null) {
|
||||
// For queries using the default collation (no explicit case) we can use the WHERE .. IN .. syntax,
|
||||
// providing simpler SQL than many WHERE .. OR .. fragments.
|
||||
$column = $this->getDbName();
|
||||
$placeholders = DB::placeholders($values);
|
||||
return $query->where(array(
|
||||
"$column IN ($placeholders)" => $values
|
||||
));
|
||||
} else {
|
||||
foreach($values as &$v) {
|
||||
$v = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
$v,
|
||||
true, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
$whereClause = array();
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
null,
|
||||
true, // exact?
|
||||
false, // negate?
|
||||
$caseSensitive,
|
||||
true
|
||||
);
|
||||
foreach($values as $value) {
|
||||
$whereClause[] = array($comparisonClause => $value);
|
||||
}
|
||||
$where = implode(' OR ', $values);
|
||||
return $query->where($where);
|
||||
return $query->whereAny($whereClause);
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,15 +84,15 @@ class ExactMatchFilter extends SearchFilter {
|
||||
*/
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$where = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue()),
|
||||
null,
|
||||
true, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
return $query->where($where);
|
||||
return $query->where(array($where => $this->getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,30 +103,30 @@ class ExactMatchFilter extends SearchFilter {
|
||||
*/
|
||||
protected function excludeMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$values = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$values[] = Convert::raw2sql($value);
|
||||
}
|
||||
if(!in_array('case', $modifiers) && !in_array('nocase', $modifiers)) {
|
||||
$valueStr = "'" . implode("', '", $values) . "'";
|
||||
return $query->where(sprintf(
|
||||
'%s NOT IN (%s)',
|
||||
$this->getDbName(),
|
||||
$valueStr
|
||||
$caseSensitive = $this->getCaseSensitive();
|
||||
$values = $this->getValue();
|
||||
if($caseSensitive === null) {
|
||||
// For queries using the default collation (no explicit case) we can use the WHERE .. NOT IN .. syntax,
|
||||
// providing simpler SQL than many WHERE .. AND .. fragments.
|
||||
$column = $this->getDbName();
|
||||
$placeholders = DB::placeholders($values);
|
||||
return $query->where(array(
|
||||
"$column NOT IN ($placeholders)" => $values
|
||||
));
|
||||
} else {
|
||||
foreach($values as &$v) {
|
||||
$v = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
$v,
|
||||
true, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
}
|
||||
$where = implode(' OR ', $values);
|
||||
return $query->where($where);
|
||||
// Generate reusable comparison clause
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
null,
|
||||
true, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
// Since query connective is ambiguous, use AND explicitly here
|
||||
$count = count($values);
|
||||
$predicate = implode(' AND ', array_fill(0, $count, $comparisonClause));
|
||||
return $query->where(array($predicate => $values));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,19 +28,13 @@
|
||||
class FulltextFilter extends SearchFilter {
|
||||
|
||||
protected function applyOne(DataQuery $query) {
|
||||
return $query->where(sprintf(
|
||||
"MATCH (%s) AGAINST ('%s')",
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue())
|
||||
));
|
||||
$predicate = sprintf("MATCH (%s) AGAINST (?)", $this->getDbName());
|
||||
return $query->where(array($predicate => $this->getValue()));
|
||||
}
|
||||
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
return $query->where(sprintf(
|
||||
"NOT MATCH (%s) AGAINST ('%s')",
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue())
|
||||
));
|
||||
$predicate = sprintf("NOT MATCH (%s) AGAINST (?)", $this->getDbName());
|
||||
return $query->where(array($predicate => $this->getValue()));
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
|
@ -21,66 +21,82 @@ class PartialMatchFilter extends SearchFilter {
|
||||
parent::setModifiers($modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the match filter to the given variable value
|
||||
*
|
||||
* @param string $value The raw value
|
||||
* @return string
|
||||
*/
|
||||
protected function getMatchPattern($value) {
|
||||
return "%$value%";
|
||||
}
|
||||
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($this->getValue()) . '%',
|
||||
null,
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
|
||||
return $query->where($where);
|
||||
return $query->where(array(
|
||||
$comparisonClause => $this->getMatchPattern($this->getValue())
|
||||
));
|
||||
}
|
||||
|
||||
protected function applyMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$where = array();
|
||||
$modifiers = $this->getModifiers();
|
||||
$whereClause = array();
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
null,
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
foreach($this->getValue() as $value) {
|
||||
$where[]= DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($value) . '%',
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
$whereClause[] = array($comparisonClause => $this->getMatchPattern($value));
|
||||
}
|
||||
|
||||
return $query->where(implode(' OR ', $where));
|
||||
return $query->whereAny($whereClause);
|
||||
}
|
||||
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($this->getValue()) . '%',
|
||||
null,
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
|
||||
return $query->where($where);
|
||||
return $query->where(array(
|
||||
$comparisonClause => $this->getMatchPattern($this->getValue())
|
||||
));
|
||||
}
|
||||
|
||||
protected function excludeMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$where = array();
|
||||
$modifiers = $this->getModifiers();
|
||||
foreach($this->getValue() as $value) {
|
||||
$where[]= DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
'%' . Convert::raw2sql($value) . '%',
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
$values = $this->getValue();
|
||||
$comparisonClause = DB::get_conn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
null,
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive(),
|
||||
true
|
||||
);
|
||||
$parameters = array();
|
||||
foreach($values as $value) {
|
||||
$parameters[] = $this->getMatchPattern($value);
|
||||
}
|
||||
|
||||
return $query->where(implode(' AND ', $where));
|
||||
// Since query connective is ambiguous, use AND explicitly here
|
||||
$count = count($values);
|
||||
$predicate = implode(' AND ', array_fill(0, $count, $comparisonClause));
|
||||
return $query->where(array($predicate => $parameters));
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
|
@ -86,9 +86,9 @@ abstract class SearchFilter extends Object {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current value to be filtered on.
|
||||
* Set the current value(s) to be filtered on.
|
||||
*
|
||||
* @param string $value
|
||||
* @param string|array $value
|
||||
*/
|
||||
public function setValue($value) {
|
||||
$this->value = $value;
|
||||
@ -96,9 +96,8 @@ abstract class SearchFilter extends Object {
|
||||
|
||||
/**
|
||||
* Accessor for the current value to be filtered on.
|
||||
* Caution: Data is not escaped.
|
||||
*
|
||||
* @return string
|
||||
* @return string|array
|
||||
*/
|
||||
public function getValue() {
|
||||
return $this->value;
|
||||
@ -172,8 +171,9 @@ abstract class SearchFilter extends Object {
|
||||
$candidateClass = $this->model;
|
||||
|
||||
while($candidateClass != 'DataObject') {
|
||||
if(DataObject::has_own_table($candidateClass)
|
||||
&& singleton($candidateClass)->hasOwnTableDatabaseField($this->name)) {
|
||||
if( DataObject::has_own_table($candidateClass)
|
||||
&& DataObject::has_own_table_database_field($candidateClass, $this->name)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -16,100 +16,9 @@
|
||||
* @package framework
|
||||
* @subpackage search
|
||||
*/
|
||||
class StartsWithFilter extends SearchFilter {
|
||||
class StartsWithFilter extends PartialMatchFilter {
|
||||
|
||||
public function setModifiers(array $modifiers) {
|
||||
if(($extras = array_diff($modifiers, array('not', 'nocase', 'case'))) != array()) {
|
||||
throw new InvalidArgumentException(
|
||||
get_class($this) . ' does not accept ' . implode(', ', $extras) . ' as modifiers');
|
||||
}
|
||||
|
||||
parent::setModifiers($modifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a match on the starting characters of a field value.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue()) . '%',
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
return $query->where($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a match on the starting characters of a field value.
|
||||
* Matches against one of the many values.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function applyMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$connectives = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$connectives[] = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($value) . '%',
|
||||
false, // exact?
|
||||
false, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
}
|
||||
$whereClause = implode(' OR ', $connectives);
|
||||
return $query->where($whereClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes a match on the starting characters of a field value.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$where = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->getValue()) . '%',
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
return $query->where($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes a match on the starting characters of a field value.
|
||||
* Excludes a field if it matches any of the values.
|
||||
*
|
||||
* @return DataQuery
|
||||
*/
|
||||
protected function excludeMany(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
$modifiers = $this->getModifiers();
|
||||
$connectives = array();
|
||||
foreach($this->getValue() as $value) {
|
||||
$connectives[] = DB::getConn()->comparisonClause(
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($value) . '%',
|
||||
false, // exact?
|
||||
true, // negate?
|
||||
$this->getCaseSensitive()
|
||||
);
|
||||
}
|
||||
$whereClause = implode(' AND ', $connectives);
|
||||
return $query->where($whereClause);
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return $this->getValue() === array() || $this->getValue() === null || $this->getValue() === '';
|
||||
protected function getMatchPattern($value) {
|
||||
return "$value%";
|
||||
}
|
||||
}
|
||||
|
@ -27,23 +27,23 @@ class WithinRangeFilter extends SearchFilter {
|
||||
|
||||
protected function applyOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
return $query->where(sprintf(
|
||||
"%s >= '%s' AND %s <= '%s'",
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->min),
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->max)
|
||||
$predicate = sprintf('%1$s >= ? AND %1$s <= ?', $this->getDbName());
|
||||
return $query->where(array(
|
||||
$predicate => array(
|
||||
$this->min,
|
||||
$this->max
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
protected function excludeOne(DataQuery $query) {
|
||||
$this->model = $query->applyRelation($this->relation);
|
||||
return $query->where(sprintf(
|
||||
"%s < '%s' OR %s > '%s'",
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->min),
|
||||
$this->getDbName(),
|
||||
Convert::raw2sql($this->max)
|
||||
$predicate = sprintf('%1$s < ? OR %1$s > ?', $this->getDbName());
|
||||
return $query->where(array(
|
||||
$predicate => array(
|
||||
$this->min,
|
||||
$this->max
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,10 @@ class Group extends DataObject {
|
||||
public function getAllChildren() {
|
||||
$doSet = new ArrayList();
|
||||
|
||||
if ($children = DataObject::get('Group', '"ParentID" = '.$this->ID)) {
|
||||
foreach($children as $child) {
|
||||
$doSet->push($child);
|
||||
$doSet->merge($child->getAllChildren());
|
||||
}
|
||||
$children = DataObject::get('Group')->filter("ParentID", $this->ID);
|
||||
foreach($children as $child) {
|
||||
$doSet->push($child);
|
||||
$doSet->merge($child->getAllChildren());
|
||||
}
|
||||
|
||||
return $doSet;
|
||||
@ -183,9 +182,10 @@ class Group extends DataObject {
|
||||
);
|
||||
|
||||
// Add roles (and disable all checkboxes for inherited roles)
|
||||
$allRoles = Permission::check('ADMIN')
|
||||
? DataObject::get('PermissionRole')
|
||||
: DataObject::get('PermissionRole', 'OnlyAdminCanApply = 0');
|
||||
$allRoles = PermissionRole::get();
|
||||
if(Permission::check('ADMIN')) {
|
||||
$allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
|
||||
}
|
||||
if($this->ID) {
|
||||
$groupRoles = $this->Roles();
|
||||
$inheritedRoles = new ArrayList();
|
||||
@ -298,11 +298,10 @@ class Group extends DataObject {
|
||||
|
||||
while($chunkToAdd) {
|
||||
$familyIDs = array_merge($familyIDs,$chunkToAdd);
|
||||
$idList = implode(',', $chunkToAdd);
|
||||
|
||||
// Get the children of *all* the groups identified in the previous chunk.
|
||||
// This minimises the number of SQL queries necessary
|
||||
$chunkToAdd = Group::get()->where("\"ParentID\" IN ($idList)")->column('ID');
|
||||
// This minimises the number of SQL queries necessary
|
||||
$chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID');
|
||||
}
|
||||
|
||||
return $familyIDs;
|
||||
@ -331,11 +330,10 @@ class Group extends DataObject {
|
||||
* Override this so groups are ordered in the CMS
|
||||
*/
|
||||
public function stageChildren() {
|
||||
return DataObject::get(
|
||||
'Group',
|
||||
"\"Group\".\"ParentID\" = " . (int)$this->ID . " AND \"Group\".\"ID\" != " . (int)$this->ID,
|
||||
'"Sort"'
|
||||
);
|
||||
return Group::get()
|
||||
->filter("ParentID", $this->ID)
|
||||
->exclude("ID", $this->ID)
|
||||
->sort('"Sort"');
|
||||
}
|
||||
|
||||
public function getTreeTitle() {
|
||||
|
@ -28,10 +28,9 @@ class GroupCsvBulkLoader extends CsvBulkLoader {
|
||||
// are imported to avoid missing "early" references to parents
|
||||
// which are imported later on in the CSV file.
|
||||
if(isset($record['ParentCode']) && $record['ParentCode']) {
|
||||
$parentGroup = DataObject::get_one(
|
||||
'Group',
|
||||
sprintf('"Code" = \'%s\'', Convert::raw2sql($record['ParentCode']))
|
||||
);
|
||||
$parentGroup = DataObject::get_one('Group', array(
|
||||
'"Group"."Code"' => $record['ParentCode']
|
||||
));
|
||||
if($parentGroup) {
|
||||
$group->ParentID = $parentGroup->ID;
|
||||
$group->write();
|
||||
@ -42,14 +41,10 @@ class GroupCsvBulkLoader extends CsvBulkLoader {
|
||||
// existing permissions arent cleared.
|
||||
if(isset($record['PermissionCodes']) && $record['PermissionCodes']) {
|
||||
foreach(explode(',', $record['PermissionCodes']) as $code) {
|
||||
$p = DataObject::get_one(
|
||||
'Permission',
|
||||
sprintf(
|
||||
'"Code" = \'%s\' AND "GroupID" = %d',
|
||||
Convert::raw2sql($code),
|
||||
$group->ID
|
||||
)
|
||||
);
|
||||
$p = DataObject::get_one('Permission', array(
|
||||
'"Permission"."Code"' => $code,
|
||||
'"Permission"."GroupID"' => $group->ID
|
||||
));
|
||||
if(!$p) {
|
||||
$p = new Permission(array('Code' => $code));
|
||||
$p->write();
|
||||
|
@ -404,7 +404,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
}
|
||||
|
||||
// Don't set column if its not built yet (the login might be precursor to a /dev/build...)
|
||||
if(array_key_exists('LockedOutUntil', DB::fieldList('Member'))) {
|
||||
if(array_key_exists('LockedOutUntil', DB::field_list('Member'))) {
|
||||
$this->LockedOutUntil = null;
|
||||
}
|
||||
|
||||
@ -443,9 +443,8 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
|
||||
if(strpos(Cookie::get('alc_enc'), ':') && !Session::get("loggedInAs")) {
|
||||
list($uid, $token) = explode(':', Cookie::get('alc_enc'), 2);
|
||||
$SQL_uid = Convert::raw2sql($uid);
|
||||
|
||||
$member = DataObject::get_one("Member", "\"Member\".\"ID\" = '$SQL_uid'");
|
||||
|
||||
$member = DataObject::get_by_id("Member", $uid);
|
||||
|
||||
// check if autologin token matches
|
||||
if($member) {
|
||||
@ -536,7 +535,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$generator = new RandomGenerator();
|
||||
$token = $generator->randomToken();
|
||||
$hash = $this->encryptWithUserSettings($token);
|
||||
} while(DataObject::get_one('Member', "\"AutoLoginHash\" = '$hash'"));
|
||||
} while(DataObject::get_one('Member', array(
|
||||
'"Member"."AutoLoginHash"' => $hash
|
||||
)));
|
||||
|
||||
$this->AutoLoginHash = $hash;
|
||||
$this->AutoLoginExpired = date('Y-m-d', time() + (86400 * $lifetime));
|
||||
@ -555,30 +556,27 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
*/
|
||||
public function validateAutoLoginToken($autologinToken) {
|
||||
$hash = $this->encryptWithUserSettings($autologinToken);
|
||||
|
||||
$member = DataObject::get_one(
|
||||
'Member',
|
||||
"\"AutoLoginHash\"='" . $hash . "' AND \"AutoLoginExpired\" > " . DB::getConn()->now()
|
||||
);
|
||||
|
||||
$member = self::member_from_autologinhash($hash, false);
|
||||
return (bool)$member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the member for the auto login hash
|
||||
*
|
||||
* @param string $hash The hash key
|
||||
* @param bool $login Should the member be logged in?
|
||||
*
|
||||
* @return Member the matching member, if valid
|
||||
*/
|
||||
public static function member_from_autologinhash($RAW_hash, $login = false) {
|
||||
$SQL_hash = Convert::raw2sql($RAW_hash);
|
||||
public static function member_from_autologinhash($hash, $login = false) {
|
||||
|
||||
$nowExpression = DB::get_conn()->now();
|
||||
$member = DataObject::get_one('Member', array(
|
||||
"\"Member\".\"AutoLoginHash\"" => $hash,
|
||||
"\"Member\".\"AutoLoginExpired\" > $nowExpression" // NOW() can't be parameterised
|
||||
));
|
||||
|
||||
$member = DataObject::get_one(
|
||||
'Member',
|
||||
"\"AutoLoginHash\"='" . $SQL_hash . "' AND \"AutoLoginExpired\" > " . DB::getConn()->now()
|
||||
);
|
||||
|
||||
if($login && $member)
|
||||
$member->logIn();
|
||||
if($login && $member) $member->logIn();
|
||||
|
||||
return $member;
|
||||
}
|
||||
@ -706,17 +704,14 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
// but rather a last line of defense against data inconsistencies.
|
||||
$identifierField = Member::config()->unique_identifier_field;
|
||||
if($this->$identifierField) {
|
||||
|
||||
// Note: Same logic as Member_Validator class
|
||||
$idClause = ($this->ID) ? sprintf(" AND \"Member\".\"ID\" <> %d", (int)$this->ID) : '';
|
||||
$existingRecord = DataObject::get_one(
|
||||
'Member',
|
||||
sprintf(
|
||||
"\"%s\" = '%s' %s",
|
||||
$identifierField,
|
||||
Convert::raw2sql($this->$identifierField),
|
||||
$idClause
|
||||
)
|
||||
);
|
||||
$filter = array("\"$identifierField\"" => $this->$identifierField);
|
||||
if($this->ID) {
|
||||
$filter[] = array('"Member"."ID" <> ?' => $this->ID);
|
||||
}
|
||||
$existingRecord = DataObject::get_one('Member', $filter);
|
||||
|
||||
if($existingRecord) {
|
||||
throw new ValidationException(ValidationResult::create(false, _t(
|
||||
'Member.ValidationIdentifierFailed',
|
||||
@ -836,8 +831,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
if(is_numeric($group)) {
|
||||
$groupCheckObj = DataObject::get_by_id('Group', $group);
|
||||
} elseif(is_string($group)) {
|
||||
$SQL_group = Convert::raw2sql($group);
|
||||
$groupCheckObj = DataObject::get_one('Group', "\"Code\" = '{$SQL_group}'");
|
||||
$groupCheckObj = DataObject::get_one('Group', array(
|
||||
'"Group"."Code"' => $group
|
||||
));
|
||||
} elseif($group instanceof Group) {
|
||||
$groupCheckObj = $group;
|
||||
} else {
|
||||
@ -862,12 +858,13 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
* @param string Title of the group
|
||||
*/
|
||||
public function addToGroupByCode($groupcode, $title = "") {
|
||||
$group = DataObject::get_one('Group', "\"Code\" = '" . Convert::raw2sql($groupcode). "'");
|
||||
$group = DataObject::get_one('Group', array(
|
||||
'"Group"."Code"' => $groupcode
|
||||
));
|
||||
|
||||
if($group) {
|
||||
$this->Groups()->add($group);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if(!$title) $title = $groupcode;
|
||||
|
||||
$group = new Group();
|
||||
@ -946,7 +943,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
*/
|
||||
public static function get_title_sql($tableName = 'Member') {
|
||||
// This should be abstracted to SSDatabase concatOperator or similar.
|
||||
$op = (DB::getConn() instanceof MSSQLDatabase) ? " + " : " || ";
|
||||
$op = (DB::get_conn() instanceof MSSQLDatabase) ? " + " : " || ";
|
||||
|
||||
$format = self::config()->title_format;
|
||||
if ($format) {
|
||||
@ -1098,7 +1095,7 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
*
|
||||
* @param array $groups Groups to consider or NULL to use all groups with
|
||||
* CMS permissions.
|
||||
* @return SQLMap Returns a map of all members in the groups given that
|
||||
* @return SS_Map Returns a map of all members in the groups given that
|
||||
* have CMS permissions.
|
||||
*/
|
||||
public static function mapInCMSGroups($groups = null) {
|
||||
@ -1114,14 +1111,13 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
if(!empty($cmsPerms)) {
|
||||
$perms = array_unique(array_merge($perms, array_keys($cmsPerms)));
|
||||
}
|
||||
|
||||
$SQL_perms = "'" . implode("', '", Convert::raw2sql($perms)) . "'";
|
||||
|
||||
|
||||
$permsClause = DB::placeholders($perms);
|
||||
$groups = DataObject::get('Group')
|
||||
->innerJoin(
|
||||
"Permission",
|
||||
"\"Permission\".\"GroupID\" = \"Group\".\"ID\" AND \"Permission\".\"Code\" IN ($SQL_perms)"
|
||||
);
|
||||
->innerJoin("Permission", '"Permission"."GroupID" = "Group"."ID"')
|
||||
->where(array(
|
||||
"\"Permission\".\"Code\" IN ($permsClause)" => $perms
|
||||
));
|
||||
}
|
||||
|
||||
$groupIDList = array();
|
||||
@ -1134,14 +1130,17 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
$groupIDList = $groups;
|
||||
}
|
||||
|
||||
$filterClause = ($groupIDList)
|
||||
? "\"GroupID\" IN (" . implode( ',', $groupIDList ) . ")"
|
||||
: "";
|
||||
$members = Member::get()
|
||||
->innerJoin("Group_Members", '"Group_Members"."MemberID" = "Member"."ID"')
|
||||
->innerJoin("Group", '"Group"."ID" = "Group_Members"."GroupID"');
|
||||
if($groupIDList) {
|
||||
$groupClause = DB::placeholders($groupIDList);
|
||||
$members = $members->where(array(
|
||||
"\"Group\".\"ID\" IN ($groupClause)" => $groupIDList
|
||||
));
|
||||
}
|
||||
|
||||
return Member::get()->where($filterClause)->sort("\"Surname\", \"FirstName\"")
|
||||
->innerJoin("Group_Members", "\"MemberID\"=\"Member\".\"ID\"")
|
||||
->innerJoin("Group", "\"Group\".\"ID\"=\"GroupID\"")
|
||||
->map();
|
||||
return $members->sort('"Member"."Surname", "Member"."FirstName"')->map();
|
||||
}
|
||||
|
||||
|
||||
@ -1497,7 +1496,9 @@ class Member extends DataObject implements TemplateGlobalProvider {
|
||||
* @subpackage security
|
||||
*/
|
||||
class Member_GroupSet extends ManyManyList {
|
||||
|
||||
public function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) {
|
||||
|
||||
// Bypass the many-many constructor
|
||||
DataList::__construct($dataClass);
|
||||
|
||||
@ -1509,15 +1510,23 @@ class Member_GroupSet extends ManyManyList {
|
||||
|
||||
/**
|
||||
* Link this group set to a specific member.
|
||||
*
|
||||
* Recursively selects all groups applied to this member, as well as any
|
||||
* parent groups of any applied groups
|
||||
*
|
||||
* @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current
|
||||
* ids as per getForeignID
|
||||
* @return array Condition In array(SQL => parameters format)
|
||||
*/
|
||||
public function foreignIDFilter($id = null) {
|
||||
if ($id === null) $id = $this->getForeignID();
|
||||
|
||||
// Find directly applied groups
|
||||
$manyManyFilter = parent::foreignIDFilter($id);
|
||||
$groupIDs = DB::query('SELECT "GroupID" FROM "Group_Members" WHERE ' . $manyManyFilter)->column();
|
||||
$query = new SQLSelect('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter);
|
||||
$groupIDs = $query->execute()->column();
|
||||
|
||||
// Get all ancestors
|
||||
// Get all ancestors, iteratively merging these into the master set
|
||||
$allGroupIDs = array();
|
||||
while($groupIDs) {
|
||||
$allGroupIDs = array_merge($allGroupIDs, $groupIDs);
|
||||
@ -1526,15 +1535,17 @@ class Member_GroupSet extends ManyManyList {
|
||||
}
|
||||
|
||||
// Add a filter to this DataList
|
||||
if($allGroupIDs) {
|
||||
return "\"Group\".\"ID\" IN (" . implode(',', $allGroupIDs) .")";
|
||||
}
|
||||
else {
|
||||
return "\"Group\".\"ID\" = 0";
|
||||
if(!empty($allGroupIDs)) {
|
||||
$allGroupIDsPlaceholders = DB::placeholders($allGroupIDs);
|
||||
return array("\"Group\".\"ID\" IN ($allGroupIDsPlaceholders)" => $allGroupIDs);
|
||||
} else {
|
||||
return array('"Group"."ID"' => 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function foreignIDWriteFilter($id = null) {
|
||||
// Use the ManyManyList::foreignIDFilter rather than the one
|
||||
// in this class, otherwise we end up selecting all inherited groups
|
||||
return parent::foreignIDFilter($id);
|
||||
}
|
||||
}
|
||||
@ -1628,8 +1639,9 @@ class Member_Validator extends RequiredFields {
|
||||
$valid = parent::php($data);
|
||||
|
||||
$identifierField = Member::config()->unique_identifier_field;
|
||||
$SQL_identifierField = Convert::raw2sql($data[$identifierField]);
|
||||
$member = DataObject::get_one('Member', "\"$identifierField\" = '{$SQL_identifierField}'");
|
||||
$member = DataObject::get_one('Member', array(
|
||||
"\"$identifierField\"" => $data[$identifierField]
|
||||
));
|
||||
|
||||
// if we are in a complex table field popup, use ctf[childID], else use ID
|
||||
if(isset($_REQUEST['ctf']['childID'])) {
|
||||
|
@ -9,9 +9,11 @@
|
||||
class MemberAuthenticator extends Authenticator {
|
||||
|
||||
/**
|
||||
* @var Array Contains encryption algorithm identifiers.
|
||||
* If set, will migrate to new precision-safe password hashing
|
||||
* upon login. See http://open.silverstripe.org/ticket/3004
|
||||
* Contains encryption algorithm identifiers.
|
||||
* If set, will migrate to new precision-safe password hashing
|
||||
* upon login. See http://open.silverstripe.org/ticket/3004
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $migrate_legacy_hashes = array(
|
||||
'md5' => 'md5_v2.4',
|
||||
@ -29,24 +31,26 @@ class MemberAuthenticator extends Authenticator {
|
||||
* the member object
|
||||
* @see Security::setDefaultAdmin()
|
||||
*/
|
||||
public static function authenticate($RAW_data, Form $form = null) {
|
||||
if(array_key_exists('Email', $RAW_data) && $RAW_data['Email']){
|
||||
$SQL_user = Convert::raw2sql($RAW_data['Email']);
|
||||
} else {
|
||||
public static function authenticate($RAW_data, Form $form = null) {
|
||||
// Check for email
|
||||
if(empty($RAW_data['Email'])) return false;
|
||||
|
||||
$userEmail = $RAW_data['Email'];
|
||||
if(is_array($userEmail)) {
|
||||
user_error("Bad email passed to MemberAuthenticator::authenticate()", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
$isLockedOut = false;
|
||||
$result = null;
|
||||
|
||||
// Default login (see Security::setDefaultAdmin())
|
||||
if(Security::check_default_admin($RAW_data['Email'], $RAW_data['Password'])) {
|
||||
if(Security::check_default_admin($userEmail, $RAW_data['Password'])) {
|
||||
$member = Security::findAnAdministrator();
|
||||
} else {
|
||||
$member = DataObject::get_one(
|
||||
"Member",
|
||||
"\"" . Member::config()->unique_identifier_field . "\" = '$SQL_user' AND \"Password\" IS NOT NULL"
|
||||
);
|
||||
$member = Member::get()
|
||||
->filter(Member::config()->unique_identifier_field, $userEmail)
|
||||
->where('"Member"."Password" IS NOT NULL')
|
||||
->first();
|
||||
|
||||
if($member) {
|
||||
$result = $member->checkPassword($RAW_data['Password']);
|
||||
@ -75,10 +79,9 @@ class MemberAuthenticator extends Authenticator {
|
||||
$member->extend('authenticated');
|
||||
} else {
|
||||
// failed login - we're trying to see if a user exists with this email (disregarding wrong passwords)
|
||||
$existingMember = DataObject::get_one(
|
||||
"Member",
|
||||
"\"" . Member::config()->unique_identifier_field . "\" = '$SQL_user'"
|
||||
);
|
||||
$existingMember = DataObject::get_one("Member", array(
|
||||
'"'.Member::config()->unique_identifier_field.'"' => $userEmail
|
||||
));
|
||||
if($existingMember) {
|
||||
$attempt->MemberID = $existingMember->ID;
|
||||
|
||||
@ -91,12 +94,8 @@ class MemberAuthenticator extends Authenticator {
|
||||
}
|
||||
$attempt->Status = 'Failure';
|
||||
}
|
||||
if(is_array($RAW_data['Email'])) {
|
||||
user_error("Bad email passed to MemberAuthenticator::authenticate(): $RAW_data[Email]", E_USER_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
$attempt->Email = $RAW_data['Email'];
|
||||
$attempt->Email = $userEmail;
|
||||
$attempt->IP = Controller::curr()->getRequest()->getIP();
|
||||
$attempt->write();
|
||||
}
|
||||
|
@ -41,10 +41,7 @@ class MemberCsvBulkLoader extends CsvBulkLoader {
|
||||
$groupCodes = explode(',', $record['Groups']);
|
||||
foreach($groupCodes as $groupCode) {
|
||||
if(!isset($_cache_groupByCode[$groupCode])) {
|
||||
$group = DataObject::get_one(
|
||||
'Group',
|
||||
sprintf('"Code" = \'%s\'', Convert::raw2sql($groupCode))
|
||||
);
|
||||
$group = Group::get()->filter('Code', $groupCode)->first();
|
||||
if(!$group) {
|
||||
$group = new Group();
|
||||
$group->Code = $groupCode;
|
||||
|
@ -287,16 +287,26 @@ JS;
|
||||
* @param array $data Submitted data
|
||||
*/
|
||||
public function forgotPassword($data) {
|
||||
$SQL_data = Convert::raw2sql($data);
|
||||
$SQL_email = $SQL_data['Email'];
|
||||
$member = DataObject::get_one('Member', "\"Email\" = '{$SQL_email}'");
|
||||
// Ensure password is given
|
||||
if(empty($data['Email'])) {
|
||||
$this->sessionMessage(
|
||||
_t('Member.ENTEREMAIL', 'Please enter an email address to get a password reset link.'),
|
||||
'bad'
|
||||
);
|
||||
|
||||
$this->controller->redirect('Security/lostpassword');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find existing member
|
||||
$member = Member::get()->filter("Email", $data['Email'])->first();
|
||||
|
||||
// Allow vetoing forgot password requests
|
||||
$results = $this->extend('forgotPassword', $member);
|
||||
if($results && is_array($results) && in_array(false, $results, true)) {
|
||||
return $this->controller->redirect('Security/lostpassword');
|
||||
}
|
||||
|
||||
|
||||
if($member) {
|
||||
$token = $member->generateAutologinTokenAndStoreHash();
|
||||
|
||||
@ -310,8 +320,8 @@ JS;
|
||||
|
||||
$this->controller->redirect('Security/passwordsent/' . urlencode($data['Email']));
|
||||
} elseif($data['Email']) {
|
||||
// Avoid information disclosure by displaying the same status,
|
||||
// regardless wether the email address actually exists
|
||||
// Avoid information disclosure by displaying the same status,
|
||||
// regardless wether the email address actually exists
|
||||
$this->controller->redirect('Security/passwordsent/' . rawurlencode($data['Email']));
|
||||
} else {
|
||||
$this->sessionMessage(
|
||||
@ -320,7 +330,8 @@ JS;
|
||||
);
|
||||
|
||||
$this->controller->redirect('Security/lostpassword');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -333,9 +333,7 @@ class PasswordEncryptor_LegacyPHPHash extends PasswordEncryptor_PHPHash {
|
||||
*/
|
||||
class PasswordEncryptor_MySQLPassword extends PasswordEncryptor {
|
||||
public function encrypt($password, $salt = null, $member = null) {
|
||||
return DB::query(
|
||||
sprintf("SELECT PASSWORD('%s')", Convert::raw2sql($password))
|
||||
)->value();
|
||||
return DB::prepared_query("SELECT PASSWORD(?)", array($password))->value();
|
||||
}
|
||||
|
||||
public function salt($password, $member = null) {
|
||||
@ -351,9 +349,7 @@ class PasswordEncryptor_MySQLPassword extends PasswordEncryptor {
|
||||
*/
|
||||
class PasswordEncryptor_MySQLOldPassword extends PasswordEncryptor {
|
||||
public function encrypt($password, $salt = null, $member = null) {
|
||||
return DB::query(
|
||||
sprintf("SELECT OLD_PASSWORD('%s')", Convert::raw2sql($password))
|
||||
)->value();
|
||||
return DB::prepared_query("SELECT OLD_PASSWORD(?)", array($password))->value();
|
||||
}
|
||||
|
||||
public function salt($password, $member = null) {
|
||||
|
@ -109,13 +109,10 @@ class PasswordValidator extends Object {
|
||||
}
|
||||
|
||||
if($this->historicalPasswordCount) {
|
||||
$previousPasswords = DataObject::get(
|
||||
"MemberPassword",
|
||||
"\"MemberID\" = $member->ID",
|
||||
"\"Created\" DESC, \"ID\" DESC",
|
||||
"",
|
||||
$this->historicalPasswordCount
|
||||
);
|
||||
$previousPasswords = MemberPassword::get()
|
||||
->where(array('"MemberPassword"."MemberID"' => $member->ID))
|
||||
->sort('"Created" DESC, "ID" DESC')
|
||||
->limit($this->historicalPasswordCount);
|
||||
if($previousPasswords) foreach($previousPasswords as $previousPasswords) {
|
||||
if($previousPasswords->checkPassword($password)) {
|
||||
$valid->error(
|
||||
|
@ -177,65 +177,71 @@ class Permission extends DataObject implements TemplateGlobalProvider {
|
||||
// Multiple $code values - return true if at least one matches, ie, intersection exists
|
||||
return (bool)array_intersect($code, self::$cache_permissions[$memberID]);
|
||||
}
|
||||
|
||||
// Code filters
|
||||
$codeParams = is_array($code) ? $code : array($code);
|
||||
$codeClause = DB::placeholders($codes);
|
||||
$adminParams = (self::$admin_implies_all) ? array('ADMIN') : array();
|
||||
$adminClause = (self::$admin_implies_all) ? ", ?" : '';
|
||||
|
||||
// The following code should only be used if you're not using the "any" arg. This is kind
|
||||
// of obselete functionality and could possibly be deprecated.
|
||||
|
||||
$groupList = self::groupList($memberID);
|
||||
if(!$groupList) return false;
|
||||
|
||||
$groupCSV = implode(", ", $groupList);
|
||||
$groupParams = self::groupList($memberID);
|
||||
if(empty($groupParams)) return false;
|
||||
$groupClause = DB::placeholders($groupParams);
|
||||
|
||||
// Arg component
|
||||
$argClause = "";
|
||||
$argParams = array();
|
||||
switch($arg) {
|
||||
case "any":
|
||||
$argClause = "";
|
||||
break;
|
||||
case "all":
|
||||
$argClause = " AND \"Arg\" = -1";
|
||||
$argClause = " AND \"Arg\" = ?";
|
||||
$argParams = array(-1);
|
||||
break;
|
||||
default:
|
||||
if(is_numeric($arg)) {
|
||||
$argClause = "AND \"Arg\" IN (-1, $arg) ";
|
||||
$argClause = "AND \"Arg\" IN (?, ?) ";
|
||||
$argParams = array(-1, $arg);
|
||||
} else {
|
||||
user_error("Permission::checkMember: bad arg '$arg'", E_USER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if(is_array($code)) {
|
||||
$SQL_codeList = "'" . implode("', '", Convert::raw2sql($code)) . "'";
|
||||
} else {
|
||||
$SQL_codeList = "'" . Convert::raw2sql($code) . "'";
|
||||
}
|
||||
|
||||
$SQL_code = Convert::raw2sql($code);
|
||||
|
||||
$adminFilter = (Config::inst()->get('Permission', 'admin_implies_all')) ? ",'ADMIN'" : '';
|
||||
|
||||
// Raw SQL for efficiency
|
||||
$permission = DB::query("
|
||||
SELECT \"ID\"
|
||||
$permission = DB::prepared_query(
|
||||
"SELECT \"ID\"
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
\"Code\" IN ($SQL_codeList $adminFilter)
|
||||
AND \"Type\" = " . self::GRANT_PERMISSION . "
|
||||
AND \"GroupID\" IN ($groupCSV)
|
||||
\"Code\" IN ($codeClause $adminClause)
|
||||
AND \"Type\" = ?
|
||||
AND \"GroupID\" IN ($groupClause)
|
||||
$argClause
|
||||
)",
|
||||
array_merge(
|
||||
$codeParams,
|
||||
$adminParams,
|
||||
array(self::GRANT_PERMISSION),
|
||||
$groupParams,
|
||||
$argParams
|
||||
)
|
||||
")->value();
|
||||
)->value();
|
||||
|
||||
if($permission) return $permission;
|
||||
|
||||
// Strict checking disabled?
|
||||
if(!Config::inst()->get('Permission', 'strict_checking') || !$strict) {
|
||||
$hasPermission = DB::query("
|
||||
SELECT COUNT(*)
|
||||
$hasPermission = DB::prepared_query(
|
||||
"SELECT COUNT(*)
|
||||
FROM \"Permission\"
|
||||
WHERE (
|
||||
(\"Code\" IN '$SQL_code')'
|
||||
AND (\"Type\" = " . self::GRANT_PERMISSION . ")
|
||||
)
|
||||
")->value();
|
||||
\"Code\" IN ($codeClause) AND
|
||||
\"Type\" = ?
|
||||
)",
|
||||
array_merge($codeParams, array(self::GRANT_PERMISSION))
|
||||
)->value();
|
||||
|
||||
if(!$hasPermission) return;
|
||||
}
|
||||
@ -412,11 +418,13 @@ class Permission extends DataObject implements TemplateGlobalProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if(!count($groupIDs)) return new ArrayList();
|
||||
if(empty($groupIDs)) return new ArrayList();
|
||||
|
||||
$members = DataObject::get('Member')->where("\"Group\".\"ID\" IN (" . implode(",",$groupIDs) . ")")
|
||||
->leftJoin("Group_Members", "\"Member\".\"ID\" = \"Group_Members\".\"MemberID\"")
|
||||
->leftJoin("Group", "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"");
|
||||
$groupClause = DB::placeholders($groupIDs);
|
||||
$members = Member::get()
|
||||
->where(array("\"Group\".\"ID\" IN ($groupClause)" => $groupIDs))
|
||||
->leftJoin("Group_Members", '"Member"."ID" = "Group_Members"."MemberID"')
|
||||
->leftJoin("Group", '"Group_Members"."GroupID" = "Group"."ID"');
|
||||
|
||||
return $members;
|
||||
}
|
||||
@ -427,14 +435,15 @@ class Permission extends DataObject implements TemplateGlobalProvider {
|
||||
* @return SS_List The matching group objects
|
||||
*/
|
||||
public static function get_groups_by_permission($codes) {
|
||||
if(!is_array($codes)) $codes = array($codes);
|
||||
|
||||
$SQLa_codes = Convert::raw2sql($codes);
|
||||
$SQL_codes = join("','", $SQLa_codes);
|
||||
$codeParams = is_array($codes) ? $codes : array($codes);
|
||||
$codeClause = DB::placeholders($codeParams);
|
||||
|
||||
// Via Roles are groups that have the permission via a role
|
||||
return DataObject::get('Group')
|
||||
->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')")
|
||||
->where(array(
|
||||
"\"PermissionRoleCode\".\"Code\" IN ($codeClause) OR \"Permission\".\"Code\" IN ($codeClause)"
|
||||
=> array_merge($codeParams, $codeParams)
|
||||
))
|
||||
->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
|
||||
->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
|
||||
->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
|
||||
|
@ -755,9 +755,9 @@ class Security extends Controller implements TemplateGlobalProvider {
|
||||
|
||||
// find a group with ADMIN permission
|
||||
$adminGroup = DataObject::get('Group')
|
||||
->where("\"Permission\".\"Code\" = 'ADMIN'")
|
||||
->sort("\"Group\".\"ID\"")
|
||||
->innerJoin("Permission", "\"Group\".\"ID\"=\"Permission\".\"GroupID\"")
|
||||
->where(array('"Permission"."Code"' => 'ADMIN'))
|
||||
->sort('"Group"."ID"')
|
||||
->innerJoin("Permission", '"Group"."ID" = "Permission"."GroupID"')
|
||||
->First();
|
||||
|
||||
if(is_callable('Subsite::changeSubsite')) {
|
||||
@ -945,10 +945,10 @@ class Security extends Controller implements TemplateGlobalProvider {
|
||||
singleton($table);
|
||||
|
||||
// if any of the tables don't have all fields mapped as table columns
|
||||
$dbFields = DB::fieldList($table);
|
||||
$dbFields = DB::field_list($table);
|
||||
if(!$dbFields) return false;
|
||||
|
||||
$objFields = DataObject::database_fields($table);
|
||||
$objFields = DataObject::database_fields($table, false);
|
||||
$missingFields = array_diff_key($objFields, $dbFields);
|
||||
|
||||
if($missingFields) return false;
|
||||
|
@ -35,10 +35,10 @@ class EncryptAllPasswordsTask extends BuildTask {
|
||||
}
|
||||
|
||||
// Are there members with a clear text password?
|
||||
$members = DataObject::get(
|
||||
"Member",
|
||||
"\"PasswordEncryption\" = 'none' AND \"Password\" IS NOT NULL"
|
||||
);
|
||||
$members = DataObject::get("Member")->where(array(
|
||||
'"Member"."PasswordEncryption"' => 'none',
|
||||
'"Member"."Password" IS NOT NULL'
|
||||
));
|
||||
|
||||
if(!$members) {
|
||||
$this->debugMessage('No passwords to encrypt');
|
||||
|
@ -155,8 +155,8 @@ class ConvertTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Helper function for comparing characters with significant whitespaces
|
||||
* @param type $expected
|
||||
* @param type $actual
|
||||
* @param string $expected
|
||||
* @param string $actual
|
||||
*/
|
||||
protected function assertEqualsQuoted($expected, $actual) {
|
||||
$message = sprintf(
|
||||
|
@ -30,7 +30,9 @@ class CsvBulkLoaderTest extends SapphireTest {
|
||||
$this->assertEquals(4, $results->Count(), 'Test correct count of imported data');
|
||||
|
||||
// Test that columns were correctly imported
|
||||
$obj = DataObject::get_one("CsvBulkLoaderTest_Player", "\"FirstName\" = 'John'");
|
||||
$obj = DataObject::get_one("CsvBulkLoaderTest_Player", array(
|
||||
'"CsvBulkLoaderTest_Player"."FirstName"' => 'John'
|
||||
));
|
||||
$this->assertNotNull($obj);
|
||||
$this->assertEquals("He's a good guy", $obj->Biography);
|
||||
$this->assertEquals("1988-01-31", $obj->Birthday);
|
||||
@ -81,13 +83,17 @@ class CsvBulkLoaderTest extends SapphireTest {
|
||||
$this->assertEquals(4, $results->Count(), 'Test correct count of imported data');
|
||||
|
||||
// Test that columns were correctly imported
|
||||
$obj = DataObject::get_one("CsvBulkLoaderTest_Player", "\"FirstName\" = 'John'");
|
||||
$obj = DataObject::get_one("CsvBulkLoaderTest_Player", array(
|
||||
'"CsvBulkLoaderTest_Player"."FirstName"' => 'John'
|
||||
));
|
||||
$this->assertNotNull($obj);
|
||||
$this->assertEquals("He's a good guy", $obj->Biography);
|
||||
$this->assertEquals("1988-01-31", $obj->Birthday);
|
||||
$this->assertEquals("1", $obj->IsRegistered);
|
||||
|
||||
$obj2 = DataObject::get_one('CsvBulkLoaderTest_Player', "\"FirstName\" = 'Jane'");
|
||||
$obj2 = DataObject::get_one("CsvBulkLoaderTest_Player", array(
|
||||
'"CsvBulkLoaderTest_Player"."FirstName"' => 'Jane'
|
||||
));
|
||||
$this->assertNotNull($obj2);
|
||||
$this->assertEquals('0', $obj2->IsRegistered);
|
||||
|
||||
@ -131,7 +137,9 @@ class CsvBulkLoaderTest extends SapphireTest {
|
||||
|
||||
// Test of creating relation
|
||||
$testContract = DataObject::get_one('CsvBulkLoaderTest_PlayerContract');
|
||||
$testPlayer = Dataobject::get_one("CsvBulkLoaderTest_Player", "\"FirstName\" = 'John'");
|
||||
$testPlayer = DataObject::get_one("CsvBulkLoaderTest_Player", array(
|
||||
'"CsvBulkLoaderTest_Player"."FirstName"' => 'John'
|
||||
));
|
||||
$this->assertEquals($testPlayer->ContractID, $testContract->ID, 'Creating new has_one relation works');
|
||||
|
||||
// Test nested setting of relation properties
|
||||
@ -262,8 +270,9 @@ class CsvBulkLoaderTest_Player extends DataObject implements TestOnly {
|
||||
);
|
||||
|
||||
public function getTeamByTitle($title) {
|
||||
$SQL_title = Convert::raw2sql($title);
|
||||
return DataObject::get_one('CsvBulkLoaderTest_Team', "\"Title\" = '{$SQL_title}'");
|
||||
return DataObject::get_one("CsvBulkLoaderTest_Team", array(
|
||||
'"CsvBulkLoaderTest_Team"."Title"' => $title
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
134
tests/dev/MySQLDatabaseConfigurationHelperTest.php
Normal file
134
tests/dev/MySQLDatabaseConfigurationHelperTest.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
*/
|
||||
class MySQLDatabaseConfigurationHelperTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Tests that invalid names are disallowed
|
||||
*/
|
||||
public function testInvalidDatabaseNames() {
|
||||
$helper = new MySQLDatabaseConfigurationHelper();
|
||||
|
||||
// Reject filename unsafe characters
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database%name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database?name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database|name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database<name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database"name'));
|
||||
|
||||
// Reject additional characters
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database.name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database\name'));
|
||||
$this->assertEmpty($helper->checkValidDatabaseName('database/name'));
|
||||
|
||||
// Reject blank
|
||||
$this->assertEmpty($helper->checkValidDatabaseName(""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that valid names are allowed
|
||||
*/
|
||||
public function testValidDatabaseNames() {
|
||||
$helper = new MySQLDatabaseConfigurationHelper();
|
||||
|
||||
// Names with spaces
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('database name'));
|
||||
|
||||
// Basic latin characters
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('database_name'));
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('UPPERCASE_NAME'));
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('name_with_numbers_1234'));
|
||||
|
||||
// Extended unicode names
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('亝亞亟')); // U+4E9D, U+4E9E, U+4E9F
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('おかが')); // U+304A, U+304B, U+304C
|
||||
$this->assertNotEmpty($helper->checkValidDatabaseName('¶»Ã')); // U+00B6, U+00BB, U+00C3
|
||||
}
|
||||
|
||||
public function testDatabaseCreateCheck() {
|
||||
|
||||
$helper = new MySQLDatabaseConfigurationHelper();
|
||||
|
||||
// Accept all privileges
|
||||
$this->assertNotEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Accept create (mysql syntax)
|
||||
$this->assertNotEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT CREATE, SELECT ON *.* TO 'root'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Accept create on this database only
|
||||
$this->assertNotEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT ALL PRIVILEGES, CREATE ON \"database_name\".* TO 'root'@'localhost' IDENTIFIED BY PASSWORD 'XXXX'"
|
||||
. " WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Accept create on any database (alternate wildcard syntax)
|
||||
$this->assertNotEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT CREATE ON \"%\".* TO 'root'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
}
|
||||
|
||||
public function testDatabaseCreateFail() {
|
||||
|
||||
$helper = new MySQLDatabaseConfigurationHelper();
|
||||
|
||||
// Don't be fooled by create routine
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT SELECT, CREATE ROUTINE ON *.* TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Or create view
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT CREATE VIEW, SELECT ON *.* TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Don't accept permission if only given on a single subtable
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT CREATE, SELECT ON *.\"onetable\" TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' "
|
||||
. "WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Don't accept permission on wrong database
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT ALL PRIVILEGES, CREATE ON \"wrongdb\".* TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' "
|
||||
. "WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Don't accept wrong permission
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'database_name',
|
||||
'create',
|
||||
"GRANT UPDATE ON \"%\".* TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH GRANT OPTION"
|
||||
));
|
||||
|
||||
// Don't accept sneaky table name
|
||||
$this->assertEmpty($helper->checkDatabasePermissionGrant(
|
||||
'grant create on . to',
|
||||
'create',
|
||||
"GRANT UPDATE ON \"grant create on . to\".* TO 'user'@'localhost' IDENTIFIED BY PASSWORD 'XXXX' WITH "
|
||||
. "GRANT OPTION"
|
||||
));
|
||||
}
|
||||
}
|
@ -39,7 +39,9 @@ class FolderTest extends SapphireTest {
|
||||
);
|
||||
|
||||
$this->assertTrue(file_exists(ASSETS_PATH . $path), 'File');
|
||||
$parentFolder = DataObject::get_one('Folder', '"Name" = \'FolderTest\'');
|
||||
$parentFolder = DataObject::get_one('Folder', array(
|
||||
'"File"."Name"' => 'FolderTest'
|
||||
));
|
||||
$this->assertNotNull($parentFolder);
|
||||
$this->assertEquals($parentFolder->ID, $folder->ParentID);
|
||||
|
||||
@ -149,7 +151,9 @@ class FolderTest extends SapphireTest {
|
||||
*/
|
||||
public function testRenameFolderAndCheckTheFile() {
|
||||
// ID is prefixed in case Folder is subclassed by project/other module.
|
||||
$folder1 = DataObject::get_one('Folder', '"File"."ID"='.$this->idFromFixture('Folder', 'folder1'));
|
||||
$folder1 = DataObject::get_one('Folder', array(
|
||||
'"File"."ID"' => $this->idFromFixture('Folder', 'folder1')
|
||||
));
|
||||
|
||||
$folder1->Name = 'FileTest-folder1-changed';
|
||||
$folder1->write();
|
||||
|
@ -25,7 +25,7 @@ class GDTest extends SapphireTest {
|
||||
$gds = array();
|
||||
foreach(self::$filenames as $type => $file) {
|
||||
$fullPath = realpath(dirname(__FILE__) . '/gdtest/' . $file);
|
||||
$gd = new GD($fullPath);
|
||||
$gd = new GDBackend($fullPath);
|
||||
if($callback) {
|
||||
$gd = $callback($gd);
|
||||
}
|
||||
@ -36,13 +36,13 @@ class GDTest extends SapphireTest {
|
||||
|
||||
/**
|
||||
* Takes samples from the given GD at 5 pixel increments
|
||||
* @param GD $gd The source image
|
||||
* @param GDBackend $gd The source image
|
||||
* @param integer $horizontal Number of samples to take horizontally
|
||||
* @param integer $vertical Number of samples to take vertically
|
||||
* @return array List of colours for each sample, each given as an associative
|
||||
* array with red, blue, green, and alpha components
|
||||
*/
|
||||
protected function sampleAreas(GD $gd, $horizontal = 4, $vertical = 4) {
|
||||
protected function sampleAreas(GDBackend $gd, $horizontal = 4, $vertical = 4) {
|
||||
$samples = array();
|
||||
for($y = 0; $y < $vertical; $y++) {
|
||||
for($x = 0; $x < $horizontal; $x++) {
|
||||
@ -115,7 +115,7 @@ class GDTest extends SapphireTest {
|
||||
function testGreyscale() {
|
||||
|
||||
// Apply greyscaling to each image
|
||||
$images = $this->applyToEachImage(function(GD $gd) {
|
||||
$images = $this->applyToEachImage(function(GDBackend $gd) {
|
||||
return $gd->greyscale();
|
||||
});
|
||||
|
||||
|
@ -59,10 +59,10 @@ class CheckboxSetFieldTest extends SapphireTest {
|
||||
$field->saveInto($article);
|
||||
|
||||
$this->assertNull(
|
||||
DB::query("SELECT *
|
||||
DB::prepared_query("SELECT *
|
||||
FROM \"CheckboxSetFieldTest_Article_Tags\"
|
||||
WHERE \"CheckboxSetFieldTest_Article_Tags\".\"CheckboxSetFieldTest_ArticleID\" = $article->ID
|
||||
")->value(),
|
||||
WHERE \"CheckboxSetFieldTest_Article_Tags\".\"CheckboxSetFieldTest_ArticleID\" = ?", array($article->ID)
|
||||
)->value(),
|
||||
'Nothing should go into manymany join table for a saved field without any ticked boxes'
|
||||
);
|
||||
}
|
||||
@ -85,10 +85,10 @@ class CheckboxSetFieldTest extends SapphireTest {
|
||||
|
||||
$this->assertEquals(
|
||||
array($tag1->ID,$tag2->ID),
|
||||
DB::query("SELECT \"CheckboxSetFieldTest_TagID\"
|
||||
DB::prepared_query("SELECT \"CheckboxSetFieldTest_TagID\"
|
||||
FROM \"CheckboxSetFieldTest_Article_Tags\"
|
||||
WHERE \"CheckboxSetFieldTest_Article_Tags\".\"CheckboxSetFieldTest_ArticleID\" = $article->ID
|
||||
")->column(),
|
||||
WHERE \"CheckboxSetFieldTest_Article_Tags\".\"CheckboxSetFieldTest_ArticleID\" = ?", array($article->ID)
|
||||
)->column(),
|
||||
'Data shold be saved into CheckboxSetField manymany relation table on the "right end"'
|
||||
);
|
||||
$this->assertEquals(
|
||||
|
@ -26,7 +26,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$response = $this->mockFileUpload('NoRelationField', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
|
||||
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
|
||||
$uploadedFile = DataObject::get_one('File', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
}
|
||||
|
||||
@ -46,7 +48,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$response = $this->mockFileUpload('HasOneFile', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
|
||||
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
|
||||
$uploadedFile = DataObject::get_one('File', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
|
||||
// Secondly, ensure that simply uploading an object does not save the file against the relation
|
||||
@ -77,7 +81,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$response = $this->mockFileUpload('HasOneExtendedFile', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
|
||||
$uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', sprintf('"Name" = \'%s\'', $tmpFileName));
|
||||
$uploadedFile = DataObject::get_one('UploadFieldTest_ExtendedFile', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
|
||||
// Test that the record isn't written to automatically
|
||||
@ -106,7 +112,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$response = $this->mockFileUpload('HasManyFiles', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
|
||||
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
|
||||
$uploadedFile = DataObject::get_one('File', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
|
||||
// Test that the record isn't written to automatically
|
||||
@ -135,7 +143,9 @@ class UploadFieldTest extends FunctionalTest {
|
||||
$response = $this->mockFileUpload('ManyManyFiles', $tmpFileName);
|
||||
$this->assertFalse($response->isError());
|
||||
$this->assertFileExists(ASSETS_PATH . "/UploadFieldTest/$tmpFileName");
|
||||
$uploadedFile = DataObject::get_one('File', sprintf('"Name" = \'%s\'', $tmpFileName));
|
||||
$uploadedFile = DataObject::get_one('File', array(
|
||||
'"File"."Name"' => $tmpFileName
|
||||
));
|
||||
$this->assertTrue(is_object($uploadedFile), 'The file object is created');
|
||||
|
||||
// Test that the record isn't written to automatically
|
||||
|
@ -22,116 +22,122 @@ class DBFieldTest extends SapphireTest {
|
||||
* Test the prepValueForDB() method on DBField.
|
||||
*/
|
||||
public function testPrepValueForDB() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
|
||||
/* Float behaviour, asserting we have 0 */
|
||||
$this->assertEquals('0', singleton('Float')->prepValueForDB(0));
|
||||
$this->assertEquals('0', singleton('Float')->prepValueForDB(null));
|
||||
$this->assertEquals('0', singleton('Float')->prepValueForDB(false));
|
||||
$this->assertEquals('0', singleton('Float')->prepValueForDB(''));
|
||||
$this->assertEquals(0, singleton('Float')->prepValueForDB(0));
|
||||
$this->assertEquals(0, singleton('Float')->prepValueForDB(null));
|
||||
$this->assertEquals(0, singleton('Float')->prepValueForDB(false));
|
||||
$this->assertEquals(0, singleton('Float')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Float')->prepValueForDB('0'));
|
||||
|
||||
/* Double behaviour, asserting we have 0 */
|
||||
$this->assertEquals('0', singleton('Double')->prepValueForDB(0));
|
||||
$this->assertEquals('0', singleton('Double')->prepValueForDB(null));
|
||||
$this->assertEquals('0', singleton('Double')->prepValueForDB(false));
|
||||
$this->assertEquals('0', singleton('Double')->prepValueForDB(''));
|
||||
$this->assertEquals(0, singleton('Double')->prepValueForDB(0));
|
||||
$this->assertEquals(0, singleton('Double')->prepValueForDB(null));
|
||||
$this->assertEquals(0, singleton('Double')->prepValueForDB(false));
|
||||
$this->assertEquals(0, singleton('Double')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Double')->prepValueForDB('0'));
|
||||
|
||||
/* Integer behaviour, asserting we have 0 */
|
||||
$this->assertEquals('0', singleton('Int')->prepValueForDB(0));
|
||||
$this->assertEquals('0', singleton('Int')->prepValueForDB(null));
|
||||
$this->assertEquals('0', singleton('Int')->prepValueForDB(false));
|
||||
$this->assertEquals('0', singleton('Int')->prepValueForDB(''));
|
||||
$this->assertEquals(0, singleton('Int')->prepValueForDB(0));
|
||||
$this->assertEquals(0, singleton('Int')->prepValueForDB(null));
|
||||
$this->assertEquals(0, singleton('Int')->prepValueForDB(false));
|
||||
$this->assertEquals(0, singleton('Int')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Int')->prepValueForDB('0'));
|
||||
|
||||
/* Integer behaviour, asserting we have 1 */
|
||||
$this->assertEquals('1', singleton('Int')->prepValueForDB(true));
|
||||
$this->assertEquals('1', singleton('Int')->prepValueForDB(1));
|
||||
$this->assertEquals(1, singleton('Int')->prepValueForDB(true));
|
||||
$this->assertEquals(1, singleton('Int')->prepValueForDB(1));
|
||||
$this->assertEquals('1', singleton('Int')->prepValueForDB('1'));
|
||||
|
||||
/* Decimal behaviour, asserting we have 0 */
|
||||
$this->assertEquals('0', singleton('Decimal')->prepValueForDB(0));
|
||||
$this->assertEquals('0', singleton('Decimal')->prepValueForDB(null));
|
||||
$this->assertEquals('0', singleton('Decimal')->prepValueForDB(false));
|
||||
$this->assertEquals('0', singleton('Decimal')->prepValueForDB(''));
|
||||
$this->assertEquals(0, singleton('Decimal')->prepValueForDB(0));
|
||||
$this->assertEquals(0, singleton('Decimal')->prepValueForDB(null));
|
||||
$this->assertEquals(0, singleton('Decimal')->prepValueForDB(false));
|
||||
$this->assertEquals(0, singleton('Decimal')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Decimal')->prepValueForDB('0'));
|
||||
|
||||
/* Decimal behaviour, asserting we have 1 */
|
||||
$this->assertEquals('1', singleton('Decimal')->prepValueForDB(true));
|
||||
$this->assertEquals('1', singleton('Decimal')->prepValueForDB(1));
|
||||
$this->assertEquals(1, singleton('Decimal')->prepValueForDB(true));
|
||||
$this->assertEquals(1, singleton('Decimal')->prepValueForDB(1));
|
||||
$this->assertEquals('1', singleton('Decimal')->prepValueForDB('1'));
|
||||
|
||||
/* Boolean behaviour, asserting we have 0 */
|
||||
$this->assertEquals("'0'", singleton('Boolean')->prepValueForDB(0));
|
||||
$this->assertEquals("'0'", singleton('Boolean')->prepValueForDB(null));
|
||||
$this->assertEquals("'0'", singleton('Boolean')->prepValueForDB(false));
|
||||
$this->assertEquals("'0'", singleton('Boolean')->prepValueForDB(''));
|
||||
$this->assertEquals("'0'", singleton('Boolean')->prepValueForDB('0'));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB(0));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB(null));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB(false));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB('false'));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB('f'));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB(''));
|
||||
$this->assertEquals(false, singleton('Boolean')->prepValueForDB('0'));
|
||||
|
||||
/* Boolean behaviour, asserting we have 1 */
|
||||
$this->assertEquals("'1'", singleton('Boolean')->prepValueForDB(true));
|
||||
$this->assertEquals("'1'", singleton('Boolean')->prepValueForDB(1));
|
||||
$this->assertEquals("'1'", singleton('Boolean')->prepValueForDB('1'));
|
||||
|
||||
$this->assertEquals(true, singleton('Boolean')->prepValueForDB(true));
|
||||
$this->assertEquals(true, singleton('Boolean')->prepValueForDB('true'));
|
||||
$this->assertEquals(true, singleton('Boolean')->prepValueForDB('t'));
|
||||
$this->assertEquals(true, singleton('Boolean')->prepValueForDB(1));
|
||||
$this->assertEquals(true, singleton('Boolean')->prepValueForDB('1'));
|
||||
|
||||
// @todo - Revisit Varchar to evaluate correct behaviour of nullifyEmpty
|
||||
|
||||
/* Varchar behaviour */
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Varchar')->prepValueForDB(0));
|
||||
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(null));
|
||||
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(false));
|
||||
$this->assertEquals("null", singleton('Varchar')->prepValueForDB(''));
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Varchar')->prepValueForDB('0'));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Varchar')->prepValueForDB(1));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Varchar')->prepValueForDB(true));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Varchar')->prepValueForDB('1'));
|
||||
$this->assertEquals($db->prepStringForDB("00000"), singleton('Varchar')->prepValueForDB('00000'));
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Varchar')->prepValueForDB(0000));
|
||||
$this->assertEquals($db->prepStringForDB("test"), singleton('Varchar')->prepValueForDB('test'));
|
||||
$this->assertEquals($db->prepStringForDB("123"), singleton('Varchar')->prepValueForDB(123));
|
||||
$this->assertEquals(0, singleton('Varchar')->prepValueForDB(0));
|
||||
$this->assertEquals(null, singleton('Varchar')->prepValueForDB(null));
|
||||
$this->assertEquals(null, singleton('Varchar')->prepValueForDB(false));
|
||||
$this->assertEquals(null, singleton('Varchar')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Varchar')->prepValueForDB('0'));
|
||||
$this->assertEquals(1, singleton('Varchar')->prepValueForDB(1));
|
||||
$this->assertEquals(true, singleton('Varchar')->prepValueForDB(true));
|
||||
$this->assertEquals('1', singleton('Varchar')->prepValueForDB('1'));
|
||||
$this->assertEquals('00000', singleton('Varchar')->prepValueForDB('00000'));
|
||||
$this->assertEquals(0, singleton('Varchar')->prepValueForDB(0000));
|
||||
$this->assertEquals('test', singleton('Varchar')->prepValueForDB('test'));
|
||||
$this->assertEquals(123, singleton('Varchar')->prepValueForDB(123));
|
||||
|
||||
/* AllowEmpty Varchar behaviour */
|
||||
$varcharField = new Varchar("testfield", 50, array("nullifyEmpty"=>false));
|
||||
$this->assertSame($db->prepStringForDB("0"), $varcharField->prepValueForDB(0));
|
||||
$this->assertSame("null", $varcharField->prepValueForDB(null));
|
||||
$this->assertSame("null", $varcharField->prepValueForDB(false));
|
||||
$this->assertSame($db->prepStringForDB(""), $varcharField->prepValueForDB(''));
|
||||
$this->assertSame($db->prepStringForDB("0"), $varcharField->prepValueForDB('0'));
|
||||
$this->assertSame($db->prepStringForDB("1"), $varcharField->prepValueForDB(1));
|
||||
$this->assertSame($db->prepStringForDB("1"), $varcharField->prepValueForDB(true));
|
||||
$this->assertSame($db->prepStringForDB("1"), $varcharField->prepValueForDB('1'));
|
||||
$this->assertSame($db->prepStringForDB("00000"), $varcharField->prepValueForDB('00000'));
|
||||
$this->assertSame($db->prepStringForDB("0"), $varcharField->prepValueForDB(0000));
|
||||
$this->assertSame($db->prepStringForDB("test"), $varcharField->prepValueForDB('test'));
|
||||
$this->assertSame($db->prepStringForDB("123"), $varcharField->prepValueForDB(123));
|
||||
$this->assertSame(0, $varcharField->prepValueForDB(0));
|
||||
$this->assertSame(null, $varcharField->prepValueForDB(null));
|
||||
$this->assertSame(null, $varcharField->prepValueForDB(false));
|
||||
$this->assertSame('', $varcharField->prepValueForDB(''));
|
||||
$this->assertSame('0', $varcharField->prepValueForDB('0'));
|
||||
$this->assertSame(1, $varcharField->prepValueForDB(1));
|
||||
$this->assertSame(true, $varcharField->prepValueForDB(true));
|
||||
$this->assertSame('1', $varcharField->prepValueForDB('1'));
|
||||
$this->assertSame('00000', $varcharField->prepValueForDB('00000'));
|
||||
$this->assertSame(0, $varcharField->prepValueForDB(0000));
|
||||
$this->assertSame('test', $varcharField->prepValueForDB('test'));
|
||||
$this->assertSame(123, $varcharField->prepValueForDB(123));
|
||||
unset($varcharField);
|
||||
|
||||
/* Text behaviour */
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Text')->prepValueForDB(0));
|
||||
$this->assertEquals("null", singleton('Text')->prepValueForDB(null));
|
||||
$this->assertEquals("null", singleton('Text')->prepValueForDB(false));
|
||||
$this->assertEquals("null", singleton('Text')->prepValueForDB(''));
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Text')->prepValueForDB('0'));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Text')->prepValueForDB(1));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Text')->prepValueForDB(true));
|
||||
$this->assertEquals($db->prepStringForDB("1"), singleton('Text')->prepValueForDB('1'));
|
||||
$this->assertEquals($db->prepStringForDB("00000"), singleton('Text')->prepValueForDB('00000'));
|
||||
$this->assertEquals($db->prepStringForDB("0"), singleton('Text')->prepValueForDB(0000));
|
||||
$this->assertEquals($db->prepStringForDB("test"), singleton('Text')->prepValueForDB('test'));
|
||||
$this->assertEquals($db->prepStringForDB("123"), singleton('Text')->prepValueForDB(123));
|
||||
$this->assertEquals(0, singleton('Text')->prepValueForDB(0));
|
||||
$this->assertEquals(null, singleton('Text')->prepValueForDB(null));
|
||||
$this->assertEquals(null, singleton('Text')->prepValueForDB(false));
|
||||
$this->assertEquals(null, singleton('Text')->prepValueForDB(''));
|
||||
$this->assertEquals('0', singleton('Text')->prepValueForDB('0'));
|
||||
$this->assertEquals(1, singleton('Text')->prepValueForDB(1));
|
||||
$this->assertEquals(true, singleton('Text')->prepValueForDB(true));
|
||||
$this->assertEquals('1', singleton('Text')->prepValueForDB('1'));
|
||||
$this->assertEquals('00000', singleton('Text')->prepValueForDB('00000'));
|
||||
$this->assertEquals(0, singleton('Text')->prepValueForDB(0000));
|
||||
$this->assertEquals('test', singleton('Text')->prepValueForDB('test'));
|
||||
$this->assertEquals(123, singleton('Text')->prepValueForDB(123));
|
||||
|
||||
/* AllowEmpty Text behaviour */
|
||||
$textField = new Text("testfield", array("nullifyEmpty"=>false));
|
||||
$this->assertSame($db->prepStringForDB("0"), $textField->prepValueForDB(0));
|
||||
$this->assertSame("null", $textField->prepValueForDB(null));
|
||||
$this->assertSame("null", $textField->prepValueForDB(false));
|
||||
$this->assertSame($db->prepStringForDB(""), $textField->prepValueForDB(''));
|
||||
$this->assertSame($db->prepStringForDB("0"), $textField->prepValueForDB('0'));
|
||||
$this->assertSame($db->prepStringForDB("1"), $textField->prepValueForDB(1));
|
||||
$this->assertSame($db->prepStringForDB("1"), $textField->prepValueForDB(true));
|
||||
$this->assertSame($db->prepStringForDB("1"), $textField->prepValueForDB('1'));
|
||||
$this->assertSame($db->prepStringForDB("00000"), $textField->prepValueForDB('00000'));
|
||||
$this->assertSame($db->prepStringForDB("0"), $textField->prepValueForDB(0000));
|
||||
$this->assertSame($db->prepStringForDB("test"), $textField->prepValueForDB('test'));
|
||||
$this->assertSame($db->prepStringForDB("123"), $textField->prepValueForDB(123));
|
||||
$this->assertSame(0, $textField->prepValueForDB(0));
|
||||
$this->assertSame(null, $textField->prepValueForDB(null));
|
||||
$this->assertSame(null, $textField->prepValueForDB(false));
|
||||
$this->assertSame('', $textField->prepValueForDB(''));
|
||||
$this->assertSame('0', $textField->prepValueForDB('0'));
|
||||
$this->assertSame(1, $textField->prepValueForDB(1));
|
||||
$this->assertSame(true, $textField->prepValueForDB(true));
|
||||
$this->assertSame('1', $textField->prepValueForDB('1'));
|
||||
$this->assertSame('00000', $textField->prepValueForDB('00000'));
|
||||
$this->assertSame(0, $textField->prepValueForDB(0000));
|
||||
$this->assertSame('test', $textField->prepValueForDB('test'));
|
||||
$this->assertSame(123, $textField->prepValueForDB(123));
|
||||
unset($textField);
|
||||
|
||||
/* Time behaviour */
|
||||
|
@ -112,7 +112,7 @@ class DataDifferencerTest_HasOneRelationObject extends DataObject implements Tes
|
||||
class DataDifferencerTest_MockImage extends Image implements TestOnly {
|
||||
public function generateFormattedImage($format, $arg1 = null, $arg2 = null) {
|
||||
$cacheFile = $this->cacheFilename($format, $arg1, $arg2);
|
||||
$gd = new GD(Director::baseFolder()."/" . $this->Filename);
|
||||
$gd = new GDBackend(Director::baseFolder()."/" . $this->Filename);
|
||||
// Skip aktual generation
|
||||
return $gd;
|
||||
}
|
||||
|
@ -38,8 +38,12 @@ class DataExtensionTest extends SapphireTest {
|
||||
unset($contact);
|
||||
unset($object);
|
||||
|
||||
$contact = DataObject::get_one("DataExtensionTest_Member", "\"Website\"='http://www.example.com'");
|
||||
$object = DataObject::get_one('DataExtensionTest_RelatedObject', "\"ContactID\" = {$contactID}");
|
||||
$contact = DataObject::get_one("DataExtensionTest_Member", array(
|
||||
'"DataExtensionTest_Member"."Website"' => 'http://www.example.com'
|
||||
));
|
||||
$object = DataObject::get_one('DataExtensionTest_RelatedObject', array(
|
||||
'"DataExtensionTest_RelatedObject"."ContactID"' => $contactID
|
||||
));
|
||||
|
||||
$this->assertNotNull($object, 'Related object not null');
|
||||
$this->assertInstanceOf('DataExtensionTest_Member', $object->Contact(),
|
||||
@ -93,7 +97,9 @@ class DataExtensionTest extends SapphireTest {
|
||||
unset($player);
|
||||
|
||||
// Pull the record out of the DB and examine the extended fields
|
||||
$player = DataObject::get_one('DataExtensionTest_Player', "\"Name\" = 'Joe'");
|
||||
$player = DataObject::get_one('DataExtensionTest_Player', array(
|
||||
'"DataExtensionTest_Player"."Name"' => 'Joe'
|
||||
));
|
||||
$this->assertEquals($player->DateBirth, '1990-05-10');
|
||||
$this->assertEquals($player->Address, '123 somewhere street');
|
||||
$this->assertEquals($player->Status, 'Goalie');
|
||||
|
@ -133,19 +133,21 @@ class DataListTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testSql() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
$list = DataObjectTest_TeamComment::get();
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created",'
|
||||
. ' "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name",'
|
||||
. ' "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID",'
|
||||
. ' "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL'
|
||||
. ' THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment')
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", '
|
||||
. '"DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Created", '
|
||||
. '"DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", '
|
||||
. '"DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", '
|
||||
. 'CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL '
|
||||
. 'THEN "DataObjectTest_TeamComment"."ClassName" ELSE '
|
||||
. $db->quoteString('DataObjectTest_TeamComment')
|
||||
. ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment"';
|
||||
$this->assertEquals($expected, $list->sql());
|
||||
$this->assertSQLEquals($expected, $list->sql($parameters));
|
||||
}
|
||||
|
||||
public function testInnerJoin() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
|
||||
$list = DataObjectTest_TeamComment::get();
|
||||
$list = $list->innerJoin(
|
||||
@ -154,19 +156,22 @@ class DataListTest extends SapphireTest {
|
||||
'Team'
|
||||
);
|
||||
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created",'
|
||||
. ' "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name",'
|
||||
. ' "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID",'
|
||||
. ' "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL'
|
||||
. ' THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment')
|
||||
. ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" INNER JOIN "DataObjectTest_Team" AS "Team"'
|
||||
. ' ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", '
|
||||
. '"DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Created", '
|
||||
. '"DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", '
|
||||
. '"DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", '
|
||||
. 'CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL'
|
||||
. ' THEN "DataObjectTest_TeamComment"."ClassName" ELSE '
|
||||
. $db->quoteString('DataObjectTest_TeamComment')
|
||||
. ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" INNER JOIN '
|
||||
. '"DataObjectTest_Team" AS "Team" ON "DataObjectTest_Team"."ID" = '
|
||||
. '"DataObjectTest_TeamComment"."TeamID"';
|
||||
|
||||
$this->assertEquals($expected, $list->sql());
|
||||
$this->assertSQLEquals($expected, $list->sql($parameters));
|
||||
}
|
||||
|
||||
public function testLeftJoin() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
|
||||
$list = DataObjectTest_TeamComment::get();
|
||||
$list = $list->leftJoin(
|
||||
@ -175,15 +180,17 @@ class DataListTest extends SapphireTest {
|
||||
'Team'
|
||||
);
|
||||
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", "DataObjectTest_TeamComment"."Created",'
|
||||
. ' "DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Name",'
|
||||
. ' "DataObjectTest_TeamComment"."Comment", "DataObjectTest_TeamComment"."TeamID",'
|
||||
. ' "DataObjectTest_TeamComment"."ID", CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL'
|
||||
. ' THEN "DataObjectTest_TeamComment"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_TeamComment')
|
||||
. ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" LEFT JOIN "DataObjectTest_Team" AS "Team"'
|
||||
. ' ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", '
|
||||
. '"DataObjectTest_TeamComment"."LastEdited", "DataObjectTest_TeamComment"."Created", '
|
||||
. '"DataObjectTest_TeamComment"."Name", "DataObjectTest_TeamComment"."Comment", '
|
||||
. '"DataObjectTest_TeamComment"."TeamID", "DataObjectTest_TeamComment"."ID", '
|
||||
. 'CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL '
|
||||
. 'THEN "DataObjectTest_TeamComment"."ClassName" ELSE '
|
||||
. $db->quoteString('DataObjectTest_TeamComment')
|
||||
. ' END AS "RecordClassName" FROM "DataObjectTest_TeamComment" LEFT JOIN "DataObjectTest_Team" '
|
||||
. 'AS "Team" ON "DataObjectTest_Team"."ID" = "DataObjectTest_TeamComment"."TeamID"';
|
||||
|
||||
$this->assertEquals($expected, $list->sql());
|
||||
$this->assertSQLEquals($expected, $list->sql($parameters));
|
||||
|
||||
// Test with namespaces (with non-sensical join, but good enough for testing)
|
||||
$list = DataObjectTest_TeamComment::get();
|
||||
@ -193,19 +200,19 @@ class DataListTest extends SapphireTest {
|
||||
);
|
||||
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_TeamComment"."ClassName", '
|
||||
. '"DataObjectTest_TeamComment"."Created", '
|
||||
. '"DataObjectTest_TeamComment"."LastEdited", '
|
||||
. '"DataObjectTest_TeamComment"."Created", '
|
||||
. '"DataObjectTest_TeamComment"."Name", '
|
||||
. '"DataObjectTest_TeamComment"."Comment", '
|
||||
. '"DataObjectTest_TeamComment"."TeamID", '
|
||||
. '"DataObjectTest_TeamComment"."ID", '
|
||||
. 'CASE WHEN "DataObjectTest_TeamComment"."ClassName" IS NOT NULL '
|
||||
. 'THEN "DataObjectTest_TeamComment"."ClassName" '
|
||||
. 'ELSE ' . $db->prepStringForDB('DataObjectTest_TeamComment') . ' END AS "RecordClassName" '
|
||||
. 'ELSE ' . $db->quoteString('DataObjectTest_TeamComment') . ' END AS "RecordClassName" '
|
||||
. 'FROM "DataObjectTest_TeamComment" '
|
||||
. 'LEFT JOIN "DataObjectTest\NamespacedClass" ON '
|
||||
. '"DataObjectTest\NamespacedClass"."ID" = "DataObjectTest_TeamComment"."ID"';
|
||||
$this->assertEquals($expected, $list->sql(), 'Retains backslashes in namespaced classes');
|
||||
$this->assertSQLEquals($expected, $list->sql($parameters), 'Retains backslashes in namespaced classes');
|
||||
|
||||
}
|
||||
|
||||
@ -755,17 +762,20 @@ class DataListTest extends SapphireTest {
|
||||
$list = $list->filter('Comment', 'Phil is a unique guy, and comments on team2');
|
||||
$list = $list->exclude('Name', 'Bob');
|
||||
|
||||
$this->assertContains(
|
||||
'WHERE ("DataObjectTest_TeamComment"."Comment" = '
|
||||
. '\'Phil is a unique guy, and comments on team2\') '
|
||||
. 'AND (("DataObjectTest_TeamComment"."Name" != \'Bob\'))',
|
||||
$list->sql());
|
||||
$sql = $list->sql($parameters);
|
||||
$this->assertSQLContains(
|
||||
'WHERE ("DataObjectTest_TeamComment"."Comment" = ?) AND (("DataObjectTest_TeamComment"."Name" != ?))',
|
||||
$sql);
|
||||
$this->assertEquals(array('Phil is a unique guy, and comments on team2', 'Bob'), $parameters);
|
||||
}
|
||||
|
||||
public function testExcludeWithSearchFilter() {
|
||||
$list = DataObjectTest_TeamComment::get();
|
||||
$list = $list->exclude('Name:LessThan', 'Bob');
|
||||
$this->assertContains('WHERE (("DataObjectTest_TeamComment"."Name" >= \'Bob\'))', $list->sql());
|
||||
|
||||
$sql = $list->sql($parameters);
|
||||
$this->assertSQLContains('WHERE (("DataObjectTest_TeamComment"."Name" >= ?))', $sql);
|
||||
$this->assertEquals(array('Bob'), $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -853,10 +863,11 @@ class DataListTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testSortByComplexExpression() {
|
||||
// Test an expression with both spaces and commas
|
||||
// This test also tests that column() can be called with a complex sort expression, so keep using column() below
|
||||
// Test an expression with both spaces and commas. This test also tests that column() can be called
|
||||
// with a complex sort expression, so keep using column() below
|
||||
$list = DataObjectTest_Team::get()->sort(
|
||||
'CASE WHEN "DataObjectTest_Team"."ClassName" = \'DataObjectTest_SubTeam\' THEN 0 ELSE 1 END, "Title" DESC');
|
||||
'CASE WHEN "DataObjectTest_Team"."ClassName" = \'DataObjectTest_SubTeam\' THEN 0 ELSE 1 END, "Title" DESC'
|
||||
);
|
||||
$this->assertEquals(array(
|
||||
'Subteam 3',
|
||||
'Subteam 2',
|
||||
|
@ -27,63 +27,62 @@ class DataObjectLazyLoadingTest extends SapphireTest {
|
||||
);
|
||||
|
||||
public function testQueriedColumnsID() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||
$playerList = $playerList->setQueriedColumns(array('ID'));
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."ID", CASE WHEN '.
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."LastEdited", ' .
|
||||
'"DataObjectTest_Team"."Created", "DataObjectTest_Team"."ID", CASE WHEN '.
|
||||
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName", "DataObjectTest_Team"."Title" '.
|
||||
$db->quoteString('DataObjectTest_Team').' END AS "RecordClassName", "DataObjectTest_Team"."Title" '.
|
||||
'FROM "DataObjectTest_Team" ' .
|
||||
'LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = "DataObjectTest_Team"."ID" ' .
|
||||
'WHERE ("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').'))' .
|
||||
'WHERE ("DataObjectTest_Team"."ClassName" IN (?))' .
|
||||
' ORDER BY "DataObjectTest_Team"."Title" ASC';
|
||||
$this->assertEquals($expected, $playerList->sql());
|
||||
$this->assertSQLEquals($expected, $playerList->sql($parameters));
|
||||
}
|
||||
|
||||
public function testQueriedColumnsFromBaseTableAndSubTable() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||
$playerList = $playerList->setQueriedColumns(array('Title', 'SubclassDatabaseField'));
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", ' .
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."LastEdited", ' .
|
||||
'"DataObjectTest_Team"."Created", "DataObjectTest_Team"."Title", ' .
|
||||
'"DataObjectTest_SubTeam"."SubclassDatabaseField", "DataObjectTest_Team"."ID", CASE WHEN ' .
|
||||
'"DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" ' .
|
||||
$db->quoteString('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" ' .
|
||||
'LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = "DataObjectTest_Team"."ID" WHERE ' .
|
||||
'("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').')) ' .
|
||||
'("DataObjectTest_Team"."ClassName" IN (?)) ' .
|
||||
'ORDER BY "DataObjectTest_Team"."Title" ASC';
|
||||
$this->assertEquals($expected, $playerList->sql());
|
||||
$this->assertSQLEquals($expected, $playerList->sql($parameters));
|
||||
}
|
||||
|
||||
public function testQueriedColumnsFromBaseTable() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||
$playerList = $playerList->setQueriedColumns(array('Title'));
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_Team"."Title", "DataObjectTest_Team"."ID", ' .
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."LastEdited", ' .
|
||||
'"DataObjectTest_Team"."Created", "DataObjectTest_Team"."Title", "DataObjectTest_Team"."ID", ' .
|
||||
'CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN "DataObjectTest_Team"."ClassName" ELSE ' .
|
||||
$db->prepStringForDB('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" ' .
|
||||
$db->quoteString('DataObjectTest_Team').' END AS "RecordClassName" FROM "DataObjectTest_Team" ' .
|
||||
'LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = "DataObjectTest_Team"."ID" WHERE ' .
|
||||
'("DataObjectTest_Team"."ClassName" IN ('.$db->prepStringForDB('DataObjectTest_SubTeam').')) ' .
|
||||
'("DataObjectTest_Team"."ClassName" IN (?)) ' .
|
||||
'ORDER BY "DataObjectTest_Team"."Title" ASC';
|
||||
$this->assertEquals($expected, $playerList->sql());
|
||||
$this->assertSQLEquals($expected, $playerList->sql($parameters));
|
||||
}
|
||||
|
||||
public function testQueriedColumnsFromSubTable() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||
$playerList = $playerList->setQueriedColumns(array('SubclassDatabaseField'));
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."Created", ' .
|
||||
'"DataObjectTest_Team"."LastEdited", "DataObjectTest_SubTeam"."SubclassDatabaseField", ' .
|
||||
$expected = 'SELECT DISTINCT "DataObjectTest_Team"."ClassName", "DataObjectTest_Team"."LastEdited", ' .
|
||||
'"DataObjectTest_Team"."Created", "DataObjectTest_SubTeam"."SubclassDatabaseField", ' .
|
||||
'"DataObjectTest_Team"."ID", CASE WHEN "DataObjectTest_Team"."ClassName" IS NOT NULL THEN ' .
|
||||
'"DataObjectTest_Team"."ClassName" ELSE '.$db->prepStringForDB('DataObjectTest_Team').' END ' .
|
||||
'"DataObjectTest_Team"."ClassName" ELSE '.$db->quoteString('DataObjectTest_Team').' END ' .
|
||||
'AS "RecordClassName", "DataObjectTest_Team"."Title" ' .
|
||||
'FROM "DataObjectTest_Team" LEFT JOIN "DataObjectTest_SubTeam" ON "DataObjectTest_SubTeam"."ID" = ' .
|
||||
'"DataObjectTest_Team"."ID" WHERE ("DataObjectTest_Team"."ClassName" IN (' .
|
||||
$db->prepStringForDB('DataObjectTest_SubTeam').')) ' .
|
||||
'"DataObjectTest_Team"."ID" WHERE ("DataObjectTest_Team"."ClassName" IN (?)) ' .
|
||||
'ORDER BY "DataObjectTest_Team"."Title" ASC';
|
||||
$this->assertEquals($expected, $playerList->sql());
|
||||
$this->assertSQLEquals($expected, $playerList->sql($parameters));
|
||||
}
|
||||
|
||||
public function testNoSpecificColumnNamesBaseDataObjectQuery() {
|
||||
@ -91,15 +90,23 @@ class DataObjectLazyLoadingTest extends SapphireTest {
|
||||
$playerList = new DataList('DataObjectTest_Team');
|
||||
// Shouldn't be a left join in here.
|
||||
$this->assertEquals(0,
|
||||
preg_match('/SELECT DISTINCT "DataObjectTest_Team"."ID" .* LEFT JOIN .* FROM "DataObjectTest_Team"/',
|
||||
$playerList->sql()));
|
||||
preg_match(
|
||||
$this->normaliseSQL(
|
||||
'/SELECT DISTINCT "DataObjectTest_Team"."ID" .* LEFT JOIN .* FROM "DataObjectTest_Team"/'
|
||||
),
|
||||
$this->normaliseSQL($playerList->sql($parameters))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testNoSpecificColumnNamesSubclassDataObjectQuery() {
|
||||
// This queries all columns from base table and subtable
|
||||
$playerList = new DataList('DataObjectTest_SubTeam');
|
||||
// Should be a left join.
|
||||
$this->assertEquals(1, preg_match('/SELECT DISTINCT .* LEFT JOIN .* /', $playerList->sql()));
|
||||
$this->assertEquals(1, preg_match(
|
||||
$this->normaliseSQL('/SELECT DISTINCT .* LEFT JOIN .* /'),
|
||||
$this->normaliseSQL($playerList->sql($parameters))
|
||||
));
|
||||
}
|
||||
|
||||
public function testLazyLoadedFieldsHasField() {
|
||||
|
@ -10,7 +10,7 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
|
||||
// enable fulltext option on this table
|
||||
Config::inst()->update('DataObjectSchemaGenerationTest_IndexDO', 'create_table_options',
|
||||
array('MySQLDatabase' => 'ENGINE=MyISAM'));
|
||||
array(MySQLSchemaManager::ID => 'ENGINE=MyISAM'));
|
||||
|
||||
parent::setUpOnce();
|
||||
}
|
||||
@ -19,25 +19,28 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
* Check that once a schema has been generated, then it doesn't need any more updating
|
||||
*/
|
||||
public function testFieldsDontRerequestChanges() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
$test = $this;
|
||||
DB::quiet();
|
||||
|
||||
// Table will have been initially created by the $extraDataObjects setting
|
||||
|
||||
// Verify that it doesn't need to be recreated
|
||||
$db->beginSchemaUpdate();
|
||||
$obj = new DataObjectSchemaGenerationTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertFalse($needsUpdating);
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj = new DataObjectSchemaGenerationTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertFalse($needsUpdating);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that updates to a class fields are reflected in the database
|
||||
*/
|
||||
public function testFieldsRequestChanges() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
$test = $this;
|
||||
DB::quiet();
|
||||
|
||||
// Table will have been initially created by the $extraDataObjects setting
|
||||
@ -49,12 +52,13 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
));
|
||||
|
||||
// Verify that the above extra field triggered a schema update
|
||||
$db->beginSchemaUpdate();
|
||||
$obj = new DataObjectSchemaGenerationTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertTrue($needsUpdating);
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj = new DataObjectSchemaGenerationTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertTrue($needsUpdating);
|
||||
});
|
||||
|
||||
// Restore db configuration
|
||||
Config::unnest();
|
||||
@ -64,18 +68,20 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
* Check that indexes on a newly generated class do not subsequently request modification
|
||||
*/
|
||||
public function testIndexesDontRerequestChanges() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
$test = $this;
|
||||
DB::quiet();
|
||||
|
||||
// Table will have been initially created by the $extraDataObjects setting
|
||||
|
||||
// Verify that it doesn't need to be recreated
|
||||
$db->beginSchemaUpdate();
|
||||
$obj = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertFalse($needsUpdating);
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertFalse($needsUpdating);
|
||||
});
|
||||
|
||||
// Test with alternate index format, although these indexes are the same
|
||||
Config::nest();
|
||||
@ -85,12 +91,13 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
);
|
||||
|
||||
// Verify that it still doesn't need to be recreated
|
||||
$db->beginSchemaUpdate();
|
||||
$obj2 = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj2->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertFalse($needsUpdating);
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj2 = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj2->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertFalse($needsUpdating);
|
||||
});
|
||||
|
||||
// Restore old index format
|
||||
Config::unnest();
|
||||
@ -100,7 +107,8 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
* Check that updates to a dataobject's indexes are reflected in DDL
|
||||
*/
|
||||
public function testIndexesRerequestChanges() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
$test = $this;
|
||||
DB::quiet();
|
||||
|
||||
// Table will have been initially created by the $extraDataObjects setting
|
||||
@ -114,12 +122,13 @@ class DataObjectSchemaGenerationTest extends SapphireTest {
|
||||
));
|
||||
|
||||
// Verify that the above index change triggered a schema update
|
||||
$db->beginSchemaUpdate();
|
||||
$obj = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertTrue($needsUpdating);
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj = new DataObjectSchemaGenerationTest_IndexDO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertTrue($needsUpdating);
|
||||
});
|
||||
|
||||
// Restore old indexes
|
||||
Config::unnest();
|
||||
|
@ -154,17 +154,25 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals('Captain', $captain1->FirstName);
|
||||
|
||||
// Test get_one() without caching
|
||||
$comment1 = DataObject::get_one('DataObjectTest_TeamComment', "\"Name\" = 'Joe'", false);
|
||||
$comment1 = DataObject::get_one('DataObjectTest_TeamComment', array(
|
||||
'"DataObjectTest_TeamComment"."Name"' => 'Joe'
|
||||
), false);
|
||||
$comment1->Comment = "Something Else";
|
||||
|
||||
$comment2 = DataObject::get_one('DataObjectTest_TeamComment', "\"Name\" = 'Joe'", false);
|
||||
$comment2 = DataObject::get_one('DataObjectTest_TeamComment', array(
|
||||
'"DataObjectTest_TeamComment"."Name"' => 'Joe'
|
||||
), false);
|
||||
$this->assertNotEquals($comment1->Comment, $comment2->Comment);
|
||||
|
||||
// Test get_one() with caching
|
||||
$comment1 = DataObject::get_one('DataObjectTest_TeamComment', "\"Name\" = 'Bob'", true);
|
||||
$comment1 = DataObject::get_one('DataObjectTest_TeamComment', array(
|
||||
'"DataObjectTest_TeamComment"."Name"' => 'Bob'
|
||||
), true);
|
||||
$comment1->Comment = "Something Else";
|
||||
|
||||
$comment2 = DataObject::get_one('DataObjectTest_TeamComment', "\"Name\" = 'Bob'", true);
|
||||
$comment2 = DataObject::get_one('DataObjectTest_TeamComment', array(
|
||||
'"DataObjectTest_TeamComment"."Name"' => 'Bob'
|
||||
), true);
|
||||
$this->assertEquals((string)$comment1->Comment, (string)$comment2->Comment);
|
||||
|
||||
// Test get_one() with order by without caching
|
||||
@ -179,9 +187,20 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals('Bob', $comment->Name);
|
||||
$comment = DataObject::get_one('DataObjectTest_TeamComment', '', true, '"Name" DESC');
|
||||
$this->assertEquals('Phil', $comment->Name);
|
||||
|
||||
}
|
||||
|
||||
public function testGetCaseInsensitive() {
|
||||
// Test get_one() with bad case on the classname
|
||||
$subteam1 = DataObject::get_one('dataobjecttest_subteam', "\"Title\" = 'Subteam 1'", true);
|
||||
// Note: This will succeed only if the underlying DB server supports case-insensitive
|
||||
// table names (e.g. such as MySQL, but not SQLite3)
|
||||
if(!(DB::get_conn() instanceof MySQLDatabase)) {
|
||||
$this->markTestSkipped('MySQL only');
|
||||
}
|
||||
|
||||
$subteam1 = DataObject::get_one('dataobjecttest_subteam', array(
|
||||
'"DataObjectTest_Team"."Title"' => 'Subteam 1'
|
||||
), true);
|
||||
$this->assertNotEmpty($subteam1);
|
||||
$this->assertEquals($subteam1->Title, "Subteam 1");
|
||||
}
|
||||
|
||||
@ -545,10 +564,10 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertFalse($obj->isChanged());
|
||||
|
||||
/* If we perform the same random query twice, it shouldn't return the same results */
|
||||
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsC = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsD = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random());
|
||||
$itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::get_conn()->random());
|
||||
$itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::get_conn()->random());
|
||||
$itemsC = DataObject::get("DataObjectTest_TeamComment", "", DB::get_conn()->random());
|
||||
$itemsD = DataObject::get("DataObjectTest_TeamComment", "", DB::get_conn()->random());
|
||||
foreach($itemsA as $item) $keysA[] = $item->ID;
|
||||
foreach($itemsB as $item) $keysB[] = $item->ID;
|
||||
foreach($itemsC as $item) $keysC[] = $item->ID;
|
||||
@ -742,12 +761,12 @@ class DataObjectTest extends SapphireTest {
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array_keys(DataObject::database_fields('DataObjectTest_Team')),
|
||||
array_keys(DataObject::database_fields('DataObjectTest_Team', false)),
|
||||
array(
|
||||
//'ID',
|
||||
'ClassName',
|
||||
'Created',
|
||||
'LastEdited',
|
||||
'Created',
|
||||
'Title',
|
||||
'DatabaseField',
|
||||
'ExtendedDatabaseField',
|
||||
@ -778,7 +797,7 @@ class DataObjectTest extends SapphireTest {
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array_keys(DataObject::database_fields('DataObjectTest_SubTeam')),
|
||||
array_keys(DataObject::database_fields('DataObjectTest_SubTeam', false)),
|
||||
array(
|
||||
'SubclassDatabaseField',
|
||||
'ParentTeamID',
|
||||
@ -900,7 +919,7 @@ class DataObjectTest extends SapphireTest {
|
||||
|
||||
public function testForceInsert() {
|
||||
/* If you set an ID on an object and pass forceInsert = true, then the object should be correctly created */
|
||||
$conn = DB::getConn();
|
||||
$conn = DB::get_conn();
|
||||
if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing('DataObjectTest_Team', true);
|
||||
$obj = new DataObjectTest_SubTeam();
|
||||
$obj->ID = 1001;
|
||||
@ -1265,6 +1284,7 @@ class DataObjectTest extends SapphireTest {
|
||||
$this->assertEquals($ceo->ID, $ceo->Company()->CEOID, 'Remote IDs are automatically set.');
|
||||
|
||||
// Write object with components
|
||||
$ceo->Name = 'Edward Scissorhands';
|
||||
$ceo->write(false, false, false, true);
|
||||
$this->assertTrue($ceo->Company()->isInDB(), 'write() writes belongs_to components to the database.');
|
||||
|
||||
|
@ -18,13 +18,13 @@ class DataQueryTest extends SapphireTest {
|
||||
public function testJoins() {
|
||||
$dq = new DataQuery('Member');
|
||||
$dq->innerJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
$this->assertContains("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"",
|
||||
$dq->sql());
|
||||
$this->assertSQLContains("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"",
|
||||
$dq->sql($parameters));
|
||||
|
||||
$dq = new DataQuery('Member');
|
||||
$dq->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"");
|
||||
$this->assertContains("LEFT JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"",
|
||||
$dq->sql());
|
||||
$this->assertSQLContains("LEFT JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"",
|
||||
$dq->sql($parameters));
|
||||
}
|
||||
|
||||
public function testRelationReturn() {
|
||||
@ -58,9 +58,9 @@ class DataQueryTest extends SapphireTest {
|
||||
$subDq->where('DataQueryTest_A.Name = \'John\'');
|
||||
$subDq->where('DataQueryTest_A.Name = \'Bob\'');
|
||||
|
||||
$this->assertContains(
|
||||
$this->assertSQLContains(
|
||||
"WHERE (DataQueryTest_A.ID = 2) AND ((DataQueryTest_A.Name = 'John') OR (DataQueryTest_A.Name = 'Bob'))",
|
||||
$dq->sql()
|
||||
$dq->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
@ -72,12 +72,15 @@ class DataQueryTest extends SapphireTest {
|
||||
$subDq->where('DataQueryTest_A.Name = \'John\'');
|
||||
$subDq->where('DataQueryTest_A.Name = \'Bob\'');
|
||||
|
||||
$this->assertContains(
|
||||
$this->assertSQLContains(
|
||||
"WHERE (DataQueryTest_A.ID = 2) AND ((DataQueryTest_A.Name = 'John') AND (DataQueryTest_A.Name = 'Bob'))",
|
||||
$dq->sql()
|
||||
$dq->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Test paramaterised
|
||||
*/
|
||||
public function testNestedGroups() {
|
||||
$dq = new DataQuery('DataQueryTest_A');
|
||||
|
||||
@ -89,10 +92,10 @@ class DataQueryTest extends SapphireTest {
|
||||
$subSubDq->where('DataQueryTest_A.Age = 50');
|
||||
$subDq->where('DataQueryTest_A.Name = \'Bob\'');
|
||||
|
||||
$this->assertContains(
|
||||
$this->assertSQLContains(
|
||||
"WHERE (DataQueryTest_A.ID = 2) AND ((DataQueryTest_A.Name = 'John') OR ((DataQueryTest_A.Age = 18) "
|
||||
. "AND (DataQueryTest_A.Age = 50)) OR (DataQueryTest_A.Name = 'Bob'))",
|
||||
$dq->sql()
|
||||
$dq->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
@ -100,7 +103,8 @@ class DataQueryTest extends SapphireTest {
|
||||
$dq = new DataQuery('DataQueryTest_A');
|
||||
$dq->conjunctiveGroup();
|
||||
|
||||
$this->assertContains('WHERE (1=1)', $dq->sql());
|
||||
// Empty groups should have no where condition at all
|
||||
$this->assertSQLNotContains('WHERE', $dq->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSubgroupHandoff() {
|
||||
@ -112,20 +116,20 @@ class DataQueryTest extends SapphireTest {
|
||||
$subDq->sort('"DataQueryTest_A"."Name"');
|
||||
$orgDq->sort('"DataQueryTest_A"."Name"');
|
||||
|
||||
$this->assertEquals($dq->sql(), $orgDq->sql());
|
||||
$this->assertSQLEquals($dq->sql($parameters), $orgDq->sql($parameters));
|
||||
|
||||
$subDq->limit(5, 7);
|
||||
$orgDq->limit(5, 7);
|
||||
|
||||
$this->assertEquals($dq->sql(), $orgDq->sql());
|
||||
$this->assertSQLEquals($dq->sql($parameters), $orgDq->sql($parameters));
|
||||
}
|
||||
|
||||
public function testOrderByMultiple() {
|
||||
$dq = new DataQuery('SQLQueryTest_DO');
|
||||
$dq = $dq->sort('"Name" ASC, MID("Name", 8, 1) DESC');
|
||||
$this->assertContains(
|
||||
'ORDER BY "Name" ASC, "_SortColumn0" DESC',
|
||||
$dq->sql()
|
||||
'ORDER BY "SQLQueryTest_DO"."Name" ASC, "_SortColumn0" DESC',
|
||||
$dq->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,17 +12,17 @@ class DatabaseTest extends SapphireTest {
|
||||
protected $usesDatabase = true;
|
||||
|
||||
public function testDontRequireField() {
|
||||
$conn = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
$this->assertArrayHasKey(
|
||||
'MyField',
|
||||
$conn->fieldList('DatabaseTest_MyObject')
|
||||
$schema->fieldList('DatabaseTest_MyObject')
|
||||
);
|
||||
|
||||
$conn->dontRequireField('DatabaseTest_MyObject', 'MyField');
|
||||
$schema->dontRequireField('DatabaseTest_MyObject', 'MyField');
|
||||
|
||||
$this->assertArrayHasKey(
|
||||
'_obsolete_MyField',
|
||||
$conn->fieldList('DatabaseTest_MyObject'),
|
||||
$schema->fieldList('DatabaseTest_MyObject'),
|
||||
'Field is renamed to _obsolete_<fieldname> through dontRequireField()'
|
||||
);
|
||||
|
||||
@ -30,20 +30,20 @@ class DatabaseTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testRenameField() {
|
||||
$conn = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
|
||||
$conn->clearCachedFieldlist();
|
||||
$schema->clearCachedFieldlist();
|
||||
|
||||
$conn->renameField('DatabaseTest_MyObject', 'MyField', 'MyRenamedField');
|
||||
$schema->renameField('DatabaseTest_MyObject', 'MyField', 'MyRenamedField');
|
||||
|
||||
$this->assertArrayHasKey(
|
||||
'MyRenamedField',
|
||||
$conn->fieldList('DatabaseTest_MyObject'),
|
||||
$schema->fieldList('DatabaseTest_MyObject'),
|
||||
'New fieldname is set through renameField()'
|
||||
);
|
||||
$this->assertArrayNotHasKey(
|
||||
'MyField',
|
||||
$conn->fieldList('DatabaseTest_MyObject'),
|
||||
$schema->fieldList('DatabaseTest_MyObject'),
|
||||
'Old fieldname isnt preserved through renameField()'
|
||||
);
|
||||
|
||||
@ -51,7 +51,7 @@ class DatabaseTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testMySQLCreateTableOptions() {
|
||||
if(!(DB::getConn() instanceof MySQLDatabase)) {
|
||||
if(!(DB::get_conn() instanceof MySQLDatabase)) {
|
||||
$this->markTestSkipped('MySQL only');
|
||||
}
|
||||
|
||||
@ -66,44 +66,49 @@ class DatabaseTest extends SapphireTest {
|
||||
}
|
||||
|
||||
function testIsSchemaUpdating() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
|
||||
$this->assertFalse($db->isSchemaUpdating(), 'Before the transaction the flag is false.');
|
||||
$this->assertFalse($schema->isSchemaUpdating(), 'Before the transaction the flag is false.');
|
||||
|
||||
$db->beginSchemaUpdate();
|
||||
$this->assertTrue($db->isSchemaUpdating(), 'During the transaction the flag is true.');
|
||||
// Test complete schema update
|
||||
$test = $this;
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$test->assertTrue($schema->isSchemaUpdating(), 'During the transaction the flag is true.');
|
||||
});
|
||||
$this->assertFalse($schema->isSchemaUpdating(), 'After the transaction the flag is false.');
|
||||
|
||||
$db->endSchemaUpdate();
|
||||
$this->assertFalse($db->isSchemaUpdating(), 'After the transaction the flag is false.');
|
||||
|
||||
$db->beginSchemaUpdate();
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertFalse($db->doesSchemaNeedUpdating(), 'After cancelling the transaction the flag is false');
|
||||
// Test cancelled schema update
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertFalse($schema->doesSchemaNeedUpdating(), 'After cancelling the transaction the flag is false');
|
||||
});
|
||||
}
|
||||
|
||||
public function testSchemaUpdateChecking() {
|
||||
$db = DB::getConn();
|
||||
$schema = DB::get_schema();
|
||||
|
||||
// Initially, no schema changes necessary
|
||||
$db->beginSchemaUpdate();
|
||||
$this->assertFalse($db->doesSchemaNeedUpdating());
|
||||
$test = $this;
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$test->assertFalse($schema->doesSchemaNeedUpdating());
|
||||
|
||||
// If we make a change, then the schema will need updating
|
||||
$db->transCreateTable("TestTable");
|
||||
$this->assertTrue($db->doesSchemaNeedUpdating());
|
||||
// If we make a change, then the schema will need updating
|
||||
$schema->transCreateTable("TestTable");
|
||||
$test->assertTrue($schema->doesSchemaNeedUpdating());
|
||||
|
||||
// If we make cancel the change, then schema updates are no longer necessary
|
||||
$db->cancelSchemaUpdate();
|
||||
$this->assertFalse($db->doesSchemaNeedUpdating());
|
||||
// If we make cancel the change, then schema updates are no longer necessary
|
||||
$schema->cancelSchemaUpdate();
|
||||
$test->assertFalse($schema->doesSchemaNeedUpdating());
|
||||
});
|
||||
}
|
||||
|
||||
public function testHasTable() {
|
||||
$this->assertTrue(DB::getConn()->hasTable('DatabaseTest_MyObject'));
|
||||
$this->assertFalse(DB::getConn()->hasTable('asdfasdfasdf'));
|
||||
$this->assertTrue(DB::get_schema()->hasTable('DatabaseTest_MyObject'));
|
||||
$this->assertFalse(DB::get_schema()->hasTable('asdfasdfasdf'));
|
||||
}
|
||||
|
||||
public function testGetAndReleaseLock() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
|
||||
if(!$db->supportsLocks()) {
|
||||
return $this->markTestSkipped('Tested database doesn\'t support application locks');
|
||||
@ -129,7 +134,7 @@ class DatabaseTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testCanLock() {
|
||||
$db = DB::getConn();
|
||||
$db = DB::get_conn();
|
||||
|
||||
if(!$db->supportsLocks()) {
|
||||
return $this->markTestSkipped('Database doesn\'t support locks');
|
||||
@ -150,7 +155,7 @@ class DatabaseTest extends SapphireTest {
|
||||
|
||||
class DatabaseTest_MyObject extends DataObject implements TestOnly {
|
||||
|
||||
private static $create_table_options = array('MySQLDatabase' => 'ENGINE=InnoDB');
|
||||
private static $create_table_options = array(MySQLSchemaManager::ID => 'ENGINE=InnoDB');
|
||||
|
||||
private static $db = array(
|
||||
'MyField' => 'Varchar'
|
||||
|
@ -36,7 +36,7 @@ class DbDatetimeTest extends FunctionalTest {
|
||||
*/
|
||||
private function checkPreconditions() {
|
||||
// number of seconds of php and db time are out of sync
|
||||
$offset = time() - strtotime(DB::query('SELECT ' . DB::getConn()->now())->value());
|
||||
$offset = time() - strtotime(DB::query('SELECT ' . DB::get_conn()->now())->value());
|
||||
$threshold = 5; // seconds
|
||||
|
||||
if($offset > 5) {
|
||||
@ -53,7 +53,7 @@ class DbDatetimeTest extends FunctionalTest {
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$this->adapter = DB::getConn();
|
||||
$this->adapter = DB::get_conn();
|
||||
}
|
||||
|
||||
public function testCorrectNow() {
|
||||
@ -95,7 +95,7 @@ class DbDatetimeTest extends FunctionalTest {
|
||||
$this->matchesRoughly($result, date('Y-m-d H:i:s', strtotime('+1 Day', $this->getDbNow())), 'tomorrow',
|
||||
$offset);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array());
|
||||
$query->selectField($this->adapter->datetimeIntervalClause('"Created"', '-15 Minutes'), 'test')
|
||||
->setFrom('"DbDateTimeTest_Team"')
|
||||
@ -123,7 +123,7 @@ class DbDatetimeTest extends FunctionalTest {
|
||||
$result = DB::query('SELECT ' . $clause)->value();
|
||||
$this->matchesRoughly($result, -45 * 60, 'now - 45 minutes ahead', $offset);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array());
|
||||
$query->selectField($this->adapter->datetimeDifferenceClause('"LastEdited"', '"Created"'), 'test')
|
||||
->setFrom('"DbDateTimeTest_Team"')
|
||||
|
@ -10,7 +10,7 @@ class MySQLDatabaseTest extends SapphireTest {
|
||||
);
|
||||
|
||||
public function setUp() {
|
||||
if(DB::getConn() instanceof MySQLDatabase) {
|
||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
||||
MySQLDatabaseTest_DO::config()->db = array(
|
||||
'MultiEnum1' => 'MultiEnum("A, B, C, D","")',
|
||||
'MultiEnum2' => 'MultiEnum("A, B, C, D","A")',
|
||||
@ -26,18 +26,20 @@ class MySQLDatabaseTest extends SapphireTest {
|
||||
*/
|
||||
public function testFieldsDontRerequestChanges() {
|
||||
// These are MySQL specific :-S
|
||||
if(DB::getConn() instanceof MySQLDatabase) {
|
||||
$db = DB::getConn();
|
||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
||||
$schema = DB::get_schema();
|
||||
$test = $this;
|
||||
DB::quiet();
|
||||
|
||||
// Verify that it doesn't need to be recreated
|
||||
$db->beginSchemaUpdate();
|
||||
$obj = new MySQLDatabaseTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $db->doesSchemaNeedUpdating();
|
||||
$db->cancelSchemaUpdate();
|
||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
||||
$obj = new MySQLDatabaseTest_DO();
|
||||
$obj->requireTable();
|
||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
||||
$schema->cancelSchemaUpdate();
|
||||
|
||||
$this->assertFalse($needsUpdating);
|
||||
$test->assertFalse($needsUpdating);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class PaginatedListTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testSetPaginationFromQuery() {
|
||||
$query = $this->getMock('SQLQuery');
|
||||
$query = $this->getMock('SQLSelect');
|
||||
$query->expects($this->once())
|
||||
->method('getLimit')
|
||||
->will($this->returnValue(array('limit' => 15, 'start' => 30)));
|
||||
|
94
tests/model/SQLInsertTest.php
Normal file
94
tests/model/SQLInsertTest.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@see SQLInsert}
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
*/
|
||||
class SQLInsertTest extends SapphireTest {
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'SQLInsertTestBase'
|
||||
);
|
||||
|
||||
public function testEmptyQueryReturnsNothing() {
|
||||
$query = new SQLInsert();
|
||||
$this->assertSQLEquals('', $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testBasicInsert() {
|
||||
$query = SQLInsert::create()
|
||||
->setInto('"SQLInsertTestBase"')
|
||||
->assign('"Title"', 'My Object')
|
||||
->assign('"HasFun"', 1)
|
||||
->assign('"Age"', 10)
|
||||
->assign('"Description"', 'No description');
|
||||
$sql = $query->sql($parameters);
|
||||
// Only test this case if using the default query builder
|
||||
if(get_class(DB::get_conn()->getQueryBuilder()) === 'DBQueryBuilder') {
|
||||
$this->assertSQLEquals(
|
||||
'INSERT INTO "SQLInsertTestBase" ("Title", "HasFun", "Age", "Description") VALUES (?, ?, ?, ?)',
|
||||
$sql
|
||||
);
|
||||
}
|
||||
$this->assertEquals(array('My Object', 1, 10, 'No description'), $parameters);
|
||||
$query->execute();
|
||||
$this->assertEquals(1, DB::affected_rows());
|
||||
|
||||
// Check inserted object is correct
|
||||
$firstObject = DataObject::get_one('SQLInsertTestBase', array('"Title"' => 'My Object'), false);
|
||||
$this->assertNotEmpty($firstObject);
|
||||
$this->assertEquals($firstObject->Title, 'My Object');
|
||||
$this->assertNotEmpty($firstObject->HasFun);
|
||||
$this->assertEquals($firstObject->Age, 10);
|
||||
$this->assertEquals($firstObject->Description, 'No description');
|
||||
}
|
||||
|
||||
public function testMultipleRowInsert() {
|
||||
$query = SQLInsert::create('"SQLInsertTestBase"');
|
||||
$query->addRow(array(
|
||||
'"Title"' => 'First Object',
|
||||
'"Age"' => 10, // Can't insert non-null values into only one row in a multi-row insert
|
||||
'"Description"' => 'First the worst' // Nullable field, can be present in some rows
|
||||
));
|
||||
$query->addRow(array(
|
||||
'"Title"' => 'Second object',
|
||||
'"Age"' => 12
|
||||
));
|
||||
$sql = $query->sql($parameters);
|
||||
// Only test this case if using the default query builder
|
||||
if(get_class(DB::get_conn()->getQueryBuilder()) === 'DBQueryBuilder') {
|
||||
$this->assertSQLEquals(
|
||||
'INSERT INTO "SQLInsertTestBase" ("Title", "Age", "Description") VALUES (?, ?, ?), (?, ?, ?)',
|
||||
$sql
|
||||
);
|
||||
}
|
||||
$this->assertEquals(array('First Object', 10, 'First the worst', 'Second object', 12, null), $parameters);
|
||||
$query->execute();
|
||||
$this->assertEquals(2, DB::affected_rows());
|
||||
|
||||
// Check inserted objects are correct
|
||||
$firstObject = DataObject::get_one('SQLInsertTestBase', array('"Title"' => 'First Object'), false);
|
||||
$this->assertNotEmpty($firstObject);
|
||||
$this->assertEquals($firstObject->Title, 'First Object');
|
||||
$this->assertEquals($firstObject->Age, 10);
|
||||
$this->assertEquals($firstObject->Description, 'First the worst');
|
||||
|
||||
$secondObject = DataObject::get_one('SQLInsertTestBase', array('"Title"' => 'Second object'), false);
|
||||
$this->assertNotEmpty($secondObject);
|
||||
$this->assertEquals($secondObject->Title, 'Second object');
|
||||
$this->assertEquals($secondObject->Age, 12);
|
||||
$this->assertEmpty($secondObject->Description);
|
||||
}
|
||||
}
|
||||
|
||||
class SQLInsertTestBase extends DataObject implements TestOnly {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)',
|
||||
'HasFun' => 'Boolean',
|
||||
'Age' => 'Int',
|
||||
'Description' => 'Text',
|
||||
);
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
*/
|
||||
class SQLQueryTest extends SapphireTest {
|
||||
|
||||
protected static $fixture_file = 'SQLQueryTest.yml';
|
||||
@ -9,181 +13,188 @@ class SQLQueryTest extends SapphireTest {
|
||||
);
|
||||
|
||||
public function testEmptyQueryReturnsNothing() {
|
||||
$query = new SQLQuery();
|
||||
$this->assertEquals('', $query->sql());
|
||||
$query = new SQLSelect();
|
||||
$this->assertSQLEquals('', $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSelectFromBasicTable() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('MyTable');
|
||||
$this->assertEquals("SELECT * FROM MyTable", $query->sql());
|
||||
$this->assertSQLEquals("SELECT * FROM MyTable", $query->sql($parameters));
|
||||
$query->addFrom('MyJoin');
|
||||
$this->assertEquals("SELECT * FROM MyTable MyJoin", $query->sql());
|
||||
$this->assertSQLEquals("SELECT * FROM MyTable MyJoin", $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSelectFromUserSpecifiedFields() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array("Name", "Title", "Description"));
|
||||
$query->setFrom("MyTable");
|
||||
$this->assertEquals("SELECT Name, Title, Description FROM MyTable", $query->sql());
|
||||
$this->assertSQLEquals("SELECT Name, Title, Description FROM MyTable", $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSelectWithWhereClauseFilter() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array("Name","Meta"));
|
||||
$query->setFrom("MyTable");
|
||||
$query->setWhere("Name = 'Name'");
|
||||
$query->addWhere("Meta = 'Test'");
|
||||
$this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql());
|
||||
$this->assertSQLEquals(
|
||||
"SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')",
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSelectWithConstructorParameters() {
|
||||
$query = new SQLQuery(array("Foo", "Bar"), "FooBarTable");
|
||||
$this->assertEquals("SELECT Foo, Bar FROM FooBarTable", $query->sql());
|
||||
$query = new SQLQuery(array("Foo", "Bar"), "FooBarTable", array("Foo = 'Boo'"));
|
||||
$this->assertEquals("SELECT Foo, Bar FROM FooBarTable WHERE (Foo = 'Boo')", $query->sql());
|
||||
$query = new SQLSelect(array("Foo", "Bar"), "FooBarTable");
|
||||
$this->assertSQLEquals("SELECT Foo, Bar FROM FooBarTable", $query->sql($parameters));
|
||||
$query = new SQLSelect(array("Foo", "Bar"), "FooBarTable", array("Foo = 'Boo'"));
|
||||
$this->assertSQLEquals("SELECT Foo, Bar FROM FooBarTable WHERE (Foo = 'Boo')", $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSelectWithChainedMethods() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect("Name","Meta")->setFrom("MyTable")->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'");
|
||||
$this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql());
|
||||
$this->assertSQLEquals(
|
||||
"SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')",
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testCanSortBy() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect("Name","Meta")->setFrom("MyTable")->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'");
|
||||
$this->assertTrue($query->canSortBy('Name ASC'));
|
||||
$this->assertTrue($query->canSortBy('Name'));
|
||||
}
|
||||
|
||||
public function testSelectWithChainedFilterParameters() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array("Name","Meta"))->setFrom("MyTable");
|
||||
$query->setWhere("Name = 'Name'")->addWhere("Meta = 'Test'")->addWhere("Beta != 'Gamma'");
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
"SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test') AND (Beta != 'Gamma')",
|
||||
$query->sql());
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSelectWithLimitClause() {
|
||||
if(!(DB::getConn() instanceof MySQLDatabase || DB::getConn() instanceof SQLite3Database
|
||||
|| DB::getConn() instanceof PostgreSQLDatabase)) {
|
||||
if(!(DB::get_conn() instanceof MySQLDatabase || DB::get_conn() instanceof SQLite3Database
|
||||
|| DB::get_conn() instanceof PostgreSQLDatabase)) {
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(99);
|
||||
$this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql());
|
||||
$this->assertSQLEquals("SELECT * FROM MyTable LIMIT 99", $query->sql($parameters));
|
||||
|
||||
// array limit with start (MySQL specific)
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(99, 97);
|
||||
$this->assertEquals("SELECT * FROM MyTable LIMIT 99 OFFSET 97", $query->sql());
|
||||
$this->assertSQLEquals("SELECT * FROM MyTable LIMIT 99 OFFSET 97", $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testSelectWithOrderbyClause() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('MyName');
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName ASC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('MyName desc');
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('MyName ASC, Color DESC');
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color DESC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color DESC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('MyName ASC, Color');
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color ASC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName ASC, Color ASC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy(array('MyName' => 'desc'));
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName DESC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy(array('MyName' => 'desc', 'Color'));
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY MyName DESC, Color ASC', $query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY MyName DESC, Color ASC', $query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('implode("MyName","Color")');
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',
|
||||
$query->sql());
|
||||
$query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('implode("MyName","Color") DESC');
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" DESC',
|
||||
$query->sql());
|
||||
$query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setOrderBy('RAND()');
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT *, RAND() AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',
|
||||
$query->sql());
|
||||
$query->sql($parameters));
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->addFrom('INNER JOIN SecondTable USING (ID)');
|
||||
$query->addFrom('INNER JOIN ThirdTable USING (ID)');
|
||||
$query->setOrderBy('MyName');
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT * FROM MyTable '
|
||||
. 'INNER JOIN SecondTable USING (ID) '
|
||||
. 'INNER JOIN ThirdTable USING (ID) '
|
||||
. 'ORDER BY MyName ASC',
|
||||
$query->sql());
|
||||
$query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testNullLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(null);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT * FROM MyTable',
|
||||
$query->sql()
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testZeroLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(0);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT * FROM MyTable',
|
||||
$query->sql()
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testZeroLimitWithOffset() {
|
||||
if(!(DB::getConn() instanceof MySQLDatabase || DB::getConn() instanceof SQLite3Database
|
||||
|| DB::getConn() instanceof PostgreSQLDatabase)) {
|
||||
if(!(DB::get_conn() instanceof MySQLDatabase || DB::get_conn() instanceof SQLite3Database
|
||||
|| DB::get_conn() instanceof PostgreSQLDatabase)) {
|
||||
$this->markTestIncomplete();
|
||||
}
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom("MyTable");
|
||||
$query->setLimit(0, 99);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT * FROM MyTable LIMIT 0 OFFSET 99',
|
||||
$query->sql()
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
@ -191,7 +202,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testNegativeLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setLimit(-10);
|
||||
}
|
||||
|
||||
@ -199,7 +210,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testNegativeOffset() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setLimit(1, -10);
|
||||
}
|
||||
|
||||
@ -207,80 +218,80 @@ class SQLQueryTest extends SapphireTest {
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testNegativeOffsetAndLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setLimit(-10, -10);
|
||||
}
|
||||
|
||||
public function testReverseOrderBy() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('MyTable');
|
||||
|
||||
// default is ASC
|
||||
$query->setOrderBy("Name");
|
||||
$query->reverseOrderBy();
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql($parameters));
|
||||
|
||||
$query->setOrderBy("Name DESC");
|
||||
$query->reverseOrderBy();
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY Name ASC',$query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY Name ASC',$query->sql($parameters));
|
||||
|
||||
$query->setOrderBy(array("Name" => "ASC"));
|
||||
$query->reverseOrderBy();
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY Name DESC',$query->sql($parameters));
|
||||
|
||||
$query->setOrderBy(array("Name" => 'DESC', 'Color' => 'asc'));
|
||||
$query->reverseOrderBy();
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable ORDER BY Name ASC, Color DESC',$query->sql());
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable ORDER BY Name ASC, Color DESC',$query->sql($parameters));
|
||||
|
||||
$query->setOrderBy('implode("MyName","Color") DESC');
|
||||
$query->reverseOrderBy();
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertSQLEquals(
|
||||
'SELECT *, implode("MyName","Color") AS "_SortColumn0" FROM MyTable ORDER BY "_SortColumn0" ASC',
|
||||
$query->sql());
|
||||
$query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testFiltersOnID() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("ID = 5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnID(),
|
||||
"filtersOnID() is true with simple unquoted column name"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("ID=5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnID(),
|
||||
"filtersOnID() is true with simple unquoted column name and no spaces in equals sign"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("Identifier = 5");
|
||||
$this->assertFalse(
|
||||
$query->filtersOnID(),
|
||||
"filtersOnID() is false with custom column name (starting with 'id')"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("ParentID = 5");
|
||||
$this->assertFalse(
|
||||
$query->filtersOnID(),
|
||||
"filtersOnID() is false with column name ending in 'ID'"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("MyTable.ID = 5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnID(),
|
||||
"filtersOnID() is true with table and column name"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("MyTable.ID = 5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnID(),
|
||||
@ -289,28 +300,28 @@ class SQLQueryTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testFiltersOnFK() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("ID = 5");
|
||||
$this->assertFalse(
|
||||
$query->filtersOnFK(),
|
||||
"filtersOnFK() is true with simple unquoted column name"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("Identifier = 5");
|
||||
$this->assertFalse(
|
||||
$query->filtersOnFK(),
|
||||
"filtersOnFK() is false with custom column name (starting with 'id')"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("MyTable.ParentID = 5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnFK(),
|
||||
"filtersOnFK() is true with table and column name"
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setWhere("MyTable.`ParentID`= 5");
|
||||
$this->assertTrue(
|
||||
$query->filtersOnFK(),
|
||||
@ -319,40 +330,45 @@ class SQLQueryTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testInnerJoin() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('MyTable');
|
||||
$query->addInnerJoin('MyOtherTable', 'MyOtherTable.ID = 2');
|
||||
$query->addLeftJoin('MyLastTable', 'MyOtherTable.ID = MyLastTable.ID');
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable '.
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable '.
|
||||
'INNER JOIN "MyOtherTable" ON MyOtherTable.ID = 2 '.
|
||||
'LEFT JOIN "MyLastTable" ON MyOtherTable.ID = MyLastTable.ID',
|
||||
$query->sql()
|
||||
$query->sql($parameters)
|
||||
);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('MyTable');
|
||||
$query->addInnerJoin('MyOtherTable', 'MyOtherTable.ID = 2', 'table1');
|
||||
$query->addLeftJoin('MyLastTable', 'MyOtherTable.ID = MyLastTable.ID', 'table2');
|
||||
|
||||
$this->assertEquals('SELECT * FROM MyTable '.
|
||||
$this->assertSQLEquals('SELECT * FROM MyTable '.
|
||||
'INNER JOIN "MyOtherTable" AS "table1" ON MyOtherTable.ID = 2 '.
|
||||
'LEFT JOIN "MyLastTable" AS "table2" ON MyOtherTable.ID = MyLastTable.ID',
|
||||
$query->sql()
|
||||
$query->sql($parameters)
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetWhereAny() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('MyTable');
|
||||
|
||||
$query->setWhereAny(array("Monkey = 'Chimp'", "Color = 'Brown'"));
|
||||
$this->assertEquals("SELECT * FROM MyTable WHERE (Monkey = 'Chimp' OR Color = 'Brown')",$query->sql());
|
||||
$query->setWhereAny(array(
|
||||
'Monkey' => 'Chimp',
|
||||
'Color' => 'Brown'
|
||||
));
|
||||
$sql = $query->sql($parameters);
|
||||
$this->assertSQLEquals("SELECT * FROM MyTable WHERE ((Monkey = ?) OR (Color = ?))", $sql);
|
||||
$this->assertEquals(array('Chimp', 'Brown'), $parameters);
|
||||
}
|
||||
|
||||
public function testSelectFirst() {
|
||||
// Test first from sequence
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$result = $query->firstRow()->execute();
|
||||
@ -366,10 +382,10 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertEquals('Object 1', $records[0]['Name']);
|
||||
|
||||
// Test first from empty sequence
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$query->setWhere(array("\"Name\" = 'Nonexistent Object'"));
|
||||
$query->setWhere(array('"Name"' => 'Nonexistent Object'));
|
||||
$result = $query->firstRow()->execute();
|
||||
|
||||
$records = array();
|
||||
@ -380,7 +396,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertCount(0, $records);
|
||||
|
||||
// Test that given the last item, the 'first' in this list matches the last
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$query->setLimit(1, 1);
|
||||
@ -397,7 +413,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
|
||||
public function testSelectLast() {
|
||||
// Test last in sequence
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$result = $query->lastRow()->execute();
|
||||
@ -411,7 +427,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertEquals('Object 2', $records[0]['Name']);
|
||||
|
||||
// Test last from empty sequence
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$query->setWhere(array("\"Name\" = 'Nonexistent Object'"));
|
||||
@ -425,7 +441,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertCount(0, $records);
|
||||
|
||||
// Test that given the first item, the 'last' in this list matches the first
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('"Name"');
|
||||
$query->setLimit(1);
|
||||
@ -444,7 +460,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* Tests aggregate() function
|
||||
*/
|
||||
public function testAggregate() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect('"Common"');
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setGroupBy('"Common"');
|
||||
|
||||
@ -457,7 +473,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* Tests that an ORDER BY is only added if a LIMIT is set.
|
||||
*/
|
||||
public function testAggregateNoOrderByIfNoLimit() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('Common');
|
||||
$query->setLimit(array());
|
||||
@ -467,7 +483,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
$this->assertEquals(array(), $aggregate->getOrderBy());
|
||||
$this->assertEquals(array(), $limit);
|
||||
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy('Common');
|
||||
$query->setLimit(2);
|
||||
@ -485,7 +501,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* because a subselect needs to be done to query paginated data.
|
||||
*/
|
||||
public function testOrderByContainingAggregateAndLimitOffset() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array('"Name"', '"Meta"'));
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy(array('MAX("Date")'));
|
||||
@ -507,8 +523,8 @@ class SQLQueryTest extends SapphireTest {
|
||||
* Test that multiple order elements are maintained in the given order
|
||||
*/
|
||||
public function testOrderByMultiple() {
|
||||
if(DB::getConn() instanceof MySQLDatabase) {
|
||||
$query = new SQLQuery();
|
||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect(array('"Name"', '"Meta"'));
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
$query->setOrderBy(array('MID("Name", 8, 1) DESC', '"Name" ASC'));
|
||||
@ -532,7 +548,7 @@ class SQLQueryTest extends SapphireTest {
|
||||
* Test passing in a LIMIT with OFFSET clause string.
|
||||
*/
|
||||
public function testLimitSetFromClauseString() {
|
||||
$query = new SQLQuery();
|
||||
$query = new SQLSelect();
|
||||
$query->setSelect('*');
|
||||
$query->setFrom('"SQLQueryTest_DO"');
|
||||
|
||||
|
56
tests/model/SQLUpdateTest.php
Normal file
56
tests/model/SQLUpdateTest.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@see SQLUpdate}
|
||||
*
|
||||
* @package framework
|
||||
* @subpackage tests
|
||||
*/
|
||||
class SQLUpdateTest extends SapphireTest {
|
||||
|
||||
public static $fixture_file = 'SQLUpdateTest.yml';
|
||||
|
||||
protected $extraDataObjects = array(
|
||||
'SQLUpdateTestBase',
|
||||
'SQLUpdateChild'
|
||||
);
|
||||
|
||||
public function testEmptyQueryReturnsNothing() {
|
||||
$query = new SQLUpdate();
|
||||
$this->assertSQLEquals('', $query->sql($parameters));
|
||||
}
|
||||
|
||||
public function testBasicUpdate() {
|
||||
$query = SQLUpdate::create()
|
||||
->setTable('"SQLUpdateTestBase"')
|
||||
->assign('"Description"', 'Description 1a')
|
||||
->addWhere(array('"Title" = ?' => 'Object 1'));
|
||||
$sql = $query->sql($parameters);
|
||||
|
||||
// Check SQL
|
||||
$this->assertSQLEquals('UPDATE "SQLUpdateTestBase" SET "Description" = ? WHERE ("Title" = ?)', $sql);
|
||||
$this->assertEquals(array('Description 1a', 'Object 1'), $parameters);
|
||||
|
||||
// Check affected rows
|
||||
$query->execute();
|
||||
$this->assertEquals(1, DB::affected_rows());
|
||||
|
||||
// Check item updated
|
||||
$item = DataObject::get_one('SQLUpdateTestBase', array('"Title"' => 'Object 1'));
|
||||
$this->assertEquals('Description 1a', $item->Description);
|
||||
}
|
||||
}
|
||||
|
||||
class SQLUpdateTestBase extends DataObject implements TestOnly {
|
||||
private static $db = array(
|
||||
'Title' => 'Varchar(255)',
|
||||
'Description' => 'Text'
|
||||
);
|
||||
}
|
||||
|
||||
class SQLUpdateChild extends SQLUpdateTestBase {
|
||||
private static $db = array(
|
||||
'Details' => 'Varchar(255)'
|
||||
);
|
||||
}
|
12
tests/model/SQLUpdateTest.yml
Normal file
12
tests/model/SQLUpdateTest.yml
Normal file
@ -0,0 +1,12 @@
|
||||
SQLUpdateTestBase:
|
||||
test1:
|
||||
Title: 'Object 1'
|
||||
Description: 'Description 1'
|
||||
test2:
|
||||
Title: 'Object 2'
|
||||
Description: 'Description 2'
|
||||
SQLUpdateChild:
|
||||
test3:
|
||||
Title: 'Object 3'
|
||||
Description: 'Description 3'
|
||||
Details: 'Details 3'
|
@ -11,8 +11,8 @@ class TransactionTest extends SapphireTest {
|
||||
|
||||
public function testCreateWithTransaction() {
|
||||
|
||||
if(DB::getConn()->supportsTransactions()==true){
|
||||
DB::getConn()->transactionStart();
|
||||
if(DB::get_conn()->supportsTransactions()==true){
|
||||
DB::get_conn()->transactionStart();
|
||||
$obj=new TransactionTest_Object();
|
||||
$obj->Title='First page';
|
||||
$obj->write();
|
||||
@ -22,7 +22,7 @@ class TransactionTest extends SapphireTest {
|
||||
$obj->write();
|
||||
|
||||
//Create a savepoint here:
|
||||
DB::getConn()->transactionSavepoint('rollback');
|
||||
DB::get_conn()->transactionSavepoint('rollback');
|
||||
|
||||
$obj=new TransactionTest_Object();
|
||||
$obj->Title='Third page';
|
||||
@ -33,9 +33,9 @@ class TransactionTest extends SapphireTest {
|
||||
$obj->write();
|
||||
|
||||
//Revert to a savepoint:
|
||||
DB::getConn()->transactionRollback('rollback');
|
||||
DB::get_conn()->transactionRollback('rollback');
|
||||
|
||||
DB::getConn()->transactionEnd();
|
||||
DB::get_conn()->transactionEnd();
|
||||
|
||||
$first=DataObject::get('TransactionTest_Object', "\"Title\"='First page'");
|
||||
$second=DataObject::get('TransactionTest_Object', "\"Title\"='Second page'");
|
||||
|
@ -22,55 +22,35 @@ class VersionedTest extends SapphireTest {
|
||||
);
|
||||
|
||||
public function testUniqueIndexes() {
|
||||
$table_expectations = array(
|
||||
$tableExpectations = array(
|
||||
'VersionedTest_WithIndexes' =>
|
||||
array('value' => 1, 'message' => 'Unique indexes are unique in main table'),
|
||||
array('value' => true, 'message' => 'Unique indexes are unique in main table'),
|
||||
'VersionedTest_WithIndexes_versions' =>
|
||||
array('value' => 0, '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' =>
|
||||
array('value' => 0, 'message' => 'Unique indexes are no longer unique in _Live table'),
|
||||
array('value' => false, 'message' => 'Unique indexes are no longer unique in _Live table'),
|
||||
);
|
||||
|
||||
// Check for presence of all unique indexes
|
||||
$db = DB::getConn();
|
||||
$db_class = get_class($db);
|
||||
$tables = array_keys($table_expectations);
|
||||
switch ($db_class) {
|
||||
case 'MySQLDatabase':
|
||||
$our_indexes = array('UniqA_idx', 'UniqS_idx');
|
||||
foreach ($tables as $t) {
|
||||
$indexes = array_keys($db->indexList($t));
|
||||
sort($indexes);
|
||||
$this->assertEquals(
|
||||
array_values($our_indexes), array_values(array_intersect($indexes, $our_indexes)),
|
||||
"$t has both indexes");
|
||||
}
|
||||
break;
|
||||
case 'SQLite3Database':
|
||||
$our_indexes = array('"UniqA"', '"UniqS"');
|
||||
foreach ($tables as $t) {
|
||||
$indexes = array_values($db->indexList($t));
|
||||
sort($indexes);
|
||||
$this->assertEquals(array_values($our_indexes),
|
||||
array_values(array_intersect(array_values($indexes), $our_indexes)), "$t has both indexes");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$this->markTestSkipped("Test for DBMS $db_class not implemented; skipped.");
|
||||
break;
|
||||
}
|
||||
// Test each table's performance
|
||||
foreach ($tableExpectations as $tableName => $expectation) {
|
||||
$indexes = DB::get_schema()->indexList($tableName);
|
||||
|
||||
// Check for presence of all unique indexes
|
||||
$indexColumns = array_map(function($index) {
|
||||
return $index['value'];
|
||||
}, $indexes);
|
||||
sort($indexColumns);
|
||||
$expectedColumns = array('"UniqA"', '"UniqS"');
|
||||
$this->assertEquals(
|
||||
array_values($expectedColumns),
|
||||
array_values(array_intersect($indexColumns, $expectedColumns)),
|
||||
"$tableName has both indexes");
|
||||
|
||||
// Check unique -> non-unique conversion
|
||||
foreach ($table_expectations as $table_name => $expectation) {
|
||||
$indexes = $db->indexList($table_name);
|
||||
|
||||
foreach ($indexes as $idx_name => $idx_value) {
|
||||
if (in_array($idx_name, $our_indexes)) {
|
||||
$match_value = preg_match('/unique/', $idx_value);
|
||||
if (false === $match_value) {
|
||||
user_error('preg_match failure');
|
||||
}
|
||||
$this->assertEquals($match_value, $expectation['value'], $expectation['message']);
|
||||
// Check unique -> non-unique conversion
|
||||
foreach ($indexes as $indexKey => $indexSpec) {
|
||||
if (in_array($indexSpec['value'], $expectedColumns)) {
|
||||
$isUnique = $indexSpec['type'] === 'unique';
|
||||
$this->assertEquals($isUnique, $expectation['value'], $expectation['message']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -270,12 +250,14 @@ class VersionedTest extends SapphireTest {
|
||||
$page->URLSegment = "testWritingNewToStage";
|
||||
$page->write();
|
||||
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live',
|
||||
"\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', array(
|
||||
'"VersionedTest_DataObject_Live"."ID"' => $page->ID
|
||||
));
|
||||
$this->assertEquals(0, $live->count());
|
||||
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',
|
||||
"\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',array(
|
||||
'"VersionedTest_DataObject"."ID"' => $page->ID
|
||||
));
|
||||
$this->assertEquals(1, $stage->count());
|
||||
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
|
||||
|
||||
@ -297,13 +279,15 @@ class VersionedTest extends SapphireTest {
|
||||
$page->URLSegment = "testWritingNewToLive";
|
||||
$page->write();
|
||||
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live',
|
||||
"\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'");
|
||||
$live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live',array(
|
||||
'"VersionedTest_DataObject_Live"."ID"' => $page->ID
|
||||
));
|
||||
$this->assertEquals(1, $live->count());
|
||||
$this->assertEquals($live->First()->Title, 'testWritingNewToLive');
|
||||
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',
|
||||
"\"VersionedTest_DataObject\".\"ID\"='$page->ID'");
|
||||
$stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage',array(
|
||||
'"VersionedTest_DataObject"."ID"' => $page->ID
|
||||
));
|
||||
$this->assertEquals(0, $stage->count());
|
||||
|
||||
Versioned::reading_stage($origStage);
|
||||
@ -337,7 +321,7 @@ class VersionedTest extends SapphireTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that SQLQuery::queriedTables() applies the version-suffixes properly.
|
||||
* Test that SQLSelect::queriedTables() applies the version-suffixes properly.
|
||||
*/
|
||||
public function testQueriedTables() {
|
||||
Versioned::reading_stage('Live');
|
||||
@ -510,25 +494,25 @@ class VersionedTest extends SapphireTest {
|
||||
}
|
||||
|
||||
public function testVersionedWithSingleStage() {
|
||||
$tables = DB::tableList();
|
||||
$tables = DB::table_list();
|
||||
$this->assertContains(
|
||||
'VersionedTest_SingleStage',
|
||||
array_values($tables),
|
||||
'versionedtest_singlestage',
|
||||
array_keys($tables),
|
||||
'Contains base table'
|
||||
);
|
||||
$this->assertContains(
|
||||
'VersionedTest_SingleStage_versions',
|
||||
array_values($tables),
|
||||
'versionedtest_singlestage_versions',
|
||||
array_keys($tables),
|
||||
'Contains versions table'
|
||||
);
|
||||
$this->assertNotContains(
|
||||
'VersionedTest_SingleStage_Live',
|
||||
array_values($tables),
|
||||
'versionedtest_singlestage_live',
|
||||
array_keys($tables),
|
||||
'Does not contain separate table with _Live suffix'
|
||||
);
|
||||
$this->assertNotContains(
|
||||
'VersionedTest_SingleStage_Stage',
|
||||
array_values($tables),
|
||||
'versionedtest_singlestage_stage',
|
||||
array_keys($tables),
|
||||
'Does not contain separate table with _Stage suffix'
|
||||
);
|
||||
|
||||
@ -733,4 +717,4 @@ class VersionedTest_SingleStage extends DataObject implements TestOnly {
|
||||
private static $extensions = array(
|
||||
'Versioned("Stage")'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ class BasicAuthTest extends FunctionalTest {
|
||||
// Fixtures assume Email is the field used to identify the log in identity
|
||||
self::$original_unique_identifier_field = Member::config()->unique_identifier_field;
|
||||
Member::config()->unique_identifier_field = 'Email';
|
||||
Security::$force_database_is_ready = true; // Prevents Member test subclasses breaking ready test
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
@ -23,6 +24,7 @@ class BasicAuthTest extends FunctionalTest {
|
||||
|
||||
BasicAuth::protect_entire_site(false);
|
||||
Member::config()->unique_identifier_field = self::$original_unique_identifier_field;
|
||||
Security::$force_database_is_ready = null;
|
||||
}
|
||||
|
||||
public function testBasicAuthEnabledWithoutLogin() {
|
||||
|
@ -54,7 +54,9 @@ class MemberCsvBulkLoaderTest extends SapphireTest {
|
||||
$loader = new MemberCsvBulkLoader();
|
||||
$results = $loader->load($this->getCurrentRelativePath() . '/MemberCsvBulkLoaderTest_withGroups.csv');
|
||||
|
||||
$newgroup = DataObject::get_one('Group', sprintf('"Code" = \'%s\'', 'newgroup'));
|
||||
$newgroup = DataObject::get_one('Group', array(
|
||||
'"Group"."Code"' => 'newgroup'
|
||||
));
|
||||
$this->assertEquals($newgroup->Title, 'newgroup');
|
||||
|
||||
$created = $results->Created()->toArray();
|
||||
|
@ -367,7 +367,9 @@ class MemberTest extends FunctionalTest {
|
||||
$grouplessMember->addToGroupByCode('somegroupthatwouldneverexist', 'New Group');
|
||||
$this->assertEquals($grouplessMember->Groups()->Count(), 2);
|
||||
|
||||
$group = DataObject::get_one('Group', "\"Code\" = 'somegroupthatwouldneverexist'");
|
||||
$group = DataObject::get_one('Group', array(
|
||||
'"Group"."Code"' => 'somegroupthatwouldneverexist'
|
||||
));
|
||||
$this->assertNotNull($group);
|
||||
$this->assertEquals($group->Code, 'somegroupthatwouldneverexist');
|
||||
$this->assertEquals($group->Title, 'New Group');
|
||||
|
@ -399,16 +399,22 @@ class SecurityTest extends FunctionalTest {
|
||||
|
||||
/* UNSUCCESSFUL ATTEMPTS WITH WRONG PASSWORD FOR EXISTING USER ARE LOGGED */
|
||||
$this->doTestLoginForm('sam@silverstripe.com', 'wrongpassword');
|
||||
$attempt = DataObject::get_one('LoginAttempt', "\"Email\" = 'sam@silverstripe.com'");
|
||||
$attempt = DataObject::get_one('LoginAttempt', array(
|
||||
'"LoginAttempt"."Email"' => 'sam@silverstripe.com'
|
||||
));
|
||||
$this->assertTrue(is_object($attempt));
|
||||
$member = DataObject::get_one('Member', "\"Email\" = 'sam@silverstripe.com'");
|
||||
$member = DataObject::get_one('Member', array(
|
||||
'"Member"."Email"' => 'sam@silverstripe.com'
|
||||
));
|
||||
$this->assertEquals($attempt->Status, 'Failure');
|
||||
$this->assertEquals($attempt->Email, 'sam@silverstripe.com');
|
||||
$this->assertEquals($attempt->Member(), $member);
|
||||
|
||||
/* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */
|
||||
$this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword');
|
||||
$attempt = DataObject::get_one('LoginAttempt', "\"Email\" = 'wronguser@silverstripe.com'");
|
||||
$attempt = DataObject::get_one('LoginAttempt', array(
|
||||
'"LoginAttempt"."Email"' => 'wronguser@silverstripe.com'
|
||||
));
|
||||
$this->assertTrue(is_object($attempt));
|
||||
$this->assertEquals($attempt->Status, 'Failure');
|
||||
$this->assertEquals($attempt->Email, 'wronguser@silverstripe.com');
|
||||
@ -422,8 +428,12 @@ class SecurityTest extends FunctionalTest {
|
||||
|
||||
/* SUCCESSFUL ATTEMPTS ARE LOGGED */
|
||||
$this->doTestLoginForm('sam@silverstripe.com', '1nitialPassword');
|
||||
$attempt = DataObject::get_one('LoginAttempt', "\"Email\" = 'sam@silverstripe.com'");
|
||||
$member = DataObject::get_one('Member', "\"Email\" = 'sam@silverstripe.com'");
|
||||
$attempt = DataObject::get_one('LoginAttempt', array(
|
||||
'"LoginAttempt"."Email"' => 'sam@silverstripe.com'
|
||||
));
|
||||
$member = DataObject::get_one('Member', array(
|
||||
'"Member"."Email"' => 'sam@silverstripe.com'
|
||||
));
|
||||
$this->assertTrue(is_object($attempt));
|
||||
$this->assertEquals($attempt->Status, 'Success');
|
||||
$this->assertEquals($attempt->Email, 'sam@silverstripe.com');
|
||||
@ -438,7 +448,7 @@ class SecurityTest extends FunctionalTest {
|
||||
|
||||
// Assumption: The database has been built correctly by the test runner,
|
||||
// and has all columns present in the ORM
|
||||
DB::getConn()->renameField('Member', 'Email', 'Email_renamed');
|
||||
DB::get_schema()->renameField('Member', 'Email', 'Email_renamed');
|
||||
|
||||
// Email column is now missing, which means we're not ready to do permission checks
|
||||
$this->assertFalse(Security::database_is_ready());
|
||||
|
Loading…
x
Reference in New Issue
Block a user