Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

DBSchemaManager::requireTable()   D

Complexity

Conditions 17
Paths 300

Size

Total Lines 67
Code Lines 36

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 67
rs 4.3163
cc 17
eloc 36
nc 300
nop 6

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Represents and handles all schema management for a database
5
 *
6
 * @package framework
7
 * @subpackage model
8
 */
9
abstract class DBSchemaManager {
10
11
	/**
12
	 *
13
	 * @config
14
	 * Check tables when running /dev/build, and repair them if necessary.
15
	 * In case of large databases or more fine-grained control on how to handle
16
	 * data corruption in tables, you can disable this behaviour and handle it
17
	 * outside of this class, e.g. through a nightly system task with extended logging capabilities.
18
	 *
19
	 * @var boolean
20
	 */
21
	private static $check_and_repair_on_build = true;
22
23
	/**
24
	 * Instance of the database controller this schema belongs to
25
	 *
26
	 * @var SS_Database
27
	 */
28
	protected $database = null;
29
30
	/**
31
	 * If this is false, then information about database operations
32
	 * will be displayed, eg creation of tables.
33
	 *
34
	 * @var boolean
35
	 */
36
	protected $supressOutput = false;
37
38
	/**
39
	 * Injector injection point for database controller
40
	 *
41
	 * @param SS_Database $connector
0 ignored issues
show
Bug introduced by
There is no parameter named $connector. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
42
	 */
43
	public function setDatabase(SS_Database $database) {
44
		$this->database = $database;
45
	}
46
47
	/**
48
	 * The table list, generated by the tableList() function.
49
	 * Used by the requireTable() function.
50
	 *
51
	 * @var array
52
	 */
53
	protected $tableList;
54
55
	/**
56
	 * Keeps track whether we are currently updating the schema.
57
	 *
58
	 * @var boolean
59
	 */
60
	protected $schemaIsUpdating = false;
61
62
	/**
63
	 * Large array structure that represents a schema update transaction
64
	 *
65
	 * @var array
66
	 */
67
	protected $schemaUpdateTransaction;
68
69
	/**
70
	 * Enable supression of database messages.
71
	 */
72
	public function quiet() {
73
		$this->supressOutput = true;
74
	}
75
76
	/**
77
	 * Execute the given SQL query.
78
	 * This abstract function must be defined by subclasses as part of the actual implementation.
79
	 * It should return a subclass of SS_Query as the result.
80
	 *
81
	 * @param string $sql The SQL query to execute
82
	 * @param int $errorLevel The level of error reporting to enable for the query
83
	 * @return SS_Query
84
	 */
85
	public function query($sql, $errorLevel = E_USER_ERROR) {
86
		return $this->database->query($sql, $errorLevel);
87
	}
88
89
90
	/**
91
	 * Execute the given SQL parameterised query with the specified arguments
92
	 *
93
	 * @param string $sql The SQL query to execute. The ? character will denote parameters.
94
	 * @param array $parameters An ordered list of arguments.
95
	 * @param int $errorLevel The level of error reporting to enable for the query
96
	 * @return SS_Query
97
	 */
98
	public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) {
99
		return $this->database->preparedQuery($sql, $parameters, $errorLevel);
100
	}
101
102
	/**
103
	 * Initiates a schema update within a single callback
104
	 *
105
	 * @var callable $callback
106
	 * @throws Exception
107
	 */
108
	public function schemaUpdate($callback) {
109
		// Begin schema update
110
		$this->schemaIsUpdating = true;
111
112
		// Update table list
113
		$this->tableList = array();
114
		$tables = $this->tableList();
115
		foreach ($tables as $table) {
116
			$this->tableList[strtolower($table)] = $table;
117
		}
118
119
		// Clear update list for client code to mess around with
120
		$this->schemaUpdateTransaction = array();
121
122
		$error = null;
123
		try {
124
125
			// Yield control to client code
126
			$callback();
127
128
			// If the client code has cancelled the update then abort
129
			if(!$this->isSchemaUpdating()) return;
130
131
			// End schema update
132
			foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
133
				$advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
134
				switch ($changes['command']) {
135
					case 'create':
136
						$this->createTable($tableName, $changes['newFields'], $changes['newIndexes'],
137
										$changes['options'], $advancedOptions);
138
						break;
139
140
					case 'alter':
141
						$this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'],
142
										$changes['alteredFields'], $changes['alteredIndexes'],
143
										$changes['alteredOptions'], $advancedOptions);
144
						break;
145
				}
146
			}
147
		} catch(Exception $ex) {
148
			$error = $ex;
149
		}
150
		// finally {
151
		$this->schemaUpdateTransaction = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $schemaUpdateTransaction.

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...
152
		$this->schemaIsUpdating = false;
153
		// }
154
155
		if($error) throw $error;
156
	}
157
158
	/**
159
	 * Cancels the schema updates requested during (but not after) schemaUpdate() call.
160
	 */
161
	public function cancelSchemaUpdate() {
162
		$this->schemaUpdateTransaction = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $schemaUpdateTransaction.

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...
163
		$this->schemaIsUpdating = false;
164
	}
165
166
	/**
167
	 * Returns true if we are during a schema update.
168
	 *
169
	 * @return boolean
170
	 */
171
	function isSchemaUpdating() {
172
		return $this->schemaIsUpdating;
173
	}
174
175
	/**
176
	 * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
177
	 *
178
	 * @return boolean
179
	 */
180
	public function doesSchemaNeedUpdating() {
181
		return (bool) $this->schemaUpdateTransaction;
182
	}
183
184
	// Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
185
186
	/**
187
	 * Instruct the schema manager to record a table creation to later execute
188
	 *
189
	 * @param string $table Name of the table
190
	 * @param array $options Create table options (ENGINE, etc.)
191
	 * @param array $advanced_options Advanced table creation options
192
	 */
193
	public function transCreateTable($table, $options = null, $advanced_options = null) {
194
		$this->schemaUpdateTransaction[$table] = array(
195
			'command' => 'create',
196
			'newFields' => array(),
197
			'newIndexes' => array(),
198
			'options' => $options,
199
			'advancedOptions' => $advanced_options
200
		);
201
	}
202
203
	/**
204
	 * Instruct the schema manager to record a table alteration to later execute
205
	 *
206
	 * @param string $table Name of the table
207
	 * @param array $options Create table options (ENGINE, etc.)
208
	 * @param array $advanced_options Advanced table creation options
209
	 */
210
	public function transAlterTable($table, $options, $advanced_options) {
211
		$this->transInitTable($table);
212
		$this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
213
		$this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
214
	}
215
216
	/**
217
	 * Instruct the schema manager to record a field to be later created
218
	 *
219
	 * @param string $table Name of the table to hold this field
220
	 * @param string $field Name of the field to create
221
	 * @param string $schema Field specification as a string
222
	 */
223
	public function transCreateField($table, $field, $schema) {
224
		$this->transInitTable($table);
225
		$this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
226
	}
227
228
	/**
229
	 * Instruct the schema manager to record an index to be later created
230
	 *
231
	 * @param string $table Name of the table to hold this index
232
	 * @param string $index Name of the index to create
233
	 * @param array $schema Already parsed index specification
234
	 */
235
	public function transCreateIndex($table, $index, $schema) {
236
		$this->transInitTable($table);
237
		$this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
238
	}
239
240
	/**
241
	 * Instruct the schema manager to record a field to be later updated
242
	 *
243
	 * @param string $table Name of the table to hold this field
244
	 * @param string $field Name of the field to update
245
	 * @param string $schema Field specification as a string
246
	 */
247
	public function transAlterField($table, $field, $schema) {
248
		$this->transInitTable($table);
249
		$this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
250
	}
251
252
	/**
253
	 * Instruct the schema manager to record an index to be later updated
254
	 *
255
	 * @param string $table Name of the table to hold this index
256
	 * @param string $index Name of the index to update
257
	 * @param array $schema Already parsed index specification
258
	 */
259
	public function transAlterIndex($table, $index, $schema) {
260
		$this->transInitTable($table);
261
		$this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
262
	}
263
264
	/**
265
	 * Handler for the other transXXX methods - mark the given table as being altered
266
	 * if it doesn't already exist
267
	 *
268
	 * @param string $table Name of the table to initialise
269
	 */
270
	protected function transInitTable($table) {
271
		if (!isset($this->schemaUpdateTransaction[$table])) {
272
			$this->schemaUpdateTransaction[$table] = array(
273
				'command' => 'alter',
274
				'newFields' => array(),
275
				'newIndexes' => array(),
276
				'alteredFields' => array(),
277
				'alteredIndexes' => array(),
278
				'alteredOptions' => ''
279
			);
280
		}
281
	}
282
283
	/**
284
	 * Generate the following table in the database, modifying whatever already exists
285
	 * as necessary.
286
	 *
287
	 * @todo Change detection for CREATE TABLE $options other than "Engine"
288
	 *
289
	 * @param string $table The name of the table
290
	 * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
291
	 * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
292
	 * The values of the array can be one of:
293
	 *   - true: Create a single column index on the field named the same as the index.
294
	 *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
295
	 *     control over the index.
296
	 * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
297
	 * @param string $options SQL statement to append to the CREATE TABLE call.
298
	 * @param array $extensions List of extensions
299
	 */
300
	public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true,
301
		$options = array(), $extensions = false
302
	) {
303
		if (!isset($this->tableList[strtolower($table)])) {
304
			$this->transCreateTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 301 can also be of type string; however, DBSchemaManager::transCreateTable() 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 301 can also be of type false; however, DBSchemaManager::transCreateTable() 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...
305
			$this->alterationMessage("Table $table: created", "created");
306
		} else {
307
			if (Config::inst()->get('DBSchemaManager', 'check_and_repair_on_build')) {
308
				$this->checkAndRepairTable($table, $options);
0 ignored issues
show
Unused Code introduced by
The call to DBSchemaManager::checkAndRepairTable() has too many arguments starting with $options.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
309
			}
310
311
			// Check if options changed
312
			$tableOptionsChanged = false;
313
			// Check for DB constant on the schema class
314
			$dbIDName = sprintf('%s::ID', get_class($this));
315
			$dbID = defined($dbIDName) ? constant($dbIDName) : null;
316
			if ($dbID && isset($options[$dbID])) {
317
				if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
318
					$alteredEngine = $alteredEngineMatches[1];
319
					$tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
320
					$tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
321
				}
322
			}
323
324
			if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
0 ignored issues
show
Bug introduced by
The method supportsExtensions() cannot be called from this context as it is declared protected in class SS_Database.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
325
				$this->transAlterTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 301 can also be of type string; however, DBSchemaManager::transAlterTable() does only seem to accept array, 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 301 can also be of type false; however, DBSchemaManager::transAlterTable() does only seem to accept array, 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...
326
			}
327
		}
328
329
		//DB ABSTRACTION: we need to convert this to a db-specific version:
330
		if(!isset($fieldSchema['ID'])) {
331
			$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
332
		}
333
334
		// Create custom fields
335
		if ($fieldSchema) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldSchema of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
336
			foreach ($fieldSchema as $fieldName => $fieldSpec) {
337
338
				//Is this an array field?
339
				$arrayValue = '';
340
				if (strpos($fieldSpec, '[') !== false) {
341
					//If so, remove it and store that info separately
342
					$pos = strpos($fieldSpec, '[');
343
					$arrayValue = substr($fieldSpec, $pos);
344
					$fieldSpec = substr($fieldSpec, 0, $pos);
345
				}
346
347
				$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
348
				$fieldObj->arrayValue = $arrayValue;
349
350
				$fieldObj->setTable($table);
351
352
				if($fieldObj instanceof PrimaryKey) {
0 ignored issues
show
Bug introduced by
The class PrimaryKey does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
353
					$fieldObj->setAutoIncrement($hasAutoIncPK);
354
				}
355
356
				$fieldObj->requireField();
357
			}
358
		}
359
360
		// Create custom indexes
361
		if ($indexSchema) {
362
			foreach ($indexSchema as $indexName => $indexDetails) {
363
				$this->requireIndex($table, $indexName, $indexDetails);
364
			}
365
		}
366
	}
367
368
	/**
369
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
370
	 * @param string $table The table name.
371
	 */
372
	public function dontRequireTable($table) {
373
		if (isset($this->tableList[strtolower($table)])) {
374
			$suffix = '';
375
			while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
376
				$suffix = $suffix
377
						? ($suffix + 1)
378
						: 2;
379
			}
380
			$this->renameTable($table, "_obsolete_{$table}$suffix");
381
			$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
382
		}
383
	}
384
385
	/**
386
	 * Generate the given index in the database, modifying whatever already exists as necessary.
387
	 *
388
	 * The keys of the array are the names of the index.
389
	 * The values of the array can be one of:
390
	 *  - true: Create a single column index on the field named the same as the index.
391
	 *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
392
	 *    control over the index.
393
	 *
394
	 * @param string $table The table name.
395
	 * @param string $index The index name.
396
	 * @param string|array|boolean $spec The specification of the index in any
397
	 * loose format. See requireTable() for more information.
398
	 */
399
	public function requireIndex($table, $index, $spec) {
400
		// Detect if adding to a new table
401
		$newTable = !isset($this->tableList[strtolower($table)]);
402
403
		// Force spec into standard array format
404
		$spec = $this->parseIndexSpec($index, $spec);
405
		$specString = $this->convertIndexSpec($spec);
406
407
		// Check existing index
408
		if (!$newTable) {
409
			$indexKey = $this->indexKey($table, $index, $spec);
410
			$indexList = $this->indexList($table);
411
			if (isset($indexList[$indexKey])) {
412
413
				// $oldSpec should be in standard array format
414
				$oldSpec = $indexList[$indexKey];
415
				$oldSpecString = $this->convertIndexSpec($oldSpec);
416
			}
417
		}
418
419
		// Initiate either generation or modification of index
420
		if ($newTable || !isset($indexList[$indexKey])) {
421
			// New index
422
			$this->transCreateIndex($table, $index, $spec);
423
			$this->alterationMessage("Index $table.$index: created as $specString", "created");
424
		} else if ($oldSpecString != $specString) {
425
			// Updated index
426
			$this->transAlterIndex($table, $index, $spec);
427
			$this->alterationMessage(
428
				"Index $table.$index: changed to $specString <i style=\"color: #AAA\">(from $oldSpecString)</i>",
0 ignored issues
show
Bug introduced by
The variable $oldSpecString does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
429
				"changed"
430
			);
431
		}
432
	}
433
434
	/**
435
	 * Splits a spec string safely, considering quoted columns, whitespace,
436
	 * and cleaning brackets
437
	 *
438
	 * @param string $spec The input index specification string
439
	 * @return array List of columns in the spec
440
	 */
441
	protected function explodeColumnString($spec) {
442
		// Remove any leading/trailing brackets and outlying modifiers
443
		// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
444
		$containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
445
446
		// Split potentially quoted modifiers
447
		// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
448
		return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
449
	}
450
451
	/**
452
	 * Builds a properly quoted column list from an array
453
	 *
454
	 * @param array $columns List of columns to implode
455
	 * @return string A properly quoted list of column names
456
	 */
457
	protected function implodeColumnList($columns) {
458
		if(empty($columns)) return '';
459
		return '"' . implode('","', $columns) . '"';
460
	}
461
462
	/**
463
	 * Given an index specification in the form of a string ensure that each
464
	 * column name is property quoted, stripping brackets and modifiers.
465
	 * This index may also be in the form of a "CREATE INDEX..." sql fragment
466
	 *
467
	 * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
468
	 * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
469
	 */
470
	protected function quoteColumnSpecString($spec) {
471
		$bits = $this->explodeColumnString($spec);
472
		return $this->implodeColumnList($bits);
473
	}
474
475
	/**
476
	 * Given an index spec determines the index type
477
	 *
478
	 * @param array|string $spec
479
	 * @return string
480
	 */
481
	protected function determineIndexType($spec) {
482
		// check array spec
483
		if(is_array($spec) && isset($spec['type'])) {
484
			return $spec['type'];
485
		} elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
486
			return strtolower($matchType['type']);
487
		} else {
488
			return 'index';
489
		}
490
	}
491
492
	/**
493
	 * Converts an array or string index spec into a universally useful array
494
	 *
495
	 * @see convertIndexSpec() for approximate inverse
496
	 * @param string|array $spec
497
	 * @return array The resulting spec array with the required fields name, type, and value
498
	 */
499
	protected function parseIndexSpec($name, $spec) {
500
		// Support $indexes = array('ColumnName' => true) for quick indexes
501
		if ($spec === true) {
502
			return array(
503
				'name' => $name,
504
				'value' => $this->quoteColumnSpecString($name),
505
				'type' => 'index'
506
			);
507
		}
508
509
		// Do minimal cleanup on any already parsed spec
510
		if(is_array($spec)) {
511
			$spec['value'] = $this->quoteColumnSpecString($spec['value']);
512
			$spec['type'] = empty($spec['type']) ? 'index' : trim($spec['type']);
513
			return $spec;
514
		}
515
516
		// Nicely formatted spec!
517
		return array(
518
			'name' => $name,
519
			'value' => $this->quoteColumnSpecString($spec),
520
			'type' => $this->determineIndexType($spec)
521
		);
522
	}
523
524
	/**
525
	 * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
526
	 * and turns it into a proper string.
527
	 * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
528
	 * arrays to be created. See {@link requireTable()} for details on the index format.
529
	 *
530
	 * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
531
	 * @see parseIndexSpec() for approximate inverse
532
	 *
533
	 * @param string|array $indexSpec
534
	 */
535
	protected function convertIndexSpec($indexSpec) {
536
		// Return already converted spec
537
		if (!is_array($indexSpec)) return $indexSpec;
538
539
		// Combine elements into standard string format
540
		return "{$indexSpec['type']} ({$indexSpec['value']})";
541
	}
542
543
	/**
544
	 * Returns true if the given table is exists in the current database
545
	 *
546
	 * @param string $table Name of table to check
0 ignored issues
show
Bug introduced by
There is no parameter named $table. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
547
	 * @return boolean Flag indicating existence of table
548
	 */
549
	abstract public function hasTable($tableName);
550
551
	/**
552
	 * Return true if the table exists and already has a the field specified
553
	 *
554
	 * @param string $tableName - The table to check
555
	 * @param string $fieldName - The field to check
556
	 * @return bool - True if the table exists and the field exists on the table
557
	 */
558
	public function hasField($tableName, $fieldName) {
559
		if (!$this->hasTable($tableName)) return false;
560
		$fields = $this->fieldList($tableName);
561
		return array_key_exists($fieldName, $fields);
562
	}
563
564
	/**
565
	 * Generate the given field on the table, modifying whatever already exists as necessary.
566
	 *
567
	 * @param string $table The table name.
568
	 * @param string $field The field name.
569
	 * @param array|string $spec The field specification. If passed in array syntax, the specific database
570
	 * 	driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
571
	 * 	be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
572
	 * 	need to take care of database abstraction in your DBField subclass.
573
	 */
574
	public function requireField($table, $field, $spec) {
575
		//TODO: this is starting to get extremely fragmented.
576
		//There are two different versions of $spec floating around, and their content changes depending
577
		//on how they are structured.  This needs to be tidied up.
578
		$fieldValue = null;
579
		$newTable = false;
580
581
		// backwards compatibility patch for pre 2.4 requireField() calls
582
		$spec_orig = $spec;
583
584
		if (!is_string($spec)) {
585
			$spec['parts']['name'] = $field;
586
			$spec_orig['parts']['name'] = $field;
587
			//Convert the $spec array into a database-specific string
588
			$spec = $this->{$spec['type']}($spec['parts'], true);
589
		}
590
591
		// Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
592
		// collations.
593
		// TODO: move this to the MySQLDatabase file, or drop it altogether?
594
		if (!$this->database->supportsCollations()) {
595
			$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
596
		}
597
598
		if (!isset($this->tableList[strtolower($table)])) $newTable = true;
599
600
		if (is_array($spec)) {
601
			$specValue = $this->$spec_orig['type']($spec_orig['parts']);
602
		} else {
603
			$specValue = $spec;
604
		}
605
606
		// We need to get db-specific versions of the ID column:
607
		if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
608
			$specValue = $this->IdColumn(true);
609
		}
610
611
		if (!$newTable) {
612
			$fieldList = $this->fieldList($table);
613
			if (isset($fieldList[$field])) {
614
				if (is_array($fieldList[$field])) {
615
					$fieldValue = $fieldList[$field]['data_type'];
616
				} else {
617
					$fieldValue = $fieldList[$field];
618
				}
619
			}
620
		}
621
622
		// Get the version of the field as we would create it. This is used for comparison purposes to see if the
623
		// existing field is different to what we now want
624
		if (is_array($spec_orig)) {
625
			$spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
626
		}
627
628
		if ($newTable || $fieldValue == '') {
629
			$this->transCreateField($table, $field, $spec_orig);
630
			$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
631
		} else if ($fieldValue != $specValue) {
632
			// If enums/sets are being modified, then we need to fix existing data in the table.
633
			// Update any records where the enum is set to a legacy value to be set to the default.
634
			foreach (array('enum', 'set') as $enumtype) {
635
				if (preg_match("/^$enumtype/i", $specValue)) {
636
					$newStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $spec_orig);
637
					$new = preg_split("/'\s*,\s*'/", $newStr);
638
639
					$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue);
640
					$old = preg_split("/'\s*,\s*'/", $newStr);
641
642
					$holder = array();
643
					foreach ($old as $check) {
644
						if (!in_array($check, $new)) {
645
							$holder[] = $check;
646
						}
647
					}
648
					if (count($holder)) {
649
						$default = explode('default ', $spec_orig);
650
						$default = $default[1];
651
						$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
652
						for ($i = 0; $i + 1 < count($holder); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
653
							$query .= "'{$holder[$i]}', ";
654
						}
655
						$query .= "'{$holder[$i]}')";
656
						$this->query($query);
657
						$amount = $this->database->affectedRows();
658
						$this->alterationMessage("Changed $amount rows to default value of field $field"
659
								. " (Value: $default)");
660
					}
661
				}
662
			}
663
			$this->transAlterField($table, $field, $spec_orig);
664
			$this->alterationMessage(
665
				"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
666
				"changed"
667
			);
668
		}
669
	}
670
671
	/**
672
	 * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
673
	 *
674
	 * @param string $table
675
	 * @param string $fieldName
676
	 */
677
	public function dontRequireField($table, $fieldName) {
678
		$fieldList = $this->fieldList($table);
679
		if (array_key_exists($fieldName, $fieldList)) {
680
			$suffix = '';
681
			while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
682
				$suffix = $suffix
683
						? ($suffix + 1)
684
						: 2;
685
			}
686
			$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
687
			$this->alterationMessage(
688
				"Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
689
				"obsolete"
690
			);
691
		}
692
	}
693
694
	/**
695
	 * Show a message about database alteration
696
	 *
697
	 * @param string $message to display
698
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
699
	 */
700
	public function alterationMessage($message, $type = "") {
701
		if (!$this->supressOutput) {
702
			if (Director::is_cli()) {
703
				switch ($type) {
704
					case "created":
705
					case "changed":
706
					case "repaired":
707
						$sign = "+";
708
						break;
709
					case "obsolete":
710
					case "deleted":
711
						$sign = '-';
712
						break;
713
					case "notice":
714
						$sign = '*';
715
						break;
716
					case "error":
717
						$sign = "!";
718
						break;
719
					default:
720
						$sign = " ";
721
				}
722
				$message = strip_tags($message);
723
				echo "  $sign $message\n";
724
			} else {
725
				switch ($type) {
726
					case "created":
727
						$color = "green";
728
						break;
729
					case "obsolete":
730
						$color = "red";
731
						break;
732
					case "notice":
733
						$color = "orange";
734
						break;
735
					case "error":
736
						$color = "red";
737
						break;
738
					case "deleted":
739
						$color = "red";
740
						break;
741
					case "changed":
742
						$color = "blue";
743
						break;
744
					case "repaired":
745
						$color = "blue";
746
						break;
747
					default:
748
						$color = "";
749
				}
750
				echo "<li style=\"color: $color\">$message</li>";
751
			}
752
		}
753
	}
754
755
	/**
756
	 * This returns the data type for the id column which is the primary key for each table
757
	 *
758
	 * @param boolean $asDbValue
759
	 * @param boolean $hasAutoIncPK
760
	 * @return string
761
	 */
762
	abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
763
764
	/**
765
	 * Checks a table's integrity and repairs it if necessary.
766
	 *
767
	 * @param string $tableName The name of the table.
768
	 * @return boolean Return true if the table has integrity after the method is complete.
769
	 */
770
	abstract public function checkAndRepairTable($tableName);
771
772
	/**
773
	 * Returns the values of the given enum field
774
	 *
775
	 * @param string $tableName Name of table to check
776
	 * @param string $fieldName name of enum field to check
777
	 * @return array List of enum values
778
	 */
779
	abstract public function enumValuesForField($tableName, $fieldName);
780
781
782
	/*
783
	 * This is a lookup table for data types.
784
	 * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
785
	 * So this is a DB-specific list of equivilents.
786
	 *
787
	 * @param string $type
788
	 * @return string
789
	 */
790
	abstract public function dbDataType($type);
791
792
	/**
793
	 * Retrieves the list of all databases the user has access to
794
	 *
795
	 * @return array List of database names
796
	 */
797
	abstract public function databaseList();
798
799
	/**
800
	 * Determine if the database with the specified name exists
801
	 *
802
	 * @param string $name Name of the database to check for
803
	 * @return boolean Flag indicating whether this database exists
804
	 */
805
	abstract public function databaseExists($name);
806
807
	/**
808
	 * Create a database with the specified name
809
	 *
810
	 * @param string $name Name of the database to create
811
	 * @return boolean True if successful
812
	 */
813
	abstract public function createDatabase($name);
814
815
	/**
816
	 * Drops a database with the specified name
817
	 *
818
	 * @param string $name Name of the database to drop
819
	 */
820
	abstract public function dropDatabase($name);
821
822
	/**
823
	 * Alter an index on a table.
824
	 *
825
	 * @param string $tableName The name of the table.
826
	 * @param string $indexName The name of the index.
827
	 * @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
828
	 *                          for more details.
829
	 * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
830
	 */
831
	abstract public function alterIndex($tableName, $indexName, $indexSpec);
832
833
	/**
834
	 * Determines the key that should be used to identify this index
835
	 * when retrieved from DBSchemaManager->indexList.
836
	 * In some connectors this is the database-visible name, in others the
837
	 * usercode-visible name.
838
	 *
839
	 * @param string $table
840
	 * @param string $index
841
	 * @param array $spec
842
	 * @return string Key for this index
843
	 */
844
	abstract protected function indexKey($table, $index, $spec);
845
846
	/**
847
	 * Return the list of indexes in a table.
848
	 *
849
	 * @param string $table The table name.
850
	 * @return array[array] List of current indexes in the table, each in standard
0 ignored issues
show
Documentation introduced by
The doc-type array[array] could not be parsed: Expected "]" at position 2, but found "array". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
851
	 * array form. The key for this array should be predictable using the indexKey
852
	 * method
853
	 */
854
	abstract public function indexList($table);
855
856
	/**
857
	 * Returns a list of all tables in the database.
858
	 * Keys are table names in lower case, values are table names in case that
859
	 * database expects.
860
	 *
861
	 * @return array
862
	 */
863
	abstract public function tableList();
864
865
	/**
866
	 * Create a new table.
867
	 *
868
	 * @param string $table The name of the table
869
	 * @param array $fields A map of field names to field types
870
	 * @param array $indexes A map of indexes
871
	 * @param array $options An map of additional options.  The available keys are as follows:
872
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
873
	 *   - 'temporary' - If true, then a temporary table will be created
874
	 * @param $advancedOptions Advanced creation options
875
	 * @return string The table name generated.  This may be different from the table name, for example with temporary
876
	 * tables.
877
	 */
878
	abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
879
										$advancedOptions = null);
880
881
	/**
882
	 * Alter a table's schema.
883
	 *
884
	 * @param string $table The name of the table to alter
885
	 * @param array $newFields New fields, a map of field name => field schema
886
	 * @param array $newIndexes New indexes, a map of index name => index type
887
	 * @param array $alteredFields Updated fields, a map of field name => field schema
888
	 * @param array $alteredIndexes Updated indexes, a map of index name => index type
889
	 * @param array $alteredOptions
890
	 * @param array $advancedOptions
891
	 */
892
	abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
893
										$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null);
894
895
	/**
896
	 * Rename a table.
897
	 *
898
	 * @param string $oldTableName The old table name.
899
	 * @param string $newTableName The new table name.
900
	 */
901
	abstract public function renameTable($oldTableName, $newTableName);
902
903
	/**
904
	 * Create a new field on a table.
905
	 *
906
	 * @param string $table Name of the table.
907
	 * @param string $field Name of the field to add.
908
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
909
	 */
910
	abstract public function createField($table, $field, $spec);
911
912
	/**
913
	 * Change the database column name of the given field.
914
	 *
915
	 * @param string $tableName The name of the tbale the field is in.
916
	 * @param string $oldName The name of the field to change.
917
	 * @param string $newName The new name of the field
918
	 */
919
	abstract public function renameField($tableName, $oldName, $newName);
920
921
	/**
922
	 * Get a list of all the fields for the given table.
923
	 * Returns a map of field name => field spec.
924
	 *
925
	 * @param string $table The table name.
926
	 * @return array
927
	 */
928
	abstract public function fieldList($table);
929
930
	/**
931
	 *
932
	 * This allows the cached values for a table's field list to be erased.
933
	 * If $tablename is empty, then the whole cache is erased.
934
	 *
935
	 * @param string $tableName
936
	 *
937
	 * @return boolean
938
	 */
939
	public function clearCachedFieldlist($tableName = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
940
		return true;
941
	}
942
943
}
944