Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

DBSchemaManager::parseIndexSpec()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 14
nc 4
nop 2
dl 0
loc 24
rs 8.6845
c 0
b 0
f 0
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() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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
		$this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
331
332
		// Create custom fields
333
		if ($fieldSchema) {
334
			foreach ($fieldSchema as $fieldName => $fieldSpec) {
335
336
				//Is this an array field?
337
				$arrayValue = '';
338
				if (strpos($fieldSpec, '[') !== false) {
339
					//If so, remove it and store that info separately
340
					$pos = strpos($fieldSpec, '[');
341
					$arrayValue = substr($fieldSpec, $pos);
342
					$fieldSpec = substr($fieldSpec, 0, $pos);
343
				}
344
345
				$fieldObj = Object::create_from_string($fieldSpec, $fieldName);
346
				$fieldObj->arrayValue = $arrayValue;
347
348
				$fieldObj->setTable($table);
349
				$fieldObj->requireField();
350
			}
351
		}
352
353
		// Create custom indexes
354
		if ($indexSchema) {
355
			foreach ($indexSchema as $indexName => $indexDetails) {
356
				$this->requireIndex($table, $indexName, $indexDetails);
357
			}
358
		}
359
	}
360
361
	/**
362
	 * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
363
	 * @param string $table The table name.
364
	 */
365
	public function dontRequireTable($table) {
366
		if (isset($this->tableList[strtolower($table)])) {
367
			$suffix = '';
368
			while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
369
				$suffix = $suffix
370
						? ($suffix + 1)
371
						: 2;
372
			}
373
			$this->renameTable($table, "_obsolete_{$table}$suffix");
374
			$this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
375
		}
376
	}
377
378
	/**
379
	 * Generate the given index in the database, modifying whatever already exists as necessary.
380
	 *
381
	 * The keys of the array are the names of the index.
382
	 * The values of the array can be one of:
383
	 *  - true: Create a single column index on the field named the same as the index.
384
	 *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
385
	 *    control over the index.
386
	 *
387
	 * @param string $table The table name.
388
	 * @param string $index The index name.
389
	 * @param string|array|boolean $spec The specification of the index in any
390
	 * loose format. See requireTable() for more information.
391
	 */
392
	public function requireIndex($table, $index, $spec) {
393
		// Detect if adding to a new table
394
		$newTable = !isset($this->tableList[strtolower($table)]);
395
396
		// Force spec into standard array format
397
		$spec = $this->parseIndexSpec($index, $spec);
398
		$specString = $this->convertIndexSpec($spec);
399
400
		// Check existing index
401
		if (!$newTable) {
402
			$indexKey = $this->indexKey($table, $index, $spec);
403
			$indexList = $this->indexList($table);
404
			if (isset($indexList[$indexKey])) {
405
406
				// $oldSpec should be in standard array format
407
				$oldSpec = $indexList[$indexKey];
408
				$oldSpecString = $this->convertIndexSpec($oldSpec);
409
			}
410
		}
411
412
		// Initiate either generation or modification of index
413
		if ($newTable || !isset($indexList[$indexKey])) {
414
			// New index
415
			$this->transCreateIndex($table, $index, $spec);
416
			$this->alterationMessage("Index $table.$index: created as $specString", "created");
417
		} else if ($oldSpecString != $specString) {
418
			// Updated index
419
			$this->transAlterIndex($table, $index, $spec);
420
			$this->alterationMessage(
421
				"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...
422
				"changed"
423
			);
424
		}
425
	}
426
427
	/**
428
	 * Splits a spec string safely, considering quoted columns, whitespace,
429
	 * and cleaning brackets
430
	 *
431
	 * @param string $spec The input index specification string
432
	 * @return array List of columns in the spec
433
	 */
434
	protected function explodeColumnString($spec) {
435
		// Remove any leading/trailing brackets and outlying modifiers
436
		// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
437
		$containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
438
439
		// Split potentially quoted modifiers
440
		// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
441
		return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
442
	}
443
444
	/**
445
	 * Builds a properly quoted column list from an array
446
	 *
447
	 * @param array $columns List of columns to implode
448
	 * @return string A properly quoted list of column names
449
	 */
450
	protected function implodeColumnList($columns) {
451
		if(empty($columns)) return '';
452
		return '"' . implode('","', $columns) . '"';
453
	}
454
455
	/**
456
	 * Given an index specification in the form of a string ensure that each
457
	 * column name is property quoted, stripping brackets and modifiers.
458
	 * This index may also be in the form of a "CREATE INDEX..." sql fragment
459
	 *
460
	 * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
461
	 * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
462
	 */
463
	protected function quoteColumnSpecString($spec) {
464
		$bits = $this->explodeColumnString($spec);
465
		return $this->implodeColumnList($bits);
466
	}
467
468
	/**
469
	 * Given an index spec determines the index type
470
	 *
471
	 * @param array|string $spec
472
	 * @return string
473
	 */
474
	protected function determineIndexType($spec) {
475
		// check array spec
476
		if(is_array($spec) && isset($spec['type'])) {
477
			return $spec['type'];
478
		} elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
479
			return strtolower($matchType['type']);
480
		} else {
481
			return 'index';
482
		}
483
	}
484
485
	/**
486
	 * Converts an array or string index spec into a universally useful array
487
	 *
488
	 * @see convertIndexSpec() for approximate inverse
489
	 * @param string|array $spec
490
	 * @return array The resulting spec array with the required fields name, type, and value
491
	 */
492
	protected function parseIndexSpec($name, $spec) {
493
		// Support $indexes = array('ColumnName' => true) for quick indexes
494
		if ($spec === true) {
495
			return array(
496
				'name' => $name,
497
				'value' => $this->quoteColumnSpecString($name),
498
				'type' => 'index'
499
			);
500
		}
501
502
		// Do minimal cleanup on any already parsed spec
503
		if(is_array($spec)) {
504
			$spec['value'] = $this->quoteColumnSpecString($spec['value']);
505
			$spec['type'] = empty($spec['type']) ? 'index' : trim($spec['type']);
506
			return $spec;
507
		}
508
509
		// Nicely formatted spec!
510
		return array(
511
			'name' => $name,
512
			'value' => $this->quoteColumnSpecString($spec),
513
			'type' => $this->determineIndexType($spec)
514
		);
515
	}
516
517
	/**
518
	 * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
519
	 * and turns it into a proper string.
520
	 * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
521
	 * arrays to be created. See {@link requireTable()} for details on the index format.
522
	 *
523
	 * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
524
	 * @see parseIndexSpec() for approximate inverse
525
	 *
526
	 * @param string|array $indexSpec
527
	 */
528
	protected function convertIndexSpec($indexSpec) {
529
		// Return already converted spec
530
		if (!is_array($indexSpec)) return $indexSpec;
531
532
		// Combine elements into standard string format
533
		return "{$indexSpec['type']} ({$indexSpec['value']})";
534
	}
535
536
	/**
537
	 * Returns true if the given table is exists in the current database
538
	 *
539
	 * @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...
540
	 * @return boolean Flag indicating existence of table
541
	 */
542
	abstract public function hasTable($tableName);
543
544
	/**
545
	 * Return true if the table exists and already has a the field specified
546
	 *
547
	 * @param string $tableName - The table to check
548
	 * @param string $fieldName - The field to check
549
	 * @return bool - True if the table exists and the field exists on the table
550
	 */
551
	public function hasField($tableName, $fieldName) {
552
		if (!$this->hasTable($tableName)) return false;
553
		$fields = $this->fieldList($tableName);
554
		return array_key_exists($fieldName, $fields);
555
	}
556
557
	/**
558
	 * Generate the given field on the table, modifying whatever already exists as necessary.
559
	 *
560
	 * @param string $table The table name.
561
	 * @param string $field The field name.
562
	 * @param array|string $spec The field specification. If passed in array syntax, the specific database
563
	 * 	driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
564
	 * 	be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
565
	 * 	need to take care of database abstraction in your DBField subclass.
566
	 */
567
	public function requireField($table, $field, $spec) {
568
		//TODO: this is starting to get extremely fragmented.
569
		//There are two different versions of $spec floating around, and their content changes depending
570
		//on how they are structured.  This needs to be tidied up.
571
		$fieldValue = null;
572
		$newTable = false;
573
574
		// backwards compatibility patch for pre 2.4 requireField() calls
575
		$spec_orig = $spec;
576
577
		if (!is_string($spec)) {
578
			$spec['parts']['name'] = $field;
579
			$spec_orig['parts']['name'] = $field;
580
			//Convert the $spec array into a database-specific string
581
			$spec = $this->$spec['type']($spec['parts'], true);
582
		}
583
584
		// Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
585
		// collations.
586
		// TODO: move this to the MySQLDatabase file, or drop it altogether?
587
		if (!$this->database->supportsCollations()) {
588
			$spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
589
		}
590
591
		if (!isset($this->tableList[strtolower($table)])) $newTable = true;
592
593
		if (is_array($spec)) {
594
			$specValue = $this->$spec_orig['type']($spec_orig['parts']);
595
		} else {
596
			$specValue = $spec;
597
		}
598
599
		// We need to get db-specific versions of the ID column:
600
		if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
601
			$specValue = $this->IdColumn(true);
602
		}
603
604
		if (!$newTable) {
605
			$fieldList = $this->fieldList($table);
606
			if (isset($fieldList[$field])) {
607
				if (is_array($fieldList[$field])) {
608
					$fieldValue = $fieldList[$field]['data_type'];
609
				} else {
610
					$fieldValue = $fieldList[$field];
611
				}
612
			}
613
		}
614
615
		// Get the version of the field as we would create it. This is used for comparison purposes to see if the
616
		// existing field is different to what we now want
617
		if (is_array($spec_orig)) {
618
			$spec_orig = $this->$spec_orig['type']($spec_orig['parts']);
619
		}
620
621
		if ($newTable || $fieldValue == '') {
622
			$this->transCreateField($table, $field, $spec_orig);
623
			$this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
624
		} else if ($fieldValue != $specValue) {
625
			// If enums/sets are being modified, then we need to fix existing data in the table.
626
			// Update any records where the enum is set to a legacy value to be set to the default.
627
			// One hard-coded exception is SiteTree - the default for this is Page.
628
			foreach (array('enum', 'set') as $enumtype) {
629
				if (preg_match("/^$enumtype/i", $specValue)) {
630
					$newStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $spec_orig);
631
					$new = preg_split("/'\s*,\s*'/", $newStr);
632
633
					$oldStr = preg_replace("/(^$enumtype\s*\(')|('$\).*)/i", "", $fieldValue);
0 ignored issues
show
Unused Code introduced by
$oldStr is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
634
					$old = preg_split("/'\s*,\s*'/", $newStr);
635
636
					$holder = array();
637
					foreach ($old as $check) {
638
						if (!in_array($check, $new)) {
639
							$holder[] = $check;
640
						}
641
					}
642
					if (count($holder)) {
643
						$default = explode('default ', $spec_orig);
644
						$default = $default[1];
645
						if ($default == "'SiteTree'") $default = "'Page'";
646
						$query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
647
						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...
648
							$query .= "'{$holder[$i]}', ";
649
						}
650
						$query .= "'{$holder[$i]}')";
651
						$this->query($query);
652
						$amount = $this->database->affectedRows();
653
						$this->alterationMessage("Changed $amount rows to default value of field $field"
654
								. " (Value: $default)");
655
					}
656
				}
657
			}
658
			$this->transAlterField($table, $field, $spec_orig);
659
			$this->alterationMessage(
660
				"Field $table.$field: changed to $specValue <i style=\"color: #AAA\">(from {$fieldValue})</i>",
661
				"changed"
662
			);
663
		}
664
	}
665
666
	/**
667
	 * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
668
	 *
669
	 * @param string $table
670
	 * @param string $fieldName
671
	 */
672
	public function dontRequireField($table, $fieldName) {
673
		$fieldList = $this->fieldList($table);
674
		if (array_key_exists($fieldName, $fieldList)) {
675
			$suffix = '';
676
			while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
677
				$suffix = $suffix
678
						? ($suffix + 1)
679
						: 2;
680
			}
681
			$this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
682
			$this->alterationMessage(
683
				"Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
684
				"obsolete"
685
			);
686
		}
687
	}
688
689
	/**
690
	 * Show a message about database alteration
691
	 *
692
	 * @param string $message to display
693
	 * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
694
	 */
695
	public function alterationMessage($message, $type = "") {
696
		if (!$this->supressOutput) {
697
			if (Director::is_cli()) {
698
				switch ($type) {
699
					case "created":
700
					case "changed":
701
					case "repaired":
702
						$sign = "+";
703
						break;
704
					case "obsolete":
705
					case "deleted":
706
						$sign = '-';
707
						break;
708
					case "notice":
709
						$sign = '*';
710
						break;
711
					case "error":
712
						$sign = "!";
713
						break;
714
					default:
715
						$sign = " ";
716
				}
717
				$message = strip_tags($message);
718
				echo "  $sign $message\n";
719
			} else {
720
				switch ($type) {
721
					case "created":
722
						$color = "green";
723
						break;
724
					case "obsolete":
725
						$color = "red";
726
						break;
727
					case "notice":
728
						$color = "orange";
729
						break;
730
					case "error":
731
						$color = "red";
732
						break;
733
					case "deleted":
734
						$color = "red";
735
						break;
736
					case "changed":
737
						$color = "blue";
738
						break;
739
					case "repaired":
740
						$color = "blue";
741
						break;
742
					default:
743
						$color = "";
744
				}
745
				echo "<li style=\"color: $color\">$message</li>";
746
			}
747
		}
748
	}
749
750
	/**
751
	 * This returns the data type for the id column which is the primary key for each table
752
	 *
753
	 * @param boolean $asDbValue
754
	 * @param boolean $hasAutoIncPK
755
	 * @return string
756
	 */
757
	abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
758
759
	/**
760
	 * Checks a table's integrity and repairs it if necessary.
761
	 *
762
	 * @param string $tableName The name of the table.
763
	 * @return boolean Return true if the table has integrity after the method is complete.
764
	 */
765
	abstract public function checkAndRepairTable($tableName);
766
767
	/**
768
	 * Returns the values of the given enum field
769
	 *
770
	 * @param string $tableName Name of table to check
771
	 * @param string $fieldName name of enum field to check
772
	 * @return array List of enum values
773
	 */
774
	abstract public function enumValuesForField($tableName, $fieldName);
775
776
777
	/*
778
	 * This is a lookup table for data types.
779
	 * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
780
	 * So this is a DB-specific list of equivilents.
781
	 *
782
	 * @param string $type
783
	 * @return string
784
	 */
785
	abstract public function dbDataType($type);
786
787
	/**
788
	 * Retrieves the list of all databases the user has access to
789
	 *
790
	 * @return array List of database names
791
	 */
792
	abstract public function databaseList();
793
794
	/**
795
	 * Determine if the database with the specified name exists
796
	 *
797
	 * @param string $name Name of the database to check for
798
	 * @return boolean Flag indicating whether this database exists
799
	 */
800
	abstract public function databaseExists($name);
801
802
	/**
803
	 * Create a database with the specified name
804
	 *
805
	 * @param string $name Name of the database to create
806
	 * @return boolean True if successful
807
	 */
808
	abstract public function createDatabase($name);
809
810
	/**
811
	 * Drops a database with the specified name
812
	 *
813
	 * @param string $name Name of the database to drop
814
	 */
815
	abstract public function dropDatabase($name);
816
817
	/**
818
	 * Alter an index on a table.
819
	 *
820
	 * @param string $tableName The name of the table.
821
	 * @param string $indexName The name of the index.
822
	 * @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()}
823
	 *                          for more details.
824
	 * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
825
	 */
826
	abstract public function alterIndex($tableName, $indexName, $indexSpec);
827
828
	/**
829
	 * Determines the key that should be used to identify this index
830
	 * when retrieved from DBSchemaManager->indexList.
831
	 * In some connectors this is the database-visible name, in others the
832
	 * usercode-visible name.
833
	 *
834
	 * @param string $table
835
	 * @param string $index
836
	 * @param array $spec
837
	 * @return string Key for this index
838
	 */
839
	abstract protected function indexKey($table, $index, $spec);
840
841
	/**
842
	 * Return the list of indexes in a table.
843
	 *
844
	 * @param string $table The table name.
845
	 * @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...
846
	 * array form. The key for this array should be predictable using the indexKey
847
	 * method
848
	 */
849
	abstract public function indexList($table);
850
851
	/**
852
	 * Returns a list of all tables in the database.
853
	 * Keys are table names in lower case, values are table names in case that
854
	 * database expects.
855
	 *
856
	 * @return array
857
	 */
858
	abstract public function tableList();
859
860
	/**
861
	 * Create a new table.
862
	 *
863
	 * @param string $table The name of the table
864
	 * @param array $fields A map of field names to field types
865
	 * @param array $indexes A map of indexes
866
	 * @param array $options An map of additional options.  The available keys are as follows:
867
	 *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
868
	 *   - 'temporary' - If true, then a temporary table will be created
869
	 * @param $advancedOptions Advanced creation options
870
	 * @return string The table name generated.  This may be different from the table name, for example with temporary
871
	 * tables.
872
	 */
873
	abstract public function createTable($table, $fields = null, $indexes = null, $options = null,
874
										$advancedOptions = null);
875
876
	/**
877
	 * Alter a table's schema.
878
	 *
879
	 * @param string $table The name of the table to alter
880
	 * @param array $newFields New fields, a map of field name => field schema
881
	 * @param array $newIndexes New indexes, a map of index name => index type
882
	 * @param array $alteredFields Updated fields, a map of field name => field schema
883
	 * @param array $alteredIndexes Updated indexes, a map of index name => index type
884
	 * @param array $alteredOptions
885
	 * @param array $advancedOptions
886
	 */
887
	abstract public function alterTable($table, $newFields = null, $newIndexes = null, $alteredFields = null,
888
										$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null);
889
890
	/**
891
	 * Rename a table.
892
	 *
893
	 * @param string $oldTableName The old table name.
894
	 * @param string $newTableName The new table name.
895
	 */
896
	abstract public function renameTable($oldTableName, $newTableName);
897
898
	/**
899
	 * Create a new field on a table.
900
	 *
901
	 * @param string $table Name of the table.
902
	 * @param string $field Name of the field to add.
903
	 * @param string $spec The field specification, eg 'INTEGER NOT NULL'
904
	 */
905
	abstract public function createField($table, $field, $spec);
906
907
	/**
908
	 * Change the database column name of the given field.
909
	 *
910
	 * @param string $tableName The name of the tbale the field is in.
911
	 * @param string $oldName The name of the field to change.
912
	 * @param string $newName The new name of the field
913
	 */
914
	abstract public function renameField($tableName, $oldName, $newName);
915
916
	/**
917
	 * Get a list of all the fields for the given table.
918
	 * Returns a map of field name => field spec.
919
	 *
920
	 * @param string $table The table name.
921
	 * @return array
922
	 */
923
	abstract public function fieldList($table);
924
925
	/**
926
	 *
927
	 * This allows the cached values for a table's field list to be erased.
928
	 * If $tablename is empty, then the whole cache is erased.
929
	 *
930
	 * @param string $tableName
931
	 *
932
	 * @return boolean
933
	 */
934
	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...
935
		return true;
936
	}
937
938
}
939