CheckClassNames   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Importance

Changes 17
Bugs 5 Features 0
Metric Value
wmc 56
eloc 198
c 17
b 5
f 0
dl 0
loc 316
rs 5.5199

4 Methods

Rating   Name   Duplication   Size   Complexity  
C performMigration() 0 71 16
B bestClassName() 0 40 7
A fixFieldSize() 0 4 1
F fixClassNames() 0 145 32

How to fix   Complexity   

Complex Class

Complex classes like CheckClassNames often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CheckClassNames, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Sunnysideup\MigrateData\Tasks;
4
5
use Page;
0 ignored issues
show
Bug introduced by
The type Page 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...
6
use SilverStripe\CMS\Model\SiteTree;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTree 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...
7
use SilverStripe\CMS\Model\SiteTreeLink;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Model\SiteTreeLink 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...
8
use SilverStripe\Core\ClassInfo;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataObjectSchema;
13
use SilverStripe\ORM\DB;
14
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...
15
16
class CheckClassNames extends MigrateDataTaskBase
17
{
18
    protected $title = 'Check all tables for valid class names';
19
20
    protected $description = 'Check all tables for valid class names';
21
22
    protected $enabled = true;
23
24
    protected $listOfAllClasses = [];
25
26
    protected $countsOfAllClasses = [];
27
28
    protected $dbTablesPresent = [];
29
30
    protected $fixErrors = true;
31
32
    protected $extendFieldSize = true;
33
34
    protected $forReal = true;
35
36
    protected $dataObjectSchema;
37
38
    protected $onlyRunFor = [];
39
40
    protected $bestClassNameStore = [];
41
42
    /**
43
     * example:
44
     *     [
45
     *         ClassName => [
46
     *             FieldA,
47
     *             FieldB,
48
     *     ].
49
     *
50
     * @var array
51
     */
52
    private static $other_fields_to_check = [
53
        '\\DNADesign\\\Elemental\\Models\\ElementalArea' => [
54
            'OwnerClassName',
55
        ],
56
        SiteTreeLink::class => [
57
            'ParentClass',
58
        ],
59
    ];
60
61
    protected function performMigration()
62
    {
63
        $this->dataObjectSchema = Injector::inst()->get(DataObjectSchema::class);
64
65
        //get tables in DB
66
        $this->dbTablesPresent = [];
67
        $rows = DB::query('SHOW tables');
68
        foreach ($rows as $row) {
69
            $table = array_pop($row);
70
            $this->dbTablesPresent[$table] = $table;
71
        }
72
        // make a list of all classes
73
        // include baseclass = false
74
        $objectClassNames = ClassInfo::subclassesFor(DataObject::class, false);
75
        foreach ($objectClassNames as $objectClassName) {
76
            $slashed = addslashes($objectClassName);
77
            $this->listOfAllClasses[$slashed] = ClassInfo::shortName($objectClassName);
78
        }
79
        $this->countsOfAllClasses = array_count_values($this->listOfAllClasses);
80
        $allOK = true;
81
82
        //check all classes
83
        foreach ($objectClassNames as $objectClassName) {
84
            if (count($this->onlyRunFor) && ! in_array($objectClassName, $this->onlyRunFor, true)) {
85
                continue;
86
            }
87
            $fields = $this->dataObjectSchema->databaseFields($objectClassName, false);
88
            if (count($fields) > 0) {
89
                $tableName = $this->dataObjectSchema->tableName($objectClassName);
90
                $this->flushNow('');
91
                $this->flushNowLine();
92
                $this->flushNow('Checking ' . $objectClassName . ' => ' . $tableName);
93
                $this->flushNowLine();
94
                $tableNameStaticValue = Config::inst()->get($objectClassName, 'table_name');
95
                if ($tableNameStaticValue !== $tableName && 'Page' !== $objectClassName) {
96
                    $this->flushNow('... ' . $objectClassName . ' POTENTIALLY has a table with a full class name: ' . $tableName . ' it is recommended that you set the private static table_name', 'error');
97
                    $allOK = false;
98
                }
99
                if (! $tableName) {
100
                    $this->flushNow('... Can not find: ' . $objectClassName . '.table_name in code ', 'error');
101
                    $allOK = false;
102
                } elseif ($this->tableExists($tableName)) {
103
                    // NB. we still run for zero rows, because we may need to fix versioned records
104
                    $count = DB::query('SELECT COUNT("ID") FROM "' . $tableName . '"')->value();
105
                    $this->flushNow('... ' . $count . ' rows');
106
                    $allFields = [
107
                        'ClassName',
108
                    ];
109
                    $moreFields = $this->Config()->other_fields_to_check;
110
                    if (isset($moreFields[$objectClassName])) {
111
                        foreach ($moreFields[$objectClassName] as $additionalField) {
112
                            $allFields[] = $additionalField;
113
                        }
114
                    }
115
                    foreach ($allFields as $fieldName) {
116
                        if ($this->fieldExists($tableName, $fieldName)) {
117
                            $this->fixClassNames($tableName, $objectClassName, $fieldName);
118
                        } else {
119
                            $this->flushNow('... Can not find: ' . $tableName . '.' . $fieldName . ' in database.');
120
                        }
121
                    }
122
                } else {
123
                    $this->flushNow('... Can not find: ' . $tableName . ' in database.', 'error');
124
                }
125
            } else {
126
                $this->flushNow('... No table needed');
127
            }
128
            if ($allOK) {
129
                $this->flushNow('... OK', 'created');
130
            } else {
131
                $this->flushNow('... ERRORS', 'error');
132
            }
133
        }
134
    }
135
136
    protected function fixClassNames(string $tableName, string $objectClassName, ?string $fieldName = 'ClassName', ?bool $versionedTable = false)
137
    {
138
        $this->flushNow('... CHECKING ' . $tableName . '.' . $fieldName . ' ...');
139
        $count = DB::query('SELECT COUNT("ID") FROM "' . $tableName . '"')->value();
140
        $where = '"' . $fieldName . '" NOT IN (\'' . implode("', '", array_keys($this->listOfAllClasses)) . "')";
141
        $whereA = $where . ' AND ' . '(' . '"' . $fieldName . '" IS NULL OR "' . $fieldName . '" = \'\' )';
142
        $whereB = $where . ' AND NOT ' . '(' . '"' . $fieldName . '" IS NULL OR "' . $fieldName . '" = \'\' )';
143
        $rowsToFix = DB::query('SELECT COUNT("ID") FROM "' . $tableName . '" WHERE ' . $where)->value();
144
        $rowsToFixA = DB::query('SELECT COUNT("ID") FROM "' . $tableName . '" WHERE ' . $whereA)->value();
145
        $rowsToFixB = DB::query('SELECT COUNT("ID") FROM "' . $tableName . '" WHERE ' . $whereB)->value();
146
        if ($rowsToFix > 0) {
147
            if ($count === $rowsToFix) {
148
                $this->flushNow('... All rows ' . $count . ' in table ' . $tableName . ' are broken: ', 'error');
149
            } else {
150
                $this->flushNow('... ' . $rowsToFix . ' errors in "' . $fieldName . '" values:');
151
                if ($rowsToFixA) {
152
                    $this->flushNow('... ... ' . $rowsToFixA . ' in table ' . $tableName . ' do not have a ' . $fieldName . ' at all and ', 'error');
153
                }
154
                if ($rowsToFixB) {
155
                    $this->flushNow('... ... ' . $rowsToFixB . ' in table ' . $tableName . ' have a bad ' . $fieldName . '');
156
                }
157
            }
158
            if ($this->fixErrors) {
159
                if ($this->extendFieldSize) {
160
                    $this->fixFieldSize($tableName);
161
                }
162
                //work out if we can set it to the long form of a short ClassName
163
                $rows = DB::query(
164
                    '
165
                    SELECT "' . $fieldName . '", COUNT("ID") AS C
166
                    FROM "' . $tableName . '"
167
                    GROUP BY "' . $fieldName . '"
168
                    HAVING ' . $where . '
169
                    ORDER BY C DESC'
170
                );
171
                foreach ($rows as $row) {
172
                    if (! $row[$fieldName]) {
173
                        $row[$fieldName] = '--- NO VALUE ---';
174
                    }
175
                    $this->flushNow('... ... ' . $row['C'] . ' ' . $row[$fieldName]);
176
                    if (isset($this->countsOfAllClasses[$row[$fieldName]])) {
177
                        if (1 === $this->countsOfAllClasses[$row[$fieldName]]) {
178
                            $longNameAlreadySlashed = array_search($row[$fieldName], $this->listOfAllClasses, true);
179
                            if ($longNameAlreadySlashed) {
180
                                $this->flushNow('... ... ... Updating ' . $row[$fieldName] . ' to ' . $longNameAlreadySlashed . ' - based in short to long mapping of the ' . $fieldName . ' field. ', 'created');
181
                                if ($this->forReal) {
182
                                    $this->runUpdateQuery(
183
                                        '
184
                                        UPDATE "' . $tableName . '"
185
                                        SET "' . $tableName . '"."' . $fieldName . '" = \'' . $longNameAlreadySlashed . '\'
186
                                        WHERE "' . $fieldName . '" = \'' . $row[$fieldName] . "'",
187
                                        2
188
                                    );
189
                                }
190
                            }
191
                        }
192
                    }
193
                }
194
195
                //only try to work out what is going on when it is a ClassName Field!
196
                if ('ClassName' === $fieldName) {
197
                    $options = ClassInfo::subclassesFor($objectClassName);
198
                    $checkTables = [];
199
                    foreach ($options as $key => $optionClassName) {
200
                        if ($optionClassName !== $objectClassName) {
201
                            $optionTableName = $this->dataObjectSchema->tableName($objectClassName);
202
                            if (! $this->tableExists($optionTableName) || $optionTableName === $tableName) {
203
                                unset($options[$key]);
204
                            } else {
205
                                $checkTables[$optionClassName] = $optionTableName;
206
                            }
207
                        }
208
                    }
209
                    //fix bad rows....
210
                    $rows = DB::query('SELECT "ID", "' . $fieldName . '" FROM "' . $tableName . '" WHERE ' . $where);
211
                    foreach ($rows as $row) {
212
                        //check if it is the short name ...
213
                        $optionCount = 0;
214
                        $matchedClassName = '';
215
                        foreach ($checkTables as $optionClassName => $optionTableName) {
216
                            $hasMatch = DB::query('
217
                                    SELECT COUNT("' . $tableName . '"."ID")
218
                                    FROM "' . $tableName . '"
219
                                        INNER JOIN "' . $optionTableName . '"
220
                                            ON "' . $optionTableName . '"."ID" = "' . $tableName . '"."ID"
221
                                    WHERE "' . $tableName . '"."ID" = ' . $row['ID'])->value();
222
                            if (1 === $hasMatch) {
223
                                ++$optionCount;
224
                                $matchedClassName = $optionClassName;
225
                                if ($optionCount > 1) {
226
                                    break;
227
                                }
228
                            }
229
                        }
230
                        if (0 === $optionCount) {
231
                            if (! $row[$fieldName]) {
232
                                $row[$fieldName] = '--- NO VALUE ---';
233
                            }
234
                            $this->flushNow('... Updating ' . $fieldName . ' to ' . $objectClassName . ' for ID = ' . $row['ID'] . ' from ' . $fieldName . ' = ' . $row[$fieldName] . ' - based on inability to find matching IDs in any child class tables', 'created');
235
                            if ($this->forReal) {
236
                                $this->runUpdateQuery(
237
                                    '
238
                                    UPDATE "' . $tableName . '"
239
                                    SET "' . $tableName . '"."' . $fieldName . '" = \'' . addslashes($objectClassName) . '\'
240
                                    WHERE ID = ' . $row['ID'],
241
                                    2
242
                                );
243
                            }
244
                        } elseif (1 === $optionCount && $matchedClassName) {
245
                            $this->flushNow('... Updating ' . $fieldName . ' to ' . $matchedClassName . ' ID = ' . $row['ID'] . ', ' . $fieldName . ' = ' . $row[$fieldName] . ' - based on matching row in exactly one child class table', 'created');
246
                            if ($this->forReal) {
247
                                $this->runUpdateQuery(
248
                                    'UPDATE "' . $tableName . '"
249
                                    SET "' . $tableName . '"."' . $fieldName . '" = \'' . addslashes($matchedClassName) . '\'
250
                                    WHERE ID = ' . $row['ID'],
251
                                    2
252
                                );
253
                            }
254
                        } else {
255
                            $bestValue = $this->bestClassName($objectClassName, $tableName, $fieldName);
256
                            $this->flushNow('... ERROR: can not find best ' . $fieldName . ' for ' . $tableName . '.ID = ' . $row['ID'] . ' current value: ' . $row[$fieldName] . ' we recommend: ' . $bestValue, 'error');
257
                            $this->runUpdateQuery(
258
                                'UPDATE "' . $tableName . '"
259
                                SET "' . $tableName . '"."' . $fieldName . '" = \'' . addslashes($bestValue) . '\'
260
                                WHERE ID = ' . $row['ID'],
261
                                2
262
                            );
263
                        }
264
                    }
265
                } else {
266
                    $this->flushNow('... Updating "' . $tableName . '"."' . $fieldName . '" TO NULL WHERE ' . $where, 'created');
267
                    if ($this->forReal) {
268
                        $this->runUpdateQuery('UPDATE "' . $tableName . '" SET "' . $fieldName . '" = \'\' WHERE ' . $where, 2);
269
                    }
270
                }
271
            }
272
        }
273
        //run again with versioned tables ...
274
        if (false === $versionedTable) {
275
            foreach (['_Live', '_Versions'] as $extension) {
276
                $testTable = $tableName . $extension;
277
                if ($this->tableExists($testTable)) {
278
                    $this->fixClassNames($testTable, $objectClassName, $fieldName, true);
279
                } else {
280
                    $this->flushNow('... ... there is no table called: ' . $testTable);
281
                }
282
            }
283
        }
284
    }
285
286
    protected function fixFieldSize($tableName)
287
    {
288
        $databaseName = DB::get_conn()->getSelectedDatabase();
289
        DB::query('ALTER TABLE "' . $databaseName . '"."' . $tableName . '" CHANGE ClassName ClassName VARCHAR(255);');
290
    }
291
292
    protected function bestClassName(string $objectClassName, string $tableName, string $fieldName): string
293
    {
294
        $keyForStore = $objectClassName . '_' . $tableName . '_' . $fieldName;
295
        if (! isset($this->bestClassNameStore[$keyForStore])) {
296
            $obj = Injector::inst()
297
                ->get($objectClassName)
298
            ;
299
            if ($obj instanceof SiteTree) {
300
                if (class_exists(Page::class)) {
301
                    $this->bestClassNameStore[$keyForStore] = 'Page';
302
303
                    return $this->bestClassNameStore[$keyForStore];
304
                }
305
            }
306
            $values = $obj
307
                ->dbObject($fieldName)
308
                ->enumValues(false)
309
            ;
310
            $sql = '
311
                SELECT ' . $fieldName . ', COUNT(*) AS magnitude
312
                FROM ' . $tableName . '
313
                GROUP BY ' . $fieldName . '
314
                ORDER BY magnitude DESC
315
                LIMIT 1';
316
            $bestValue = '';
317
            $rowsForBestValue = DB::query($sql);
318
            foreach ($rowsForBestValue as $rowForBestValue) {
319
                if (in_array($rowForBestValue[$fieldName], $values, true)) {
320
                    $bestValue = $rowForBestValue[$fieldName];
321
322
                    break;
323
                }
324
            }
325
            if (! $bestValue) {
326
                $bestValue = key($values);
327
            }
328
            $this->bestClassNameStore[$keyForStore] = $bestValue;
329
        }
330
331
        return $this->bestClassNameStore[$keyForStore];
332
    }
333
}
334