Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

DBSchemaManager::requireField()   F

Complexity

Conditions 20
Paths 3328

Size

Total Lines 96
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 55
nc 3328
nop 3
dl 0
loc 96
rs 2
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
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\ORM\FieldType\DBPrimaryKey;
9
use SilverStripe\ORM\FieldType\DBField;
10
use Exception;
11
12
/**
13
 * Represents and handles all schema management for a database
14
 */
15
abstract class DBSchemaManager
16
{
17
18
    /**
19
     *
20
     * @config
21
     * Check tables when running /dev/build, and repair them if necessary.
22
     * In case of large databases or more fine-grained control on how to handle
23
     * data corruption in tables, you can disable this behaviour and handle it
24
     * outside of this class, e.g. through a nightly system task with extended logging capabilities.
25
     *
26
     * @var bool
27
     */
28
    private static $check_and_repair_on_build = true;
29
30
    /**
31
     * Check if tables should be renamed in a case-sensitive fashion.
32
     * Note: This should still work even on case-insensitive databases.
33
     *
34
     * @var bool
35
     */
36
    private static $fix_table_case_on_build = true;
37
38
    /**
39
     * Instance of the database controller this schema belongs to
40
     *
41
     * @var Database
42
     */
43
    protected $database = null;
44
45
    /**
46
     * If this is false, then information about database operations
47
     * will be displayed, eg creation of tables.
48
     *
49
     * @var boolean
50
     */
51
    protected $supressOutput = false;
52
53
    /**
54
     * Injector injection point for database controller
55
     *
56
     * @param Database $database
57
     */
58
    public function setDatabase(Database $database)
59
    {
60
        $this->database = $database;
61
    }
62
63
    /**
64
     * The table list, generated by the tableList() function.
65
     * Used by the requireTable() function.
66
     *
67
     * @var array
68
     */
69
    protected $tableList;
70
71
    /**
72
     * Keeps track whether we are currently updating the schema.
73
     *
74
     * @var boolean
75
     */
76
    protected $schemaIsUpdating = false;
77
78
    /**
79
     * Large array structure that represents a schema update transaction
80
     *
81
     * @var array
82
     */
83
    protected $schemaUpdateTransaction;
84
85
    /**
86
     * Enable supression of database messages.
87
     */
88
    public function quiet()
89
    {
90
        $this->supressOutput = true;
91
    }
92
93
    /**
94
     * Execute the given SQL query.
95
     * This abstract function must be defined by subclasses as part of the actual implementation.
96
     * It should return a subclass of SS_Query as the result.
97
     *
98
     * @param string $sql The SQL query to execute
99
     * @param int $errorLevel The level of error reporting to enable for the query
100
     * @return Query
101
     */
102
    public function query($sql, $errorLevel = E_USER_ERROR)
103
    {
104
        return $this->database->query($sql, $errorLevel);
105
    }
106
107
108
    /**
109
     * Execute the given SQL parameterised query with the specified arguments
110
     *
111
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
112
     * @param array $parameters An ordered list of arguments.
113
     * @param int $errorLevel The level of error reporting to enable for the query
114
     * @return Query
115
     */
116
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
117
    {
118
        return $this->database->preparedQuery($sql, $parameters, $errorLevel);
119
    }
120
121
    /**
122
     * Initiates a schema update within a single callback
123
     *
124
     * @param callable $callback
125
     */
126
    public function schemaUpdate($callback)
127
    {
128
        // Begin schema update
129
        $this->schemaIsUpdating = true;
130
131
        // Update table list
132
        $this->tableList = array();
133
        $tables = $this->tableList();
134
        foreach ($tables as $table) {
135
            $this->tableList[strtolower($table)] = $table;
136
        }
137
138
        // Clear update list for client code to mess around with
139
        $this->schemaUpdateTransaction = array();
140
141
        /** @var Exception $error */
142
        $error = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $error is dead and can be removed.
Loading history...
143
        try {
144
            // Yield control to client code
145
            $callback();
146
147
            // If the client code has cancelled the update then abort
148
            if (!$this->isSchemaUpdating()) {
149
                return;
150
            }
151
152
            // End schema update
153
            foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
154
                $advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
155
                switch ($changes['command']) {
156
                    case 'create':
157
                        $this->createTable(
158
                            $tableName,
159
                            $changes['newFields'],
160
                            $changes['newIndexes'],
161
                            $changes['options'],
162
                            $advancedOptions
163
                        );
164
                        break;
165
166
                    case 'alter':
167
                        $this->alterTable(
168
                            $tableName,
169
                            $changes['newFields'],
170
                            $changes['newIndexes'],
171
                            $changes['alteredFields'],
172
                            $changes['alteredIndexes'],
173
                            $changes['alteredOptions'],
174
                            $advancedOptions
175
                        );
176
                        break;
177
                }
178
            }
179
        } finally {
180
            $this->schemaUpdateTransaction = null;
181
            $this->schemaIsUpdating = false;
182
        }
183
    }
184
185
    /**
186
     * Cancels the schema updates requested during (but not after) schemaUpdate() call.
187
     */
188
    public function cancelSchemaUpdate()
189
    {
190
        $this->schemaUpdateTransaction = null;
191
        $this->schemaIsUpdating = false;
192
    }
193
194
    /**
195
     * Returns true if we are during a schema update.
196
     *
197
     * @return boolean
198
     */
199
    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...
200
    {
201
        return $this->schemaIsUpdating;
202
    }
203
204
    /**
205
     * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
206
     *
207
     * @return boolean
208
     */
209
    public function doesSchemaNeedUpdating()
210
    {
211
        return (bool) $this->schemaUpdateTransaction;
212
    }
213
214
    // Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
215
216
    /**
217
     * Instruct the schema manager to record a table creation to later execute
218
     *
219
     * @param string $table Name of the table
220
     * @param array $options Create table options (ENGINE, etc.)
221
     * @param array $advanced_options Advanced table creation options
222
     */
223
    public function transCreateTable($table, $options = null, $advanced_options = null)
224
    {
225
        $this->schemaUpdateTransaction[$table] = array(
226
            'command' => 'create',
227
            'newFields' => array(),
228
            'newIndexes' => array(),
229
            'options' => $options,
230
            'advancedOptions' => $advanced_options
231
        );
232
    }
233
234
    /**
235
     * Instruct the schema manager to record a table alteration to later execute
236
     *
237
     * @param string $table Name of the table
238
     * @param array $options Create table options (ENGINE, etc.)
239
     * @param array $advanced_options Advanced table creation options
240
     */
241
    public function transAlterTable($table, $options, $advanced_options)
242
    {
243
        $this->transInitTable($table);
244
        $this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
245
        $this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
246
    }
247
248
    /**
249
     * Instruct the schema manager to record a field to be later created
250
     *
251
     * @param string $table Name of the table to hold this field
252
     * @param string $field Name of the field to create
253
     * @param string $schema Field specification as a string
254
     */
255
    public function transCreateField($table, $field, $schema)
256
    {
257
        $this->transInitTable($table);
258
        $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
259
    }
260
261
    /**
262
     * Instruct the schema manager to record an index to be later created
263
     *
264
     * @param string $table Name of the table to hold this index
265
     * @param string $index Name of the index to create
266
     * @param array $schema Already parsed index specification
267
     */
268
    public function transCreateIndex($table, $index, $schema)
269
    {
270
        $this->transInitTable($table);
271
        $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
272
    }
273
274
    /**
275
     * Instruct the schema manager to record a field to be later updated
276
     *
277
     * @param string $table Name of the table to hold this field
278
     * @param string $field Name of the field to update
279
     * @param string $schema Field specification as a string
280
     */
281
    public function transAlterField($table, $field, $schema)
282
    {
283
        $this->transInitTable($table);
284
        $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
285
    }
286
287
    /**
288
     * Instruct the schema manager to record an index to be later updated
289
     *
290
     * @param string $table Name of the table to hold this index
291
     * @param string $index Name of the index to update
292
     * @param array $schema Already parsed index specification
293
     */
294
    public function transAlterIndex($table, $index, $schema)
295
    {
296
        $this->transInitTable($table);
297
        $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
298
    }
299
300
    /**
301
     * Handler for the other transXXX methods - mark the given table as being altered
302
     * if it doesn't already exist
303
     *
304
     * @param string $table Name of the table to initialise
305
     */
306
    protected function transInitTable($table)
307
    {
308
        if (!isset($this->schemaUpdateTransaction[$table])) {
309
            $this->schemaUpdateTransaction[$table] = array(
310
                'command' => 'alter',
311
                'newFields' => array(),
312
                'newIndexes' => array(),
313
                'alteredFields' => array(),
314
                'alteredIndexes' => array(),
315
                'alteredOptions' => ''
316
            );
317
        }
318
    }
319
320
    /**
321
     * Generate the following table in the database, modifying whatever already exists
322
     * as necessary.
323
     *
324
     * @todo Change detection for CREATE TABLE $options other than "Engine"
325
     *
326
     * @param string $table The name of the table
327
     * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
328
     * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
329
     * The values of the array can be one of:
330
     *   - true: Create a single column index on the field named the same as the index.
331
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
332
     *     control over the index.
333
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
334
     * @param array $options Create table options (ENGINE, etc.)
335
     * @param array|bool $extensions List of extensions
336
     */
337
    public function requireTable(
338
        $table,
339
        $fieldSchema = null,
340
        $indexSchema = null,
341
        $hasAutoIncPK = true,
342
        $options = array(),
343
        $extensions = false
344
    ) {
345
        if (!isset($this->tableList[strtolower($table)])) {
346
            $this->transCreateTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type boolean; however, parameter $advanced_options of SilverStripe\ORM\Connect...ger::transCreateTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

346
            $this->transCreateTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
347
            $this->alterationMessage("Table $table: created", "created");
348
        } else {
349
            if (Config::inst()->get(static::class, 'fix_table_case_on_build')) {
350
                $this->fixTableCase($table);
351
            }
352
            if (Config::inst()->get(static::class, 'check_and_repair_on_build')) {
353
                $this->checkAndRepairTable($table);
354
            }
355
356
            // Check if options changed
357
            $tableOptionsChanged = false;
358
            // Check for DB constant on the schema class
359
            $dbIDName = sprintf('%s::ID', static::class);
360
            $dbID = defined($dbIDName) ? constant($dbIDName) : null;
361
            if ($dbID && isset($options[$dbID])) {
362
                if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
363
                    $alteredEngine = $alteredEngineMatches[1];
364
                    $tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
365
                    $tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
366
                }
367
            }
368
369
            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions($extensions))) {
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type true; however, parameter $extensions of SilverStripe\ORM\Connect...e::supportsExtensions() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

369
            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions(/** @scrutinizer ignore-type */ $extensions))) {
Loading history...
370
                $this->transAlterTable($table, $options, $extensions);
0 ignored issues
show
Bug introduced by
It seems like $extensions can also be of type boolean; however, parameter $advanced_options of SilverStripe\ORM\Connect...ager::transAlterTable() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

370
                $this->transAlterTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
371
            }
372
        }
373
374
        //DB ABSTRACTION: we need to convert this to a db-specific version:
375
        if (!isset($fieldSchema['ID'])) {
376
            $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
377
        }
378
379
        // Create custom fields
380
        if ($fieldSchema) {
381
            foreach ($fieldSchema as $fieldName => $fieldSpec) {
382
                //Is this an array field?
383
                $arrayValue = '';
384
                if (strpos($fieldSpec, '[') !== false) {
385
                    //If so, remove it and store that info separately
386
                    $pos = strpos($fieldSpec, '[');
387
                    $arrayValue = substr($fieldSpec, $pos);
388
                    $fieldSpec = substr($fieldSpec, 0, $pos);
389
                }
390
391
                /** @var DBField $fieldObj */
392
                $fieldObj = Injector::inst()->create($fieldSpec, $fieldName);
393
                $fieldObj->setArrayValue($arrayValue);
0 ignored issues
show
Bug introduced by
$arrayValue of type string is incompatible with the type array expected by parameter $value of SilverStripe\ORM\FieldTy...BField::setArrayValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

393
                $fieldObj->setArrayValue(/** @scrutinizer ignore-type */ $arrayValue);
Loading history...
394
395
                $fieldObj->setTable($table);
396
397
                if ($fieldObj instanceof DBPrimaryKey) {
398
                    /** @var DBPrimaryKey $fieldObj */
399
                    $fieldObj->setAutoIncrement($hasAutoIncPK);
400
                }
401
402
                $fieldObj->requireField();
403
            }
404
        }
405
406
        // Create custom indexes
407
        if ($indexSchema) {
408
            foreach ($indexSchema as $indexName => $indexSpec) {
409
                $this->requireIndex($table, $indexName, $indexSpec);
410
            }
411
        }
412
    }
413
414
    /**
415
     * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename).
416
     * @param string $table The table name.
417
     */
418
    public function dontRequireTable($table)
419
    {
420
        if (isset($this->tableList[strtolower($table)])) {
421
            $suffix = '';
422
            while (isset($this->tableList[strtolower("_obsolete_{$table}$suffix")])) {
423
                $suffix = $suffix
424
                        ? ((int)$suffix + 1)
425
                        : 2;
426
            }
427
            $this->renameTable($table, "_obsolete_{$table}$suffix");
428
            $this->alterationMessage("Table $table: renamed to _obsolete_{$table}$suffix", "obsolete");
429
        }
430
    }
431
432
    /**
433
     * Generate the given index in the database, modifying whatever already exists as necessary.
434
     *
435
     * The keys of the array are the names of the index.
436
     * The values of the array can be one of:
437
     *  - true: Create a single column index on the field named the same as the index.
438
     *  - array('type' => 'index|unique|fulltext', 'value' => 'FieldA, FieldB'): This gives you full
439
     *    control over the index.
440
     *
441
     * @param string $table The table name.
442
     * @param string $index The index name.
443
     * @param string|array|boolean $spec The specification of the index in any
444
     * loose format. See requireTable() for more information.
445
     */
446
    public function requireIndex($table, $index, $spec)
447
    {
448
        // Detect if adding to a new table
449
        $newTable = !isset($this->tableList[strtolower($table)]);
450
451
        // Force spec into standard array format
452
        $specString = $this->convertIndexSpec($spec);
453
454
        // Check existing index
455
        $oldSpecString = null;
456
        $indexKey = null;
457
        if (!$newTable) {
458
            $indexKey = $this->indexKey($table, $index, $spec);
459
            $indexList = $this->indexList($table);
460
            if (isset($indexList[$indexKey])) {
461
                // $oldSpec should be in standard array format
462
                $oldSpec = $indexList[$indexKey];
463
                $oldSpecString = $this->convertIndexSpec($oldSpec);
464
            }
465
        }
466
467
        // Initiate either generation or modification of index
468
        if ($newTable || !isset($indexList[$indexKey])) {
469
            // New index
470
            $this->transCreateIndex($table, $index, $spec);
471
            $this->alterationMessage("Index $table.$index: created as $specString", "created");
472
        } elseif ($oldSpecString != $specString) {
473
            // Updated index
474
            $this->transAlterIndex($table, $index, $spec);
475
            $this->alterationMessage(
476
                "Index $table.$index: changed to $specString <i class=\"build-info-before\">(from $oldSpecString)</i>",
477
                "changed"
478
            );
479
        }
480
    }
481
482
    /**
483
     * Splits a spec string safely, considering quoted columns, whitespace,
484
     * and cleaning brackets
485
     *
486
     * @param string $spec The input index specification string
487
     * @return array List of columns in the spec
488
     */
489
    protected function explodeColumnString($spec)
490
    {
491
        // Remove any leading/trailing brackets and outlying modifiers
492
        // E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
493
        $containedSpec = preg_replace('/(.*\(\s*)|(\s*\).*)/', '', $spec);
494
495
        // Split potentially quoted modifiers
496
        // 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...
497
        return preg_split('/"?\s*,\s*"?/', trim($containedSpec, '(") '));
498
    }
499
500
    /**
501
     * Builds a properly quoted column list from an array
502
     *
503
     * @param array $columns List of columns to implode
504
     * @return string A properly quoted list of column names
505
     */
506
    protected function implodeColumnList($columns)
507
    {
508
        if (empty($columns)) {
509
            return '';
510
        }
511
        return '"' . implode('","', $columns) . '"';
512
    }
513
514
    /**
515
     * Given an index specification in the form of a string ensure that each
516
     * column name is property quoted, stripping brackets and modifiers.
517
     * This index may also be in the form of a "CREATE INDEX..." sql fragment
518
     *
519
     * @param string $spec The input specification or query. E.g. 'unique (Column1, Column2)'
520
     * @return string The properly quoted column list. E.g. '"Column1", "Column2"'
521
     */
522
    protected function quoteColumnSpecString($spec)
523
    {
524
        $bits = $this->explodeColumnString($spec);
525
        return $this->implodeColumnList($bits);
526
    }
527
528
    /**
529
     * Given an index spec determines the index type
530
     *
531
     * @param array|string $spec
532
     * @return string
533
     */
534
    protected function determineIndexType($spec)
535
    {
536
        // check array spec
537
        if (is_array($spec) && isset($spec['type'])) {
538
            return $spec['type'];
539
        } elseif (!is_array($spec) && preg_match('/(?<type>\w+)\s*\(/', $spec, $matchType)) {
540
            return strtolower($matchType['type']);
541
        } else {
542
            return 'index';
543
        }
544
    }
545
546
    /**
547
     * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
548
     * and turns it into a proper string.
549
     * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
550
     * arrays to be created. See {@link requireTable()} for details on the index format.
551
     *
552
     * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
553
     * @see parseIndexSpec() for approximate inverse
554
     *
555
     * @param string|array $indexSpec
556
     * @return string
557
     */
558
    protected function convertIndexSpec($indexSpec)
559
    {
560
        // Return already converted spec
561
        if (!is_array($indexSpec)
562
            || !array_key_exists('type', $indexSpec)
563
            || !array_key_exists('columns', $indexSpec)
564
            || !is_array($indexSpec['columns'])
565
            || array_key_exists('value', $indexSpec)
566
        ) {
567
            throw new \InvalidArgumentException(
568
                sprintf(
569
                    'argument to convertIndexSpec must be correct indexSpec, %s given',
570
                    var_export($indexSpec, true)
571
                )
572
            );
573
        }
574
575
        // Combine elements into standard string format
576
        return sprintf('%s (%s)', $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
0 ignored issues
show
Bug introduced by
It seems like $indexSpec['type'] can also be of type array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

576
        return sprintf('%s (%s)', /** @scrutinizer ignore-type */ $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
Loading history...
577
    }
578
579
    /**
580
     * Returns true if the given table is exists in the current database
581
     *
582
     * @param string $tableName Name of table to check
583
     * @return boolean Flag indicating existence of table
584
     */
585
    abstract public function hasTable($tableName);
586
587
    /**
588
     * Return true if the table exists and already has a the field specified
589
     *
590
     * @param string $tableName - The table to check
591
     * @param string $fieldName - The field to check
592
     * @return bool - True if the table exists and the field exists on the table
593
     */
594
    public function hasField($tableName, $fieldName)
595
    {
596
        if (!$this->hasTable($tableName)) {
597
            return false;
598
        }
599
        $fields = $this->fieldList($tableName);
600
        return array_key_exists($fieldName, $fields);
601
    }
602
603
    /**
604
     * Generate the given field on the table, modifying whatever already exists as necessary.
605
     *
606
     * @param string $table The table name.
607
     * @param string $field The field name.
608
     * @param array|string $spec The field specification. If passed in array syntax, the specific database
609
     *  driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
610
     *  be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
611
     *  need to take care of database abstraction in your DBField subclass.
612
     */
613
    public function requireField($table, $field, $spec)
614
    {
615
        //TODO: this is starting to get extremely fragmented.
616
        //There are two different versions of $spec floating around, and their content changes depending
617
        //on how they are structured.  This needs to be tidied up.
618
        $fieldValue = null;
619
        $newTable = false;
620
621
        // backwards compatibility patch for pre 2.4 requireField() calls
622
        $spec_orig = $spec;
623
624
        if (!is_string($spec)) {
625
            $spec['parts']['name'] = $field;
626
            $spec_orig['parts']['name'] = $field;
627
            //Convert the $spec array into a database-specific string
628
            $spec = $this->{$spec['type']}($spec['parts'], true);
629
        }
630
631
        // Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
632
        // collations.
633
        // TODO: move this to the MySQLDatabase file, or drop it altogether?
634
        if (!$this->database->supportsCollations()) {
635
            $spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
636
        }
637
638
        if (!isset($this->tableList[strtolower($table)])) {
639
            $newTable = true;
640
        }
641
642
        if (is_array($spec)) {
643
            $specValue = $this->$spec_orig['type']($spec_orig['parts']);
644
        } else {
645
            $specValue = $spec;
646
        }
647
648
        // We need to get db-specific versions of the ID column:
649
        if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
650
            $specValue = $this->IdColumn(true);
651
        }
652
653
        if (!$newTable) {
654
            $fieldList = $this->fieldList($table);
655
            if (isset($fieldList[$field])) {
656
                if (is_array($fieldList[$field])) {
657
                    $fieldValue = $fieldList[$field]['data_type'];
658
                } else {
659
                    $fieldValue = $fieldList[$field];
660
                }
661
            }
662
        }
663
664
        // Get the version of the field as we would create it. This is used for comparison purposes to see if the
665
        // existing field is different to what we now want
666
        if (is_array($spec_orig)) {
667
            $spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
668
        }
669
670
        if ($newTable || $fieldValue == '') {
671
            $this->transCreateField($table, $field, $spec_orig);
672
            $this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
673
        } elseif ($fieldValue != $specValue) {
674
            // If enums/sets are being modified, then we need to fix existing data in the table.
675
            // Update any records where the enum is set to a legacy value to be set to the default.
676
            foreach (array('enum', 'set') as $enumtype) {
677
                if (preg_match("/^$enumtype/i", $specValue)) {
678
                    $newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
679
                    $new = preg_split("/'\\s*,\\s*'/", $newStr);
680
681
                    $oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
682
                    $old = preg_split("/'\\s*,\\s*'/", $oldStr);
683
684
                    $holder = array();
685
                    foreach ($old as $check) {
686
                        if (!in_array($check, $new)) {
0 ignored issues
show
Bug introduced by
It seems like $new can also be of type false; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

686
                        if (!in_array($check, /** @scrutinizer ignore-type */ $new)) {
Loading history...
687
                            $holder[] = $check;
688
                        }
689
                    }
690
                    if (count($holder)) {
691
                        $default = explode('default ', $spec_orig);
692
                        $default = $default[1];
693
                        $query = "UPDATE \"$table\" SET $field=$default WHERE $field IN (";
694
                        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...
695
                            $query .= "'{$holder[$i]}', ";
696
                        }
697
                        $query .= "'{$holder[$i]}')";
698
                        $this->query($query);
699
                        $amount = $this->database->affectedRows();
700
                        $this->alterationMessage("Changed $amount rows to default value of field $field"
701
                                . " (Value: $default)");
702
                    }
703
                }
704
            }
705
            $this->transAlterField($table, $field, $spec_orig);
706
            $this->alterationMessage(
707
                "Field $table.$field: changed to $specValue <i class=\"build-info-before\">(from {$fieldValue})</i>",
708
                "changed"
709
            );
710
        }
711
    }
712
713
    /**
714
     * If the given field exists, move it out of the way by renaming it to _obsolete_(fieldname).
715
     *
716
     * @param string $table
717
     * @param string $fieldName
718
     */
719
    public function dontRequireField($table, $fieldName)
720
    {
721
        $fieldList = $this->fieldList($table);
722
        if (array_key_exists($fieldName, $fieldList)) {
723
            $suffix = '';
724
            while (isset($fieldList[strtolower("_obsolete_{$fieldName}$suffix")])) {
725
                $suffix = $suffix
726
                        ? ((int)$suffix + 1)
727
                        : 2;
728
            }
729
            $this->renameField($table, $fieldName, "_obsolete_{$fieldName}$suffix");
730
            $this->alterationMessage(
731
                "Field $table.$fieldName: renamed to $table._obsolete_{$fieldName}$suffix",
732
                "obsolete"
733
            );
734
        }
735
    }
736
737
    /**
738
     * Show a message about database alteration
739
     *
740
     * @param string $message to display
741
     * @param string $type one of [created|changed|repaired|obsolete|deleted|error]
742
     */
743
    public function alterationMessage($message, $type = "")
744
    {
745
        if (!$this->supressOutput) {
746
            if (Director::is_cli()) {
747
                switch ($type) {
748
                    case "created":
749
                    case "changed":
750
                    case "repaired":
751
                        $sign = "+";
752
                        break;
753
                    case "obsolete":
754
                    case "deleted":
755
                        $sign = '-';
756
                        break;
757
                    case "notice":
758
                        $sign = '*';
759
                        break;
760
                    case "error":
761
                        $sign = "!";
762
                        break;
763
                    default:
764
                        $sign = " ";
765
                }
766
                $message = strip_tags($message);
767
                echo "  $sign $message\n";
768
            } else {
769
                switch ($type) {
770
                    case "created":
771
                        $class = "success";
772
                        break;
773
                    case "obsolete":
774
                        $class = "error";
775
                        break;
776
                    case "notice":
777
                        $class = "warning";
778
                        break;
779
                    case "error":
780
                        $class = "error";
781
                        break;
782
                    case "deleted":
783
                        $class = "error";
784
                        break;
785
                    case "changed":
786
                        $class = "info";
787
                        break;
788
                    case "repaired":
789
                        $class = "info";
790
                        break;
791
                    default:
792
                        $class = "";
793
                }
794
                echo "<li class=\"$class\">$message</li>";
795
            }
796
        }
797
    }
798
799
    /**
800
     * This returns the data type for the id column which is the primary key for each table
801
     *
802
     * @param boolean $asDbValue
803
     * @param boolean $hasAutoIncPK
804
     * @return string
805
     */
806
    abstract public function IdColumn($asDbValue = false, $hasAutoIncPK = true);
807
808
    /**
809
     * Checks a table's integrity and repairs it if necessary.
810
     *
811
     * @param string $tableName The name of the table.
812
     * @return boolean Return true if the table has integrity after the method is complete.
813
     */
814
    abstract public function checkAndRepairTable($tableName);
815
816
817
    /**
818
     * Ensure the given table has the correct case
819
     *
820
     * @param string $tableName Name of table in desired case
821
     */
822
    public function fixTableCase($tableName)
823
    {
824
        // Check if table exists
825
        $tables = $this->tableList();
826
        if (!array_key_exists(strtolower($tableName), $tables)) {
827
            return;
828
        }
829
830
        // Check if case differs
831
        $currentName = $tables[strtolower($tableName)];
832
        if ($currentName === $tableName) {
833
            return;
834
        }
835
836
        $this->alterationMessage(
837
            "Table $tableName: renamed from $currentName",
838
            "repaired"
839
        );
840
841
        // Rename via temp table to avoid case-sensitivity issues
842
        $tempTable = "__TEMP__{$tableName}";
843
        $this->renameTable($currentName, $tempTable);
844
        $this->renameTable($tempTable, $tableName);
845
    }
846
847
    /**
848
     * Returns the values of the given enum field
849
     *
850
     * @param string $tableName Name of table to check
851
     * @param string $fieldName name of enum field to check
852
     * @return array List of enum values
853
     */
854
    abstract public function enumValuesForField($tableName, $fieldName);
855
856
857
    /*
858
     * This is a lookup table for data types.
859
     * For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
860
     * So this is a DB-specific list of equivilents.
861
     *
862
     * @param string $type
863
     * @return string
864
     */
865
    abstract public function dbDataType($type);
866
867
    /**
868
     * Retrieves the list of all databases the user has access to
869
     *
870
     * @return array List of database names
871
     */
872
    abstract public function databaseList();
873
874
    /**
875
     * Determine if the database with the specified name exists
876
     *
877
     * @param string $name Name of the database to check for
878
     * @return boolean Flag indicating whether this database exists
879
     */
880
    abstract public function databaseExists($name);
881
882
    /**
883
     * Create a database with the specified name
884
     *
885
     * @param string $name Name of the database to create
886
     * @return boolean True if successful
887
     */
888
    abstract public function createDatabase($name);
889
890
    /**
891
     * Drops a database with the specified name
892
     *
893
     * @param string $name Name of the database to drop
894
     */
895
    abstract public function dropDatabase($name);
896
897
    /**
898
     * Alter an index on a table.
899
     *
900
     * @param string $tableName The name of the table.
901
     * @param string $indexName The name of the index.
902
     * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
903
     * @todo Find out where this is called from - Is it even used? Aren't indexes always dropped and re-added?
904
     */
905
    abstract public function alterIndex($tableName, $indexName, $indexSpec);
906
907
    /**
908
     * Determines the key that should be used to identify this index
909
     * when retrieved from DBSchemaManager->indexList.
910
     * In some connectors this is the database-visible name, in others the
911
     * usercode-visible name.
912
     *
913
     * @param string $table
914
     * @param string $index
915
     * @param array $spec
916
     * @return string Key for this index
917
     */
918
    abstract protected function indexKey($table, $index, $spec);
919
920
    /**
921
     * Return the list of indexes in a table.
922
     *
923
     * @param string $table The table name.
924
     * @return array[array] List of current indexes in the table, each in standard
925
     * array form. The key for this array should be predictable using the indexKey
926
     * method
927
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[array] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
928
    abstract public function indexList($table);
929
930
    /**
931
     * Returns a list of all tables in the database.
932
     * Keys are table names in lower case, values are table names in case that
933
     * database expects.
934
     *
935
     * @return array
936
     */
937
    abstract public function tableList();
938
939
    /**
940
     * Create a new table.
941
     *
942
     * @param string $table The name of the table
943
     * @param array $fields A map of field names to field types
944
     * @param array $indexes A map of indexes
945
     * @param array $options An map of additional options.  The available keys are as follows:
946
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
947
     *   - 'temporary' - If true, then a temporary table will be created
948
     * @param array $advancedOptions Advanced creation options
949
     * @return string The table name generated.  This may be different from the table name, for example with temporary
950
     * tables.
951
     */
952
    abstract public function createTable(
953
        $table,
954
        $fields = null,
955
        $indexes = null,
956
        $options = null,
957
        $advancedOptions = null
958
    );
959
960
    /**
961
     * Alter a table's schema.
962
     *
963
     * @param string $table The name of the table to alter
964
     * @param array $newFields New fields, a map of field name => field schema
965
     * @param array $newIndexes New indexes, a map of index name => index type
966
     * @param array $alteredFields Updated fields, a map of field name => field schema
967
     * @param array $alteredIndexes Updated indexes, a map of index name => index type
968
     * @param array $alteredOptions
969
     * @param array $advancedOptions
970
     */
971
    abstract public function alterTable(
972
        $table,
973
        $newFields = null,
974
        $newIndexes = null,
975
        $alteredFields = null,
976
        $alteredIndexes = null,
977
        $alteredOptions = null,
978
        $advancedOptions = null
979
    );
980
981
    /**
982
     * Rename a table.
983
     *
984
     * @param string $oldTableName The old table name.
985
     * @param string $newTableName The new table name.
986
     */
987
    abstract public function renameTable($oldTableName, $newTableName);
988
989
    /**
990
     * Create a new field on a table.
991
     *
992
     * @param string $table Name of the table.
993
     * @param string $field Name of the field to add.
994
     * @param string $spec The field specification, eg 'INTEGER NOT NULL'
995
     */
996
    abstract public function createField($table, $field, $spec);
997
998
    /**
999
     * Change the database column name of the given field.
1000
     *
1001
     * @param string $tableName The name of the tbale the field is in.
1002
     * @param string $oldName The name of the field to change.
1003
     * @param string $newName The new name of the field
1004
     */
1005
    abstract public function renameField($tableName, $oldName, $newName);
1006
1007
    /**
1008
     * Get a list of all the fields for the given table.
1009
     * Returns a map of field name => field spec.
1010
     *
1011
     * @param string $table The table name.
1012
     * @return array
1013
     */
1014
    abstract public function fieldList($table);
1015
1016
    /**
1017
     *
1018
     * This allows the cached values for a table's field list to be erased.
1019
     * If $tablename is empty, then the whole cache is erased.
1020
     *
1021
     * @param string $tableName
1022
     * @return boolean
1023
     */
1024
    public function clearCachedFieldlist($tableName = null)
1025
    {
1026
        return true;
1027
    }
1028
1029
1030
    /**
1031
     * Returns data type for 'boolean' column
1032
     *
1033
     * @param array $values Contains a tokenised list of info about this data type
1034
     * @return string
1035
     */
1036
    abstract public function boolean($values);
1037
1038
    /**
1039
     * Returns data type for 'date' column
1040
     *
1041
     * @param array $values Contains a tokenised list of info about this data type
1042
     * @return string
1043
     */
1044
    abstract public function date($values);
1045
1046
    /**
1047
     * Returns data type for 'decimal' column
1048
     *
1049
     * @param array $values Contains a tokenised list of info about this data type
1050
     * @return string
1051
     */
1052
    abstract public function decimal($values);
1053
1054
    /**
1055
     * Returns data type for 'set' column
1056
     *
1057
     * @param array $values Contains a tokenised list of info about this data type
1058
     * @return string
1059
     */
1060
    abstract public function enum($values);
1061
1062
    /**
1063
     * Returns data type for 'set' column
1064
     *
1065
     * @param array $values Contains a tokenised list of info about this data type
1066
     * @return string
1067
     */
1068
    abstract public function set($values);
1069
1070
    /**
1071
     * Returns data type for 'float' column
1072
     *
1073
     * @param array $values Contains a tokenised list of info about this data type
1074
     * @return string
1075
     */
1076
    abstract public function float($values);
1077
1078
    /**
1079
     * Returns data type for 'int' column
1080
     *
1081
     * @param array $values Contains a tokenised list of info about this data type
1082
     * @return string
1083
     */
1084
    abstract public function int($values);
1085
1086
    /**
1087
     * Returns data type for 'datetime' column
1088
     *
1089
     * @param array $values Contains a tokenised list of info about this data type
1090
     * @return string
1091
     */
1092
    abstract public function datetime($values);
1093
1094
    /**
1095
     * Returns data type for 'text' column
1096
     *
1097
     * @param array $values Contains a tokenised list of info about this data type
1098
     * @return string
1099
     */
1100
    abstract public function text($values);
1101
1102
    /**
1103
     * Returns data type for 'time' column
1104
     *
1105
     * @param array $values Contains a tokenised list of info about this data type
1106
     * @return string
1107
     */
1108
    abstract public function time($values);
1109
1110
    /**
1111
     * Returns data type for 'varchar' column
1112
     *
1113
     * @param array $values Contains a tokenised list of info about this data type
1114
     * @return string
1115
     */
1116
    abstract public function varchar($values);
1117
1118
    /*
1119
     * Returns data type for 'year' column
1120
     *
1121
     * @param array $values Contains a tokenised list of info about this data type
1122
     * @return string
1123
     */
1124
    abstract public function year($values);
1125
}
1126