ENHANCEMENT Refactor of blog migration task to be more reliable.

Previous version of the migration task often cased site breakages. This goes back to basics, moving the data with SQL calls and limited ORM use to ease the data is not modified when mapped to the new blog 2.0 data structure.
This commit is contained in:
Cam Findlay 2015-11-01 10:47:44 +13:00
parent adbbf1ab00
commit c27c62d9ce
8 changed files with 357 additions and 431 deletions

View File

@ -1,112 +0,0 @@
<?php
/**
* @deprecated since version 2.0
*
* @property int $ParentID
* @property string $Date
* @property string $PublishDate
* @property string $Tags
*/
class BlogEntry extends BlogPost implements MigratableObject {
/**
* @var string
*/
private static $hide_ancestor = 'BlogEntry';
/**
* @var array
*/
private static $db = array(
'Date' => 'SS_Datetime',
'Author' => 'Text',
'Tags' => 'Text',
);
/**
* {@inheritdoc}
*/
public function canCreate($member = null) {
return false;
}
/**
* {@inheritdoc}
*/
public function up() {
//Migrate comma separated tags into BlogTag objects.
foreach($this->TagNames() as $tag) {
$existingTag = BlogTag::get()->filter(array('Title' => $tag, 'BlogID' => $this->ParentID));
if($existingTag->count()) {
//if tag already exists we will simply add it to this post.
$tagObject = $existingTag->First();
} else {
//if the tag is now we create it and add it to this post.
$tagObject = new BlogTag();
$tagObject->Title = $tag;
$tagObject->BlogID = $this->ParentID;
$tagObject->write();
}
if($tagObject){
$this->Tags()->add($tagObject);
}
}
//Store if the original entity was published or not (draft)
$published = $this->IsPublished();
// If a user has subclassed BlogEntry, it should not be turned into a BlogPost.
if($this->ClassName === 'BlogEntry') {
$this->ClassName = 'BlogPost';
$this->RecordClassName = 'BlogPost';
}
//Migrate these key data attributes
$this->PublishDate = $this->Date;
$this->AuthorNames = $this->Author;
$this->InheritSideBar = true;
//Write and additionally publish the item if it was published before.
$this->write();
if($published){
$this->publish('Stage','Live');
$message = "PUBLISHED: ";
} else {
$message = "DRAFT: ";
}
return $message . $this->Title;
}
/**
* Safely split and parse all distinct tags assigned to this BlogEntry.
*
* @deprecated since version 2.0
*
* @return array
*/
public function TagNames() {
$tags = preg_split('/\s*,\s*/', trim($this->Tags));
$results = array();
foreach($tags as $tag) {
if($tag) $results[mb_strtolower($tag)] = $tag;
}
return $results;
}
}
/**
* @deprecated since version 2.0
*/
class BlogEntry_Controller extends BlogPost_Controller {
}

View File

@ -1,73 +0,0 @@
<?php
/**
* @deprecated since version 2.0
*/
class BlogHolder extends BlogTree implements MigratableObject {
/**
* @var string
*/
private static $hide_ancestor = 'BlogHolder';
/**
* @var array
*/
private static $db = array(
'AllowCustomAuthors' => 'Boolean',
'ShowFullEntry' => 'Boolean',
);
/**
* @var array
*/
private static $has_one = array(
'Owner' => 'Member',
);
/**
* {@inheritdoc}
*/
public function canCreate($member = null) {
return false;
}
//Overload these to stop the Uncaught Exception: Object->__call(): the method 'parent' does not exist on 'BlogHolder' error.
public function validURLSegment() {
return true;
}
public function syncLinkTracking() {
return null;
}
/**
* {@inheritdoc}
*/
public function up() {
$published = $this->IsPublished();
if($this->ClassName === 'BlogHolder') {
$this->ClassName = 'Blog';
$this->RecordClassName = 'Blog';
$this->PostsPerPage = 10;
$this->write();
}
if($published){
$this->publish('Stage','Live');
$message = "PUBLISHED: ";
} else {
$message = "DRAFT: ";
}
return $message . $this->Title;
}
}
/**
* @deprecated since version 2.0
*/
class BlogHolder_Controller extends BlogTree_Controller {
}

View File

@ -1,53 +0,0 @@
<?php
/**
* @deprecated since version 2.0
*/
class BlogTree extends Page implements MigratableObject {
/**
* @var string
*/
private static $hide_ancestor = 'BlogTree';
/**
* @var array
*/
private static $db = array(
'Name' => 'Varchar(255)',
'LandingPageFreshness' => 'Varchar',
);
/**
* {@inheritdoc}
*/
public function canCreate($member = null) {
return false;
}
/**
* {@inheritdoc}
*/
public function up() {
$published = $this->IsPublished();
if($this->ClassName === 'BlogTree') {
$this->ClassName = 'Page';
$this->RecordClassName = 'Page';
$this->write();
}
if($published){
$this->publish('Stage','Live');
$message = "PUBLISHED: ";
} else {
$message = "DRAFT: ";
}
return $message . $this->Title;
}
}
/**
* @deprecated since version 2.0
*/
class BlogTree_Controller extends Page_Controller {
}

View File

@ -1,89 +0,0 @@
<?php
class BlogMigrationTask extends MigrationTask {
/**
* Should this task be invoked automatically via dev/build?
*
* @config
*
* @var bool
*/
private static $run_during_dev_build = true;
/**
* {@inheritdoc}
*/
public function up() {
$classes = ClassInfo::implementorsOf('MigratableObject');
$this->message('Migrating legacy blog records');
foreach($classes as $class) {
$this->upClass($class);
}
}
/**
* @param string $text
*/
protected function message($text) {
if(Controller::curr() instanceof DatabaseAdmin) {
DB::alteration_message($text, 'obsolete');
} else {
echo $text . "<br/>";
}
}
/**
* Migrate records of a single class
*
* @param string $class
* @param null|string $stage
*/
protected function upClass($class) {
if(!class_exists($class)) {
return;
}
if(is_subclass_of($class, 'SiteTree')) {
$items = SiteTree::get()->filter('ClassName', $class);
} else {
$items = $class::get();
}
if($count = $items->count()) {
$this->message(
sprintf(
'Migrating %s legacy %s records.',
$count,
$class
)
);
foreach($items as $item) {
$cancel = $item->extend('onBeforeUp');
if($cancel && min($cancel) === false) {
continue;
}
/**
* @var MigratableObject $item
*/
$result = $item->up();
$this->message($result);
$item->extend('onAfterUp');
}
}
}
/**
* {@inheritdoc}
*/
public function down() {
$this->message('BlogMigrationTask::down() not implemented');
}
}

View File

@ -1,8 +0,0 @@
<?php
interface MigratableObject {
/**
* Migrate the object up to the current version.
*/
public function up();
}

View File

@ -1,52 +0,0 @@
<?php
if(!class_exists('Widget')) {
return;
}
/**
* @deprecated since version 2.0
*
* @property string $DisplayMode
* @property string $ArchiveType
*/
class ArchiveWidget extends BlogArchiveWidget implements MigratableObject {
/**
* @var array
*/
private static $db = array(
'DisplayMode' => 'Varchar',
);
/**
* @var array
*/
private static $only_available_in = array(
'none',
);
/**
* {@inheritdoc}
*/
public function canCreate($member = null) {
return false;
}
/**
* {@inheritdoc}
*/
public function up() {
if($this->DisplayMode) {
$this->ArchiveType = 'Monthly';
if($this->DisplayMode === 'year') {
$this->ArchiveType = 'Yearly';
}
}
$this->ClassName = 'BlogArchiveWidget';
$this->write();
return "Migrated " . $this->ArchiveType . " archive widget";
}
}

View File

@ -1,44 +0,0 @@
<?php
if(!class_exists('Widget')) {
return;
}
/**
* A list of tags associated with blog posts.
*
* @package blog
*/
class TagCloudWidget extends BlogTagsWidget implements MigratableObject {
/**
* @var array
*/
private static $db = array(
'Title' => 'Varchar',
'Limit' => 'Int',
'Sortby' => 'Varchar',
);
/**
* @var array
*/
private static $only_available_in = array(
'none',
);
/**
* {@inheritdoc}
*/
public function canCreate($member = null) {
return false;
}
/**
* {@inheritdoc}
*/
public function up() {
$this->ClassName = 'BlogTagsWidget';
$this->write();
return "Migrated " . $this->Title . " widget";
}
}

View File

@ -0,0 +1,357 @@
<?php
/*
* BlogMigrationTask
*
* Migrates Blog 1.0 to the 2.0 data structure and maps existing content.
*
* Includes extension points as it is a common pattern to have subclassed BlogEntry, BlogHolder etc
* and customised the data model. You can add an Extension to BlogMigrationTask to run custom
* migration before or after the main migration or after the cleanup operation.
*
* @package silverstripe
* @subpackage blog
*
* @method none run() Runs the migration, must pass ?migration=1 to trigger.
* @method int migrateTags() Iterates through all comma-separated tags from blog 1.0 and creates BlogTag objects.
* @method array tagNames() Splits passed in comma separated string of tags
* @method none migrateWidgets() Migrates legacy Widgets if Widget module is installed.
* @method boolean cleanUp() Clears out blog 1.0 obsolete tables if you pass ?cleanup=1 to the task.
*/
class BlogMigrationTask extends BuildTask
{
/**
* @var string $title Shown in the overview on the {@link TaskRunner}
* HTML or CLI interface. Should be short and concise, no HTML allowed.
*/
protected $title = 'Blog 2.0 migration';
/**
* @var string $description Describe the implications the task has,
* and the changes it makes. Accepts HTML formatting.
*/
protected $description = 'Migrates blog 1.0 to 2.0 data structure.';
/*
* Run the migrate
*
* @param object $request
*/
public function run($request) {
// end of line output depending on CLI or browser run.
$this->eol = Director::is_cli() ? PHP_EOL : "<br>";
//PRE-FLIGHT CHECK
// Ensure a dev build has been run by check for some expected tables.
try {
DB::query('SELECT "ID" FROM "Blog"');
DB::query('SELECT "ID" FROM "BlogPost"');
echo "Blog and BlogPost tables exist, you are good to migrate" . $this->eol;
} catch (Exception $e) {
echo 'Ensure you have run a <a href="/dev/build" target="_blank">dev/build</a>' . $this->eol;
}
//THE MIGRATION
if($request->getVar('migration') != 1){
echo $this->eol . 'Ready to run the migration? ' . $this->eol . $this->eol .
'<a href="/dev/tasks/'.__CLASS__.'/?migration=1">Run migration only</a>' . $this->eol . $this->eol .
'<a href="/dev/tasks/'.__CLASS__.'/?migration=1&cleanup=1">Run migration and clean old blog tables</a>' . $this->eol;
exit;
}
$this->extend('onBeforeBlogMigration',$request, $this->eol);
//BlogPost Migration
//Migrate BlogEntry to BlogPost include _Live and _versions
try {
DB::query('
INSERT INTO "BlogPost" ("ID", "PublishDate", "AuthorNames")
(
SELECT "ID", "Date", "Author"
FROM "BlogEntry"
)
');
echo "Migrated BlogEntry to BlogPost" . $this->eol;
} catch (Exception $e) {
echo "BlogEntry to BlogPost migration already run, moving along..." . $this->eol;
}
//BlogPost_Live
try {
DB::query('
INSERT INTO "BlogPost_Live" ("ID", "PublishDate", "AuthorNames")
(
SELECT "ID", "Date", "Author"
FROM "BlogEntry_Live"
)
');
echo "Migrated BlogEntry_Live to BlogPost_Live" . $this->eol;
} catch (Exception $e) {
echo "BlogEntry_Live to BlogPost_Live migration already run, moving along..." . $this->eol;
}
//BlogPost_version
try {
DB::query('
INSERT INTO "BlogPost_versions" ("ID", "RecordID", "Version", "PublishDate", "AuthorNames")
(
SELECT "ID", "RecordID", "Version", "Date", "Author"
FROM "BlogEntry_versions"
)
');
echo "Migrated BlogEntry_versions to BlogPost_versions" . $this->eol;
} catch (Exception $e) {
echo "BlogEntry_versions to BlogPost_versions migration already run, moving along..." . $this->eol;
}
//SiteTree ClassName BlogEntry to BlogPost
try {
DB::query('UPDATE "SiteTree" SET "ClassName" = \'BlogPost\' WHERE "ClassName" = \'BlogEntry\'');
DB::query('UPDATE "SiteTree_Live" SET "ClassName" = \'BlogPost\' WHERE "ClassName" = \'BlogEntry\'');
DB::query('UPDATE "SiteTree_versions" SET "ClassName" = \'BlogPost\' WHERE "ClassName" = \'BlogEntry\'');
echo "Updated ClassName reference to BlogPost" . $this->eol;
} catch (Exception $e) {
echo $e;
echo "SiteTree BlogPost ClassName migration already run, moving along..." . $this->eol;
}
//Migrate BlogHolder to Blog
//Migrate BlogHolder to Blog include _Live and _versions
try {
DB::query('
INSERT INTO "Blog" ("ID", "PostsPerPage")
(
SELECT "BlogHolder"."ID", 10
FROM "BlogHolder"
)
');
echo "Migrated BlogHolder to Blog" . $this->eol;
} catch (Exception $e) {
echo "BlogHolder to Blog migration already run, moving along..." . $this->eol;
}
//Blog_Live
try {
DB::query('
INSERT INTO "Blog_Live" ("ID", "PostsPerPage")
(
SELECT "BlogHolder_Live"."ID", 10
FROM "BlogHolder_Live"
)
');
echo "Migrated_Live BlogHolder to Blog_Live" . $this->eol;
} catch (Exception $e) {
echo "BlogHolder to Blog migration already run, moving along..." . $this->eol;
}
//Blog_version
try {
DB::query('
INSERT INTO "Blog_versions" ("ID", "RecordID", "Version", "PostsPerPage")
(
SELECT "BlogHolder_versions"."ID", "BlogHolder_versions"."RecordID", "BlogHolder_versions"."Version", 10
FROM "BlogHolder_versions"
)
');
echo "Migrated BlogHolder versions to Blog_versions" . $this->eol;
} catch (Exception $e) {
echo "BlogHolder to Blog migration already run, moving along..." . $this->eol;
}
//SiteTree ClassName BlogEntry to BlogPost
try {
DB::query('UPDATE "SiteTree" SET "ClassName" = \'Blog\' WHERE "ClassName" = \'BlogHolder\'');
DB::query('UPDATE "SiteTree_Live" SET "ClassName" = \'Blog\' WHERE "ClassName" = \'BlogHolder\'');
DB::query('UPDATE "SiteTree_versions" SET "ClassName" = \'Blog\' WHERE "ClassName" = \'BlogHolder\'');
echo "Updated ClassName reference to Blog" . $this->eol;
} catch (Exception $e) {
echo $e;
}
//Migrate BlogTree to Page
try {
DB::query('UPDATE "SiteTree" SET "ClassName" = \'Page\' WHERE "ClassName" = \'SiteTree\'');
DB::query('UPDATE "SiteTree_Live" SET "ClassName" = \'Page\' WHERE "ClassName" = \'SiteTree\'');
DB::query('UPDATE "SiteTree_versions" SET "ClassName" = \'Page\' WHERE "ClassName" = \'SiteTree\'');
echo "Migrated BlogTree to Page" . $this->eol;
} catch (Exception $e) {
echo $e;
}
//Tags migration
try {
$tagcount = $this->migrateTags();
echo "Migrated " . $tagcount . " tags" . $this->eol;
} catch (Exception $e) {
echo "Error in tag migration (may have already run), moving along..." . $this->eol;
}
//Legacy Widget migration
if(class_exists('Widget')) {
try {
$this->migrateWidgets();
} catch (Exception $e) {
echo "Error migrating legacy widgets" . $this->eol;
}
}
$this->extend('onAfterBlogMigration', $request, $this->eol);
//IT'S CLEANUP TIME
if($request->getVar('cleanup') == 1){
try {
$this->cleanUp();
echo "Cleaned up all old blog tables" . $this->eol;
} catch (Exception $e) {
echo "Error with blog table cleanup (may have already cleaned up), moving along..." . $this->eol;
}
}
echo "Migration complete." . $this->eol;
exit;
}
/*
* Migrate the tags
*
* @return int number of migrated tags
*/
protected function migrateTags(){
//1. Get all BlogEntry ID and comma separated tags into an array
$blogtags = DB::query('SELECT ID, Tags FROM BlogEntry_Live')->map('ID','Tags');
$tagcount = 0;
//2. Foreach split the tags into own array
foreach($blogtags as $blogpostid => $tags){
foreach($this->tagNames($tags) as $tag) {
//3a. If it's an existing tag, connect the BlogPost and BlogTag as many_many
$existingTagID = DB::query('SELECT ID FROM BlogTag WHERE Title = \'' .$tag. '\'')->value();
if($existingTagID) {
$tagID = $existingTagID;
} else {
//3b. If it's a new tag, add the BlogTag and then connect the BlogPost and BlogTag as many_many
//Get the ParentID of the BlogPost
$parentID = DB::query('
SELECT "ParentID"
FROM "BlogPost_Live"
LEFT JOIN "SiteTree_Live" ON "SiteTree_Live"."ID" = "BlogPost_Live"."ID"
WHERE "BlogPost_Live"."ID" = \'' . $blogpostid . '\'')->value();
//Write the new tag using ORM to ensure the URLSegment is generated correctly.
$tagObject = BlogTag::create();
$tagObject->Title = $tag;
$tagObject->BlogID = $parentID;
$tagObject->write();
$tagID = $tagObject->ID;
$tagcount++;
}
// 4. Add the tag to the blogpost
DB::query('
INSERT INTO "BlogPost_Tags"
SET
"BlogPostID" = \'' . $blogpostid . '\',
"BlogTagID" = \'' . $tagID . '\''
);
}
}
return $tagcount;
}
/**
* Safely split and parse all distinct tags assigned to a BlogEntry.
*
* @param string comma-separated tags
* @return array
*/
protected function tagNames($tags) {
$tags = preg_split('/\s*,\s*/', trim($tags));
$results = array();
foreach($tags as $tag) {
if($tag) $results[mb_strtolower($tag)] = $tag;
}
return $results;
}
/*
* Migrate ArchiveWidget
*
* We set the BlogID to the first Live BlogID that can be found as we cannot know this value from
* the old blog data structure.
*
* Set some default values for the NumberToDisplay as this was also not part of Blog 1.0 data.
*/
protected function migrateWidgets() {
//Get the first Blog ID else set to 0.
$widgetblogid = DB::query('SELECT "ID" FROM "Blog_Live" ORDER BY "ID" LIMIT 1')->value();
if (!$widgetblogid) {
$widgetblogid = 0;
}
//ArchiveWidget to BlogArchiveWidget
//Yearly
DB::query('
INSERT INTO "BlogArchiveWidget" ("ID", "NumberToDispay", "ArchiveType", "BlogID")
(
SELECT "ID", 10, \'Yearly\', ' . $widgetblogid . '
FROM "ArchiveWidget"
WHERE "DisplayMode" = \'year\'
)
');
//Monthly
DB::query('
INSERT INTO "BlogArchiveWidget" ("ID", "NumberToDispay", "ArchiveType", "BlogID")
(
SELECT "ID", 10, \'Monthly\', ' . $widgetblogid . '
FROM "ArchiveWidget"
WHERE "DisplayMode" = \'month\'
)
');
//Update the Widget ClassName
DB::query('UPDATE "Widget" SET "ClassName" = \'BlogArchiveWidget\' WHERE "ClassName" = \'ArchiveWidget\'');
echo "Migrated ArchiveWidget to BlogArchiveWidget" . $this->eol;
// TagCloudWidget to BlogTagsWidget - sort set to Title ASC as no equivalent of 'frequency' exists in blog 2.0
DB::query('
INSERT INTO "BlogTagsWidget" ("ID", "Limit", "Order", "Direction", "BlogID")
(
SELECT "ID", "Limit", \'Title\', \'ASC\', ' . $widgetblogid . '
FROM "TagCloudWidget"
)
');
//Update the Widget ClassName
DB::query('UPDATE "Widget" SET "ClassName" = \'BlogTagsWidget\' WHERE "ClassName" = \'TagCloudWidget\'');
echo "Migrated TagCloudWidget to BlogTagsWidget" . $this->eol;
}
/*
* Clean up old tables
*
* @return boolean true of the clean up ran without an issue.
*/
protected function cleanUp() {
DB::query('DROP TABLE "BlogEntry"');
DB::query('DROP TABLE "BlogEntry_Live"');
DB::query('DROP TABLE "BlogEntry_versions"');
DB::query('DROP TABLE "BlogHolder"');
DB::query('DROP TABLE "BlogHolder_Live"');
DB::query('DROP TABLE "BlogHolder_versions"');
DB::query('DROP TABLE "BlogTree"');
DB::query('DROP TABLE "BlogTree_Live"');
DB::query('DROP TABLE "BlogTree_versions"');
DB::query('DROP TABLE "ArchiveWidget"');
DB::query('DROP TABLE "TagCloudWidget"');
$this->extend('updateTablesToCleanUp');
return true;
}
}