Skip to content

Architecture

Design Overview

The Image Link Formatter module provides a Drupal field formatter plugin that extends core's image formatters (standard and responsive) to wrap images in custom link field URLs.

Core Principles

  1. Extends, not overrides — Inherits from core ImageFormatter and ResponsiveImageFormatter
  2. Trait-based — Common logic in ImageLinkFormatterTrait for reusability
  3. Dependency injection — Services injected via the create() factory method
  4. Zero duplication — Leverages core formatter behavior; only adds link wrapping

Plugin Architecture

File Structure

web/modules/contrib/image_link_formatter/
├── image_link_formatter.info.yml
├── src/Plugin/Field/FieldFormatter/
│   ├── ImageLinkFormatter.php
│   └── ImageLinkFormatterTrait.php
├── responsive_image_link_formatter/
│   └── src/Plugin/Field/FieldFormatter/
│       └── ResponsiveImageLinkFormatter.php
└── tests/

Two Formatter Plugins

1. ImageLinkFormatter (Main Module)

class ImageLinkFormatter extends ImageFormatter {
  use ImageLinkFormatterTrait;
}
  • Extends: Core ImageFormatter
  • ID: image_link_formatter
  • Label: "Image wrapped within link field"
  • Field types: image

2. ResponsiveImageLinkFormatter (Sub-module)

class ResponsiveImageLinkFormatter extends ResponsiveImageFormatter {
  use ImageLinkFormatterTrait;
}
  • Extends: Core ResponsiveImageFormatter
  • ID: responsive_image_link_formatter
  • Label: "Responsive image wrapped within link field"
  • Field types: image

Trait Implementation

ImageLinkFormatterTrait

Shared functionality for both formatters:

Key Methods

Method Purpose
create() Static factory; injects entity_field.manager service
settingsForm() Extends parent form; adds link field options to dropdown
settingsSummary() Displays selected link field in summary
viewElements() Wraps rendered images with link URLs
getLinkFieldsOptions() Retrieves available link fields (cached)

Key Properties

Property Type Purpose
$entityFieldManager EntityFieldManagerInterface Service to query field definitions
$imageLinkFieldsOptions array<string, string>\|null Cached link field options

Service Injection

The trait injects the entity_field.manager service:

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
  $instance = parent::create(...);
  $instance->entityFieldManager = $container->get('entity_field.manager');
  return $instance;
}

This allows dynamic querying of link fields on the entity.

How It Works

Step 1: Settings Form Configuration

public function settingsForm(array $form, FormStateInterface $form_state): array {
  $element = parent::settingsForm($form, $form_state);
  // Add link fields to existing "image_link" dropdown
  $element['image_link']['#options'] += $this->getLinkFieldsOptions();
  return $element;
}

The form extends core's image_link setting dropdown with custom link fields.

protected function getLinkFieldsOptions(): array {
  if ($this->imageLinkFieldsOptions === NULL) {
    // Query entity_field.manager for link fields on this entity/bundle
    // Cache result for performance
  }
  return $this->imageLinkFieldsOptions;
}

Queries available link fields and caches them (lazy initialization).

public function viewElements(FieldItemListInterface $items, $langcode): array {
  $elements = parent::viewElements($items, $langcode);

  // Get selected link field
  $image_link_setting = $this->getSetting('image_link');
  $link_items = $items->getEntity()->get($image_link_setting);

  // For each rendered image, set its URL to the matching link field value
  foreach (array_keys($elements) as $delta) {
    $link_item_value = $link_items->get($delta);
    if (isset($link_item_value) && !$link_item_value->isEmpty()) {
      $elements[$delta]['#url'] = $link_item_value->getUrl();
    }
  }

  return $elements;
}

Key logic: 1. Get core formatter's rendered images 2. Retrieve selected link field's values 3. Match by delta (position) 4. Set each image's #url to link value

Why This Design?

Inheritance Over Override

Good: Extending core formatters ensures: - Automatic compatibility with core updates - Reduced code maintenance - Consistent behavior with parent formatter

Bad alternative: Overriding plugin class → conflicts with other formatters

Trait for Code Reuse

Good: Both formatters share identical link-wrapping logic in a trait

Bad alternative: Duplicate code in each formatter class

Service Injection

Good: Dependency injection enables: - Testability (mock entity_field.manager) - Extensibility (subclasses can add more services) - Decoupling (no static \Drupal:: calls)

Performance Considerations

Link field options are lazy-initialized and cached to avoid repeated queries:

// First call: queries database
$options = $this->getLinkFieldsOptions();

// Second call: returns cached result
$options = $this->getLinkFieldsOptions();

Core Formatter Caching

The formatted output is cached by Drupal's render cache system via: - Cache tags: ['node:123'] (entity-based) - Cache contexts: ['user.roles'] (for access control)

Extensibility

Extending the Formatter

Subclasses can inject additional services:

class CustomImageLinkFormatter extends ImageLinkFormatter {
  private CurrentUserInterface $currentUser;

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->currentUser = $container->get('current_user');
    return $instance;
  }
}

Override viewElements() to customize link generation:

public function viewElements(FieldItemListInterface $items, $langcode): array {
  $elements = parent::viewElements($items, $langcode);
  // Add custom logic (tracking, authorization, etc.)
  return $elements;
}

Next Steps

Extending Guide