-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FR: Add migration from Fruit Link It field #51
Comments
If you have any guides, docs or scripts that would help other users with the migration we can include them with this package, feel free to open a pull request to add those. |
I've been working on this for a recent migration. It's unfortunately a pretty big pain in the butt to do. I'm going to go ahead and post my progress here as I proceed in case someone else finds it useful. These methods assume you've installed Link It in Craft 3 and run the migration. It can be safely uninstalled after that migration has ran. Updating field settings - tested and working on basic link types and settings: /**
* Convert field settings for a LinkIt field to a Typed Link Field.
* Run Craft::$app->fields->saveField() on the output of this method
*
* @param $settings - array of JSON decoded settings
* @param $field - array for row in craft_fields table
* @return LinkField
*/
private function processFieldSettings($settings, $field)
{
$typeMap = [
'fruitstudios\\linkit\\models\\Email' => 'email',
'fruitstudios\\linkit\\models\\Url' => 'url',
'fruitstudios\\linkit\\models\\Entry' => 'entry',
'fruitstudios\\linkit\\models\\Asset' => 'asset',
];
$newField = new LinkField();
$newField->allowCustomText = $settings['allowCustomText'] ?? true;
$newField->defaultText = $settings['defaultText'] ?? '';
$newField->allowTarget = $settings['allowTarget'] ?? true;
$newField->allowedLinkNames = [];
$types = $settings['types'];
foreach ($types as $typeClass => $typeSettings) {
if (!array_key_exists($typeClass, $typeMap)) {
print('Failed to find class for link type: ' . $typeClass);
continue;
}
$mappedType = $typeMap[$typeClass];
$newField->allowedLinkNames[] = $mappedType;
$settings = [];
if (array_key_exists('sources', $typeSettings)) {
$settings['sources'] = $typeSettings['sources'];
}
$newField->typeSettings[$mappedType] = $settings;
}
$newField->defaultLinkName = $newField->allowedLinkNames[0];
$newField->id = $field['id'];
$newField->groupId = $field['groupId'];
$newField->name = $field['name'];
$newField->handle = $field['handle'];
$newField->context = $field['context'];
$newField->instructions = $field['instructions'];
$newField->searchable = $field['searchable'];
$newField->translationKeyFormat = $field['translationKeyFormat'];
$newField->uid = $field['uid'];
return $newField;
} Updating content table. Works on global context fields and Neo fields. Doesn't work on Matrix fields. /**
* Processes globally scoped link field settings. Does not work on matrix fields yet
*
* @param LinkField $field
*/
private function processFieldContent($field)
{
$handle = $field->handle;
$prefix = Craft::$app->content->fieldColumnPrefix;
$column = $prefix . $handle;
switch ($field->context) {
case 'global':
$content = (new Query())->select([$column, 'id', 'elementId'])
->from(Table::CONTENT)
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$oldSettings = \GuzzleHttp\json_decode($row[$column], true);
$settings = new Link(['linkField' => $field]);
$settings->type = $oldSettings['type']; // yay it's the same
switch ($oldSettings['type']) {
case 'custom':
$settings->value = $oldSettings['custom'];
break;
case 'entry':
$settings->value = array_pop($oldSettings['entry']);
break;
case 'category':
$settings->value = array_pop($oldSettings['category']);
break;
case 'asset':
$settings->value = array_pop($oldSettings['asset']);
break;
case 'email':
$settings->value = $oldSettings['email'];
break;
}
if (array_key_exists('target', $oldSettings)) {
$settings->target = $oldSettings['target'];
}
if (array_key_exists('title', $oldSettings)) {
$settings->title = $oldSettings['title'];
}
$newVal = \GuzzleHttp\json_encode($settings->toArray());
$qb = Craft::$app->db;
$updated = $qb->createCommand()->update(Table::CONTENT, [$column => $newVal], ['id' => $row['id']]);
print('Updated content row ' . $row['id'] . ' for element ' . $row['elementId'] . PHP_EOL);
}
}
} |
I'll close this issue due to inactivity. I've added a link to this issue in the readme for others that run into the same problem. |
To further the solution from @Mosnar I've created a console command others might find useful. The main difference is that it will switch the field from LinkIt to TypedLink instead of creating a new field. It also supports Matrix and Super Table. This is coming from Craft 2.9.2, LinkIt 0.9.1 to Craft 3.7.57, Typed Link 1.0.25. I realise that Linkit 0.9.1 might be out of date on Craft 2 already, but that's what most of our sites had installed. So there are some API changes compared to the answer above. <?php
namespace modules\sitemigration\console\controllers;
use Craft;
use craft\db\Query;
use craft\helpers\App;
use craft\helpers\ArrayHelper;
use craft\helpers\Console;
use craft\helpers\ElementHelper;
use craft\helpers\Json;
use yii\console\Controller;
use yii\console\ExitCode;
use typedlinkfield\fields\LinkField;
use typedlinkfield\models\Link;
class LinkitController extends Controller
{
// Public Methods
// =========================================================================
public function actionMigrate()
{
App::maxPowerCaptain();
// Update both the field content and the field settings
$this->processFieldSettings();
$this->processFieldContent();
$this->stderr('FINISHED' . PHP_EOL, Console::FG_GREEN);
}
private function processFieldSettings()
{
$db = Craft::$app->getDb()->createCommand();
$fields = (new Query())
->from('{{%fields}}')
->where(['type' => 'Linkit_Linkit'])
->all();
foreach ($fields as $field) {
$settings = Json::decode($field['settings']);
$text = ArrayHelper::remove($settings, 'text');
$target = ArrayHelper::remove($settings, 'target');
$targetLocale = ArrayHelper::remove($settings, 'targetLocale');
$defaultText = ArrayHelper::remove($settings, 'defaultText');
$types = ArrayHelper::remove($settings, 'types');
$assetSources = ArrayHelper::remove($settings, 'assetSources');
$categorySources = ArrayHelper::remove($settings, 'categorySources');
$entrySources = ArrayHelper::remove($settings, 'entrySources');
$settings['defaultLinkName'] = 'entry';
$settings['allowCustomText'] = (bool)$text;
$settings['allowTarget'] = (bool)$target;
$settings['defaultText'] = $defaultText;
if (is_array($types)) {
$settings['allowedLinkNames'] = $types;
}
$settings['typeSettings'] = [
'asset' => [
'sources' => $assetSources,
],
'category' => [
'sources' => $categorySources,
],
'entry' => [
'sources' => $entrySources,
],
];
// Create a new Linked field instance to have the settings validated correctly
$newField = new LinkField($settings);
$newField->name = $field['name'];
$newField->handle = $field['handle'];
if (!$newField->validate()) {
$this->stderr(Json::encode($newField->getErrors()) . PHP_EOL, Console::FG_RED);
continue;
}
$db->update('{{%fields}}', ['type' => LinkField::class, 'settings' => Json::encode($newField->settings)], ['id' => $field['id']])->execute();
$this->stderr('Migrated field #' . $field['id'] . PHP_EOL, Console::FG_YELLOW);
}
}
private function processFieldContent()
{
$db = Craft::$app->getDb()->createCommand();
$fields = (new Query())
->from('{{%fields}}')
->where(['type' => LinkField::class])
->all();
foreach ($fields as $fieldData) {
// Fetch the field model because we'll need it later
$field = Craft::$app->getFields()->getFieldById($fieldData['id'], false);
if ($field) {
$column = ElementHelper::fieldColumnFromField($field);
// Handle global field content
if ($field->context === 'global') {
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from('{{%content}}')
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update('{{%content}}', [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
// Handle Matrix field content
if (strstr($field->context, 'matrixBlockType')) {
// Get the Matrix field, and the content table
$blockTypeUid = explode(':', $field->context)[1];
$matrixInfo = (new Query())
->select(['fieldId', 'handle'])
->from('{{%matrixblocktypes}}')
->where(['uid' => $blockTypeUid])
->one();
if ($matrixInfo) {
$matrixFieldId = $matrixInfo['fieldId'];
$matrixBlockTypeHandle = $matrixInfo['handle'];
$matrixField = Craft::$app->getFields()->getFieldById($matrixFieldId, false);
if ($matrixField) {
$column = ElementHelper::fieldColumn($field->columnPrefix, $matrixBlockTypeHandle . '_' . $field->handle, $field->columnSuffix);
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from($matrixField->contentTable)
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update($matrixField->contentTable, [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
}
}
// Handle Super Table field content
if (strstr($field->context, 'superTableBlockType')) {
// Get the Super Table field, and the content table
$blockTypeUid = explode(':', $field->context)[1];
$superTableFieldId = (new Query())
->select(['fieldId'])
->from('{{%supertableblocktypes}}')
->where(['uid' => $blockTypeUid])
->scalar();
$superTableField = Craft::$app->getFields()->getFieldById($superTableFieldId, false);
if ($superTableField) {
$column = ElementHelper::fieldColumnFromField($superTableField);
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from($superTableField->contentTable)
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update($superTableField->contentTable, [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
}
}
}
}
private function convertModel($field, $oldSettings)
{
// Because we've already converted the field to the new Link field, but may not have done the content yet, check first
// This allows us to re-trigger this migration any time we wish without messing up the field data
if (array_key_exists('ariaLabel', $oldSettings)) {
return false;
}
$settings = new Link(['linkField' => $field]);
$settings->type = $oldSettings['type'];
switch ($oldSettings['type']) {
case 'entry':
$settings->value = array_pop($oldSettings['entry']);
break;
case 'category':
$settings->value = array_pop($oldSettings['category']);
break;
case 'asset':
$settings->value = array_pop($oldSettings['asset']);
break;
case 'custom':
$settings->value = $oldSettings['custom'];
break;
case 'email':
$settings->value = $oldSettings['email'];
break;
case 'tel':
$settings->value = $oldSettings['tel'];
break;
}
if (array_key_exists('target', $oldSettings)) {
$settings->target = $oldSettings['target'];
}
if (array_key_exists('text', $oldSettings)) {
$settings->customText = $oldSettings['text'];
}
return $settings->toArray();
}
} |
Oh, and here's a very similar one coming from LinkIt 2.3.4 on Craft 2. <?php
namespace modules\sitemigration\console\controllers;
use Craft;
use craft\db\Query;
use craft\helpers\App;
use craft\helpers\ArrayHelper;
use craft\helpers\Console;
use craft\helpers\ElementHelper;
use craft\helpers\Json;
use yii\console\Controller;
use yii\console\ExitCode;
use typedlinkfield\fields\LinkField;
use typedlinkfield\models\Link;
class LinkitController extends Controller
{
// Public Methods
// =========================================================================
public function actionMigrate()
{
App::maxPowerCaptain();
// Update both the field content and the field settings
// For LinkIt 2.x
$this->processFieldSettings();
$this->processFieldContent();
$this->stderr('FINISHED' . PHP_EOL, Console::FG_GREEN);
}
private function processFieldSettings()
{
$db = Craft::$app->getDb()->createCommand();
$fields = (new Query())
->from('{{%fields}}')
->where(['type' => 'FruitLinkIt'])
->all();
foreach ($fields as $field) {
$settings = Json::decode($field['settings']);
$allowCustomText = ArrayHelper::remove($settings, 'allowCustomText');
$allowTarget = ArrayHelper::remove($settings, 'allowTarget');
$types = ArrayHelper::remove($settings, 'types');
$assetSources = ArrayHelper::remove($settings, 'assetSources');
$categorySources = ArrayHelper::remove($settings, 'categorySources');
$entrySources = ArrayHelper::remove($settings, 'entrySources');
$assetSelectionLabel = ArrayHelper::remove($settings, 'assetSelectionLabel');
$categorySelectionLabel = ArrayHelper::remove($settings, 'categorySelectionLabel');
$entrySelectionLabel = ArrayHelper::remove($settings, 'entrySelectionLabel');
$defaultText = ArrayHelper::remove($settings, 'defaultText');
$settings['defaultLinkName'] = 'entry';
$settings['allowCustomText'] = (bool)$allowCustomText;
$settings['allowTarget'] = (bool)$allowTarget;
if (is_array($types)) {
$settings['allowedLinkNames'] = $types;
}
$settings['typeSettings'] = [
'asset' => [
'sources' => $assetSources,
],
'category' => [
'sources' => $categorySources,
],
'entry' => [
'sources' => $entrySources,
],
];
// Create a new Linked field instance to have the settings validated correctly
$newField = new LinkField($settings);
$newField->name = $field['name'];
$newField->handle = $field['handle'];
if (!$newField->validate()) {
$this->stderr(Json::encode($newField->getErrors()) . PHP_EOL, Console::FG_RED);
continue;
}
$db->update('{{%fields}}', ['type' => LinkField::class, 'settings' => Json::encode($newField->settings)], ['id' => $field['id']])->execute();
$this->stderr('Migrated field #' . $field['id'] . PHP_EOL, Console::FG_YELLOW);
}
}
private function processFieldContent()
{
$db = Craft::$app->getDb()->createCommand();
$fields = (new Query())
->from('{{%fields}}')
->where(['type' => LinkField::class])
->all();
foreach ($fields as $fieldData) {
// Fetch the field model because we'll need it later
$field = Craft::$app->getFields()->getFieldById($fieldData['id'], false);
if ($field) {
$column = ElementHelper::fieldColumnFromField($field);
// Handle global field content
if ($field->context === 'global') {
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from('{{%content}}')
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update('{{%content}}', [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
// Handle Matrix field content
if (strstr($field->context, 'matrixBlockType')) {
// Get the Matrix field, and the content table
$blockTypeUid = explode(':', $field->context)[1];
$matrixInfo = (new Query())
->select(['fieldId', 'handle'])
->from('{{%matrixblocktypes}}')
->where(['uid' => $blockTypeUid])
->one();
if ($matrixInfo) {
$matrixFieldId = $matrixInfo['fieldId'];
$matrixBlockTypeHandle = $matrixInfo['handle'];
$matrixField = Craft::$app->getFields()->getFieldById($matrixFieldId, false);
if ($matrixField) {
$column = ElementHelper::fieldColumn($field->columnPrefix, $matrixBlockTypeHandle . '_' . $field->handle, $field->columnSuffix);
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from($matrixField->contentTable)
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update($matrixField->contentTable, [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
}
}
// Handle Super Table field content
if (strstr($field->context, 'superTableBlockType')) {
// Get the Super Table field, and the content table
$blockTypeUid = explode(':', $field->context)[1];
$superTableFieldId = (new Query())
->select(['fieldId'])
->from('{{%supertableblocktypes}}')
->where(['uid' => $blockTypeUid])
->scalar();
$superTableField = Craft::$app->getFields()->getFieldById($superTableFieldId, false);
if ($superTableField) {
$column = ElementHelper::fieldColumnFromField($superTableField);
$content = (new Query())
->select([$column, 'id', 'elementId'])
->from($superTableField->contentTable)
->where(['not', [$column => null]])
->andWhere(['not', [$column => '']])
->all();
foreach ($content as $row) {
$settings = $this->convertModel($field, Json::decode($row[$column]));
if ($settings) {
$db->update($superTableField->contentTable, [$column => Json::encode($settings)], ['id' => $row['id']])->execute();
$this->stderr('Migrating content #' . $row['id'] . ' for element #' . $row['elementId'] . PHP_EOL, Console::FG_YELLOW);
}
}
}
}
}
}
}
private function convertModel($field, $oldSettings)
{
// Because we've already converted the field to the new Link field, but may not have done the content yet, check first
// This allows us to re-trigger this migration any time we wish without messing up the field data
if (array_key_exists('ariaLabel', $oldSettings)) {
return false;
}
$settings = new Link(['linkField' => $field]);
$settings->type = $oldSettings['type'];
switch ($oldSettings['type']) {
case 'entry':
$settings->value = array_pop($oldSettings['entry']);
break;
case 'category':
$settings->value = array_pop($oldSettings['category']);
break;
case 'asset':
$settings->value = array_pop($oldSettings['asset']);
break;
case 'custom':
$settings->value = $oldSettings['custom'];
break;
case 'email':
$settings->value = $oldSettings['email'];
break;
case 'tel':
$settings->value = $oldSettings['tel'];
break;
}
if (array_key_exists('target', $oldSettings)) {
$settings->target = $oldSettings['target'];
}
if (array_key_exists('customText', $oldSettings)) {
$settings->customText = $oldSettings['customText'];
}
return $settings->toArray();
}
} |
Those of us coming from Craft 2 probably used the Fruit Studio's Link It plugin, which was free at the time. While I think it's fair for developers to want to be paid, I think their price point is pretty silly (and honestly in my opinion, it ought to be free if we're trying to invest in Craft CMS as a community). We should add a CLI option or utility to convert Link It fields to typed link fields from either Craft 2 or Craft 3.
The text was updated successfully, but these errors were encountered: