Created
November 30, 2021 20:00
-
-
Save oddnoc/a1568ff6c0d0cd725bc9facbe1582ef8 to your computer and use it in GitHub Desktop.
Modified version of Tractorcow script to migrate Silverstripe Translatable to Fluent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace TractorCow\Fluent\Task; | |
use SilverStripe\CMS\Model\SiteTree; | |
use SilverStripe\Core\ClassInfo; | |
use SilverStripe\Core\Environment; | |
use SilverStripe\Dev\BuildTask; | |
use SilverStripe\Dev\Debug; | |
use SilverStripe\i18n\i18n; | |
use SilverStripe\ORM\DataObject; | |
use SilverStripe\ORM\DB; | |
use SilverStripe\ORM\Queries\SQLSelect; | |
use SilverStripe\Security\DefaultAdminService; | |
use SilverStripe\Security\Member; | |
use SilverStripe\Versioned\Versioned; | |
use TractorCow\Fluent\Extension\FluentExtension; | |
use TractorCow\Fluent\Extension\FluentFilteredExtension; | |
use TractorCow\Fluent\Model\Locale; | |
use TractorCow\Fluent\State\FluentState; | |
use TractorCow\Fluent\Task\ConvertTranslatableTask\Exception; | |
/** | |
* Provides migration from the Translatable module in a SilverStripe 3 website to the Fluent format for SilverStripe 4. | |
* This task assumes that you have upgraded your website to run on SilverStripe 4 already, and you want to migrate the | |
* existing data from your project into a format that is compatible with Fluent. | |
* | |
* Don't forget to: | |
* | |
* 1. Back up your DB | |
* 2. dev/build | |
* 3. Log into the CMS and set up the locales you want to use | |
* 4. Back up your DB again | |
* 5. Log into the CMS and check everything | |
*/ | |
class SS4TranslatableToFluentTask extends BuildTask | |
{ | |
protected $title = "SS4 Convert Translatable > Fluent Task"; | |
protected $description = "Migrates site DB from SS3 Translatable DB format to SS4 Fluent."; | |
protected $skipped_classes = [ | |
]; | |
private static $segment = 'SS4TranslatableToFluentTask'; | |
public function run($request) | |
{ | |
Environment::increaseMemoryLimitTo(); // Do we have to tell you three times? | |
$this->checkInstalled(); | |
// we may need some privileges for this to work | |
// without this, running under sake is a problem | |
// maybe sake could take care of it ... | |
Member::actAs( | |
DefaultAdminService::singleton()->findOrCreateDefaultAdmin(), | |
function () { | |
DB::get_conn()->withTransaction(function () { | |
$defaultLocale = i18n::config()->get('default_locale'); | |
Versioned::set_stage(Versioned::DRAFT); | |
$classes = $this->fluentClasses(); | |
$tables = DB::get_schema()->tableList(); | |
if (empty($classes)) { | |
Debug::message('No classes have Fluent enabled, so skipping.', false); | |
} | |
foreach ($classes as $class) { | |
$deletionTables = []; | |
$class_representative = singleton($class); | |
// Disable filter if it has been applied to the class | |
if ($class_representative->hasMethod('has_extension') | |
&& $class::has_extension(FluentFilteredExtension::class) | |
) { | |
$class::remove_extension(FluentFilteredExtension::class); | |
} | |
// Ensure that a translationgroup table exists for this class | |
$baseTable = DataObject::getSchema()->baseDataTable($class); | |
if ($class_representative->hasMethod('has_extension') | |
&& $class::has_extension(Versioned::class) | |
) { | |
// Tables that have to be corrected to unify all the translations under one base record | |
$postProcessTables = [ | |
$baseTable . '_Localised', | |
$baseTable . '_Localised_Live', | |
$baseTable . '_Versions', | |
]; | |
// Tables that have to have their URLSegment fixed | |
$URLSegmentTables = []; | |
if ($baseTable == 'SiteTree') { | |
$URLSegmentTables = [ | |
$baseTable . '_Localised', | |
$baseTable . '_Localised_Live', | |
$baseTable . '_Localised_Versions', | |
]; | |
// Flag tables to prune old translations from | |
$deletionTables[$baseTable . '_Versions'] = 1; | |
$deletionTables[$baseTable . '_Live'] = 1; | |
} | |
} else { // NOT versioned | |
$postProcessTables = [$baseTable . '_Localised',]; | |
$URLSegmentTables = []; | |
} | |
// Flag table to prune old translations from (Versioned or not) | |
$deletionTables[$baseTable] = 1; | |
$groupTable = strtolower($baseTable . "_translationgroups"); | |
if (isset($tables[$groupTable])) { | |
$groupTable = $tables[$groupTable]; | |
} else { | |
Debug::message("Ignoring class without _translationgroups table ${class}", false); | |
continue; | |
} | |
// Get all of Translatable's translation group IDs | |
// Translation Groups are the old, Translatable collections of translated pages | |
$translation_groups = DB::query(sprintf('SELECT DISTINCT TranslationGroupID FROM %s', $groupTable)); | |
foreach ($translation_groups as $translation_group) { | |
$translationGroupSet = []; | |
$translationGroupID = $translation_group['TranslationGroupID']; | |
$itemIDs = DB::query(sprintf("SELECT OriginalID FROM %s WHERE TranslationGroupID = %d", $groupTable, $translationGroupID))->column(); | |
$instanceIDs = $class::get()->sort('Created')->byIDs($itemIDs)->column('ID'); | |
if (!count($instanceIDs)) { | |
continue; | |
} | |
Debug::message(sprintf("\n%d instances for %s: [%s]", count($instanceIDs), implode(', ', $itemIDs), implode(', ', $instanceIDs)), false); | |
$noSyncedLocalesString = false; | |
$noSyncedLocales = []; | |
$defaultURLSegment = false; | |
$itemIDsString = join(',', $itemIDs); | |
$defaultInstanceIDColumn = DB::query("SELECT ID FROM SiteTree WHERE Locale = '${defaultLocale}' AND ID IN (${itemIDsString})")->column(); | |
$defaultInstanceID = $defaultInstanceIDColumn && count($defaultInstanceIDColumn) ? $defaultInstanceIDColumn[0] : false; | |
$defaultInstance = $defaultInstanceID ? $class::get()->byID($defaultInstanceID) : false; | |
if ($defaultInstance) { | |
$defaultURLSegment = $defaultInstance->URLSegment; | |
// check to see if there are any "synced" locales. | |
// if there are, remove them from the list of instances | |
$noSyncedLocalesString = DB::query("SELECT NoSyncToTheseLocales FROM Page WHERE ID = ${defaultInstanceID}")->column(); | |
$noSyncedLocalesString = count($noSyncedLocalesString) ? $noSyncedLocalesString[0] : false; | |
if ($noSyncedLocalesString) { | |
$noSyncedLocales = explode(',', $noSyncedLocalesString); | |
} | |
Debug::message("NoSyncToTheseLocales: ${noSyncedLocalesString}", false); | |
Debug::message("Default URLSegment: {$defaultURLSegment}", false); | |
} | |
// re-order to put default locale first. EDIT THIS TO MATCH YOUR SET OF LOCALES!! | |
$keyedByLocale = ['en_US' => false, 'zh_CN' => false, 'zh_TW' => false]; | |
foreach ($instanceIDs as $id) { | |
$instance = $class::get()->byID($id); | |
// Get the Locale column directly from the base table, because the SS ORM will set it to the default | |
$instanceLocale = SQLSelect::create() | |
->setFrom("\"{$baseTable}\"") | |
->setSelect('"Locale"') | |
->setWhere(["\"{$baseTable}\".\"ID\"" => $instance->ID]) | |
->execute() | |
->first(); | |
if ($instanceLocale) { | |
$instanceLocale = $instanceLocale['Locale']; | |
$instance->Locale = $instanceLocale; | |
$keyedByLocale[$instanceLocale] = $id; | |
} | |
$instance->destroy(); | |
} | |
foreach ($keyedByLocale as $instanceLocale => $id) { | |
$instance = $class::get()->byID($id); | |
// Ensure that we have an instance | |
if (!$instance) { | |
continue; | |
} | |
// Ensure that we got the Locale out of the base table | |
if (empty($instanceLocale)) { | |
Debug::message("Skipping {$instance->Title} with ID {$instance->ID} - couldn't find Locale", false); | |
continue; | |
} | |
// Check for obsolete classes that don't need to be handled any more | |
if ($instance->ObsoleteClassName) { | |
Debug::message( | |
"Skipping {$instance->ClassName} with ID {$instance->ID} because it from an obsolete class", | |
false | |
); | |
continue; | |
} | |
// skip this if not either the default instance or in the noSyncLocale array. | |
// also delete it from DB | |
// $noSyncedLocales = list of locales that CAN have their own translations | |
// and should NOT be deleted | |
if ($defaultInstance | |
&& $instance->ID != $defaultInstance->ID | |
&& !in_array($instanceLocale, $noSyncedLocales)) { | |
// delete since this is a should-fallback (formerly synced) Page | |
Debug::message("Deleting synced page {$instance->Title} with ID {$instance->ID}", false); | |
SiteTree::config()->update('enforce_strict_hierarchy', false); | |
$instance->doArchive(); | |
$instance->destroy(); | |
SiteTree::config()->update('enforce_strict_hierarchy', true); | |
// skip it; | |
continue; | |
} | |
$translationGroupSet[$instanceLocale] = $id; | |
$instance->destroy(); | |
} | |
if (empty($translationGroupSet)) { | |
continue; | |
} | |
// Now write out the records for the translation group | |
if (array_key_exists($defaultLocale, $translationGroupSet)) { | |
$originalRecordID = $translationGroupSet[$defaultLocale]; | |
} else { | |
$originalRecordID = reset($translationGroupSet); // Just use the first one | |
} | |
foreach ($translationGroupSet as $locale => $id) { | |
$instance = $class::get()->byID($id); | |
if (!$instance) { | |
Debug::message("Couldn't find {$class} id:{$id} locale: ${locale}", false); | |
} | |
Debug::message( | |
"Updating {$instance->ClassName} {$instance->Title} ({$instance->ID}) [RecordID: {$originalRecordID}] with locale {$locale}", | |
false | |
); | |
FluentState::singleton() | |
->withState(function (FluentState $state) use ($instance, $locale, $originalRecordID) { | |
// Use Fluent's ORM to write and/or publish the record into the correct locale | |
// from Translatable | |
$state->setLocale($locale); | |
if (!$this->isPublished($instance)) { | |
$instance->write(); | |
Debug::message(" -- Saved to draft", false); | |
} else { | |
try { | |
$success = $instance->publishRecursive(); | |
} catch (\Throwable $th) { | |
Debug::message(" -- Publishing FAILED", false); | |
// throw new Exception("Failed to publish"); | |
//throw $th; | |
} | |
if (isset($success) && $success !== false) { | |
Debug::message(" -- Published", false); | |
} | |
} | |
}); | |
$instance->destroy(); | |
} | |
foreach ($postProcessTables as $table) { | |
$query = sprintf("UPDATE %s SET RecordID = %d WHERE RecordID IN (%s)", $table, $originalRecordID, implode(', ', $itemIDs)); | |
Debug::message($query, false); | |
DB::query($query); | |
} | |
// force URLSegments for SiteTree | |
if (count($URLSegmentTables)) { | |
foreach ($URLSegmentTables as $table) { | |
// if we're a localized table, we need to use RecordID | |
if (in_array($table, $postProcessTables)) { | |
$query = sprintf("UPDATE %s SET URLSegment = '%s' WHERE RecordID IN (%s)", $table, $defaultURLSegment, implode(', ', $itemIDs)); | |
} | |
// otherwise ID | |
elseif ($defaultInstance) { | |
$query = sprintf("UPDATE %s SET URLSegment = '%s' WHERE ID = %d", $table, $defaultURLSegment, $defaultInstance->ID); | |
} else { | |
Debug::message(sprintf( | |
'Unable to update URLSegment to %s in table %s because $defaultInstance is no object', | |
$defaultURLSegment, | |
$table | |
), false); | |
continue; | |
} | |
Debug::message($query, false); | |
DB::query($query); | |
} | |
} | |
} | |
// Delete old base items that don't have the default locale | |
foreach (array_keys($deletionTables) as $table) { | |
$query = sprintf("DELETE FROM %s WHERE Locale != '%s'", $table, $defaultLocale); | |
Debug::message($query, false); | |
DB::query($query); | |
} | |
// Drop the "Locale" column from the base table | |
Debug::message('Dropping "Locale" column from ' . $baseTable, false); | |
DB::query(sprintf('ALTER TABLE "%s" DROP COLUMN "Locale"', $baseTable)); | |
// Drop the "_translationgroups" translatable table | |
Debug::message('Deleting Translatable table ' . $groupTable, false); | |
DB::query(sprintf('DROP TABLE IF EXISTS "%s"', $groupTable)); | |
} | |
}); | |
} | |
); | |
} | |
/** | |
* Gets all classes with FluentExtension | |
* | |
* @return array Array of classes to migrate | |
*/ | |
protected function fluentClasses() | |
{ | |
$classes = []; | |
$dataClasses = ClassInfo::subclassesFor(DataObject::class); | |
array_shift($dataClasses); | |
foreach ($dataClasses as $class) { | |
// is this a skipped class? | |
if (in_array($class, $this->skipped_classes)) { | |
continue; | |
} | |
$base = DataObject::getSchema()->baseDataClass($class); | |
foreach (DataObject::get_extensions($base) as $extension) { | |
if (is_a($extension, FluentExtension::class, true)) { | |
$classes[] = $base; | |
break; | |
} | |
} | |
} | |
return array_unique($classes); | |
} | |
/** | |
* Checks that fluent is configured correctly | |
* | |
* @throws ConvertTranslatableTask\Exception | |
*/ | |
protected function checkInstalled() | |
{ | |
// Assert that fluent is configured | |
$locales = Locale::getLocales(); | |
if (empty($locales)) { | |
throw new Exception("Please configure Fluent locales (in the CMS) prior to migrating from translatable"); | |
} | |
$defaultLocale = Locale::getDefault(); | |
if (empty($defaultLocale)) { | |
throw new Exception( | |
"Please configure a Fluent default locale (in the CMS) prior to migrating from translatable" | |
); | |
} | |
} | |
/** | |
* Determine whether the record has been published previously/is currently published | |
* | |
* @param DataObject $instance | |
* @return bool | |
*/ | |
protected function isPublished(DataObject $instance) | |
{ | |
$isPublished = false; | |
if ($instance->hasMethod('isPublished')) { | |
$isPublished = $instance->isPublished(); | |
} | |
return $isPublished; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Remember to edit line 150 to match your list of locales.