-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEW Add migration task from gorriecoe/silverstripe-link
- Loading branch information
1 parent
967c457
commit b992618
Showing
2 changed files
with
375 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
# Title to shut my IDE linter up | ||
|
||
> [!WARNING] | ||
> This guide and the associated migration task assume all of the data for your links are in the base table for `gorriecoe\Link\Models\Link` or in automatically generated tables (e.g. join tables for `many_many` relations). | ||
> If you have subclassed `gorriecoe\Link\Models\Link`, there may be additional steps you need to take to migrate the data for your subclass. | ||
Remove the gorriecoe modules and add silverstripe/linkfield: | ||
|
||
```bash | ||
composer require silverstripe/linkfield:^4 | ||
composer remove gorriecoe/silverstripe-link gorriecoe/silverstripe-linkfield | ||
``` | ||
|
||
If you added any database columns to the `Link` class for sorting `has_many` relations, or any `has_one` relations for storing them, remove the extension or yaml configuration for that now. | ||
|
||
```diff | ||
- gorriecoe\Link\Models\Link: | ||
- db: | ||
- MySortColumn: Int | ||
- has_one: | ||
- Record: App\Model\MyRecord | ||
- belongs_many_many: | ||
- BelongsRecord : App\Model\MyRecord.LinkListTwo | ||
``` | ||
|
||
Update namespaces and relations. | ||
|
||
```diff | ||
namespace App\Model; | ||
|
||
- use gorriecoe\Link\Models\Link; | ||
- use gorriecoe\LinkField\LinkField; | ||
+ use SilverStripe\LinkField\Models\Link; | ||
+ use SilverStripe\LinkField\Form\LinkField; | ||
+ use SilverStripe\LinkField\Form\MultiLinkField; | ||
use SilverStripe\ORM\DataObject; | ||
|
||
class MyRecord extends DataObject | ||
{ | ||
private static array $has_one = [ | ||
'HasOneLink' => Link::class, | ||
]; | ||
|
||
private static array $has_many = [ | ||
- 'LinkListOne' => Link::class . '.Record', | ||
+ 'LinkListOne' => Link::class . '.Owner', | ||
+ 'LinkListTwo' => Link::class . '.Owner', | ||
]; | ||
|
||
+ private static array $owns = [ | ||
+ 'HasOneLink', | ||
+ 'LinkListOne', | ||
+ 'LinkListTwo', | ||
+ ]; | ||
+ | ||
- private static array $many_many = [ | ||
- 'LinkListTwo' => Link::class, | ||
- ]; | ||
- | ||
- private static array $many_many_extraFields = [ | ||
- 'LinkListTwo' => [ | ||
- 'Sort' => 'Int', | ||
- ] | ||
- ]; | ||
|
||
public function getCMSFields() | ||
{ | ||
$fields = parent::getCMSFields(); | ||
+ $fields->removeByName(['LinkListOneID', 'LinkListOne', 'LinkListTwo']); | ||
$fields->addFieldsToTab( | ||
'Root.Main', | ||
[ | ||
- LinkField::create('HasOneLink', 'Has one link', $this), | ||
- LinkField::create('LinkListOne', 'List list one', $this)->setSortColumn('MySortColumn'), | ||
- LinkField::create('LinkListTwo', 'Link list two', $this), | ||
+ LinkField::create('HasOneLink', 'Has one link'), | ||
+ MultiLinkField::create('LinkListOne', 'List list one'), | ||
+ MultiLinkField::create('LinkListTwo', 'Link list two'), | ||
] | ||
); | ||
return $fields; | ||
} | ||
} | ||
``` | ||
|
||
If you applied [linkfield configuration](https://github.com/elliot-sawyer/silverstripe-linkfield?tab=readme-ov-file#configuration), update that now also. | ||
|
||
```diff | ||
Example here. | ||
``` | ||
|
||
Custom link implementations you may be using include: | ||
|
||
- [gorriecoe/silverstripe-securitylinks](https://github.com/gorriecoe/silverstripe-securitylinks) | ||
- [gorriecoe/silverstripe-directionslink](https://github.com/gorriecoe/silverstripe-directionslink) | ||
- [gorriecoe/silverstripe-advancedemaillink](https://github.com/gorriecoe/silverstripe-advancedemaillinks) | ||
|
||
Other customisations you may be using that will require manual migration include: | ||
|
||
- [gorriecoe/silverstripe-linkicon](https://github.com/gorriecoe/silverstripe-linkicon) | ||
- [gorriecoe/silverstripe-ymlpresetlinks](https://github.com/gorriecoe/silverstripe-ymlpresetlinks) | ||
|
||
Things devs need to handle or be aware of: | ||
|
||
- Phone number validation | ||
- External URL link doesn't allow relative URLs | ||
- No `addExtraClass()` or related methods for templates | ||
- `getLinkURL()` is now just `getURL()` | ||
- No `SiteTree` helpers like `isCurrent()`, `isOrphaned()` etc (you can check those methods on the `Page` in `SiteTreeLink` instead) | ||
- Permission checks are based on `Owner` (previously just returned `true` for everything) | ||
- No `link_to_folders` config - uses `UploadField` instead. | ||
- No graphql helper methods - just use regular GraphQL scaffolding if you need to fetch the links via GraphQL. | ||
- No "Settings" tab | ||
- Can't swap link type. | ||
- https://github.com/elliot-sawyer/silverstripe-link/blob/master/src/extensions/DefineableMarkupID.php | ||
- https://github.com/elliot-sawyer/silverstripe-link/blob/master/src/extensions/DBStringLink.php | ||
- May want to update localisations. Link to transifex. | ||
|
||
One config for DB fields that are shared across all links | ||
|
||
```yml | ||
base_link_columns: | ||
ID: 'ID' | ||
OpenInNewWindow: 'OpenInNew' | ||
Title: 'LinkText' | ||
# Can add sort for has_many here too | ||
Sort: 'Sort' | ||
``` | ||
Sort is ascending in gorriecoe's module | ||
One config for per-type (i.e. dependent on the "Type" column) to class | ||
```yml | ||
link_type_columns: | ||
URL: | ||
class: 'SilverStripe\LinkField\Models\ExternalLink' | ||
fields: | ||
URL: 'ExternalUrl' | ||
Email: | ||
class: 'SilverStripe\LinkField\Models\EmailLink' | ||
fields: | ||
Email: 'Email' | ||
Phone: | ||
class: 'SilverStripe\LinkField\Models\PhoneLink' | ||
fields: | ||
Phone: 'Phone' | ||
File: | ||
class: 'SilverStripe\LinkField\Models\FileLink' | ||
fields: | ||
FileID: 'FileID' | ||
SiteTree: | ||
class: 'SilverStripe\LinkField\Models\SiteTreeLink' | ||
fields: | ||
SiteTreeID: 'PageID' | ||
``` | ||
Note that `SiteTreeLink` also needs to take the `Anchor` column and split it out between `Anchor` and `QueryString`, and remove the `#` and `?`prefixes respectively. | ||
If there's no prefix, make these assumptions: | ||
It's a query string if there's a `=` or `&` anywhere in it | ||
It's an anchor in all other cases. | ||
|
||
Note also that the `Title` may be equal to the default title, which differs based on the type. | ||
We do not want to copy default titles into `LinkText`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
<?php | ||
|
||
namespace SilverStripe\LinkField\Tasks; | ||
|
||
use SilverStripe\Control\Director; | ||
use SilverStripe\Dev\BuildTask; | ||
use SilverStripe\Dev\Deprecation; | ||
use SilverStripe\LinkField\Models\EmailLink; | ||
use SilverStripe\LinkField\Models\ExternalLink; | ||
use SilverStripe\LinkField\Models\FileLink; | ||
use SilverStripe\LinkField\Models\Link; | ||
use SilverStripe\LinkField\Models\PhoneLink; | ||
use SilverStripe\LinkField\Models\SiteTreeLink; | ||
use SilverStripe\ORM\DataObject; | ||
use SilverStripe\ORM\DB; | ||
use SilverStripe\ORM\Queries\SQLInsert; | ||
use SilverStripe\ORM\Queries\SQLSelect; | ||
|
||
/** | ||
* @deprecated 4.0.0 Will be removed without equivalent functionality. | ||
*/ | ||
class GorriecoeMigrationTask extends BuildTask | ||
{ | ||
private static $segment = 'gorriecoe-to-linkfield-migration-task'; | ||
|
||
protected $title = 'Gorriecoe to Linkfield Migration Task'; | ||
|
||
protected $description = 'Migrate from gorriecoe/silverstripe-link to silverstripe/linkfield'; | ||
|
||
/** | ||
* Enable via YAML configuration if you need to run this task | ||
*/ | ||
private static ?bool $is_enabled = false; | ||
|
||
/** | ||
* The name of the table for the gorriecoe\Link\Models\Link model. | ||
* | ||
* Configurable since it's such a generic name, there's a chance people configured | ||
* it to something different to avoid collisions. | ||
*/ | ||
private static string $old_link_table = 'Link'; | ||
|
||
/** | ||
* Classes which should be skipped when finding owners of links. | ||
* These classes and all of their subclasses will be skipped. | ||
*/ | ||
private static array $skip_hierarchies_for_owner = [ | ||
// Skip models that are used for internal tracking purposes and cannot own links | ||
ChangeSet::class, | ||
ChangeSetItem::class, | ||
WYSIWYGFileLink::class, | ||
WYSIWYGSiteTreeLink::class, | ||
]; | ||
|
||
/** | ||
* Mapping for columns in the base link table. | ||
* Doesn't include | ||
*/ | ||
private static array $base_link_columns = [ | ||
'OpenInNewWindow' => 'OpenInNew', | ||
'Title' => 'LinkText', | ||
]; | ||
|
||
/** | ||
* Mapping for different types of links, including the class to map to and | ||
* database column mappings. | ||
*/ | ||
private static array $link_type_columns = [ | ||
'URL' => [ | ||
'class' => ExternalLink::class, | ||
'fields' => [ | ||
'URL' => 'ExternalUrl', | ||
], | ||
], | ||
'Email' => [ | ||
'class' => EmailLink::class, | ||
'fields' => [ | ||
'Email' => 'Email', | ||
], | ||
], | ||
'Phone' => [ | ||
'class' => PhoneLink::class, | ||
'fields' => [ | ||
'Phone' => 'Phone', | ||
], | ||
], | ||
'File' => [ | ||
'class' => FileLink::class, | ||
'fields' => [ | ||
'FileID' => 'FileID', | ||
], | ||
], | ||
'SiteTree' => [ | ||
'class' => SiteTreeLink::class, | ||
'fields' => [ | ||
'SiteTreeID' => 'PageID', | ||
], | ||
], | ||
]; | ||
|
||
/** | ||
* The table name for the base gorriecoe link model. | ||
*/ | ||
private string $oldTableName; | ||
|
||
public function __construct() | ||
{ | ||
// Use withNoReplacement() because otherwise even viewing the dev/tasks list will trigger this warning. | ||
Deprecation::withNoReplacement( | ||
fn () => Deprecation::notice('4.0.0', 'Will be removed without equivalent functionality.', Deprecation::SCOPE_CLASS) | ||
); | ||
parent::__construct(); | ||
} | ||
|
||
public function run($request): void | ||
{ | ||
// If we don't need to migrate, exit early. | ||
if (!$this->getNeedsMigration()) { | ||
$this->print('Cannot perform migration.'); | ||
return; | ||
} | ||
|
||
$db = DB::get_conn(); | ||
if (!$db->supportsTransactions()) { | ||
$this->print('Database transactions are not supported for this database. Errors may result in a partially-migrated state.'); | ||
} | ||
|
||
$db->withTransaction([$this, 'performMigration'], [$this, 'failedTransaction']); | ||
|
||
$this->print('Done.'); | ||
} | ||
|
||
/** | ||
* Check if we actually need to migrate anything, and if not give clear output as to why not. | ||
*/ | ||
public function getNeedsMigration(): bool | ||
{ | ||
$oldTableName = static::config()->get('old_link_table'); | ||
$allTables = DB::table_list(); | ||
if (!in_array(strtolower($oldTableName), $allTables)) { | ||
$oldTableName = '_obsolete_' . $oldTableName; | ||
if (!in_array(strtolower($oldTableName), $allTables)) { | ||
$this->print('Nothing to migrate - old link table doesn\'t exist.'); | ||
return false; | ||
} | ||
} | ||
$this->oldTableName = $oldTableName; | ||
return true; | ||
} | ||
|
||
/** | ||
* Used in a callback if there is an error with the migration that causes a rolled back DB transaction | ||
*/ | ||
public function failedTransaction() | ||
{ | ||
if (DB::get_conn()->supportsTransactions()) { | ||
$this->print('There was an error with the migration. Rolling back.'); | ||
} | ||
} | ||
|
||
/** | ||
* Perform the actual data migration and publish links as appropriate | ||
*/ | ||
public function performMigration() | ||
{ | ||
// Get a full map of columns to migrate that applies to all link types | ||
$baseTableColumnMap = static::config()->get('base_link_columns'); | ||
foreach (array_keys(DataObject::config()->uninherited('fixed_fields')) as $fixedField) { | ||
// ClassName will need to be handled per link type | ||
if ($fixedField === 'ClassName') { | ||
continue; | ||
} | ||
$baseTableColumnMap[$fixedField] = $fixedField; | ||
} | ||
|
||
/* @TODO | ||
- Check if the `Link` table (or `_obsolete_Link` table) exists | ||
- If not, we can bail here. Nothing to migrate. | ||
- SQLInsert for each link using base column manpping | ||
- For each link, depending on the type: | ||
- Set correct ClassName in base table | ||
- SQLInsert into table for link subclass | ||
- throw error if class doesn't exist | ||
- throw error if class exists but table doesn't (unless mapping is empty) | ||
- Set owners for has_one relations | ||
- Update HasOneLinkClass as appropriate for polymorphic has_one | ||
- Update HasOneLinkRelation as appropriate for multi-relational has_one | ||
- If the relation the old value points at still exists, skip unless extension says do not skip | ||
- Set owners for has_many relations | ||
- Must handle polymorphic has_one from the link side | ||
- Set owners for old-many_many relations | ||
- Including sort column mapping | ||
- Handle regular many_many as well as many_many through! | ||
- When fetching data from the old table (if you can't do everything in SQL), | ||
make sure to fetch in chunks of 2000 or so (make that number configurable) | ||
so that if there are a billion links we don't hit memory issues. | ||
*/ | ||
} | ||
|
||
/** | ||
* A convenience method for printing a line to the browser or terminal with appropriate line breaks. | ||
*/ | ||
private function print(string $message): void | ||
{ | ||
$eol = Director::is_cli() ? "\n" : '<br>'; | ||
echo $message . $eol; | ||
} | ||
} |