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:
Simon Welsh 2014-07-09 16:21:37 +10:00
commit ece95d3580
155 changed files with 10971 additions and 6432 deletions

16
_config/database.yml Normal file
View 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

View File

@ -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(/[\/\\:*?&quot;<>|. \t]+/g,'');"
)
)
)
)
);
);

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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

View File

@ -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'
);

View File

@ -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();

View File

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

View File

@ -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();

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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']);

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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]);

View File

@ -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);

View File

@ -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;

View File

@ -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() {

View File

@ -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(/[\/\\:*?&quot;<>|. \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;
}
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

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

View File

@ -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.

View File

@ -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

View File

@ -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'");

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b>
<li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b>
</ul>
@ -199,11 +248,11 @@ Template (see above):
:::php
<ul>
// output: <a href="#" title="foo &amp; &#quot;bar&quot;">foo &amp; "bar"</a>
<li><a href="#" title="$Title.ATT">$Title</a></li>
<li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b>
<li>$MyUnescapedValue.XML</li> // output: &lt;b&gt;bold&lt;b&gt;
// output: <a href="#" title="foo &amp; &#quot;bar&quot;">foo &amp; "bar"</a>
<li><a href="#" title="$Title.ATT">$Title</a></li>
<li>$MyEscapedValue</li> // output: &lt;b&gt;not bold&lt;b&gt;
<li>$MyUnescapedValue</li> // output: <b>bold</b>
<li>$MyUnescapedValue.XML</li> // output: &lt;b&gt;bold&lt;b&gt;
</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: &lt;b&gt;not bold&lt;b&gt;
<li>$Title.RAW</li> // output: <b>not bold</b>
<li>$TitleWithHTMLSuffix</li> // output: <b>not bold</b>: <small>(...)</small>
<li>$Title</li> // output: &lt;b&gt;not bold&lt;b&gt;
<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');
}
}

View File

@ -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));
}
}
/**

View File

@ -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'])
);
}
}

View File

@ -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
*/

View File

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

View File

@ -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() {

View File

@ -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();
}
/**

View File

@ -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;

View File

@ -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);

View File

@ -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');

View File

@ -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) {

View File

@ -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');

View File

@ -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();

View File

@ -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))) {

View File

@ -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) {

View File

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

View File

@ -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 {
}
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -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;
}
}

View File

@ -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 {
}
}

View File

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

View File

@ -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 {
}
}

View File

@ -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) {

View File

@ -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 {

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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') {

View 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();
}

View 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;
}
}

View 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
View 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);
}
}

View 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;
}
}

View 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()';
}
}

View 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;
}
}
}

View 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 '';
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;
}
/**

View File

@ -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);
}
/**

View File

@ -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);
}
/**

View File

@ -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;
}
}

View File

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

View File

@ -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);
}
/**

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
/**

View File

@ -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) {

View File

@ -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);
}
/**

View File

@ -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) {

View 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;
}
}

View 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);
}

View 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;
}
}

View 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;
}
}
}

View 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
View 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;
}
}

View 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
View 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
View 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();
}
}

View 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);
}

View File

@ -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;

View File

@ -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() {

View File

@ -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() {

View File

@ -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";
}
}

View File

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

View File

@ -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() {

View File

@ -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() {

View File

@ -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;
}

View File

@ -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%";
}
}

View File

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

View File

@ -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() {

View File

@ -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();

View File

@ -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'])) {

View File

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

View File

@ -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;

View File

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

View File

@ -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) {

View File

@ -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(

View File

@ -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\"")

View File

@ -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;

View File

@ -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');

View File

@ -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(

View File

@ -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
));
}
/**

View 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"
));
}
}

View File

@ -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();

View File

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

View File

@ -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(

View File

@ -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

View File

@ -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 */

View File

@ -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;
}

View File

@ -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');

View File

@ -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',

View File

@ -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() {

View File

@ -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();

View File

@ -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.');

View File

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

View File

@ -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'

View File

@ -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"')

View File

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

View File

@ -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)));

View 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',
);
}

View File

@ -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"');

View 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)'
);
}

View 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'

View File

@ -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'");

View File

@ -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")'
);
}
}

View File

@ -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() {

View File

@ -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();

View File

@ -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');

View File

@ -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());