Completed
Push — 4.0 ( 9aadd8...6be554 )
by Damian
33s
created

DBSchemaManager::showTableNameWarning()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
     * @var array
55
     */
56
    protected static $table_name_warnings = [];
57
58
    /**
59
     * @param string
60
     * @deprecated 4.0..5.0
61
     */
62
    public static function showTableNameWarning($table, $class)
63
    {
64
        static::$table_name_warnings[$table] = $class;
65
    }
66
67
    /**
68
     * Injector injection point for database controller
69
     *
70
     * @param Database $database
71
     */
72
    public function setDatabase(Database $database)
73
    {
74
        $this->database = $database;
75
    }
76
77
    /**
78
     * The table list, generated by the tableList() function.
79
     * Used by the requireTable() function.
80
     *
81
     * @var array
82
     */
83
    protected $tableList;
84
85
    /**
86
     * Keeps track whether we are currently updating the schema.
87
     *
88
     * @var boolean
89
     */
90
    protected $schemaIsUpdating = false;
91
92
    /**
93
     * Large array structure that represents a schema update transaction
94
     *
95
     * @var array
96
     */
97
    protected $schemaUpdateTransaction;
98
99
    /**
100
     * Enable supression of database messages.
101
     */
102
    public function quiet()
103
    {
104
        $this->supressOutput = true;
105
    }
106
107
    /**
108
     * Execute the given SQL query.
109
     * This abstract function must be defined by subclasses as part of the actual implementation.
110
     * It should return a subclass of SS_Query as the result.
111
     *
112
     * @param string $sql The SQL query to execute
113
     * @param int $errorLevel The level of error reporting to enable for the query
114
     * @return Query
115
     */
116
    public function query($sql, $errorLevel = E_USER_ERROR)
117
    {
118
        return $this->database->query($sql, $errorLevel);
119
    }
120
121
122
    /**
123
     * Execute the given SQL parameterised query with the specified arguments
124
     *
125
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
126
     * @param array $parameters An ordered list of arguments.
127
     * @param int $errorLevel The level of error reporting to enable for the query
128
     * @return Query
129
     */
130
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
131
    {
132
        return $this->database->preparedQuery($sql, $parameters, $errorLevel);
133
    }
134
135
    /**
136
     * Initiates a schema update within a single callback
137
     *
138
     * @param callable $callback
139
     */
140
    public function schemaUpdate($callback)
141
    {
142
        // Begin schema update
143
        $this->schemaIsUpdating = true;
144
145
        // Update table list
146
        $this->tableList = array();
147
        $tables = $this->tableList();
148
        foreach ($tables as $table) {
149
            $this->tableList[strtolower($table)] = $table;
150
        }
151
152
        // Clear update list for client code to mess around with
153
        $this->schemaUpdateTransaction = array();
154
155
        /** @var Exception $error */
156
        $error = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $error is dead and can be removed.
Loading history...
157
        try {
158
            // Yield control to client code
159
            $callback();
160
161
            // If the client code has cancelled the update then abort
162
            if (!$this->isSchemaUpdating()) {
163
                return;
164
            }
165
166
            // End schema update
167
            foreach ($this->schemaUpdateTransaction as $tableName => $changes) {
168
                $advancedOptions = isset($changes['advancedOptions']) ? $changes['advancedOptions'] : null;
169
                switch ($changes['command']) {
170
                    case 'create':
171
                        $this->createTable(
172
                            $tableName,
173
                            $changes['newFields'],
174
                            $changes['newIndexes'],
175
                            $changes['options'],
176
                            $advancedOptions
177
                        );
178
                        break;
179
180
                    case 'alter':
181
                        $this->alterTable(
182
                            $tableName,
183
                            $changes['newFields'],
184
                            $changes['newIndexes'],
185
                            $changes['alteredFields'],
186
                            $changes['alteredIndexes'],
187
                            $changes['alteredOptions'],
188
                            $advancedOptions
189
                        );
190
                        break;
191
                }
192
            }
193
        } finally {
194
            $this->schemaUpdateTransaction = null;
195
            $this->schemaIsUpdating = false;
196
        }
197
    }
198
199
    /**
200
     * Cancels the schema updates requested during (but not after) schemaUpdate() call.
201
     */
202
    public function cancelSchemaUpdate()
203
    {
204
        $this->schemaUpdateTransaction = null;
205
        $this->schemaIsUpdating = false;
206
    }
207
208
    /**
209
     * Returns true if we are during a schema update.
210
     *
211
     * @return boolean
212
     */
213
    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...
214
    {
215
        return $this->schemaIsUpdating;
216
    }
217
218
    /**
219
     * Returns true if schema modifications were requested during (but not after) schemaUpdate() call.
220
     *
221
     * @return boolean
222
     */
223
    public function doesSchemaNeedUpdating()
224
    {
225
        return (bool) $this->schemaUpdateTransaction;
226
    }
227
228
    // Transactional schema altering functions - they don't do anything except for update schemaUpdateTransaction
229
230
    /**
231
     * Instruct the schema manager to record a table creation to later execute
232
     *
233
     * @param string $table Name of the table
234
     * @param array $options Create table options (ENGINE, etc.)
235
     * @param array $advanced_options Advanced table creation options
236
     */
237
    public function transCreateTable($table, $options = null, $advanced_options = null)
238
    {
239
        $this->schemaUpdateTransaction[$table] = array(
240
            'command' => 'create',
241
            'newFields' => array(),
242
            'newIndexes' => array(),
243
            'options' => $options,
244
            'advancedOptions' => $advanced_options
245
        );
246
    }
247
248
    /**
249
     * Instruct the schema manager to record a table alteration to later execute
250
     *
251
     * @param string $table Name of the table
252
     * @param array $options Create table options (ENGINE, etc.)
253
     * @param array $advanced_options Advanced table creation options
254
     */
255
    public function transAlterTable($table, $options, $advanced_options)
256
    {
257
        $this->transInitTable($table);
258
        $this->schemaUpdateTransaction[$table]['alteredOptions'] = $options;
259
        $this->schemaUpdateTransaction[$table]['advancedOptions'] = $advanced_options;
260
    }
261
262
    /**
263
     * Instruct the schema manager to record a field to be later created
264
     *
265
     * @param string $table Name of the table to hold this field
266
     * @param string $field Name of the field to create
267
     * @param string $schema Field specification as a string
268
     */
269
    public function transCreateField($table, $field, $schema)
270
    {
271
        $this->transInitTable($table);
272
        $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema;
273
    }
274
275
    /**
276
     * Instruct the schema manager to record an index to be later created
277
     *
278
     * @param string $table Name of the table to hold this index
279
     * @param string $index Name of the index to create
280
     * @param array $schema Already parsed index specification
281
     */
282
    public function transCreateIndex($table, $index, $schema)
283
    {
284
        $this->transInitTable($table);
285
        $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema;
286
    }
287
288
    /**
289
     * Instruct the schema manager to record a field to be later updated
290
     *
291
     * @param string $table Name of the table to hold this field
292
     * @param string $field Name of the field to update
293
     * @param string $schema Field specification as a string
294
     */
295
    public function transAlterField($table, $field, $schema)
296
    {
297
        $this->transInitTable($table);
298
        $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema;
299
    }
300
301
    /**
302
     * Instruct the schema manager to record an index to be later updated
303
     *
304
     * @param string $table Name of the table to hold this index
305
     * @param string $index Name of the index to update
306
     * @param array $schema Already parsed index specification
307
     */
308
    public function transAlterIndex($table, $index, $schema)
309
    {
310
        $this->transInitTable($table);
311
        $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema;
312
    }
313
314
    /**
315
     * Handler for the other transXXX methods - mark the given table as being altered
316
     * if it doesn't already exist
317
     *
318
     * @param string $table Name of the table to initialise
319
     */
320
    protected function transInitTable($table)
321
    {
322
        if (!isset($this->schemaUpdateTransaction[$table])) {
323
            $this->schemaUpdateTransaction[$table] = array(
324
                'command' => 'alter',
325
                'newFields' => array(),
326
                'newIndexes' => array(),
327
                'alteredFields' => array(),
328
                'alteredIndexes' => array(),
329
                'alteredOptions' => ''
330
            );
331
        }
332
    }
333
334
    /**
335
     * Generate the following table in the database, modifying whatever already exists
336
     * as necessary.
337
     *
338
     * @todo Change detection for CREATE TABLE $options other than "Engine"
339
     *
340
     * @param string $table The name of the table
341
     * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db
342
     * @param array $indexSchema A list of indexes to create. See {@link requireIndex()}
343
     * The values of the array can be one of:
344
     *   - true: Create a single column index on the field named the same as the index.
345
     *   - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full
346
     *     control over the index.
347
     * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type
348
     * @param array $options Create table options (ENGINE, etc.)
349
     * @param array|bool $extensions List of extensions
350
     */
351
    public function requireTable(
352
        $table,
353
        $fieldSchema = null,
354
        $indexSchema = null,
355
        $hasAutoIncPK = true,
356
        $options = array(),
357
        $extensions = false
358
    ) {
359
        if (!isset($this->tableList[strtolower($table)])) {
360
            $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

360
            $this->transCreateTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
361
            $this->alterationMessage("Table $table: created", "created");
362
        } else {
363
            if (Config::inst()->get(static::class, 'fix_table_case_on_build')) {
364
                $this->fixTableCase($table);
365
            }
366
            if (Config::inst()->get(static::class, 'check_and_repair_on_build')) {
367
                $this->checkAndRepairTable($table);
368
            }
369
370
            // Check if options changed
371
            $tableOptionsChanged = false;
372
            // Check for DB constant on the schema class
373
            $dbIDName = sprintf('%s::ID', static::class);
374
            $dbID = defined($dbIDName) ? constant($dbIDName) : null;
375
            if ($dbID && isset($options[$dbID])) {
376
                if (preg_match('/ENGINE=([^\s]*)/', $options[$dbID], $alteredEngineMatches)) {
377
                    $alteredEngine = $alteredEngineMatches[1];
378
                    $tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first();
379
                    $tableOptionsChanged = ($tableStatus['Engine'] != $alteredEngine);
380
                }
381
            }
382
383
            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

383
            if ($tableOptionsChanged || ($extensions && $this->database->supportsExtensions(/** @scrutinizer ignore-type */ $extensions))) {
Loading history...
384
                $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

384
                $this->transAlterTable($table, $options, /** @scrutinizer ignore-type */ $extensions);
Loading history...
385
            }
386
        }
387
388
        //DB ABSTRACTION: we need to convert this to a db-specific version:
389
        if (!isset($fieldSchema['ID'])) {
390
            $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK));
391
        }
392
393
        // Create custom fields
394
        if ($fieldSchema) {
395
            foreach ($fieldSchema as $fieldName => $fieldSpec) {
396
                //Is this an array field?
397
                $arrayValue = '';
398
                if (strpos($fieldSpec, '[') !== false) {
399
                    //If so, remove it and store that info separately
400
                    $pos = strpos($fieldSpec, '[');
401
                    $arrayValue = substr($fieldSpec, $pos);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type false; however, parameter $start of substr() does only seem to accept integer, 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

401
                    $arrayValue = substr($fieldSpec, /** @scrutinizer ignore-type */ $pos);
Loading history...
402
                    $fieldSpec = substr($fieldSpec, 0, $pos);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type false; however, parameter $length of substr() does only seem to accept integer, 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

402
                    $fieldSpec = substr($fieldSpec, 0, /** @scrutinizer ignore-type */ $pos);
Loading history...
403
                }
404
405
                /** @var DBField $fieldObj */
406
                $fieldObj = Injector::inst()->create($fieldSpec, $fieldName);
407
                $fieldObj->setArrayValue($arrayValue);
0 ignored issues
show
Bug introduced by
$arrayValue of type false|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

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

611
        return sprintf('%s (%s)', /** @scrutinizer ignore-type */ $indexSpec['type'], $this->implodeColumnList($indexSpec['columns']));
Loading history...
612
    }
613
614
    /**
615
     * Returns true if the given table is exists in the current database
616
     *
617
     * @param string $tableName Name of table to check
618
     * @return boolean Flag indicating existence of table
619
     */
620
    abstract public function hasTable($tableName);
621
622
    /**
623
     * Return true if the table exists and already has a the field specified
624
     *
625
     * @param string $tableName - The table to check
626
     * @param string $fieldName - The field to check
627
     * @return bool - True if the table exists and the field exists on the table
628
     */
629
    public function hasField($tableName, $fieldName)
630
    {
631
        if (!$this->hasTable($tableName)) {
632
            return false;
633
        }
634
        $fields = $this->fieldList($tableName);
635
        return array_key_exists($fieldName, $fields);
636
    }
637
638
    /**
639
     * Generate the given field on the table, modifying whatever already exists as necessary.
640
     *
641
     * @param string $table The table name.
642
     * @param string $field The field name.
643
     * @param array|string $spec The field specification. If passed in array syntax, the specific database
644
     *  driver takes care of the ALTER TABLE syntax. If passed as a string, its assumed to
645
     *  be prepared as a direct SQL framgment ready for insertion into ALTER TABLE. In this case you'll
646
     *  need to take care of database abstraction in your DBField subclass.
647
     */
648
    public function requireField($table, $field, $spec)
649
    {
650
        //TODO: this is starting to get extremely fragmented.
651
        //There are two different versions of $spec floating around, and their content changes depending
652
        //on how they are structured.  This needs to be tidied up.
653
        $fieldValue = null;
654
        $newTable = false;
655
656
        // backwards compatibility patch for pre 2.4 requireField() calls
657
        $spec_orig = $spec;
658
659
        if (!is_string($spec)) {
660
            $spec['parts']['name'] = $field;
661
            $spec_orig['parts']['name'] = $field;
662
            //Convert the $spec array into a database-specific string
663
            $spec = $this->{$spec['type']}($spec['parts'], true);
664
        }
665
666
        // Collations didn't come in until MySQL 4.1.  Anything earlier will throw a syntax error if you try and use
667
        // collations.
668
        // TODO: move this to the MySQLDatabase file, or drop it altogether?
669
        if (!$this->database->supportsCollations()) {
670
            $spec = preg_replace('/ *character set [^ ]+( collate [^ ]+)?( |$)/', '\\2', $spec);
671
        }
672
673
        if (!isset($this->tableList[strtolower($table)])) {
674
            $newTable = true;
675
        }
676
677
        if (is_array($spec)) {
678
            $specValue = $this->$spec_orig['type']($spec_orig['parts']);
679
        } else {
680
            $specValue = $spec;
681
        }
682
683
        // We need to get db-specific versions of the ID column:
684
        if ($spec_orig == $this->IdColumn() || $spec_orig == $this->IdColumn(true)) {
685
            $specValue = $this->IdColumn(true);
686
        }
687
688
        if (!$newTable) {
689
            $fieldList = $this->fieldList($table);
690
            if (isset($fieldList[$field])) {
691
                if (is_array($fieldList[$field])) {
692
                    $fieldValue = $fieldList[$field]['data_type'];
693
                } else {
694
                    $fieldValue = $fieldList[$field];
695
                }
696
            }
697
        }
698
699
        // Get the version of the field as we would create it. This is used for comparison purposes to see if the
700
        // existing field is different to what we now want
701
        if (is_array($spec_orig)) {
702
            $spec_orig = $this->{$spec_orig['type']}($spec_orig['parts']);
703
        }
704
705
        if ($newTable || $fieldValue == '') {
706
            $this->transCreateField($table, $field, $spec_orig);
707
            $this->alterationMessage("Field $table.$field: created as $spec_orig", "created");
708
        } elseif ($fieldValue != $specValue) {
709
            // If enums/sets are being modified, then we need to fix existing data in the table.
710
            // Update any records where the enum is set to a legacy value to be set to the default.
711
            foreach (array('enum', 'set') as $enumtype) {
712
                if (preg_match("/^$enumtype/i", $specValue)) {
713
                    $newStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $spec_orig);
714
                    $new = preg_split("/'\\s*,\\s*'/", $newStr);
715
716
                    $oldStr = preg_replace("/(^$enumtype\\s*\\(')|('\\).*)/i", "", $fieldValue);
717
                    $old = preg_split("/'\\s*,\\s*'/", $oldStr);
718
719
                    $holder = array();
720
                    foreach ($old as $check) {
721
                        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

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