├── 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 |
--------------------------------------------------------------------------------