Passed
Push — master ( 974192...0c05e5 )
by Thomas
15:02
created

DropUnusedDatabaseObjectsTask::reorderFields()   C

Complexity

Conditions 17
Paths 77

Size

Total Lines 89
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 53
c 1
b 0
f 0
nc 77
nop 2
dl 0
loc 89
rs 5.2166

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 LeKoala\DevToolkit\Tasks;
4
5
use Exception;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\Dev\BuildTask;
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Versioned\Versioned;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Versioned\Versioned was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
11
use LeKoala\DevToolkit\BuildTaskTools;
12
use SilverStripe\Core\Environment;
13
14
/**
15
 * SilverStripe never delete your tables or fields. Be careful if your database has other tables than SilverStripe!
16
 *
17
 * @author lekoala
18
 */
19
class DropUnusedDatabaseObjectsTask extends BuildTask
20
{
21
    use BuildTaskTools;
22
23
    protected $title = "Drop unused database objects";
24
    protected $description = 'Drop unused tables and fields from your db by comparing current database tables with your dataobjects.';
25
    private static $segment = 'DropUnusedDatabaseObjectsTask';
0 ignored issues
show
introduced by
The private property $segment is not used, and could be removed.
Loading history...
26
27
    public function run($request)
28
    {
29
        // This can be very long
30
        Environment::setTimeLimitMax(0);
31
32
        $this->request = $request;
33
34
        $this->addOption("tables", "Clean unused tables", true);
35
        $this->addOption("fields", "Clean unused fields", true);
36
        $this->addOption("reorder", "Reorder fields", true);
37
        $this->addOption("go", "Tick this to proceed", false);
38
39
        $options = $this->askOptions();
40
41
        $tables = $options['tables'];
42
        $fields = $options['fields'];
43
        $reorder = $options['reorder'];
44
        $go = $options['go'];
45
46
        if (!$go) {
47
            echo ('Previewing what this task is about to do.');
48
        } else {
49
            echo ("Let's clean this up!");
50
        }
51
        echo ('<hr/>');
52
        if ($tables) {
53
            $this->removeTables($request, $go);
54
        }
55
        if ($fields) {
56
            $this->removeFields($request, $go);
57
        }
58
        if ($reorder) {
59
            $this->reorderFields($request, $go);
60
        }
61
    }
62
63
    protected function reorderFields($request, $go = false)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

63
    protected function reorderFields(/** @scrutinizer ignore-unused */ $request, $go = false)

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

Loading history...
64
    {
65
        $conn = DB::get_conn();
66
        $schema = DB::get_schema();
67
        $dataObjectSchema = DataObject::getSchema();
68
        $classes = $this->getClassesWithTables();
69
        $tableList = $schema->tableList();
70
71
        $this->message('<h2>Fields order</h2>');
72
73
        foreach ($classes as $class) {
74
            /** @var SilverStripe\ORM\DataObject $singl */
75
            $singl = $class::singleton();
76
            $baseClass = $singl->baseClass();
77
            $table = $dataObjectSchema->tableName($class);
78
            $lcTable = strtolower($table);
79
80
            // It does not exist in the list, no need to worry about
81
            if (!isset($tableList[$lcTable])) {
82
                continue;
83
            }
84
85
            $fields = $dataObjectSchema->databaseFields($class);
86
            $baseFields = $dataObjectSchema->databaseFields($baseClass);
87
88
            $realFields = $fields;
89
            if ($baseClass != $class) {
90
                foreach ($baseFields as $k => $v) {
91
                    if ($k == "ID") {
92
                        continue;
93
                    }
94
                    unset($realFields[$k]);
95
                }
96
97
                // When extending multiple classes it's a mess to track, eg SubsitesVirtualPage
98
                if (isset($realFields['VersionID'])) {
99
                    unset($realFields['VersionID']);
100
                }
101
            }
102
103
            // We must pass the regular table name
104
            $list = $schema->fieldList($table);
105
106
            $fields_keys = array_keys($realFields);
107
            $list_keys = array_keys($list);
108
109
            if (json_encode($fields_keys) == json_encode($list_keys)) {
110
                continue;
111
            }
112
113
            $fieldsThatNeedToMove = [];
114
            foreach ($fields_keys as $k => $v) {
115
                if (!isset($list_keys[$k])) {
116
                    continue; // not sure why
117
                }
118
                if ($list_keys[$k] != $v) {
119
                    $fieldsThatNeedToMove[] = $v;
120
                }
121
            }
122
123
            if ($go) {
124
                $this->message("$table: moving " . implode(", ", $fieldsThatNeedToMove));
125
126
                // $conn->transactionStart();
127
                // fields contains the right order (the one from the codebase)
128
                $after = "first";
129
                foreach ($fields_keys as $k => $v) {
130
                    if (isset($list_keys[$k]) && $list_keys[$k] != $v) {
131
                        $col = $v;
132
                        $def = $list[$v] ?? null;
133
                        if (!$def) {
134
                            // This happens when extending another model
135
                            $this->message("Ignore $v that has no definition", "error");
136
                            continue;
137
                        }
138
                        // you CANNOT combine multiple columns reordering in a single ALTER TABLE statement.
139
                        $sql = "ALTER TABLE `$table` MODIFY `$col` $def $after";
140
                        $this->message($sql);
141
                        try {
142
                            $conn->query($sql);
143
                        } catch (Exception $e) {
144
                            $this->message($e->getMessage(), "error");
145
                        }
146
                    }
147
                    $after = "after $v";
148
                }
149
                // $conn->transactionEnd();
150
            } else {
151
                $this->message("$table: would move " . implode(", ", $fieldsThatNeedToMove));
152
            }
153
        }
154
    }
155
156
    protected function removeFields($request, $go = false)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

156
    protected function removeFields(/** @scrutinizer ignore-unused */ $request, $go = false)

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

Loading history...
157
    {
158
        $conn = DB::get_conn();
0 ignored issues
show
Unused Code introduced by
The assignment to $conn is dead and can be removed.
Loading history...
159
        $schema = DB::get_schema();
160
        $dataObjectSchema = DataObject::getSchema();
161
        $classes = $this->getClassesWithTables();
162
        $tableList = $schema->tableList();
163
164
        $this->message('<h2>Fields</h2>');
165
166
        $empty = true;
167
168
        foreach ($classes as $class) {
169
            /** @var SilverStripe\ORM\DataObject $singl */
170
            $singl = $class::singleton();
171
            $baseClass = $singl->baseClass();
172
            $table = $dataObjectSchema->tableName($baseClass);
173
            $lcTable = strtolower($table);
174
175
            // It does not exist in the list, no need to worry about
176
            if (!isset($tableList[$lcTable])) {
177
                continue;
178
            }
179
            $toDrop = [];
180
181
            $fields = $dataObjectSchema->databaseFields($class);
182
            // We must pass the regular table name
183
            $list = $schema->fieldList($table);
184
            // We can compare DataObject schema with actual schema
185
            foreach ($list as $fieldName => $type) {
186
                /// Never drop ID
187
                if ($fieldName == 'ID') {
188
                    continue;
189
                }
190
                if (!isset($fields[$fieldName])) {
191
                    $toDrop[] = $fieldName;
192
                }
193
            }
194
195
            if (!empty($toDrop)) {
196
                $empty = false;
197
                if ($go) {
198
                    $this->dropColumns($table, $toDrop);
199
                    $this->message("Dropped " . implode(',', $toDrop) . " for $table", "obsolete");
200
                } else {
201
                    $this->message("Would drop " . implode(',', $toDrop) . " for $table", "obsolete");
202
                }
203
            }
204
205
            // Localised fields support
206
            if ($singl->hasExtension("\\TractorCow\\Fluent\\Extension\\FluentExtension")) {
207
                $toDrop = [];
208
                $localeTable = $table . '_Localised';
209
                $localeFields = $singl->getLocalisedFields($baseClass);
210
                $localeList = $schema->fieldList($localeTable);
211
                foreach ($localeList as $fieldName => $type) {
212
                    /// Never drop locale fields
213
                    if (in_array($fieldName, ['ID', 'RecordID', 'Locale'])) {
214
                        continue;
215
                    }
216
                    if (!isset($localeFields[$fieldName])) {
217
                        $toDrop[] = $fieldName;
218
                    }
219
                }
220
                if (!empty($toDrop)) {
221
                    $empty = false;
222
                    if ($go) {
223
                        $this->dropColumns($localeTable, $toDrop);
224
                        $this->message("Dropped " . implode(',', $toDrop) . " for $localeTable", "obsolete");
225
                    } else {
226
                        $this->message("Would drop " . implode(',', $toDrop) . " for $localeTable", "obsolete");
227
                    }
228
                }
229
            }
230
        }
231
232
        if ($empty) {
233
            $this->message("No fields to remove", "repaired");
234
        }
235
    }
236
237
    protected function removeTables($request, $go = false)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

237
    protected function removeTables(/** @scrutinizer ignore-unused */ $request, $go = false)

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

Loading history...
238
    {
239
        $conn = DB::get_conn();
0 ignored issues
show
Unused Code introduced by
The assignment to $conn is dead and can be removed.
Loading history...
240
        $schema = DB::get_schema();
241
        $dataObjectSchema = DataObject::getSchema();
242
        $classes = $this->getClassesWithTables();
243
        $allDataObjects = array_values($this->getValidDataObjects());
244
        $tableList = $schema->tableList();
245
        $tablesToRemove = $tableList;
246
247
        $this->message('<h2>Tables</h2>');
248
249
        foreach ($classes as $class) {
250
            /** @var SilverStripe\ORM\DataObject $singl */
251
            $singl = $class::singleton();
252
            $table = $dataObjectSchema->tableName($class);
253
            $lcTable = strtolower($table);
254
255
            // It does not exist in the list, keep to remove later
256
            if (!isset($tableList[$lcTable])) {
257
                continue;
258
            }
259
260
            self::removeFromArray($lcTable, $tablesToRemove);
261
            // Remove from the list versioned tables
262
            if ($singl->hasExtension(Versioned::class)) {
263
                self::removeFromArray($lcTable . '_live', $tablesToRemove);
264
                self::removeFromArray($lcTable . '_versions', $tablesToRemove);
265
            }
266
            // Remove from the list fluent tables
267
            if ($singl->hasExtension("\\TractorCow\\Fluent\\Extension\\FluentExtension")) {
268
                self::removeFromArray($lcTable . '_localised', $tablesToRemove);
269
                self::removeFromArray($lcTable . '_localised_live', $tablesToRemove);
270
                self::removeFromArray($lcTable . '_localised_versions', $tablesToRemove);
271
            }
272
273
            // Relations
274
            $hasMany = $class::config()->has_many;
275
            if (!empty($hasMany)) {
276
                foreach ($hasMany as $rel => $obj) {
277
                    self::removeFromArray($lcTable . '_' . strtolower($rel), $tablesToRemove);
278
                }
279
            }
280
            // We catch relations without own classes later on
281
            $manyMany = $class::config()->many_many;
282
            if (!empty($manyMany)) {
283
                foreach ($manyMany as $rel => $obj) {
284
                    self::removeFromArray($lcTable . '_' . strtolower($rel), $tablesToRemove);
285
                }
286
            }
287
        }
288
289
        //at this point, we should only have orphans table in dbTables var
290
        foreach ($tablesToRemove as $lcTable => $table) {
291
            // Remove many_many tables without own base table
292
            if (strpos($table, '_') !== false) {
293
                $parts = explode('_', $table);
294
                $potentialClass = $parts[0];
295
                $potentialRelation = $parts[1];
296
                foreach ($allDataObjects as $dataObjectClass) {
297
                    $classParts = explode('\\', $dataObjectClass);
298
                    $tableClass = end($classParts);
299
                    if ($tableClass == $potentialClass) {
300
                        $manyManyRelations = $dataObjectClass::config()->many_many;
301
                        if (isset($manyManyRelations[$potentialRelation])) {
302
                            unset($tablesToRemove[$lcTable]);
303
                            continue 2;
304
                        }
305
                    }
306
                }
307
            }
308
            if ($go) {
309
                DB::query('DROP TABLE `' . $table . '`');
310
                $this->message("Dropped $table", 'obsolete');
311
            } else {
312
                $this->message("Would drop $table", 'obsolete');
313
            }
314
        }
315
316
        if (empty($tablesToRemove)) {
317
            $this->message("No table to remove", "repaired");
318
        }
319
    }
320
321
    /**
322
     * @return array
323
     */
324
    protected function getClassesWithTables()
325
    {
326
        return ClassInfo::dataClassesFor(DataObject::class);
327
    }
328
329
    public static function removeFromArray($val, &$arr)
330
    {
331
        if (isset($arr[$val])) {
332
            unset($arr[$val]);
333
        }
334
    }
335
336
    public function dropColumns($table, $columns)
337
    {
338
        switch (get_class(DB::get_conn())) {
339
            case \SilverStripe\SQLite\SQLite3Database::class:
0 ignored issues
show
Bug introduced by
The type SilverStripe\SQLite\SQLite3Database was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
340
            case 'SQLite3Database':
341
                $this->sqlLiteDropColumns($table, $columns);
342
                break;
343
            default:
344
                $this->sqlDropColumns($table, $columns);
345
                break;
346
        }
347
    }
348
349
    public function sqlDropColumns($table, $columns)
350
    {
351
        DB::query("ALTER TABLE \"$table\" DROP \"" . implode('", DROP "', $columns) . "\"");
352
    }
353
354
    public function sqlLiteDropColumns($table, $columns)
355
    {
356
        $newColsSpec = $newCols = [];
357
        foreach (DataObject::getSchema()->databaseFields($table) as $name => $spec) {
358
            if (in_array($name, $columns)) {
359
                continue;
360
            }
361
            $newColsSpec[] = "\"$name\" $spec";
362
            $newCols[] = "\"$name\"";
363
        }
364
365
        $queries = [
366
            "BEGIN TRANSACTION",
367
            "CREATE TABLE \"{$table}_cleanup\" (" . implode(',', $newColsSpec) . ")",
368
            "INSERT INTO \"{$table}_cleanup\" SELECT " . implode(',', $newCols) . " FROM \"$table\"",
369
            "DROP TABLE \"$table\"",
370
            "ALTER TABLE \"{$table}_cleanup\" RENAME TO \"{$table}\"",
371
            "COMMIT"
372
        ];
373
374
        foreach ($queries as $query) {
375
            DB::query($query . ';');
376
        }
377
    }
378
}
379