├── docs └── en │ ├── index.md │ └── advanced │ ├── AdvancedUsage.md │ ├── AdvancedDataMigration.md │ └── DuplicateTableName.md ├── _config └── config.yml ├── CONTRIBUTING.md ├── .editorconfig ├── src ├── Traits │ └── BlockMigrationConfigurationTrait.php ├── Tools │ ├── Message.php │ ├── LinkableManipulator.php │ ├── ElementalAreaGenerator.php │ ├── DataManipulator.php │ └── BlockElementTranslator.php ├── Admin │ └── OrphanedElementsAdmin.php ├── Reports │ └── BrokenClassNameReport.php └── Tasks │ └── BlocksToElementsTask.php ├── phpunit.xml.dist ├── phpcs.xml.dist ├── composer.json ├── LICENSE.md └── README.md /docs/en/index.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: dynamic-blocks-to-elemental-config 3 | --- 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! Create an issue, explaining a bug or proposal. Submit pull requests if you feel brave. 4 | -------------------------------------------------------------------------------- /docs/en/advanced/AdvancedUsage.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | - [Duplicate `$table_name`](DuplicateTableName.md) 4 | - [Moving Data From One Field To Another](AdvancedDataMigration.md) -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /src/Traits/BlockMigrationConfigurationTrait.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | tests 4 | 5 | 6 | 7 | 8 | src/ 9 | 10 | tests/ 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Tools/LinkableManipulator.php: -------------------------------------------------------------------------------- 1 | setRecords($records); 23 | } 24 | 25 | /** 26 | * @param $records 27 | * @return $this 28 | */ 29 | public function setRecords($records) 30 | { 31 | $this->records = $records; 32 | 33 | return $this; 34 | } 35 | } -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coding standard for SilverStripe 4.x 4 | 5 | 6 | */vendor/* 7 | */thirdparty/* 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Admin/OrphanedElementsAdmin.php: -------------------------------------------------------------------------------- 1 | dataClass(); 39 | 40 | $list2 = $list->filterByCallback(function (BaseElement $elememt) { 41 | return $elememt->ParentID == 0; 42 | }); 43 | 44 | if($list2->count()){ 45 | $list = $class::get()->filter('ID', $list->column('ID')); 46 | } 47 | 48 | return $list; 49 | } 50 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic/silverstripe-blocks-to-elemental-migrator", 3 | "type": "silverstripe-vendormodule", 4 | "description": "Migrate data from SilverStripe Blocks to SilverStripe Elemental", 5 | "keywords": [ 6 | "silverstripe", 7 | "elemental", 8 | "blocks" 9 | ], 10 | "license": "BSD-3-Clause", 11 | "authors": [ 12 | { 13 | "name": "Dynamic", 14 | "email": "dev@dynamicagency.com", 15 | "homepage": "http://www.dynamicagency.com" 16 | } 17 | ], 18 | "require": { 19 | "dnadesign/silverstripe-elemental": "^3@dev", 20 | "sheadawson/silverstripe-blocks": "^2.0", 21 | "dynamic/silverstripe-classname-update-tasks": "^1.0@dev" 22 | }, 23 | "require-dev": { 24 | "phpunit/PHPUnit": "^5.7", 25 | "squizlabs/php_codesniffer": "*" 26 | }, 27 | "config": { 28 | "process-timeout": 600 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "2.0.x-dev" 33 | } 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Dynamic\\BlockMigration\\": "src/" 38 | } 39 | }, 40 | "minimum-stability": "dev", 41 | "prefer-stable": true, 42 | "scripts": { 43 | "lint": "vendor/bin/phpcs src/ tests/", 44 | "lint-clean": "vendor/bin/phpcbf src/ tests/" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Dynamic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /docs/en/advanced/AdvancedDataMigration.md: -------------------------------------------------------------------------------- 1 | # Advanced Data Migration 2 | 3 | ### Summary 4 | You may find that in some cases you had additional relations or fields applied to a Block, or a field name has changed from your old block to a new element. This is best resolved using SilverStripe's [DataExtension](https://github.com/silverstripe/silverstripe-framework/blob/4/src/ORM/DataExtension.php). 5 | 6 | #### Example 7 | 8 | _**MyBlock.php**_ 9 | 10 | ```php 11 | 'Boolean', 19 | ]; 20 | } 21 | ``` 22 | 23 | _**MyElement.php**_ 24 | 25 | ```php 26 | 'Boolean', 34 | ]; 35 | } 36 | ``` 37 | 38 | In this example `MyUniqueField` is now `MyNewUniqueField`. The migration tool isn't inherently aware of this change, however, we can use a `DataExtension` to handle this: 39 | 40 | ```php 41 | 'Boolean', 49 | ]; 50 | 51 | public function onBeforeWrite() { 52 | parent::onBeforeWrite(); 53 | 54 | $this->owner->MyNewUniqueField = $this->owner->MyUniqueField; 55 | } 56 | } 57 | ``` 58 | 59 | Applying the above `DataExtension` to `MyElement` will allow it to access the legacy field as it is re-implemented in the `DataExtension`'s `$db` fields. We then us an `onBeforeWrite()` to move the value from the old field to the new field. After the migration is complete, this `DataExtension` could be removed if the legacy field is no longer needed, or at minimum, updating or removing the `onBeforeWrite()` so as to not overwrite any data after the migration is complete. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Blocks to Elemental Migrator 2 | 3 | ### Summary 4 | SilverStripe 3 saw the creation of new way to manage content. One of these ways was the Blocks module. With the release of SilverStripe 4, Elemental is now the preferred "Block" type module for managing sets of flexible content. This module aims to make migrating from the Blocks module to Elemental a little easier. 5 | 6 | This module provides a base task that is customisable to allow for additional blocks you may have created to be migrated to existing elements, or new elements you have created. 7 | 8 | ## Requirements 9 | 10 | * SilverStripe ^4.0 11 | * SilverStripe Elemental ^2.0 12 | * SilverStripe Blocks ^2.0 13 | 14 | ## Installation 15 | 16 | `composer require dynamic/silverstripe-blocks-to-elemental-migrator` 17 | 18 | ## Usage 19 | 20 | ### Configuration 21 | Configuration supports mapping Blocks and their relations to DataObjects to Elements and their relations to DataObjects. Below is a sample configuration migrating `AccordionBlock `, `ImageBlock ` and `RecentBlogPostsBlock ` to `ElementAccordion `, `ElementImage ` and `ElementBlogPosts ` respectively. 22 | 23 | 24 | **blockmigration.yml** 25 | 26 | ```yml 27 | Dynamic\BlockMigration\Tasks\BlocksToElementsTask: 28 | mappings: 29 | AccordionBlock: Dynamic\DynamicBlocks\Block\AccordionBlock 30 | AccordionPanel: Dynamic\DynamicBlocks\Model\AccordionPanel 31 | ImageBlock: Dynamic\DynamicBlocks\Block\ImageBlock 32 | RecentBlogPostsBlock: Dynamic\DynamicBlocks\Block\RecentBlogPostsBlock 33 | 34 | migration_mapping: 35 | ##Accordion 36 | Dynamic\DynamicBlocks\Block\AccordionBlock: 37 | Element: Dynamic\Elements\Accordion\Elements\ElementAccordion 38 | Relations: 39 | Panels: 'Panels' 40 | MigrateOptionFromTable: 41 | Panels: 42 | AccordionPanel: Dynamic\DynamicBlocks\Model\AccordionPanel 43 | ##Image 44 | Dynamic\DynamicBlocks\Block\ImageBlock: 45 | Element: Dynamic\Elements\Image\Elements\ElementImage 46 | Relations: 47 | Image: 'Image' 48 | ##Recent Blog Posts 49 | Dynamic\DynamicBlocks\Block\RecentBlogPostsBlock: 50 | Element: Dynamic\Elements\Blog\Elements\ElementBlogPosts 51 | Relations: 52 | Blog: 'Blog' 53 | ``` 54 | 55 | You may run into some snags depending on your project. Check out the [Advanced Configuration](docs/en/advanced/AdvancedUsage.md) for additional options and suggestions. -------------------------------------------------------------------------------- /src/Tools/ElementalAreaGenerator.php: -------------------------------------------------------------------------------- 1 | config()->get('area_assignments'); 26 | 27 | if (isset($areaMappging[$area])) { 28 | $areaName = $areaMappging[$area]; 29 | } else { 30 | $areaName = static::get_default_area_by_page($object); 31 | } 32 | 33 | Message::terminal("Attempting to resolve {$area} area for {$object->ClassName} - {$object->ID} with {$areaName}"); 34 | 35 | $areaID = $areaName . 'ID'; 36 | if (!$object->$areaID) { 37 | Message::terminal("No area currently set, attempting to create"); 38 | $elementalArea = ElementalArea::create(); 39 | $elementalArea->OwnerClassName = $object->ClassName; 40 | $elementalArea->write(); 41 | $elementalArea->exists() ? Message::terminal("{$elementalArea->ClassName} created for {$object->ClassName} - {$object->ID}") : Message::terminal("Area could not be created."); 42 | 43 | // To preserve draft state for current pages that have a live version, we should set the has_one relation via SQL to prevent data disruption 44 | static::set_relations($object, $areaID, $elementalArea); 45 | 46 | $class = $object->ClassName; 47 | 48 | $object = $class::get()->byID($object->ID); 49 | 50 | $object->$areaID > 0 ? Message::terminal("Area successfully related to page.") : Message::terminal("Area unsuccessfully related to page."); 51 | } else { 52 | Message::terminal("An area already exists for that page."); 53 | } 54 | 55 | $resultingArea = ElementalArea::get()->filter('ID', $object->$areaID)->first(); 56 | 57 | Message::terminal("Resolved with area {$resultingArea->ClassName} - {$resultingArea->ID}.\n\n"); 58 | 59 | return $resultingArea; 60 | } 61 | 62 | /** 63 | * @param $object 64 | * @param $relationColumn 65 | * @param $elementalArea 66 | */ 67 | protected static function set_relations($object, $relationColumn, $elementalArea) 68 | { 69 | if ($object instanceof SiteTree) { 70 | $baseTable = $object->getSchema()->tableForField($object->ClassName, $relationColumn); 71 | 72 | if ($baseTable && $baseTable != '') { 73 | DB::prepared_query("UPDATE \"{$baseTable}\" SET \"{$relationColumn}\" = ? WHERE ID = ?", [$elementalArea->ID, $object->ID]); 74 | DB::prepared_query("UPDATE \"{$baseTable}_Live\" SET \"{$relationColumn}\" = ? WHERE ID = ?", [$elementalArea->ID, $object->ID]); 75 | DB::prepared_query("UPDATE \"{$baseTable}_Versions\" SET \"{$relationColumn}\" = ? WHERE RecordID = ?", [$elementalArea->ID, $object->ID]); 76 | } else { 77 | Message::terminal("Couldn't update relation for {$object->ClassName} - {$object->ID}, Area {$relationColumn} - {$elementalArea->ID}"); 78 | } 79 | } else { 80 | Message::terminal("{$object->ClassName} is not a decendant of SiteTree"); 81 | Debug::show($object); 82 | die; 83 | } 84 | } 85 | 86 | /** 87 | * @param $page 88 | * @return mixed 89 | */ 90 | protected static function get_default_area_by_page($page) 91 | { 92 | $config = BlocksToElementsTask::singleton()->config(); 93 | $defaults = $config->get('default_areas'); 94 | 95 | if (isset($defaults[$page->ClassName])) { 96 | return $defaults[$page->ClassName]; 97 | } 98 | 99 | return $config->get('default_area'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Reports/BrokenClassNameReport.php: -------------------------------------------------------------------------------- 1 | config()->get('mappings')) { 53 | foreach ($mapping as $old => $new) { 54 | $count = 0; 55 | 56 | $subclasses = ClassInfo::getValidSubClasses($new); 57 | 58 | foreach ($this->yieldRecords($new::get()->exclude('ClassName', $new)) as $record) { 59 | if (($record->ClassName != $new || !class_exists($record->ClassName)) && !in_array($record->ClassName, $subclasses)) { 60 | $count++; 61 | } 62 | } 63 | 64 | if ($count) { 65 | $results->push(ArrayData::create([ 66 | 'Title' => $new::singleton()->singular_name(), 67 | 'LegacyClassName' => $old, 68 | 'FQN' => $new, 69 | 'RecordsToUpdate' => $count, 70 | ])); 71 | } 72 | } 73 | } 74 | return $results; 75 | } 76 | 77 | /** 78 | * @param $records 79 | * @return \Generator 80 | */ 81 | protected function yieldRecords($records) 82 | { 83 | foreach ($records as $record) { 84 | yield $record; 85 | } 86 | } 87 | 88 | /** 89 | * Return a field, such as a {@link GridField} that is 90 | * used to show and manipulate data relating to this report. 91 | * 92 | * Generally, you should override {@link columns()} and {@link records()} to make your report, 93 | * but if they aren't sufficiently flexible, then you can override this method. 94 | * 95 | * @return \SilverStripe\Forms\FormField subclass 96 | */ 97 | public function getReportField() 98 | { 99 | $items = $this->sourceRecords(); 100 | 101 | $gridFieldConfig = GridFieldConfig::create()->addComponents( 102 | new GridFieldButtonRow('before'), 103 | new GridFieldPrintButton('buttons-before-left'), 104 | new GridFieldExportButton('buttons-before-left'), 105 | new GridFieldSortableHeader(), 106 | new GridFieldDataColumns(), 107 | new GridFieldPaginator() 108 | ); 109 | $gridField = new GridField('Report', null, $items, $gridFieldConfig); 110 | $columns = $gridField->getConfig()->getComponentByType(GridFieldDataColumns::class); 111 | 112 | $displayFields['Title'] = 'Object Name'; 113 | $displayFields['LegacyClassName'] = 'Legacy ClassName'; 114 | $displayFields['FQN'] = 'New ClassName (FQN)'; 115 | $displayFields['RecordsToUpdate'] = 'Records To Update'; 116 | 117 | $columns->setDisplayFields($displayFields); 118 | 119 | return $gridField; 120 | } 121 | } -------------------------------------------------------------------------------- /docs/en/advanced/DuplicateTableName.md: -------------------------------------------------------------------------------- 1 | # Duplicate `$table_name` 2 | 3 | ### Summary 4 | There are some caveots that can pop up while doing a migration. One of which that we have seen is duplicate table names when upgrading from SilverStripe 3 to SilverStripe 4. This particular issue was two fold, we first need to resolve the table names, then we likely need to move data from the legacy table to the new table. This will be illustrated below. 5 | 6 | ### Duplicate `$table_name` Example 7 | 8 | In this example we'll be looking at Dynamic's Accordion Block and Accordion Element. This particular instance has a table name collision in the SS4 versions of each module (the static `$table_name` config has the same value for both objects). 9 | 10 | We will look at how we configure the migration tool to handle the data: 11 | 12 | #### Current Configuration 13 | 14 | __*AccordionPanel.php (Blocks Module Implementation)*__ 15 | 16 | ```php 17 | setFromTable($configuration['SourceTable']); 78 | $this->setToClass($toClass); 79 | 80 | if (isset($configuration['ParentTable']) && $configuration['ParentTable']) { 81 | $this->setParentTable($configuration['ParentTable']); 82 | } 83 | 84 | $this->setToTable($toClass::getSchema()->tableName($configuration['ToClass'])); 85 | $this->setSchema(DB::getConfig()['database'], $configuration['SourceTable']); 86 | } 87 | 88 | /** 89 | * @param string $toClass 90 | * @return $this 91 | */ 92 | public function setToClass($toClass) 93 | { 94 | $this->to_class = $toClass; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @return string 101 | */ 102 | public function getToClass() 103 | { 104 | return $this->to_class; 105 | } 106 | 107 | /** 108 | * @param string $table 109 | * @return $this 110 | */ 111 | public function setFromTable($table) 112 | { 113 | $this->from_table = $table; 114 | return $this; 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getFromTable() 121 | { 122 | return $this->from_table; 123 | } 124 | 125 | /** 126 | * @param string $table 127 | * @return $this 128 | */ 129 | public function setToTable($table) 130 | { 131 | $this->to_table = $table; 132 | return $this; 133 | } 134 | 135 | /** 136 | * @return string 137 | */ 138 | public function getToTable() 139 | { 140 | return $this->to_table; 141 | } 142 | 143 | /** 144 | * @param $parentTable 145 | * @return $this 146 | */ 147 | public function setParentTable($parentTable) 148 | { 149 | $this->use_parent_table = $parentTable; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return bool 156 | */ 157 | public function getParentTable() 158 | { 159 | return $this->use_parent_table; 160 | } 161 | 162 | /** 163 | * @param string $database 164 | * @param string $table 165 | */ 166 | public function setSchema($database, $table) 167 | { 168 | $results = $this->getDBQuery("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'{$table}' AND TABLE_SCHEMA = '{$database}'"); 169 | 170 | $this->schema = static::results_to_array($results); 171 | return $this; 172 | } 173 | 174 | /** 175 | * @param $results 176 | * @return array 177 | */ 178 | protected static function results_to_array($results) 179 | { 180 | $schema = []; 181 | foreach ($results as $key => $result) { 182 | $schema[$result['COLUMN_NAME']] = $result['COLUMN_NAME']; 183 | } 184 | 185 | return $schema; 186 | } 187 | 188 | public function migrateData() 189 | { 190 | $class = $this->getToClass(); 191 | $table = $this->getFromTable(); 192 | 193 | $where = ($this->getParentTable()) 194 | ? ['ClassName' => $class] 195 | : null; 196 | 197 | $results = $this->getResults($table, '*', $where); 198 | 199 | foreach ($results as $result) { 200 | Message::terminal("Migrating {$class::singleton()->singular_name()}: {$result['ID']}"); 201 | $record = $class::create(); 202 | foreach ($this->prepareKeys($table, $result) as $key => $val) { 203 | if ($key != 'ClassName') { 204 | $record->$key = $val; 205 | } 206 | } 207 | 208 | if (!$record->Title) { 209 | $record->Title = $this->getDefaultTitle($class); 210 | } 211 | 212 | //$this->extend('updateMigrationRecord', $record, $result); 213 | 214 | if ($record->hasExtension(Versioned::class)) { 215 | $published = $record->isPublished(); 216 | } 217 | 218 | $record->write(); 219 | 220 | if (isset($published)) { 221 | $record->writeToStage(Versioned::DRAFT); 222 | if ($published) { 223 | $record->publishRecursive(); 224 | } 225 | } 226 | 227 | Message::terminal("\tRecord migrated"); 228 | 229 | $this->delete_records[$table][$result['ID']] = [$result['ID']]; 230 | } 231 | 232 | $this->deleteLegacyData(); 233 | } 234 | 235 | /** 236 | * @param $result 237 | * @return array 238 | */ 239 | protected function prepareKeys($table, $result) 240 | { 241 | $data = []; 242 | foreach ($result as $key => $val) { 243 | $data[$key] = $val; 244 | } 245 | 246 | return $data; 247 | } 248 | 249 | /** 250 | * @param $query 251 | * @return \SilverStripe\ORM\Connect\Query 252 | */ 253 | protected function getResults($from, $select, $where) 254 | { 255 | $query = new SQLSelect(); 256 | $query->addFrom($from); 257 | if ($where !== null) { 258 | $query->addWhere($where); 259 | } 260 | 261 | $this->extend('updateResultsQuery', $query); 262 | 263 | return $query->execute(); 264 | } 265 | 266 | /** 267 | * @param $query 268 | * @return \SilverStripe\ORM\Connect\Query 269 | */ 270 | protected function getDBQuery($query) 271 | { 272 | return DB::query($query); 273 | } 274 | 275 | /** 276 | * @param string $class 277 | * @return string 278 | */ 279 | protected function getDefaultTitle($class) 280 | { 281 | $config = $this->config()->get('default_title'); 282 | if (!is_array($config) || is_null($config) || !isset($config[$class])) { 283 | $title = "Migrated {$class::singleton()->singular_name()} record"; 284 | } else { 285 | $config[$class]; 286 | } 287 | 288 | return $title; 289 | } 290 | 291 | /** 292 | * 293 | */ 294 | protected function deleteLegacyData() 295 | { 296 | foreach ($this->delete_records as $table => $ids) { 297 | foreach ($ids as $key => $val) { 298 | //todo this doesn't consistently delete the data from the previous table as needed 299 | $where = ["\"{$table}\".\"ID\"" => $val]; 300 | $deleteQuery = SQLDelete::create() 301 | ->setFrom($table) 302 | ->setWhere($where); 303 | $deleteQuery->execute(); 304 | 305 | unset($deleteQuery); 306 | } 307 | } 308 | } 309 | } -------------------------------------------------------------------------------- /src/Tools/BlockElementTranslator.php: -------------------------------------------------------------------------------- 1 | exists()) { 43 | $element = Injector::inst()->create($elementType, $block->toMap(), false); 44 | 45 | $element->setClassName($elementType); 46 | $element->populateDefaults(); 47 | $element->forceChange(); 48 | 49 | self::singleton()->extend('updateNewElementInstance', $element); 50 | 51 | if (!empty($relations)) { 52 | static::duplicateRelations($block, $element, $relations); 53 | } 54 | 55 | $element->write(); 56 | 57 | if ($block->hasMethod('isPublished')) { 58 | $element->writeToStage(Versioned::DRAFT); 59 | 60 | if ($block->isPublished()) { 61 | $element->publishRecursive(); 62 | } 63 | } 64 | 65 | return $element; 66 | } 67 | } 68 | 69 | /** 70 | * Copies the given relations from this object to the destination. 71 | * This method was adopted from DataObject to support cross-object relation migrations. 72 | * 73 | * @param DataObject $sourceObject the source object to duplicate from 74 | * @param DataObject $destinationObject the destination object to populate with the duplicated relations 75 | * @param array $relations List of relations 76 | */ 77 | protected static function duplicateRelations($sourceObject, $destinationObject, $relations) 78 | { 79 | // Get list of duplicable relation types 80 | $manyMany = $sourceObject->manyMany(); 81 | $hasMany = $sourceObject->hasMany(); 82 | $hasOne = $sourceObject->hasOne(); 83 | $belongsTo = $sourceObject->belongsTo(); 84 | 85 | // Duplicate each relation based on type 86 | foreach ($relations as $blockRelation => $elementRelation) { 87 | switch (true) { 88 | case array_key_exists($blockRelation, $manyMany): 89 | { 90 | static::duplicateManyManyRelation($sourceObject, $destinationObject, $blockRelation, 91 | $elementRelation); 92 | break; 93 | } 94 | case array_key_exists($blockRelation, $hasMany): 95 | { 96 | static::duplicateHasManyRelation($sourceObject, $destinationObject, $blockRelation, 97 | $elementRelation); 98 | break; 99 | } 100 | case array_key_exists($blockRelation, $hasOne): 101 | { 102 | static::duplicateHasOneRelation($sourceObject, $destinationObject, $blockRelation, 103 | $elementRelation); 104 | break; 105 | } 106 | case array_key_exists($blockRelation, $belongsTo): 107 | { 108 | static::duplicateBelongsToRelation($sourceObject, $destinationObject, $blockRelation, 109 | $elementRelation); 110 | break; 111 | } 112 | default: 113 | { 114 | $sourceType = get_class($sourceObject); 115 | throw new InvalidArgumentException( 116 | "Cannot duplicate unknown relation {$relation} on parent type {$sourceType}" 117 | ); 118 | } 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Copies the many_many and belongs_many_many relations from one object to another instance of the name of object. 125 | * 126 | * @deprecated 4.1...5.0 Use duplicateRelations() instead 127 | * @param DataObject $sourceObject the source object to duplicate from 128 | * @param DataObject $destinationObject the destination object to populate with the duplicated relations 129 | * @param bool|string $filter 130 | */ 131 | protected static function duplicateManyManyRelations($sourceObject, $destinationObject, $filter) 132 | { 133 | Deprecation::notice('5.0', 'Use duplicateRelations() instead'); 134 | 135 | // Get list of relations to duplicate 136 | if ($filter === 'many_many' || $filter === 'belongs_many_many') { 137 | $relations = $sourceObject->config()->get($filter); 138 | } elseif ($filter === true) { 139 | $relations = $sourceObject->manyMany(); 140 | } else { 141 | throw new InvalidArgumentException("Invalid many_many duplication filter"); 142 | } 143 | foreach ($relations as $manyManyName => $type) { 144 | static::duplicateManyManyRelation($sourceObject, $destinationObject, $manyManyName); 145 | } 146 | } 147 | 148 | /** 149 | * Duplicates a single many_many relation from one object to another. 150 | * 151 | * @param DataObject $sourceObject 152 | * @param DataObject $destinationObject 153 | * @param string $relation 154 | */ 155 | protected static function duplicateManyManyRelation( 156 | $sourceObject, 157 | $destinationObject, 158 | $blockRelation, 159 | $elementRelation 160 | ) { 161 | // Copy all components from source to destination 162 | $source = $sourceObject->getManyManyComponents($blockRelation); 163 | $dest = $destinationObject->getManyManyComponents($elementRelation); 164 | 165 | $destClass = $dest->dataClass(); 166 | 167 | if ($source instanceof ManyManyList) { 168 | $extraFieldNames = $source->getExtraFields(); 169 | } else { 170 | $extraFieldNames = []; 171 | } 172 | 173 | foreach ($source as $item) { 174 | // Merge extra fields 175 | $extraFields = []; 176 | foreach ($extraFieldNames as $fieldName => $fieldType) { 177 | $extraFields[$fieldName] = $item->getField($fieldName); 178 | } 179 | 180 | if ($item->ClassName != $destClass) { 181 | $clonedItem = $item->newClassInstance($destClass); 182 | 183 | $clonedItem->write(); 184 | 185 | if ($clonedItem->hasExtension(Versioned::class)) { 186 | $clonedItem->writeToStage(Versioned::DRAFT); 187 | $clonedItem->publishRecursive(); 188 | } 189 | 190 | $dest->add($clonedItem, $extraFields); 191 | } else { 192 | $dest->add($item, $extraFields); 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * Duplicates a single many_many relation from one object to another. 199 | * 200 | * @param DataObject $sourceObject 201 | * @param DataObject $destinationObject 202 | * @param string $relation 203 | */ 204 | protected static function duplicateHasManyRelation( 205 | $sourceObject, 206 | $destinationObject, 207 | $blockRelation, 208 | $elementRelation 209 | ) { 210 | // Copy all components from source to destination 211 | $source = $sourceObject->getComponents($blockRelation); 212 | $dest = $destinationObject->getComponents($elementRelation); 213 | 214 | $newInstance = static::get_require_new_instance($sourceObject, $destinationObject, $blockRelation, 215 | $elementRelation); 216 | 217 | /** @var DataObject $item */ 218 | foreach ($source as $item) { 219 | // Don't write on duplicate; Wait until ParentID is available later. 220 | // writeRelations() will eventually write these records when converting 221 | // from UnsavedRelationList 222 | if (!$newInstance) { 223 | $clonedItem = $item->duplicate(false); 224 | } else { 225 | $clonedItem = $item->newClassInstance($newInstance); 226 | } 227 | 228 | /*if (static::singleton()->config()->get('explicit_data_transfer')) { 229 | $clonedItem = static::set_explicit($item, $clonedItem); 230 | }*/ 231 | 232 | $clonedItem->write(); 233 | 234 | if ($clonedItem->hasExtension(Versioned::class)) { 235 | $clonedItem->writeToStage(Versioned::DRAFT); 236 | $clonedItem->publishRecursive(); 237 | } 238 | 239 | $dest->add($clonedItem); 240 | } 241 | } 242 | 243 | /** 244 | * Duplicates a single has_one relation from one object to another. 245 | * Note: Child object will be force written. 246 | * 247 | * @param DataObject $sourceObject 248 | * @param DataObject $destinationObject 249 | * @param string $relation 250 | */ 251 | protected static function duplicateHasOneRelation( 252 | $sourceObject, 253 | $destinationObject, 254 | $blockRelation, 255 | $elementRelation 256 | ) { 257 | // Check if original object exists 258 | $item = $sourceObject->getComponent($blockRelation); 259 | if (!$item->isInDB()) { 260 | return; 261 | } 262 | 263 | $newInstance = static::get_require_new_instance($sourceObject, $destinationObject, $blockRelation, 264 | $elementRelation); 265 | $clonedItem = (!$newInstance) ? $item : $item->newClassInstance($elementRelation); 266 | 267 | $destinationObject->setComponent($elementRelation, $clonedItem); 268 | } 269 | 270 | /** 271 | * Duplicates a single belongs_to relation from one object to another. 272 | * Note: This will force a write on both parent / child objects. 273 | * 274 | * @param DataObject $sourceObject 275 | * @param DataObject $destinationObject 276 | * @param string $relation 277 | */ 278 | protected static function duplicateBelongsToRelation( 279 | $sourceObject, 280 | $destinationObject, 281 | $blockRelation, 282 | $elementRelation 283 | ) { 284 | // Check if original object exists 285 | $item = $sourceObject->getComponent($blockRelation); 286 | if (!$item->isInDB()) { 287 | return; 288 | } 289 | 290 | $newInstance = static::get_require_new_instance($sourceObject, $destinationObject, $blockRelation, 291 | $elementRelation); 292 | 293 | if (!$newInstance) { 294 | $clonedItem = $item->duplicate(false); 295 | } else { 296 | $clonedItem = $item->newClassInstance($elementRelation); 297 | } 298 | 299 | $destinationObject->setComponent($elementRelation, $clonedItem); 300 | // After $clonedItem is assigned the appropriate FieldID / FieldClass, force write 301 | // @todo Write this component in onAfterWrite instead, assigning the FieldID then 302 | // https://github.com/silverstripe/silverstripe-framework/issues/7818 303 | $clonedItem->write(); 304 | } 305 | 306 | /** 307 | * @param $sourceObject 308 | * @param $destinationObject 309 | * @param $blockRelation 310 | * @param $elementRelation 311 | * @return bool|string 312 | */ 313 | protected static function get_require_new_instance( 314 | &$sourceObject, 315 | &$destinationObject, 316 | &$blockRelation, 317 | &$elementRelation 318 | ) { 319 | return ($sourceObject->getRelationClass($blockRelation) == $destinationObject->getRelationClass($elementRelation)) 320 | ? false 321 | : $destinationObject->getRelationClass($elementRelation); 322 | } 323 | 324 | /** 325 | * @param $item 326 | * @param $clonedItem 327 | * @return mixed 328 | */ 329 | protected static function set_explicit($item, $clonedItem) 330 | { 331 | foreach ($item->db() as $field) { 332 | $clonedItem->$field = $item->$field; 333 | } 334 | 335 | return $clonedItem; 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/Tasks/BlocksToElementsTask.php: -------------------------------------------------------------------------------- 1 | config()->get('migration_mapping'); 86 | 87 | foreach ($migrationMapping as $block => $mapping) { 88 | if (isset($mapping['MigrateOptionFromTable'])) { 89 | foreach ($mapping['MigrateOptionFromTable'] as $relationName => $tableData) { 90 | if (isset($mapping['Relations']) && isset($mapping['Relations'][$relationName])) { 91 | $manipulationConfig = $this->getManipulationConfig($tableData); 92 | 93 | $manipulator = new DataManipulator($manipulationConfig); 94 | $manipulator->migrateData(); 95 | } 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * BlockManager used to get legacy block areas of a particular class. 102 | */ 103 | $manager = BlockManager::singleton(); 104 | 105 | /** 106 | * @param $blockSet 107 | */ 108 | $processSet = function ($blockSet) use (&$manager, &$migrationMapping) { 109 | 110 | /** 111 | * translate the BlockSet to an ElementalSet 112 | */ 113 | $elementalSet = $blockSet->newClassInstance(ElementalSet::class); 114 | $elementalSet->write(); 115 | 116 | /** 117 | * get the ManyManyList relation PageParents of the BlockSet and ElementalSet 118 | */ 119 | $blockPageParents = $blockSet->getManyManyComponents('PageParents'); 120 | $elementPageParents = $elementalSet->getManyManyComponents('PageParents'); 121 | 122 | /** 123 | * get the ManyManyList relation of Blocks 124 | */ 125 | $blocks = $blockSet->getManyManyComponents('Blocks'); 126 | 127 | /** 128 | * find or make related ElementalArea 129 | */ 130 | $elementalArea = ElementalAreaGenerator::find_or_make_elemental_area($elementalSet, null); 131 | 132 | /** 133 | * get many_many_extraFields field names of the Blocks relation. this will be used to get the values for migration. 134 | */ 135 | if ($blocks instanceof ManyManyList) { 136 | $extraFieldNames = $blocks->getExtraFields(); 137 | } else { 138 | $extraFieldNames = []; 139 | } 140 | 141 | /** 142 | * migrate parent pages (all child pages will show this set) 143 | */ 144 | foreach ($blockPageParents as $blockPageParent) { 145 | $elementPageParents->add($blockPageParent); 146 | } 147 | 148 | /** 149 | * migrate blocks to elements 150 | */ 151 | foreach ($blocks as $block) { 152 | Message::terminal("Migrating {$block->ClassName} - {$block->ID} for page {$elementalSet->ClassName} - {$elementalSet->ID}."); 153 | 154 | /** 155 | * if we have the mapping data for this block, migrate it to the corresponding Element 156 | */ 157 | if (isset($migrationMapping[$block->ClassName])) { 158 | /** 159 | * get the relations to migrate 160 | */ 161 | $relations = (isset($migrationMapping[$block->ClassName]['Relations'])) ?: false; 162 | 163 | /** 164 | * get the resulting Element from the block and mapping data 165 | */ 166 | $element = BlockElementTranslator::translate_block($block, 167 | $migrationMapping[$block->ClassName]['NewObject'], $relations, $elementalArea->ID); 168 | } 169 | 170 | Message::terminal("End migrating {$block->ClassName}.\n\n"); 171 | } 172 | 173 | $elementalSet->write(); 174 | $elementalSet->writeToStage(Versioned::DRAFT); 175 | $elementalSet->publishRecursive(); 176 | }; 177 | 178 | foreach ($this->yieldBlockSets() as $blockSet) { 179 | $processSet($blockSet); 180 | }//*/ 181 | 182 | /** 183 | * array used to track what we know about classes and their areas. 184 | */ 185 | $mappedAreas = []; 186 | 187 | /** 188 | * @param $page a page to process related blocks to elements 189 | */ 190 | $processPage = function ($page) use (&$manager, &$mappedAreas, &$migrationMapping) { 191 | $class = $page->ClassName; 192 | 193 | if ($page->getObsoleteClassName()) { 194 | Message::terminal("Page - {$page->ID} of obsolete class {$page->getObsoleteClassName()}"); 195 | $page->ClassName = \Page::class; 196 | } 197 | 198 | $properPage = $class::get()->byID($page->ID); 199 | 200 | if (!isset($mappedAreas[$properPage->ClassName])) { 201 | $mappedAreas[$properPage->ClassName] = $manager->getAreasForPageType($properPage->ClassName); 202 | } 203 | 204 | $original = $updated = BlockManager::singleton()->config()->get('options'); 205 | $updated['use_blocksets'] = false; 206 | 207 | Config::modify()->set(BlockManager::class, 'options', $updated); 208 | 209 | foreach ($this->yieldMulti($mappedAreas[$properPage->ClassName]) as $area => $title) { 210 | $this->processBlockRecords($properPage, $area, $page->getBlockList($area), $migrationMapping); 211 | }//*/ 212 | 213 | Config::modify()->set(BlockManager::class, 'options', $original); 214 | }; 215 | 216 | foreach ($this->yieldPages() as $page) { 217 | $processPage($page); 218 | }//*/ 219 | } 220 | 221 | /** 222 | * @return \Generator 223 | */ 224 | protected function yieldBlockSets() 225 | { 226 | foreach (BlockSet::get() as $set) { 227 | yield $set; 228 | } 229 | } 230 | 231 | /** 232 | * @return \Generator 233 | */ 234 | protected function yieldPages() 235 | { 236 | foreach (SiteTree::get()->sort('ID') as $page) { 237 | yield $page; 238 | } 239 | } 240 | 241 | /** 242 | * @return mixed 243 | */ 244 | public function getBlockClassMapping() 245 | { 246 | if (!$this->block_class_mapping) { 247 | $this->setBlockClassMapping(); 248 | } 249 | 250 | return $this->block_class_mapping; 251 | } 252 | 253 | /** 254 | * @return $this 255 | */ 256 | public function setBlockClassMapping() 257 | { 258 | $this->block_class_mapping = $this->parseMapping('mappings'); 259 | 260 | return $this; 261 | } 262 | 263 | protected function parseMapping($key = '') 264 | { 265 | return $this->config()->get($key); 266 | } 267 | 268 | /** 269 | * @param $configInformation 270 | */ 271 | protected function updateBlocksClassName($configInformation) 272 | { 273 | foreach ($this->yieldBlocks($configInformation['LegacyName']) as $block) { 274 | $block->ClassName = $configInformation['NewName']; 275 | $block->write(); 276 | } 277 | } 278 | 279 | /** 280 | * @param $className 281 | * @return \Generator 282 | */ 283 | protected function yieldBlocks($className) 284 | { 285 | foreach (Block::get()->filter('ClassName', $className) as $record) { 286 | yield $record; 287 | } 288 | } 289 | 290 | /** 291 | * @param $page The page owning the existing blocks and the new elements 292 | * @param $area The name of the BlockArea the records belong to 293 | * @param $records The legacy block records to migrate to elements 294 | * @param $mapping The block to element mapping array 295 | * @throws \SilverStripe\ORM\ValidationException 296 | */ 297 | protected function processBlockRecords($page, $area, $records, $mapping) 298 | { 299 | $area = ElementalAreaGenerator::find_or_make_elemental_area($page, $area); 300 | 301 | foreach ($this->yieldSingle($records) as $record) { 302 | Message::terminal("Migrating {$record->ClassName} - {$record->ID} for page {$page->ClassName} - {$page->ID}."); 303 | 304 | if (isset($mapping[$record->ClassName]) && isset($mapping[$record->ClassName]['NewObject'])) { 305 | if ($page->hasMethod('getElementalRelations')) { 306 | $relations = (isset($mapping[$record->ClassName]['Relations'])) ? $mapping[$record->ClassName]['Relations'] : false; 307 | $element = BlockElementTranslator::translate_block($record, 308 | $mapping[$record->ClassName]['NewObject'], $relations); 309 | 310 | if ($element instanceof BaseElement && $area instanceof ElementalArea && $record instanceof DataObject) { 311 | $element->ParentID = $area->ID; 312 | $element->LegacyID = $record->ID; 313 | } else { 314 | Message::terminal("Something is a non-object"); 315 | } 316 | 317 | if ($record->hasMethod('isPublished')) { 318 | $element->writeToStage(Versioned::DRAFT); 319 | 320 | if ($record->isPublished()) { 321 | $element->publishRecursive(); 322 | } 323 | } else { 324 | $element->write(); 325 | } 326 | } else { 327 | Message::terminal("{$record->ClassName} is not mapped. This class may not exist or needs to be added to the mapping."); 328 | } 329 | } 330 | 331 | Message::terminal("End migrating {$record->ClassName}.\n\n");//*/ 332 | }//*/ 333 | } 334 | 335 | /** 336 | * @param $records 337 | * @return \Generator 338 | */ 339 | protected function yieldSingle($records) 340 | { 341 | foreach ($records as $record) { 342 | yield $record; 343 | } 344 | } 345 | 346 | /** 347 | * @param $records 348 | * @return \Generator 349 | */ 350 | protected function yieldMulti($records) 351 | { 352 | foreach ($records as $key => $val) { 353 | yield $key => $val; 354 | } 355 | } 356 | 357 | /** 358 | * @param $tableData 359 | * @return array 360 | */ 361 | protected function getManipulationConfig($tableData) 362 | { 363 | $config = []; 364 | foreach ($tableData as $key => $val) { 365 | if ($key != 'UseParentTable') { 366 | $config['SourceTable'] = $key; 367 | $config['ToClass'] = $val; 368 | } else { 369 | $config['ParentTable'] = true; 370 | } 371 | } 372 | 373 | return $config; 374 | } 375 | } 376 | --------------------------------------------------------------------------------