Skip to content

Instantly share code, notes, and snippets.

@oddnoc
Created November 30, 2021 20:00
Show Gist options
  • Save oddnoc/a1568ff6c0d0cd725bc9facbe1582ef8 to your computer and use it in GitHub Desktop.
Save oddnoc/a1568ff6c0d0cd725bc9facbe1582ef8 to your computer and use it in GitHub Desktop.
Modified version of Tractorcow script to migrate Silverstripe Translatable to Fluent
<?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;
}
}
@oddnoc
Copy link
Author

oddnoc commented Nov 30, 2021

Remember to edit line 150 to match your list of locales.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment