1 | <?php |
||
2 | |||
3 | namespace Sunnysideup\MigrateData\Tasks; |
||
4 | |||
5 | use Page; |
||
0 ignored issues
–
show
|
|||
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||
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 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths