Updated version custom module for Drupal 10 based on other answers. This just adds tooltip plain text property to the core link field.
PS: If you were the module owner directly then would drop the "CUSTOM EDIT" edits as the fetch from the schema and pass the property ids directly.
mylink.info.yml
name: "My link"
package: "MY PROJECT"
description: "Provides extended link functionality."
type: module
core_version_requirement: ^10
dependencies:
- drupal:link
mylink.install
<?php
/**
* @file
* Install file
*
* Code copied from:
* - https://drupal.stackexchange.com/questions/174155/add-property-to-existing-field-type
* - https://gist.github.com/JPustkuchen/ce53d40303a51ca5f17ce7f48c363b9b
*/
/**
* Add extra tooltip property to link fields.
*/
function mylink_install() {
// Ensure _mylink_extra_properties_schema() can be called.
\Drupal::service('module_handler')->loadInclude('mylink', 'module');
_field_type_schema_column_add_helper('link', _mylink_extra_properties_schema());
}
/**
* Remove extra tooltip property from link fields.
*/
function mylink_uninstall() {
// Ensure _mylink_extra_properties_schema() can be called.
\Drupal::service('module_handler')->loadInclude('mylink', 'module');
_field_type_schema_column_remove_helper('link', _mylink_extra_properties_schema());
}
/**
* Helper function for HOOK_Update to update the field schema columns.
*
* Based on address.install (thanks to the maintainer!)
*
* @param $field_type The field type id.
* @param array $columns_to_add array of the column names from schema() function.
*/
function _field_type_schema_column_add_helper($field_type, array $columns_to_add = array()) {
$processed_fields = [];
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$field_definition = $field_type_manager->getDefinition($field_type);
$field_item_class = $field_definition['class'];
$schema = \Drupal::database()->schema();
$entity_type_manager = \Drupal::entityTypeManager();
$entity_field_manager = \Drupal::service('entity_field.manager');
$entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type);
// The key-value collection for tracking installed storage schema.
$entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
$entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');
foreach ($entity_field_map as $entity_type_id => $field_map) {
$entity_storage = $entity_type_manager->getStorage($entity_type_id);
// Only SQL storage based entities are supported / throw known exception.
// if (!($entity_storage instanceof SqlContentEntityStorage)) {
// continue;
// }
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
/** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_storage->getTableMapping($field_storage_definitions);
// Only need field storage definitions of our field type:
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */
foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) {
$field_name = $field_storage_definition->getName();
try {
$table = $table_mapping->getFieldTableName($field_name);
} catch (SqlContentEntityStorageException $e) {
// Custom storage? Broken site? No matter what, if there is no table
// or column, there's little we can do.
continue;
}
// See if the field has a revision table.
$revision_table = NULL;
if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
$revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
}
elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
}
}
// Load the installed field schema so that it can be updated.
$schema_key = "$entity_type_id.field_schema_data.$field_name";
$field_schema_data = $entity_storage_schema_sql->get($schema_key);
$processed_fields[] = [$entity_type_id, $field_name];
// Loop over each new column and add it as a schema column change.
foreach ($columns_to_add as $column_id => $column_data) {
$column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id);
// Add `initial_from_field` to the new spec, as this will copy over
// the entire data.
$field_schema = $field_item_class::schema($field_storage_definition);
// MYLINK CUSTOM.
//$spec = $field_schema['columns'][$column_id];
$spec = $column_data;
// Add the new column.
$schema->addField($table, $column, $spec);
if ($revision_table) {
$schema->addField($revision_table, $column, $spec);
}
// Add the new column to the installed field schema.
if (!empty($field_schema_data)) {
// CUSTOM EDIT.
//$field_schema_data[$table]['fields'][$column] = $field_schema['columns'][$column_id];
$field_schema_data[$table]['fields'][$column] = $spec;
$field_schema_data[$table]['fields'][$column]['not null'] = FALSE;
if ($revision_table) {
// CUSTOM - EDIT.
//$field_schema_data[$revision_table]['fields'][$column] = $field_schema['columns'][$column_id];
$field_schema_data[$revision_table]['fields'][$column] = $spec;
$field_schema_data[$revision_table]['fields'][$column]['not null'] = FALSE;
}
}
}
// Save changes to the installed field schema.
if (!empty($field_schema_data)) {
$entity_storage_schema_sql->set($schema_key, $field_schema_data);
}
if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
$key = "$entity_type_id.field_storage_definitions";
if ($definitions = $entity_definitions_installed->get($key)) {
$definitions[$field_name] = $field_storage_definition;
$entity_definitions_installed->set($key, $definitions);
}
}
}
}
}
/**
* Helper function for HOOK_Update to remove columns from the field schema.
*
* @param $field_type The field type id e.g. "drowl_paragraphs_settings"
* @param array $columns_to_remove array of the column names from schema() function, e.g. ["style_textalign"]
*/
function _field_type_schema_column_remove_helper($field_type, array $columns_to_remove = array()) {
$processed_fields = [];
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
$field_definition = $field_type_manager->getDefinition($field_type);
$field_item_class = $field_definition['class'];
$schema = \Drupal::database()->schema();
$entity_type_manager = \Drupal::entityTypeManager();
$entity_field_manager = \Drupal::service('entity_field.manager');
$entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type);
// The key-value collection for tracking installed storage schema.
$entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
$entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');
foreach ($entity_field_map as $entity_type_id => $field_map) {
$entity_storage = $entity_type_manager->getStorage($entity_type_id);
// Only SQL storage based entities are supported / throw known exception.
// if (!($entity_storage instanceof SqlContentEntityStorage)) {
// continue;
// }
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
/** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_storage->getTableMapping($field_storage_definitions);
// Only need field storage definitions of our field type:
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */
foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) {
$field_name = $field_storage_definition->getName();
try {
$table = $table_mapping->getFieldTableName($field_name);
} catch (SqlContentEntityStorageException $e) {
// Custom storage? Broken site? No matter what, if there is no table
// or column, there's little we can do.
continue;
}
// See if the field has a revision table.
$revision_table = NULL;
if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
$revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
}
elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
}
}
// Load the installed field schema so that it can be updated.
$schema_key = "$entity_type_id.field_schema_data.$field_name";
$field_schema_data = $entity_storage_schema_sql->get($schema_key);
$processed_fields[] = [$entity_type_id, $field_name];
// Loop over each new column and add it as a schema column change.
foreach ($columns_to_remove as $column_id => $column_data) {
$column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id);
// Add `initial_from_field` to the new spec, as this will copy over
// the entire data.
$field_schema = $field_item_class::schema($field_storage_definition);
// CUSTOM - EDIT.
//$spec = $field_schema['columns'][$column_id];
$spec = $column_data;
// Add the new column.
$schema->dropField($table, $column);
if ($revision_table) {
$schema->dropField($revision_table, $column);
}
// Remove the column from the installed field schema.
if (!empty($field_schema_data)) {
unset($field_schema_data[$table]['fields'][$column]);
if ($revision_table) {
unset($field_schema_data[$revision_table]['fields'][$column]);
}
}
}
// Save changes to the installed field schema.
if (!empty($field_schema_data)) {
$entity_storage_schema_sql->set($schema_key, $field_schema_data);
}
if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
$key = "$entity_type_id.field_storage_definitions";
if ($definitions = $entity_definitions_installed->get($key)) {
$definitions[$field_name] = $field_storage_definition;
$entity_definitions_installed->set($key, $definitions);
}
}
}
}
}
mylink.module
<?php
/**
* @file
* Module file.
*/
use Drupal\mylink\Plugin\Field\FieldFormatter\MyLinkFormatter;
use Drupal\mylink\Plugin\Field\FieldType\MyLinkItem;
use Drupal\mylink\Plugin\Field\FieldWidget\MyLinkWidget;
/**
* Extra link properties.
*/
function _mylink_extra_properties_schema($keys_only = FALSE) {
$extra_properties = [
'tooltip' => [
'description' => 'Link tooltip',
'type' => 'varchar',
'length' => 255,
],
];
return $extra_properties;
}
/**
* Implements hook_field_info_alter().
*/
function mylink_field_info_alter(&$info) {
$info['link']['class'] = MyLinkItem::class;
}
/**
* Implements hook_field_formatter_info_alter().
*/
function mylink_field_formatter_info_alter(&$info) {
$info['link']['class'] = MyLinkFormatter::class;
}
/**
* Implements hook_field_widget_info_alter().
*/
function mylink_field_widget_info_alter(&$widgets) {
$widgets['link_default']['class'] = MyLinkWidget::class;
}
src/Plugin/Field/FieldFormatter/MyLinkFormatter.php
<?php
namespace Drupal\my_link\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Render\Markup;
use Drupal\link\LinkItemInterface;
use Drupal\link\Plugin\Field\FieldFormatter\LinkFormatter;
/**
* Plugin implementation of the 'link' formatter.
*/
class MyLinkFormatter extends LinkFormatter {
/**
* Add the tooltipto \Drupal\Core\Url object for a link field item.
*/
protected function buildUrl(LinkItemInterface $item) {
$url = parent::buildUrl($item);
$tooltip = $item->getTooltip();
$options = $url->getOptions();
if (!empty($tooltip)) {
// Use markup, html tags are removed instead of escaped.
$options['attributes']['title'] = Markup::create(Html::escape($tooltip));
}
$url->setOptions($options);
return $url;
}
}
src/Plugin/Field/FieldType/MyLinkItem.php
<?php
namespace Drupal\mylink\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\link\Plugin\Field\FieldType\LinkItem;
/**
* Extension plugin implementation of the 'link' field type.
*/
class MyLinkItem extends LinkItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['tooltip'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Link tooltip'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$parent = parent::schema($field_definition);
$parent['columns'] = $parent['columns'] + _mylink_extra_properties_schema();
return $parent;
}
/**
* {@inheritdoc}
*/
public function getTooltip(): ?string {
return $this->tooltip ?: NULL;
}
}
src/Plugin/Field/FieldWidget/MyLinkWidget.php
<?php
namespace Drupal\mylink\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\link\Plugin\Field\FieldWidget\LinkWidget;
/**
* Plugin implementation of the 'link' widget.
*/
class MyLinkWidget extends LinkWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
$element['tooltip'] = [
'#type' => 'textfield',
'#title' => $this->t('Tooltip'),
'#default_value' => $items[$delta]->tooltip ?? NULL,
'#maxlength' => 255,
'#description' => $this->t('Note, screen readers will read this information after the link text, so if using on a link with text, do not repeat the same text.'),
];
return $element;
}
}
$schema->addField('mytable1', 'newcol', $spec);
See this change record (reachable from the previous link) drupal.org/node/2554097 and also this page on how to add columns to a table: drupal.org/node/2535384