Completed
Push — 3.7 ( 2228bf...d68a50 )
by Daniel
23s queued 12s
created

DBSchemaManager::requireTable()   D

Complexity

Conditions 16
Paths 198

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 198
nop 6
dl 0
loc 61
rs 4.75
c 0
b 0
f 0

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
					$engine = isset($tableStatus['Engine']) ? $tableStatus['Engine'] : null;
321
					$tableOptionsChanged = ($engine != $alteredEngine);
322
				}
323
			}
324
325
			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...
326
				$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...
327
			}
328
		}
329
330
		//DB ABSTRACTION: we need to convert this to a db-specific version:
331
		$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
332
333
		// Create custom fields
334
		if ($fieldSchema) {
335
			foreach ($fieldSchema as $fieldName => $fieldSpec) {
336
337
				//Is this an array field?
338
				$arrayValue = '';
339
				if (strpos($fieldSpec, '[') !== false) {
340
					//If so, remove it and store that info separately
341
					$pos = strpos($fieldSpec, '[');
342
					$arrayValue = substr($fieldSpec, $pos);
343
					$fieldSpec = substr($fieldSpec, 0, $pos);
344
				}
345
346
				$fieldObj = SS_Object::create_from_string($fieldSpec, $fieldName);
347
				$fieldObj->arrayValue = $arrayValue;
348
349
				$fieldObj->setTable($table);
350
				$fieldObj->requireField();
351
			}
352
		}
353
354
		// Create custom indexes
355
		if ($indexSchema) {
356
			foreach ($indexSchema as $indexName => $indexDetails) {
357
				$this->requireIndex($table, $indexName, $indexDetails);
358
			}
359
		}
360
	}
361
362
	/**
363
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
364
	 * @param string $table The table name.
365
	 */
366
	public function dontRequireTable($table) {
367
		if (isset($this->tableList[strtolower($table)])) {
368
			$suffix = '';
369
			while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
370
				$suffix = $suffix
371
						? ($suffix + 1)
372
						: 2;
373
			}
374
			$this->renameTable($table, "_obsolete_{$table}$suffix");
375
			$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
376
		}
377
	}
378
379
	/**
380
	 * Generate the given index in the database, modifying whatever already exists as necessary.
381
	 *
382
	 * The keys of the array are the names of the index.
383
	 * The values of the array can be one of:
384
	 *  - true: Create a single column index on the field named the same as the index.
385
	 *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
386
	 *    control over the index.
387
	 *
388
	 * @param string $table The table name.
389
	 * @param string $index The index name.
390
	 * @param string|array|boolean $spec The specification of the index in any
391
	 * loose format. See requireTable() for more information.
392
	 */
393
	public function requireIndex($table, $index, $spec) {
394
		// Detect if adding to a new table
395
		$newTable = !isset($this->tableList[strtolower($table)]);
396
397
		// Force spec into standard array format
398
		$spec = $this->parseIndexSpec($index, $spec);
399
		$specString = $this->convertIndexSpec($spec);
400
401
		// Check existing index
402
		if (!$newTable) {
403
			$indexKey = $this->indexKey($table, $index, $spec);
404
			$indexList = $this->indexList($table);
405
			if (isset($indexList[$indexKey])) {
406
407
				// $oldSpec should be in standard array format
408
				$oldSpec = $indexList[$indexKey];
409
				$oldSpecString = $this->convertIndexSpec($oldSpec);
410
			}
411
		}
412
413
		// Initiate either generation or modification of index
414
		if ($newTable || !isset($indexList[$indexKey])) {
415
			// New index
416
			$this->transCreateIndex($table, $index, $spec);
417
			$this->alterationMessage("Index $table.$index: created as $specString", "created");
418
		} else if ($oldSpecString != $specString) {
419
			// Updated index
420
			$this->transAlterIndex($table, $index, $spec);
421
			$this->alterationMessage(
422
				"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...
423
				"changed"
424
			);
425
		}
426
	}
427
428
	/**
429
	 * Splits a spec string safely, considering quoted columns, whitespace,
430
	 * and cleaning brackets
431
	 *
432
	 * @param string $spec The input index specification string
433
	 * @return array List of columns in the spec
434
	 */
435
	protected function explodeColumnString($spec) {
436
		// Remove any leading/trailing brackets and outlying modifiers
437
		// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
438
		$containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
439
440
		// Split potentially quoted modifiers
441
		// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
442
		return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
443
	}
444
445
	/**
446
	 * Builds a properly quoted column list from an array
447
	 *
448
	 * @param array $columns List of columns to implode
449
	 * @return string A properly quoted list of column names
450
	 */
451
	protected function implodeColumnList($columns) {
452
		if(empty($columns)) return '';
453
		return '"' . implode('","', $columns) . '"';
454
	}
455
456
	/**
457
	 * Given an index specification in the form of a string ensure that each
458
	 * column name is property quoted, stripping brackets and modifiers.
459
	 * This index may also be in the form of a "CREATE INDEX..." sql fragment
460
	 *
461
	 * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
462
	 * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
463
	 */
464
	protected function quoteColumnSpecString($spec) {
465
		$bits = $this->explodeColumnString($spec);
466
		return $this->implodeColumnList($bits);
467
	}
468
469
	/**
470
	 * Given an index spec determines the index type
471
	 *
472
	 * @param array|string $spec
473
	 * @return string
474
	 */
475
	protected function determineIndexType($spec) {
476
		// check array spec
477
		if(is_array($spec) && isset($spec['type'])) {
478
			return $spec['type'];
479
		} elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
480
			return strtolower($matchType['type']);
481
		} else {
482
			return 'index';
483
		}
484
	}
485
486
	/**
487
	 * Converts an array or string index spec into a universally useful array
488
	 *
489
	 * @see convertIndexSpec() for approximate inverse
490
	 * @param string|array $spec
491
	 * @return array The resulting spec array with the required fields name, type, and value
492
	 */
493
	protected function parseIndexSpec($name, $spec) {
494
		// Support $indexes = array('ColumnName' => true) for quick indexes
495
		if ($spec === true) {
496
			return array(
497
				'name' => $name,
498
				'value' => $this->quoteColumnSpecString($name),
499
				'type' => 'index'
500
			);
501
		}
502
503
		// Do minimal cleanup on any already parsed spec
504
		if(is_array($spec)) {
505
			$spec['value'] = $this->quoteColumnSpecString($spec['value']);
506
			$spec['type'] = empty($spec['type']) ? 'index' : trim($spec['type']);
507
			return $spec;
508
		}
509
510
		// Nicely formatted spec!
511
		return array(
512
			'name' => $name,
513
			'value' => $this->quoteColumnSpecString($spec),
514
			'type' => $this->determineIndexType($spec)
515
		);
516
	}
517
518
	/**
519
	 * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
520
	 * and turns it into a proper string.
521
	 * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
522
	 * arrays to be created. See {@link requireTable()} for details on the index format.
523
	 *
524
	 * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
525
	 * @see parseIndexSpec() for approximate inverse
526
	 *
527
	 * @param string|array $indexSpec
528
	 */
529
	protected function convertIndexSpec($indexSpec) {
530
		// Return already converted spec
531
		if (!is_array($indexSpec)) return $indexSpec;
532
533
		// Combine elements into standard string format
534
		return "{$indexSpec['type']} ({$indexSpec['value']})";
535
	}
536
537
	/**
538
	 * Returns true if the given table is exists in the current database
539
	 *
540
	 * @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...
541
	 * @return boolean Flag indicating existence of table
542
	 */
543
	abstract public function hasTable($tableName);
544
545
	/**
546
	 * Return true if the table exists and already has a the field specified
547
	 *
548
	 * @param string $tableName - The table to check
549
	 * @param string $fieldName - The field to check
550
	 * @return bool - True if the table exists and the field exists on the table
551
	 */
552
	public function hasField($tableName, $fieldName) {
553
		if (!$this->hasTable($tableName)) return false;
554
		$fields = $this->fieldList($tableName);
555
		return array_key_exists($fieldName, $fields);
556
	}
557
558
	/**
559
	 * Generate the given field on the table, modifying whatever already exists as necessary.
560
	 *
561
	 * @param string $table The table name.
562
	 * @param string $field The field name.
563
	 * @param array|string $spec The field specification. If passed in array syntax, the specific database
564
	 * 	driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
565
	 * 	be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
566
	 * 	need to take care of database abstraction in your DBField subclass.
567
	 */
568
	public function requireField($table, $field, $spec) {
569
		//TODO: this is starting to get extremely fragmented.
570
		//There are two different versions of $spec floating around, and their content changes depending
571
		//on how they are structured.  This needs to be tidied up.
572
		$fieldValue = null;
573
		$newTable = false;
574
575
		// backwards compatibility patch for pre 2.4 requireField() calls
576
		$spec_orig = $spec;
577
578
		if (!is_string($spec)) {
579
			$spec['parts']['name'] = $field;
580
			$spec_orig['parts']['name'] = $field;
581
			//Convert the $spec array into a database-specific string
582
			$spec = $this->{$spec['type']}($spec['parts'], true);
583
		}
584
585
		// Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
586
		// collations.
587
		// TODO: move this to the MySQLDatabase file, or drop it altogether?
588
		if (!$this->database->supportsCollations()) {
589
			$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
590
		}
591
592
		if (!isset($this->tableList[strtolower($table)])) $newTable = true;
593
594
		if (is_array($spec)) {
595
			$specValue = $this->{$spec_orig['type']}($spec_orig['parts']);
596
		} else {
597
			$specValue = $spec;
598
		}
599
600
		// We need to get db-specific versions of the ID column:
601
		if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
602
			$specValue = $this->IdColumn(true);
603
		}
604
605
		if (!$newTable) {
606
			$fieldList = $this->fieldList($table);
607
			if (isset($fieldList[$field])) {
608
				if (is_array($fieldList[$field])) {
609
					$fieldValue = $fieldList[$field]['data_type'];
610
				} else {
611
					$fieldValue = $fieldList[$field];
612
				}
613
			}
614
		}
615
616
		// Get the version of the field as we would create it. This is used for comparison purposes to see if the
617
		// existing field is different to what we now want
618
		if (is_array($spec_orig)) {
619
			$spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
620
		}
621
622
		if ($newTable || $fieldValue == '') {
623
			$this->transCreateField($table, $field, $spec_orig);
624
			$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
625
		} else if ($fieldValue != $specValue) {
626
			// If enums/sets are being modified, then we need to fix existing data in the table.
627
			// Update any records where the enum is set to a legacy value to be set to the default.
628
			// One hard-coded exception is SiteTree - the default for this is Page.
629
			foreach (array('enum', 'set') as $enumtype) {
630
				if (preg_match("/^$enumtype/i", $specValue)) {
631
					$newStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $spec_orig);
632
					$new = preg_split("/'\s*,\s*'/", $newStr);
633
634
					$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue);
635
					$old = preg_split("/'\s*,\s*'/", $newStr);
636
637
					$holder = array();
638
					foreach ($old as $check) {
639
						if (!in_array($check, $new)) {
640
							$holder[] = $check;
641
						}
642
					}
643
					if (count($holder)) {
644
						$default = explode('default ', $spec_orig);
645
						$default = $default[1];
646
						if ($default == "'SiteTree'") $default = "'Page'";
647
						$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
648
						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...
649
							$query .= "'{$holder[$i]}', ";
650
						}
651
						$query .= "'{$holder[$i]}')";
652
						$this->query($query);
653
						$amount = $this->database->affectedRows();
654
						$this->alterationMessage("Changed $amount rows to default value of field $field"
655
								. " (Value: $default)");
656
					}
657
				}
658
			}
659
			$this->transAlterField($table, $field, $spec_orig);
660
			$this->alterationMessage(
661
				"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
662
				"changed"
663
			);
664
		}
665
	}
666
667
	/**
668
	 * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
669
	 *
670
	 * @param string $table
671
	 * @param string $fieldName
672
	 */
673
	public function dontRequireField($table, $fieldName) {
674
		$fieldList = $this->fieldList($table);
675
		if (array_key_exists($fieldName, $fieldList)) {
676
			$suffix = '';
677
			while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
678
				$suffix = $suffix
679
						? ($suffix + 1)
680
						: 2;
681
			}
682
			$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
683
			$this->alterationMessage(
684
				"Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
685
				"obsolete"
686
			);
687
		}
688
	}
689
690
	/**
691
	 * Show a message about database alteration
692
	 *
693
	 * @param string $message to display
694
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
695
	 */
696
	public function alterationMessage($message, $type = "") {
697
		if (!$this->supressOutput) {
698
			if (Director::is_cli()) {
699
				switch ($type) {
700
					case "created":
701
					case "changed":
702
					case "repaired":
703
						$sign = "+";
704
						break;
705
					case "obsolete":
706
					case "deleted":
707
						$sign = '-';
708
						break;
709
					case "notice":
710
						$sign = '*';
711
						break;
712
					case "error":
713
						$sign = "!";
714
						break;
715
					default:
716
						$sign = " ";
717
				}
718
				$message = strip_tags($message);
719
				echo "  $sign $message\n";
720
			} else {
721
				switch ($type) {
722
					case "created":
723
						$color = "green";
724
						break;
725
					case "obsolete":
726
						$color = "red";
727
						break;
728
					case "notice":
729
						$color = "orange";
730
						break;
731
					case "error":
732
						$color = "red";
733
						break;
734
					case "deleted":
735
						$color = "red";
736
						break;
737
					case "changed":
738
						$color = "blue";
739
						break;
740
					case "repaired":
741
						$color = "blue";
742
						break;
743
					default:
744
						$color = "";
745
				}
746
				echo "<li style=\"color: $color\">$message</li>";
747
			}
748
		}
749
	}
750
751
	/**
752
	 * This returns the data type for the id column which is the primary key for each table
753
	 *
754
	 * @param boolean $asDbValue
755
	 * @param boolean $hasAutoIncPK
756
	 * @return string
757
	 */
758
	abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
759
760
	/**
761
	 * Checks a table's integrity and repairs it if necessary.
762
	 *
763
	 * @param string $tableName The name of the table.
764
	 * @return boolean Return true if the table has integrity after the method is complete.
765
	 */
766
	abstract public function checkAndRepairTable($tableName);
767
768
	/**
769
	 * Returns the values of the given enum field
770
	 *
771
	 * @param string $tableName Name of table to check
772
	 * @param string $fieldName name of enum field to check
773
	 * @return array List of enum values
774
	 */
775
	abstract public function enumValuesForField($tableName, $fieldName);
776
777
778
	/*
779
	 * This is a lookup table for data types.
780
	 * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
781
	 * So this is a DB-specific list of equivilents.
782
	 *
783
	 * @param string $type
784
	 * @return string
785
	 */
786
	abstract public function dbDataType($type);
787
788
	/**
789
	 * Retrieves the list of all databases the user has access to
790
	 *
791
	 * @return array List of database names
792
	 */
793
	abstract public function databaseList();
794
795
	/**
796
	 * Determine if the database with the specified name exists
797
	 *
798
	 * @param string $name Name of the database to check for
799
	 * @return boolean Flag indicating whether this database exists
800
	 */
801
	abstract public function databaseExists($name);
802
803
	/**
804
	 * Create a database with the specified name
805
	 *
806
	 * @param string $name Name of the database to create
807
	 * @return boolean True if successful
808
	 */
809
	abstract public function createDatabase($name);
810
811
	/**
812
	 * Drops a database with the specified name
813
	 *
814
	 * @param string $name Name of the database to drop
815
	 */
816
	abstract public function dropDatabase($name);
817
818
	/**
819
	 * Alter an index on a table.
820
	 *
821
	 * @param string $tableName The name of the table.
822
	 * @param string $indexName The name of the index.
823
	 * @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
824
	 *                          for more details.
825
	 * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
826
	 */
827
	abstract public function alterIndex($tableName, $indexName, $indexSpec);
828
829
	/**
830
	 * Determines the key that should be used to identify this index
831
	 * when retrieved from DBSchemaManager->indexList.
832
	 * In some connectors this is the database-visible name, in others the
833
	 * usercode-visible name.
834
	 *
835
	 * @param string $table
836
	 * @param string $index
837
	 * @param array $spec
838
	 * @return string Key for this index
839
	 */
840
	abstract protected function indexKey($table, $index, $spec);
841
842
	/**
843
	 * Return the list of indexes in a table.
844
	 *
845
	 * @param string $table The table name.
846
	 * @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...
847
	 * array form. The key for this array should be predictable using the indexKey
848
	 * method
849
	 */
850
	abstract public function indexList($table);
851
852
	/**
853
	 * Returns a list of all tables in the database.
854
	 * Keys are table names in lower case, values are table names in case that
855
	 * database expects.
856
	 *
857
	 * @return array
858
	 */
859
	abstract public function tableList();
860
861
	/**
862
	 * Create a new table.
863
	 *
864
	 * @param string $table The name of the table
865
	 * @param array $fields A map of field names to field types
866
	 * @param array $indexes A map of indexes
867
	 * @param array $options An map of additional options.  The available keys are as follows:
868
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
869
	 *   - 'temporary' - If true, then a temporary table will be created
870
	 * @param $advancedOptions Advanced creation options
871
	 * @return string The table name generated.  This may be different from the table name, for example with temporary
872
	 * tables.
873
	 */
874
	abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
875
										$advancedOptions = null);
876
877
	/**
878
	 * Alter a table's schema.
879
	 *
880
	 * @param string $table The name of the table to alter
881
	 * @param array $newFields New fields, a map of field name => field schema
882
	 * @param array $newIndexes New indexes, a map of index name => index type
883
	 * @param array $alteredFields Updated fields, a map of field name => field schema
884
	 * @param array $alteredIndexes Updated indexes, a map of index name => index type
885
	 * @param array $alteredOptions
886
	 * @param array $advancedOptions
887
	 */
888
	abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
889
										$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null);
890
891
	/**
892
	 * Rename a table.
893
	 *
894
	 * @param string $oldTableName The old table name.
895
	 * @param string $newTableName The new table name.
896
	 */
897
	abstract public function renameTable($oldTableName, $newTableName);
898
899
	/**
900
	 * Create a new field on a table.
901
	 *
902
	 * @param string $table Name of the table.
903
	 * @param string $field Name of the field to add.
904
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
905
	 */
906
	abstract public function createField($table, $field, $spec);
907
908
	/**
909
	 * Change the database column name of the given field.
910
	 *
911
	 * @param string $tableName The name of the tbale the field is in.
912
	 * @param string $oldName The name of the field to change.
913
	 * @param string $newName The new name of the field
914
	 */
915
	abstract public function renameField($tableName, $oldName, $newName);
916
917
	/**
918
	 * Get a list of all the fields for the given table.
919
	 * Returns a map of field name => field spec.
920
	 *
921
	 * @param string $table The table name.
922
	 * @return array
923
	 */
924
	abstract public function fieldList($table);
925
926
	/**
927
	 *
928
	 * This allows the cached values for a table's field list to be erased.
929
	 * If $tablename is empty, then the whole cache is erased.
930
	 *
931
	 * @param string $tableName
932
	 *
933
	 * @return boolean
934
	 */
935
	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...
936
		return true;
937
	}
938
939
}
940