Skip to content
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

This plugin is no longer maintained. #286

Open
dialedin-webteam opened this issue Aug 5, 2024 · 5 comments
Open

This plugin is no longer maintained. #286

dialedin-webteam opened this issue Aug 5, 2024 · 5 comments

Comments

@dialedin-webteam
Copy link

When I check for updates in Craft CMS, the plugin 'craft-linkfield' indicates that it is no longer maintained, but its features are still working fine. Will this affect my site? If I upgrade to Craft CMS 5, will it still work, and if I deactivate this plugin, will its features still work?

Craft Cms version: 4.10.4
Typed-link-field version: 2.1.5
Typed-link-field

@nicolas-haemmerli-bh
Copy link

You won't be able to upgrade to Craft 5.
But Craft 5.3 has a similar field type included by default.

@RyanRoberts
Copy link

and if I deactivate this plugin, will its features still work?

That's unlikely, you'll need to convert it to something else. V1 of the Hyper (a link field) plugin has the ability to migrate from this plugin https://verbb.io/craft-plugins/hyper/docs/v1/guides/migrating-from-typed-link

@maxfenton
Copy link

@sebastian-lenz Is the 3.0.0-beta release tested?

9262726

@dgsiegel
Copy link

dgsiegel commented Jan 3, 2025

We've been facing the same challenge with the now-unmaintained Typed Link Field module. As Craft 5.3 now provides a native Link Field, we decided to migrate to it.

Here’s how we approached it:

  • While Typed Link Field does install and work on Craft 5, it’s no longer a sustainable option due to the lack of maintenance
  • The Hyper module offers migrations from Typed Link Field, but we decided not to adopt Hyper.
  • We upgraded to Craft 5 and migrated all other fields and settings first.
  • We manually replaced each Typed Link Field with Craft’s native Link Field, copying over the configuration (ie. switching the field type in the backend). Since we only had around 10–15 fields, this was manageable without automation. Hyper’s migration would have automated this step if we had chosen to use it.
  • To transfer the data, we used a custom migration (based on what Hyper does, see https://github.com/verbb/hyper/blob/craft-5/src/migrations/MigrateTypedLinkContent.php) to move content from the lenz_linkfield table to the elements_sites table, formatting it for the native Link Field.

Here’s the migration code we used (stored in migrations/m250102_202812_typed_link_field_to_craft_link_field.php). Feel free to adapt or refine it for your own needs:

<?php

namespace craft\contentmigrations;

use Craft;
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\db\Migration;
use craft\db\Query;
use craft\db\Table;
use craft\fields\Link;
use craft\helpers\Db;
use craft\helpers\Json;
use craft\helpers\StringHelper;

/**
 * m250102_202812_typed_link_field_to_craft_link_field migration.
 */
class m250102_202812_typed_link_field_to_craft_link_field extends Migration
{
    public array $allowedTypes = [
        'asset',
        'email',
        'entry',
        'url',
    ];

    /**
     * @inheritdoc
     */
    public function safeUp(): bool
    {
        $fieldService = Craft::$app->getFields();

        $fields = (new Query())
            ->from('{{%fields}}')
            ->where(['type' => Link::class])
            ->all();

        foreach ($fields as $field) {
            echo "Preparing to migrate field “{$field['handle']}” ({$field['uid']}) content.\n";

            $fieldModel = Craft::$app->getFields()->getFieldById($field['id']);

            if ($fieldModel) {
                $contentRows = (new Query())
                    ->select(['*'])
                    ->from('{{%lenz_linkfield}}')
                    ->where(['fieldId' => $fieldModel['id']])
                    ->all();

                if ($fieldModel->context === 'global') {
                    foreach ($contentRows as $row) {
                        $settings = $this->convertLinkContent($fieldModel, $row);

                        $element = Craft::$app->getElements()->getElementById($row['elementId'], null, $row['siteId']);
                        if ($element) {
                            if ($settings) {
                                $newContent = $this->getElementContentForField($element, $fieldModel, $settings);

                                Db::update('{{%elements_sites}}', ['content' => $newContent], ['elementId' => $row['elementId'], 'siteId' => $row['siteId']]);

                                echo "    > Migrated content for element #{$row['elementId']}\n";
                            } else {
                                if ($settings !== null) {
                                    echo "    > Unable to convert content for element #{$row['elementId']}\n";
                                }
                            }
                        } else {
                            echo "    > Unable to find element #{$row['elementId']} and site #{$row['siteId']}\n";
                        }
                    }
                }
            }
            echo "> Field “{$field['handle']}” content migrated.\n\n";
        }

        return true;
    }

    /**
     * @inheritdoc
     */
    public function safeDown(): bool
    {
        echo "m250102_202812_typed_link_field_to_craft_link_field cannot be reverted.\n";
        return false;
    }

    public function convertLinkContent($field, array $settings): bool|array|null
    {
        $linkType = $settings['type'] ?? null;

        if (!$linkType) {
            return null;
        }

        if (!in_array($linkType, $this->allowedTypes)) {
            return false;
        }

        $advanced = Json::decode($settings['payload']);
        $linkValue = $settings['linkedUrl'] ?? null;
        $linkText = $advanced['customText'] ?? null;
        $linkSiteId = $settings['siteId'] ?? null;
        $linkId = $settings['linkedId'] ?? null;

        if (($linkType === 'entry' || $linkType === 'asset') && (!$linkId || !$linkSiteId)) {
            return false;
        }

        if ($linkType === 'entry') {
            $linkValue = "{entry:{$linkId}@{$linkSiteId}:url}";
        } else if ($linkType === 'asset') {
            $linkValue = "{asset:{$linkId}@{$linkSiteId}:url}";
        } else if ($linkType === 'email') {
            $linkValue = 'mailto:' . $linkValue;
        }

        return [
            'value' => $linkValue,
            'type' => $linkType,
            'label' => $linkText,
            'target' => null,
        ];
    }

    // From:
    // https://github.com/verbb/hyper/blob/craft-5/src/migrations/PluginContentMigration.php#L141
    protected function getElementContentForField(ElementInterface $element, FieldInterface $field, array $fieldValue): array
    {
        $fieldContent = [];

        // Get the field content as JSON, indexed by field layout element UID
        if ($fieldLayout = $element->getFieldLayout()) {
            foreach ($fieldLayout->getCustomFields() as $fieldLayoutField) {
                $sourceHandle = $fieldLayoutField->layoutElement?->getOriginalHandle() ?? $fieldLayoutField->handle;

                if ($field->handle === $sourceHandle) {
                    $fieldContent[$fieldLayoutField->layoutElement->uid] = $fieldValue;
                }
            }
        }

        // Fetch the current JSON content so we can merge in the new field content
        $oldContent = Json::decode((new Query())
            ->select(['content'])
            ->from('{{%elements_sites}}')
            ->where(['elementId' => $element->id, 'siteId' => $element->siteId])
            ->scalar() ?? '') ?? [];

        // Another sanity check just in cases where content is double encoded
        if (is_string($oldContent) && Json::isJsonObject($oldContent)) {
            $oldContent = Json::decode($oldContent);
        }

        return array_merge($oldContent, $fieldContent);
    }
}
  • The migration iterates through all fields with the type of Craft’s native Link Field.
  • For each field, it looks for a matching fieldId in the lenz_linkfield table.
  • If a match is found, the data is converted to the native Link Field’s JSON format.
  • During the process, we discovered many (partially) empty rows in the lenz_linkfield table. We skipped those rows. So far, we haven’t encountered any issues due to this.

@jamie-s-white
Copy link

Here's an updated version of the above which also migrates the link field settings:

<?php

namespace craft\contentmigrations;

use Craft;
use craft\base\ElementInterface;
use craft\base\FieldInterface;
use craft\db\Migration;
use craft\db\Query;
use craft\fields\Link;
use craft\helpers\Db;
use craft\helpers\Json;
use craft\helpers\App;

/**
 * m250102_202812_typed_link_field_to_craft_link_field migration.
 */
class m250102_202812_typed_link_field_to_craft_link_field extends Migration
{
    public array $allowedTypes = [
        'asset',
        'email',
        'entry',
        'url',
    ];

    /**
     * @inheritdoc
     */
    public function safeUp(): bool
    {
        // O7
        App::maxPowerCaptain();

        // Get all Lenz LinkField fields
        $fields = (new Query())
            ->from('{{%fields}}')
            ->where(['type' => 'lenz\linkfield\fields\LinkField'])
            ->all();

        foreach ($fields as $field) {
            echo "Preparing to migrate field “{$field['handle']}” ({$field['uid']}) settings.\n";

            $lenzFieldSettings = json_decode($field['settings']);
            $nativeFieldSettings = [
                'maxLength' => 255, // Can use $lenzFieldSettings['customTextMaxLength'], but that's not the same, so default
                'showLabelField' => $lenzFieldSettings->allowCustomText,
                'typeSettings' => [
                    'entry' => [
                        'sources' => $lenzFieldSettings->typeSettings->entry->sources
                    ],
                    'url' => [
                        'allowRootRelativeUrls' => '1', // Default, no Lenz equivalent?
                        'allowAnchors' => '1' // Default, no Lenz equivalent?
                    ],
                    'asset' => [
                        'sources' => $lenzFieldSettings->typeSettings->asset->sources,
                        'allowedKinds' => '*', // Default, no Lenz equivalent?
                        'showUnpermittedVolumes' => '1', // Default, no Lenz equivalent?
                        'showUnpermittedFiles' => '1' // Default, no Lenz equivalent?
                    ],
                    'category' => [
                        'sources' => $lenzFieldSettings->typeSettings->category->sources
                    ]
                ],
                'types' => [
                ],
            ];

            // Detect any equivalent advanced settings
            if ($lenzFieldSettings->allowTarget) {
                $nativeFieldSettings['advancedFields'] = ['target'];
            }

            // Test to see if each type setting is enabled
            if ($lenzFieldSettings->typeSettings->entry->enabled) {
                array_push($nativeFieldSettings['types'], 'entry');
            }
            if ($lenzFieldSettings->typeSettings->url->enabled) {
                array_push($nativeFieldSettings['types'], 'url');
            }
            if ($lenzFieldSettings->typeSettings->asset->enabled) {
                array_push($nativeFieldSettings['types'], 'asset');
            }
            if ($lenzFieldSettings->typeSettings->category->enabled) {
                array_push($nativeFieldSettings['types'], 'category');
            }
            if ($lenzFieldSettings->typeSettings->email->enabled) {
                array_push($nativeFieldSettings['types'], 'email');
            }
            if ($lenzFieldSettings->typeSettings->tel->enabled) {
                array_push($nativeFieldSettings['types'], 'tel');
            }

            // Update the type and settings for Lenz linked fields to native Craft Link field equivalents
            $this->update(
                '{{%fields}}',
                [
                    'type' => Link::class,
                    'settings' => json_encode($nativeFieldSettings)
                ],
                ['uid' => $field['uid']]
            );

            // Migrate content
            echo "Preparing to migrate field “{$field['handle']}” ({$field['uid']}) content.\n";
            $fieldModel = Craft::$app->getFields()->getFieldById($field['id']);

            if ($fieldModel) {
                // Get content from the lenz_linkfield table
                $contentRows = (new Query())
                    ->select(['*'])
                    ->from('{{%lenz_linkfield}}')
                    ->where(['fieldId' => $fieldModel['id']])
                    ->all();

                if ($fieldModel->context === 'global') {
                    foreach ($contentRows as $row) {
                        $settings = $this->convertLinkContent($fieldModel, $row);

                        $element = Craft::$app->getElements()->getElementById($row['elementId'], null, $row['siteId']);
                        if ($element) {
                            if ($settings) {
                                $newContent = $this->getElementContentForField($element, $fieldModel, $settings);

                                Db::update('{{%elements_sites}}', ['content' => $newContent], ['elementId' => $row['elementId'], 'siteId' => $row['siteId']]);

                                echo "    > Migrated content for element #{$row['elementId']}\n";
                            } else {
                                if ($settings !== null) {
                                    echo "    > Unable to convert content for element #{$row['elementId']}\n";
                                }
                            }
                        } else {
                            echo "    > Unable to find element #{$row['elementId']} and site #{$row['siteId']}\n";
                        }
                    }
                }
            }
            echo "> Field “{$field['handle']}” content migrated.\n\n";
        }

        return true;
    }

    /**
     * @inheritdoc
     */
    public function safeDown(): bool
    {
        echo "m250102_202812_typed_link_field_to_craft_link_field cannot be reverted.\n";
        return false;
    }

    public function convertLinkContent($field, array $settings): bool|array|null
    {
        $linkType = $settings['type'] ?? null;

        if (!$linkType) {
            return null;
        }

        if (!in_array($linkType, $this->allowedTypes)) {
            return false;
        }

        $advanced = Json::decode($settings['payload']);
        $linkValue = $settings['linkedUrl'] ?? null;
        $linkText = $advanced['customText'] ?? null;
        $linkSiteId = $settings['siteId'] ?? null;
        $linkId = $settings['linkedId'] ?? null;

        if (($linkType === 'entry' || $linkType === 'asset') && (!$linkId || !$linkSiteId)) {
            return false;
        }

        if ($linkType === 'entry') {
            $linkValue = "{entry:{$linkId}@{$linkSiteId}:url}";
        } elseif ($linkType === 'asset') {
            $linkValue = "{asset:{$linkId}@{$linkSiteId}:url}";
        } elseif ($linkType === 'email') {
            $linkValue = 'mailto:' . $linkValue;
        }

        return [
            'value' => $linkValue,
            'type' => $linkType,
            'label' => $linkText,
            'target' => null,
        ];
    }

    // From:
    // https://github.com/verbb/hyper/blob/craft-5/src/migrations/PluginContentMigration.php#L141
    protected function getElementContentForField(ElementInterface $element, FieldInterface $field, array $fieldValue): array
    {
        $fieldContent = [];

        // Get the field content as JSON, indexed by field layout element UID
        if ($fieldLayout = $element->getFieldLayout()) {
            foreach ($fieldLayout->getCustomFields() as $fieldLayoutField) {
                $sourceHandle = $fieldLayoutField->layoutElement?->getOriginalHandle() ?? $fieldLayoutField->handle;

                if ($field->handle === $sourceHandle) {
                    $fieldContent[$fieldLayoutField->layoutElement->uid] = $fieldValue;
                }
            }
        }

        // Fetch the current JSON content so we can merge in the new field content
        $oldContent = Json::decode((new Query())
            ->select(['content'])
            ->from('{{%elements_sites}}')
            ->where(['elementId' => $element->id, 'siteId' => $element->siteId])
            ->scalar() ?? '') ?? [];

        // Another sanity check just in cases where content is double encoded
        if (is_string($oldContent) && Json::isJsonObject($oldContent)) {
            $oldContent = Json::decode($oldContent);
        }

        return array_merge($oldContent, $fieldContent);
    }
}

It can certainly be improved, for example, I have just hard coded $nativeFieldSettings array based on what the CURRENT defaults are, rather than getting the actual defaults from craft\fields\Link, so if P&T tweak the defaults, this migration will be off.

This is also hot off the press, and we are just testing our various link field settings now to ensure the settings are migrated properly. If anybody finds any issues with this, please let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants