Issues (48)

src/Tasks/CheckClassNames.php (4 issues)

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