Migrating from sheadawson/silverstripe-linkable module#
NOTE
If your site is running Silverstripe CMS 4.x, upgrade to CMS 5 first.
You will most likely need to use a fork of sheadawson/silverstripe-linkable that is compatible with Silverstripe CMS 5 as part of this upgrade.
Once you have finished upgrading to CMS 5, return to this guide and continue the linkfield upgrade.
The sheadawson/silverstripe-linkable module was a much loved, and much used module. It is, unfortunately, no longer maintained. We have provided some steps and tasks that we hope can be used to migrate your project from Linkable to LinkField.
There are a few major changes between sheadawson/silverstripe-linkable and silverstripe/linkfield:
- Link types are defined via subclasses in
silverstripe/linkfieldas opposed to configuration within a single model. silverstripe/linkfielddoesn't supportmany_manyrelations - these will be migrated tohas_manyrelations instead.- Many fields and relations have different names.
- The default title for a link isn't stored in the database - if the
LinkTextfield is left blank, nothing gets stored in the database for that field.- This means any links migrated which had the default title set will be migrated with that title as explicit
LinkText, which will not update automatically when you change the link URL. - If you want the
LinkTextfor those links to update automatically, you will need to either customise the migration or manually unset theLinkTextfor those links afterward.
- This means any links migrated which had the default title set will be migrated with that title as explicit
The following additional items were identified as feature gaps, which may require some additional work to implement if you require them:
- Adding custom CSS classes to link. The
setCSSClass()method does not exist in Linkfield. You can still add anExtensionto theLinkclass or develop your own custom link class and implement the logic of this method. - Customizing link templates. You can still call the
renderWith()method and pass the name of your custom template, or use a template with the file path of the FQCN of the link subclass, butLinkFielddoesn't support thetemplatesconfiguration. - Limit allowed Link types. The
silverstripe/linkfieldmodule does not support theallowed_typesconfiguration. Now, in order to set a limited list of link types available to the user, you should use theLinkField::setAllowedTypes()method. Useallowed_by_defaultconfiguration to globally limit link types. - Custom query params. This functionality is not supported. You can no longer set the
data-extra-queryattribute to a link. But you can still add an extension to the link and template that will allow you to implement this logic. - The
EmbeddedObjectandEmbeddedObjectFieldclasses have no equivalent functionality insilverstripe/linkfield - If you have subclassed
Sheadawson\Linkable\Models\Link, there may be additional steps you need to take to migrate the data for your subclass.
WARNING
This guide and the associated migration task assume all of the data for your links are in the base table for Sheadawson\Linkable\Models\Link or in automatically generated tables (e.g. join tables for many_many relations).
Setup#
TIP
We strongly recommend taking a backup of your database before doing anything else. This will ensure you have a known state to revert to in case anything goes wrong.
Update your dependencies#
Remove the Linkable module and add silverstripe/linkfield:
composer remove sheadawson/silverstripe-linkable
composer require silverstripe/linkfield:^4
Configure the migration task#
NOTE
Be sure to check how the old module classes are referenced in config yml files (eg: app/_config). Update appropriately.
-
Enable the task:
yamlSilverStripe\LinkField\Tasks\LinkableMigrationTask: is_enabled: true
WARNING
The sheadawson/silverstripe-linkable documentation does not provide guidance or advice on setting up and maintaining has_many and many_many link relationships. This guide and the corresponding migration task only make an assumption how this setting was made. It is your responsibility to check that this assumption suits your case and customising the migration task as required.
-
Declare any columns that you added to the linkable link model which need to be migrated to the new base link table, for example if you added a custom sort column for your
has_manyrelations:yamlSilverStripe\LinkField\Tasks\LinkableMigrationTask: # ... base_link_columns: MySortColumn: 'Sort' -
Declare any
has_manyrelations that need to be migrated:yamlSilverStripe\LinkField\Tasks\LinkableMigrationTask: # ... has_many_links_data: # The class where the has_many relation is declared App\Model\MyClass: # The key is the name of the has_many relation # The value is the name of the old has_one relation on the Linkable link model LinkListOne: 'MyOwner' -
Declare any
many_manyrelations that need to be migrated:yamlSilverStripe\LinkField\Tasks\LinkableMigrationTask: # ... many_many_links_data: # The class where the many_many relation is declared App\Model\MyClass: # If it's a normal many_many relation with no extra fields, # you can simply set the value to null and let the migration task figure it out LinkListExample: null # If the many_many is a many_many through, or had a $many_many_extraFields configuration defined, # you will need to provide additional information LinkListTwo: # The table is required for many_many through table: 'Page_ManyManyLinks' # Extra fields is for $many_many_extraFields, or for any $db fields on a # many_many through join model extraFields: MySort: 'Sort' # For many_many through relations, you must add the names of the has_one relations # from the DataObject which was used as the join model through: from: 'FromHasOneName', to: 'ToHasOneName', -
Declare any classes that may have
has_onerelations toLink, but which do not own the link. Classes declared here will include any subclasses. For example if a custom link has ahas_manyrelation to some class which does not own the link, declare that class here so it is not incorrectly identified as the owner of the link:yamlSilverStripe\LinkField\Tasks\LinkableMigrationTask: # ... classes_that_are_not_link_owners: - App\Model\SomeClass
Update your codebase#
You should review how you are using the original Link model and LinkField, but if you don't have any customisations, then replacing the old with the new might be quite simple.
-
If you added any database columns to the
Linkclass for sortinghas_manyrelations, or anyhas_onerelations for storing them, remove the extension or YAML configuration for that now.diff- Sheadawson\Linkable\Models\Link: - db: - MySortColumn: Int - has_one: - MyOwner: App\Model\MyClass - belongs_many_many: - BelongsRecord : App\Model\MyClass.LinkListTwo -
Update use statements and relations for the classes which own links.
- Any
many_manyrelations should be swapped out forhas_manyrelations, and allhas_manyrelations should point to theOwnerrelation on the link class via dot notation. - If the models that have
has_oneorhas_manyrelations to link don't already use the$ownsconfiguration for those relations, add that now. You may also want to set$cascade_deletesand$cascade_duplicatesconfiguration. See basic usage for more details.ed.
diffnamespace App\Model; - use Sheadawson\Linkable\Models\Link; - use Sheadawson\Linkable\Forms\LinkField; + use SilverStripe\LinkField\Models\Link; + use SilverStripe\LinkField\Form\LinkField; + use SilverStripe\LinkField\Form\MultiLinkField; use SilverStripe\ORM\DataObject; class MyClass extends DataObject { private static array $has_one = [ 'HasOneLink' => Link::class, ]; private static array $has_many = [ - 'LinkListOne' => Link::class . '.MyOwner', + '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' => [ - 'MySort' => 'Int', - ] - ]; } - Any
-
If you had
many_manythrough relation, delete theDataObjectclass which was used as the join table. -
Update the usage of link field (and
GridFieldif you were using that to managehas_manyormany_manyrelations).- Note that the linkable module's
LinkFieldrequired you to specify the related field withIDappended (e.g.HasOneLinkID), whereas the newLinkFieldrequires you to specify the field withoutIDappended (e.g.HasOneLink).
diffpublic function getCMSFields() { $fields = parent::getCMSFields(); + $fields->removeByName(['HasOneLinkID', 'LinkListOne', 'LinkListTwo']); $fields->addFieldsToTab( 'Root.Main', [ - LinkField::create('HasOneLinkID', 'Has one link') + LinkField::create('HasOneLink', 'Has one link'), - GridField::create( - 'LinkListTwo', - 'Link List Two', - $this->LinkListTwo(), - GridFieldConfig_RelationEditor::create() - ->removeComponentsByType(GridFieldAddExistingAutocompleter::class) - ), + MultiLinkField::create('LinkListOne', 'List list one'), + MultiLinkField::create('LinkListTwo', 'List list two'), ] ); return $fields; } - Note that the linkable module's
-
In
sheadawson/silverstripe-linkable, the list of allowed link types was listed in the configuration file.LinkFielduses a different approach, it is necessary to specify in the configuration those types of links that will be unavailable to the user. If you need to make a certain type of link available, you must use theLinkField::setAllowedTypes()method and pass an array of class names as a parameter. Useallowed_by_defaultif it's needed to globally limit link types.- See configuring links and link fields for more information.
diff- Sheadawson\Linkable\Models\Link: - allowed_types: - - URL - - SiteTree // Now you should exclude all link types that are not allowed + SilverStripe\LinkField\Models\EmilLink: + allowed_by_default: false + SilverStripe\LinkField\Models\PhoneLink: + allowed_by_default: false + SilverStripe\LinkField\Models\FileLink: + allowed_by_default: falsediff+ use SilverStripe\LinkField\Models\ExternalLink; + use SilverStripe\LinkField\Models\SiteTreeLink; - $allowedTypes = [ - 'SiteTree', - 'URL', - ]; + $allowedTypes = [ + SiteTreeLink::class, + ExternalLink::class, + ]; $linkField->setAllowedTypes($allowedTypes);
Populate module#
If you use the dnadesign/silverstripe-populate module, you will not be able to simply "replace" the namespace. Fixture definitions for the new Linkfield module are quite different. There are entirely different models for different link types, whereas before it was just a DB field to specify the type.
See below for example:
- Sheadawson\Linkable\Models\Link:
- internal:
- Title: Internal link
- Type: SiteTree
- SiteTreeID: 1
- OpenInNewWindow: true
+ SilverStripe\LinkField\Models\SiteTreeLink:
+ internal:
+ LinkText: Internal link
+ Page: =>Page.home
+ OpenInNew: true
- external:
- Title: External link
- Type: URL
- URL: https://example.org
- OpenInNewWindow: true
+ SilverStripe\LinkField\Models\ExternalLink:
+ external:
+ LinkText: External link
+ ExternalUrl: https://example.org
+ OpenInNew: true
- file:
- Title: File link
- Type: File
- File: =>SilverStripe\Assets\File.example
+ SilverStripe\LinkField\Models\FileLink:
+ file:
+ LinkText: File link
+ File: =>SilverStripe\Assets\File.example
- phone:
- Title: Phone link
- Type: Phone
- Phone: +64 1 234 567
+ SilverStripe\LinkField\Models\PhoneLink:
+ phone:
+ LinkText: Phone link
+ Phone: +64 1 234 567
- email:
- Title: Email link
- Type: Email
- Email: foo@example.org
+ SilverStripe\LinkField\Models\EmailLink:
+ email:
+ LinkText: Email link
+ Email: foo@example.org
Replace template usages#
The link element structure is rendered using the SilverStripe/LinkField/Models/Link.ss template. You can override this template by copying it to the theme or project folder and making the necessary changes. You still can also specify a custom template to display the link by using the renderWith() method and passing the name of your custom template.
You can also provide templates for specific subclasses of Link - the file path for those templates is the FQCN for the link.
When working on your own template, you should consider the following differences in variable names.
Before: You might have had references to $LinkURL or $Link.LinkURL.
After: These would need to be updated to $URL or $Link.URL respectively.
Before: $OpenInNewWindow or $Link.OpenInNewWindow.
After: $OpenInNew or $Link.OpenInNew respectively.
Before: $Link.TargetAttr or $TargetAttr would output the appropriate target="xx".
After: There is no direct replacement.
Customising the migration#
There are many extension hooks in the LinkableMigrationTask class which you can use to change its behaviour or add additional migration steps. We strongly recommend taking a look at the source code to see if your use case requires any customisations.
Some scenarios where you may need customisations include:
- You had applied the
Versionedextension toLinkand want to retain that versioning history - You subclassed the base
Linkmodel and need to migrate data from your custom subclass - You were relying on features of
sheadawson/silverstripe-linkableorsheadawson/silverstripe-linkablewhich don't have a 1-to-1 equivalent insilverstripe/linkfield
Custom links#
If you have custom link implementations, you will need to implement an appropriate subclass of Link (or apply an extension to an existing one) with appropriate database columns and relations.
You need to update configuration LinkableMigrationTask so it knows how to handle the migration from the old link to the new one:
SilverStripe\LinkField\Tasks\LinkableMigrationTask:
# ...
link_type_columns:
# The name of the Type for your custom type as defined in =====
MyCustomType:
# The FQCN for your new custom link subclass
class: 'App\Model\Link\MyCustomLink'
# An mapping of column names from the old link to your new link subclass
# Only include columns that are defined in the $db configuration for your subclass
fields:
MyOldField: 'MyNewField'
Migrating#
NOTE
This migration process covers shifting data from the LinkableLink tables to the appropriate LinkField tables.
For databases that support transactions, the full data migration is performed within a single transaction, and any errors in the migration will result in rolling back all changes. This means you can address whatever caused the error and then run the task again.
NOTE
We strongly recommend running this task in a local development environment before trying it in production. There may be edge cases that the migration task doesn't account for which need to be resolved.
- Run dev/build and flush your cache (use the method you will be using the for next step - i.e. if you're running the task via the terminal, make sure to flush via the terminal)
- via the browser:
https://www.example.com/dev/build?flush=1 - via a terminal:
sake dev/build flush=1
- via the browser:
- Run the task
- via the browser:
https://www.example.com/dev/tasks/linkable-to-linkfield-migration-task - via a terminal:
sake dev/tasks/linkable-to-linkfield-migration-task
- via the browser:
The task performs the following steps:
- Inserts new rows into the base link table, taking values from the old link table.
- Inserts new rows into tables for link subclasses, taking values from the old link table.
- Updates
SiteTreeLinkrecords, splitting out the oldAnchorcolumn into the separateAnchorandQueryStringcolumns. - Migrates any
has_manyrelations which were declared inLinkableMigrationTask.has_many_links_data. - Migrates any
many_manyrelations which were declared in inLinkableMigrationTask.many_many_links_dataand drops the old join tables. - Set the
Ownerrelation forhas_onerelations to links. - Drops the old link table.
- Publishes all links, unless you have removed the
Versionedextension. - Output a table with any links which are lacking the data required to generate a URL.
- You can skip this step by adding
?skipBrokenLinks=1to the end of the URL:https://www.example.com/dev/tasks/linkable-to-linkfield-migration-task?skipBrokenLinks=1. - If you're running the task in a terminal, you can add
skipBrokenLinks=1as an argument:sake dev/tasks/linkable-to-linkfield-migration-task skipBrokenLinks=1.
- You can skip this step by adding
WARNING
If the same link appears in multiple many_many relation lists within the same relation (with different owner records), the link will be duplicated so that a single link exists for each has_many relation list.
Unless you were doing something custom to manage links it's unlikely this will affect you - but if it does, just be aware of this and prepare your content authors for this change in their authoring workflow.
If the same link appears in multiple many_many relation lists across different relations, you will need to handle the migration of this scenario yourself. The migration task will not duplicate these links. The link's owner will be whichever record is first identified, and any further owner records will simply not have that link in their has_many relation list.