Completed
Pull Request — master (#5653)
by Damian
12:15
created

DB   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 524
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 524
rs 7.4757
wmc 53
lcom 1
cbo 7

31 Methods

Rating   Name   Duplication   Size   Complexity  
A set_conn() 0 3 1
A get_conn() 0 6 2
A getConn() 0 4 1
A get_schema() 0 7 2
A build_sql() 0 9 2
A get_connector() 0 7 2
B set_alternative_database_name() 0 36 6
B get_alternative_database_name() 0 21 5
A valid_alternative_database_name() 0 7 3
B connect() 0 24 4
A connection_attempted() 0 3 1
A query() 0 5 1
A placeholders() 0 11 4
A prepared_query() 0 5 1
A manipulate() 0 4 1
A get_generated_id() 0 3 1
A is_active() 0 3 2
A create_database() 0 3 1
A create_table() 0 5 1
A create_field() 0 3 1
A require_table() 0 5 1
A require_field() 0 3 1
A require_index() 0 3 1
A dont_require_table() 0 3 1
A dont_require_field() 0 3 1
A check_and_repair_table() 0 3 1
A affected_rows() 0 3 1
A table_list() 0 3 1
A field_list() 0 3 1
A quiet() 0 3 1
A alteration_message() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like DB often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DB, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use Deprecation;
6
use Director;
7
use InvalidArgumentException;
8
use Config;
9
use LogicException;
10
use Cookie;
11
use Injector;
12
use SilverStripe\ORM\Connect\DBConnector;
13
use SilverStripe\ORM\Connect\DBSchemaManager;
14
use SilverStripe\ORM\Connect\SS_Query;
15
use SilverStripe\ORM\Queries\SQLExpression;
16
use SilverStripe\ORM\Connect\SS_Database;
17
18
/**
19
 * Global database interface, complete with static methods.
20
 * Use this class for interacting with the database.
21
 *
22
 * @package framework
23
 * @subpackage orm
24
 */
25
class DB {
26
27
	/**
28
	 * This constant was added in SilverStripe 2.4 to indicate that SQL-queries
29
	 * should now use ANSI-compatible syntax.  The most notable affect of this
30
	 * change is that table and field names should be escaped with double quotes
31
	 * and not backticks
32
	 */
33
	const USE_ANSI_SQL = true;
34
35
36
	/**
37
	 * The global database connection.
38
	 * @var SS_Database
39
	 */
40
	private static $connections = array();
41
42
	/**
43
	 * The last SQL query run.
44
	 * @var string
45
	 */
46
	public static $lastQuery;
47
48
	/**
49
	 * Internal flag to keep track of when db connection was attempted.
50
	 */
51
	private static $connection_attempted = false;
52
53
	/**
54
	 * Set the global database connection.
55
	 * Pass an object that's a subclass of SS_Database.  This object will be used when {@link DB::query()}
56
	 * is called.
57
	 *
58
	 * @param SS_Database $connection The connecton object to set as the connection.
59
	 * @param string $name The name to give to this connection.  If you omit this argument, the connection
60
	 * will be the default one used by the ORM.  However, you can store other named connections to
61
	 * be accessed through DB::get_conn($name).  This is useful when you have an application that
62
	 * needs to connect to more than one database.
63
	 */
64
	public static function set_conn(SS_Database $connection, $name = 'default') {
65
		self::$connections[$name] = $connection;
66
	}
67
68
	/**
69
	 * Get the global database connection.
70
	 *
71
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
72
	 * the default connection is returned.
73
	 * @return SS_Database
74
	 */
75
	public static function get_conn($name = 'default') {
76
		if(isset(self::$connections[$name])) {
77
			return self::$connections[$name];
78
		}
79
		return null;
80
	}
81
82
	/**
83
	 * @deprecated since version 4.0 Use DB::get_conn instead
84
	 */
85
	public static function getConn($name = 'default') {
86
		Deprecation::notice('4.0', 'Use DB::get_conn instead');
87
		return self::get_conn($name);
88
	}
89
90
	/**
91
	 * Retrieves the schema manager for the current database
92
	 *
93
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
94
	 * the default connection is returned.
95
	 * @return DBSchemaManager
96
	 */
97
	public static function get_schema($name = 'default') {
98
		$connection = self::get_conn($name);
99
		if($connection) {
100
			return $connection->getSchemaManager();
101
		}
102
		return null;
103
	}
104
105
	/**
106
	 * Builds a sql query with the specified connection
107
	 *
108
	 * @param SQLExpression $expression The expression object to build from
109
	 * @param array $parameters Out parameter for the resulting query parameters
110
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
111
	 * the default connection is returned.
112
	 * @return string The resulting SQL as a string
113
	 */
114
	public static function build_sql(SQLExpression $expression, &$parameters, $name = 'default') {
115
		$connection = self::get_conn($name);
116
		if($connection) {
117
			return $connection->getQueryBuilder()->buildSQL($expression, $parameters);
118
		} else {
119
			$parameters = array();
120
			return null;
121
		}
122
	}
123
124
	/**
125
	 * Retrieves the connector object for the current database
126
	 *
127
	 * @param string $name An optional name given to a connection in the DB::setConn() call.  If omitted,
128
	 * the default connection is returned.
129
	 * @return DBConnector
130
	 */
131
	public static function get_connector($name = 'default') {
132
		$connection = self::get_conn($name);
133
		if($connection) {
134
			return $connection->getConnector();
135
		}
136
		return null;
137
	}
138
139
	/**
140
	 * Set an alternative database in a browser cookie,
141
	 * with the cookie lifetime set to the browser session.
142
	 * This is useful for integration testing on temporary databases.
143
	 *
144
	 * There is a strict naming convention for temporary databases to avoid abuse:
145
	 * <prefix> (default: 'ss_') + tmpdb + <7 digits>
146
	 * As an additional security measure, temporary databases will
147
	 * be ignored in "live" mode.
148
	 *
149
	 * Note that the database will be set on the next request.
150
	 * Set it to null to revert to the main database.
151
	 * @param string $name
152
	 */
153
	public static function set_alternative_database_name($name = null) {
154
		// Skip if CLI
155
		if(Director::is_cli()) {
156
			return;
157
		}
158
		if($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
159
			if(!self::valid_alternative_database_name($name)) {
160
				throw new InvalidArgumentException(sprintf(
161
					'Invalid alternative database name: "%s"',
162
					$name
163
				));
164
			}
165
166
			$key = Config::inst()->get('SilverStripe\\Security\\Security', 'token');
167
			if(!$key) {
168
				throw new LogicException('"Security.token" not found, run "sake dev/generatesecuretoken"');
169
			}
170
			if(!function_exists('mcrypt_encrypt')) {
171
				throw new LogicException('DB::set_alternative_database_name() requires the mcrypt PHP extension');
172
			}
173
174
			$key = md5($key); // Ensure key is correct length for chosen cypher
175
			$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB);
176
			$iv = mcrypt_create_iv($ivSize);
177
			$encrypted = mcrypt_encrypt(
178
				MCRYPT_RIJNDAEL_256, $key, $name, MCRYPT_MODE_CFB, $iv
179
			);
180
181
			// Set to browser session lifetime, and restricted to HTTP access only
182
			Cookie::set("alternativeDatabaseName", base64_encode($encrypted), 0, null, null, false, true);
183
			Cookie::set("alternativeDatabaseNameIv", base64_encode($iv), 0, null, null, false, true);
184
		} else {
185
			Cookie::force_expiry("alternativeDatabaseName", null, null, false, true);
186
			Cookie::force_expiry("alternativeDatabaseNameIv", null, null, false, true);
187
		}
188
	}
189
190
	/**
191
	 * Get the name of the database in use
192
	 */
193
	public static function get_alternative_database_name() {
194
		$name = Cookie::get("alternativeDatabaseName");
195
		$iv = Cookie::get("alternativeDatabaseNameIv");
196
197
		if($name) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $name of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
198
			$key = Config::inst()->get('SilverStripe\\Security\\Security', 'token');
199
			if(!$key) {
200
				throw new LogicException('"Security.token" not found, run "sake dev/generatesecuretoken"');
201
			}
202
			if(!function_exists('mcrypt_encrypt')) {
203
				throw new LogicException('DB::set_alternative_database_name() requires the mcrypt PHP extension');
204
			}
205
			$key = md5($key); // Ensure key is correct length for chosen cypher
206
			$decrypted = mcrypt_decrypt(
207
				MCRYPT_RIJNDAEL_256, $key, base64_decode($name), MCRYPT_MODE_CFB, base64_decode($iv)
208
			);
209
			return (self::valid_alternative_database_name($decrypted)) ? $decrypted : false;
210
		} else {
211
			return false;
212
		}
213
	}
214
215
	/**
216
	 * Determines if the name is valid, as a security
217
	 * measure against setting arbitrary databases.
218
	 *
219
	 * @param  String $name
220
	 * @return Boolean
221
	 */
222
	public static function valid_alternative_database_name($name) {
223
		if(Director::isLive()) return false;
224
225
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
226
		$pattern = strtolower(sprintf('/^%stmpdb\d{7}$/', $prefix));
227
		return (bool)preg_match($pattern, $name);
228
	}
229
230
	/**
231
	 * Connect to a database.
232
	 *
233
	 * Given the database configuration, this method will create the correct
234
	 * subclass of {@link SS_Database}.
235
	 *
236
	 * @param array $databaseConfig A map of options. The 'type' is the name of the
237
	 * subclass of SS_Database to use. For the rest of the options, see the specific class.
238
	 * @param string $label identifier for the connection
239
	 * @return SS_Database
240
	 */
241
	public static function connect($databaseConfig, $label = 'default') {
242
243
		// This is used by the "testsession" module to test up a test session using an alternative name
244
		if($name = self::get_alternative_database_name()) {
245
			$databaseConfig['database'] = $name;
246
		}
247
248
		if(!isset($databaseConfig['type']) || empty($databaseConfig['type'])) {
249
			user_error("DB::connect: Not passed a valid database config", E_USER_ERROR);
250
		}
251
252
		self::$connection_attempted = true;
253
254
		$dbClass = $databaseConfig['type'];
255
256
		// Using Injector->create allows us to use registered configurations
257
		// which may or may not map to explicit objects
258
		$conn = Injector::inst()->create($dbClass);
259
		$conn->connect($databaseConfig);
260
261
		self::set_conn($conn, $label);
262
263
		return $conn;
264
	}
265
266
	/**
267
	 * Returns true if a database connection has been attempted.
268
	 * In particular, it lets the caller know if we're still so early in the execution pipeline that
269
	 * we haven't even tried to connect to the database yet.
270
	 */
271
	public static function connection_attempted() {
272
		return self::$connection_attempted;
273
	}
274
275
	/**
276
	 * Execute the given SQL query.
277
	 * @param string $sql The SQL query to execute
278
	 * @param int $errorLevel The level of error reporting to enable for the query
279
	 * @return SS_Query
280
	 */
281
	public static function query($sql, $errorLevel = E_USER_ERROR) {
282
		self::$lastQuery = $sql;
283
284
		return self::get_conn()->query($sql, $errorLevel);
285
	}
286
287
	/**
288
	 * Helper function for generating a list of parameter placeholders for the
289
	 * given argument(s)
290
	 *
291
	 * @param array|integer $input An array of items needing placeholders, or a
292
	 * number to specify the number of placeholders
293
	 * @param string $join The string to join each placeholder together with
294
	 * @return string|null Either a list of placeholders, or null
295
	 */
296
	public static function placeholders($input, $join = ', ') {
297
		if(is_array($input)) {
298
			$number = count($input);
299
		} elseif(is_numeric($input)) {
300
			$number = intval($input);
301
		} else {
302
			return null;
303
		}
304
		if($number === 0) return null;
305
		return implode($join, array_fill(0, $number, '?'));
306
	}
307
308
	/**
309
	 * Execute the given SQL parameterised query with the specified arguments
310
	 *
311
	 * @param string $sql The SQL query to execute. The ? character will denote parameters.
312
	 * @param array $parameters An ordered list of arguments.
313
	 * @param int $errorLevel The level of error reporting to enable for the query
314
	 * @return SS_Query
315
	 */
316
	public static function prepared_query($sql, $parameters, $errorLevel = E_USER_ERROR) {
317
		self::$lastQuery = $sql;
318
319
		return self::get_conn()->preparedQuery($sql, $parameters, $errorLevel);
320
	}
321
322
	/**
323
	 * Execute a complex manipulation on the database.
324
	 * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
325
	 * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
326
	 * and fields should be a map of field names to field values, including quotes.  The field value can
327
	 * also be a SQL function or similar.
328
	 *
329
	 * Example:
330
	 * <code>
331
	 * array(
332
	 *   // Command: insert
333
	 *   "table name" => array(
334
	 *      "command" => "insert",
335
	 *      "fields" => array(
336
	 *         "ClassName" => "'MyClass'", // if you're setting a literal, you need to escape and provide quotes
337
	 *         "Created" => "now()", // alternatively, you can call DB functions
338
	 *         "ID" => 234,
339
	 *       ),
340
	 *      "id" => 234 // an alternative to providing ID in the fields list
341
	 *    ),
342
	 *
343
	 *   // Command: update
344
	 *   "other table" => array(
345
	 *      "command" => "update",
346
	 *      "fields" => array(
347
	 *         "ClassName" => "'MyClass'",
348
	 *         "LastEdited" => "now()",
349
	 *       ),
350
	 *      "where" => "ID = 234",
351
	 *      "id" => 234 // an alternative to providing a where clause
352
	 *    ),
353
	 * )
354
	 * </code>
355
	 *
356
	 * You'll note that only one command on a given table can be called.
357
	 * That's a limitation of the system that's due to it being written for {@link DataObject::write()},
358
	 * which needs to do a single write on a number of different tables.
359
	 *
360
	 * @todo Update this to support paramaterised queries
361
	 *
362
	 * @param array $manipulation
363
	 */
364
	public static function manipulate($manipulation) {
365
		self::$lastQuery = $manipulation;
0 ignored issues
show
Documentation Bug introduced by
It seems like $manipulation of type array is incompatible with the declared type string of property $lastQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
366
		self::get_conn()->manipulate($manipulation);
367
	}
368
369
	/**
370
	 * Get the autogenerated ID from the previous INSERT query.
371
	 *
372
	 * @param string $table
373
	 * @return int
374
	 */
375
	public static function get_generated_id($table) {
376
		return self::get_conn()->getGeneratedID($table);
377
	}
378
379
	/**
380
	 * Check if the connection to the database is active.
381
	 *
382
	 * @return boolean
383
	 */
384
	public static function is_active() {
385
		return ($conn = self::get_conn()) && $conn->isActive();
386
	}
387
388
	/**
389
	 * Create the database and connect to it. This can be called if the
390
	 * initial database connection is not successful because the database
391
	 * does not exist.
392
	 *
393
	 * @param string $database Name of database to create
394
	 * @return boolean Returns true if successful
395
	 */
396
	public static function create_database($database) {
397
		return self::get_conn()->selectDatabase($database, true);
398
	}
399
400
	/**
401
	 * Create a new table.
402
	 * @param string $table The name of the table
403
	 * @param array$fields A map of field names to field types
404
	 * @param array $indexes A map of indexes
405
	 * @param array $options An map of additional options.  The available keys are as follows:
406
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine"
407
	 *     for MySQL.
408
	 *   - 'temporary' - If true, then a temporary table will be created
409
	 * @param array $advancedOptions Advanced creation options
410
	 * @return string The table name generated.  This may be different from the table name, for example with
411
	 * temporary tables.
412
	 */
413
	public static function create_table($table, $fields = null, $indexes = null, $options = null,
414
		$advancedOptions = null
415
	) {
416
		return self::get_schema()->createTable($table, $fields, $indexes, $options, $advancedOptions);
417
	}
418
419
	/**
420
	 * Create a new field on a table.
421
	 * @param string $table Name of the table.
422
	 * @param string $field Name of the field to add.
423
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
424
	 */
425
	public static function create_field($table, $field, $spec) {
426
		return self::get_schema()->createField($table, $field, $spec);
427
	}
428
429
	/**
430
	 * Generate the following table in the database, modifying whatever already exists
431
	 * as necessary.
432
	 *
433
	 * @param string $table The name of the table
434
	 * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db
435
	 * @param string $indexSchema A list of indexes to create.  The keys of the array are the names of the index.
436
	 * The values of the array can be one of:
437
	 *   - true: Create a single column index on the field named the same as the index.
438
	 *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
439
	 *     control over the index.
440
	 * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
441
	 * @param string $options SQL statement to append to the CREATE TABLE call.
442
	 * @param array $extensions List of extensions
443
	 */
444
	public static function require_table($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
445
		$options = null, $extensions = null
446
	) {
447
		self::get_schema()->requireTable($table, $fieldSchema, $indexSchema, $hasAutoIncPK, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $fieldSchema defined by parameter $fieldSchema on line 444 can also be of type string; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $indexSchema defined by parameter $indexSchema on line 444 can also be of type string; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $extensions defined by parameter $extensions on line 445 can also be of type null; however, SilverStripe\ORM\Connect...Manager::requireTable() does only seem to accept array|boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
448
	}
449
450
	/**
451
	 * Generate the given field on the table, modifying whatever already exists as necessary.
452
	 *
453
	 * @param string $table The table name.
454
	 * @param string $field The field name.
455
	 * @param string $spec The field specification.
456
	 */
457
	public static function require_field($table, $field, $spec) {
458
		self::get_schema()->requireField($table, $field, $spec);
459
	}
460
461
	/**
462
	 * Generate the given index in the database, modifying whatever already exists as necessary.
463
	 *
464
	 * @param string $table The table name.
465
	 * @param string $index The index name.
466
	 * @param string|boolean $spec The specification of the index. See requireTable() for more information.
467
	 */
468
	public static function require_index($table, $index, $spec) {
469
		self::get_schema()->requireIndex($table, $index, $spec);
470
	}
471
472
	/**
473
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
474
	 *
475
	 * @param string $table The table name.
476
	 */
477
	public static function dont_require_table($table) {
478
		self::get_schema()->dontRequireTable($table);
479
	}
480
481
	/**
482
	 * See {@link SS_Database->dontRequireField()}.
483
	 *
484
	 * @param string $table The table name.
485
	 * @param string $fieldName The field name not to require
486
	 */
487
	public static function dont_require_field($table, $fieldName) {
488
		self::get_schema()->dontRequireField($table, $fieldName);
489
	}
490
491
	/**
492
	 * Checks a table's integrity and repairs it if necessary.
493
	 *
494
	 * @param string $table The name of the table.
495
	 * @return boolean Return true if the table has integrity after the method is complete.
496
	 */
497
	public static function check_and_repair_table($table) {
498
		return self::get_schema()->checkAndRepairTable($table);
499
	}
500
501
	/**
502
	 * Return the number of rows affected by the previous operation.
503
	 *
504
	 * @return integer The number of affected rows
505
	 */
506
	public static function affected_rows() {
507
		return self::get_conn()->affectedRows();
508
	}
509
510
	/**
511
	 * Returns a list of all tables in the database.
512
	 * The table names will be in lower case.
513
	 *
514
	 * @return array The list of tables
515
	 */
516
	public static function table_list() {
517
		return self::get_schema()->tableList();
518
	}
519
520
	/**
521
	 * Get a list of all the fields for the given table.
522
	 * Returns a map of field name => field spec.
523
	 *
524
	 * @param string $table The table name.
525
	 * @return array The list of fields
526
	 */
527
	public static function field_list($table) {
528
		return self::get_schema()->fieldList($table);
529
	}
530
531
	/**
532
	 * Enable supression of database messages.
533
	 */
534
	public static function quiet() {
535
		self::get_schema()->quiet();
536
	}
537
538
	/**
539
	 * Show a message about database alteration
540
	 *
541
	 * @param string $message to display
542
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
543
	 */
544
	public static function alteration_message($message, $type = "") {
545
		self::get_schema()->alterationMessage($message, $type);
546
	}
547
548
}
549