Skip to content

Extending the Module

Creating Custom Formatters

The Image Link Formatter architecture is designed for extension. You can create custom formatters that inherit its link-wrapping functionality.

Extending ImageLinkFormatter

Basic Extension

Create a new formatter that extends ImageLinkFormatter and adds custom logic:

<?php

namespace Drupal\my_module\Plugin\Field\FieldFormatter;

use Drupal\image_link_formatter\Plugin\Field\FieldFormatter\ImageLinkFormatter;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Custom image formatter that wraps images in links and tracks clicks.
 *
 * @FieldFormatter(
 *   id = "tracked_image_link_formatter",
 *   label = @Translation("Tracked image wrapped in link"),
 *   field_types = {"image"}
 * )
 */
class TrackedImageLinkFormatter extends ImageLinkFormatter {

  /**
   * Custom tracking service.
   */
  private ClickTrackerInterface $clickTracker;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->clickTracker = $container->get('my_module.click_tracker');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode): array {
    $elements = parent::viewElements($items, $langcode);

    // Add tracking data to each link
    foreach ($elements as $delta => &$element) {
      if (isset($element['#url'])) {
        $element['#attributes']['data-track'] = 'true';
        $element['#attributes']['class'][] = 'tracked-link';
      }
    }

    return $elements;
  }
}

Adding Services

Inject additional services via the create() factory method:

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
  $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);

  // Add your custom services
  $instance->currentUser = $container->get('current_user');
  $instance->logger = $container->get('logger.channel.my_module');
  $instance->customService = $container->get('my_module.custom_service');

  return $instance;
}

Overriding Settings Form

Add custom settings to the formatter configuration:

public function settingsForm(array $form, FormStateInterface $form_state): array {
  $element = parent::settingsForm($form, $form_state);

  // Add custom settings
  $element['track_clicks'] = [
    '#type' => 'checkbox',
    '#title' => $this->t('Track clicks'),
    '#default_value' => $this->getSetting('track_clicks'),
    '#description' => $this->t('Log when users click linked images.'),
  ];

  return $element;
}

public function settingsSummary(): array {
  $summary = parent::settingsSummary();

  if ($this->getSetting('track_clicks')) {
    $summary[] = $this->t('Tracking enabled');
  }

  return $summary;
}

public static function defaultSettings(): array {
  return parent::defaultSettings() + [
    'track_clicks' => FALSE,
  ];
}

Extending ResponsiveImageLinkFormatter

The same patterns apply to responsive images:

<?php

namespace Drupal\my_module\Plugin\Field\FieldFormatter;

use Drupal\responsive_image_link_formatter\Plugin\Field\FieldFormatter\ResponsiveImageLinkFormatter;

/**
 * Custom responsive image formatter with link wrapping.
 *
 * @FieldFormatter(
 *   id = "custom_responsive_image_link_formatter",
 *   label = @Translation("Custom responsive image in link"),
 *   field_types = {"image"}
 * )
 */
class CustomResponsiveImageLinkFormatter extends ResponsiveImageLinkFormatter {

  // Add custom logic here
  // Same patterns as above apply
}

Registering Your Module

Create a service definition for any custom services your formatter needs.

my_module.services.yml:

services:
  my_module.click_tracker:
    class: Drupal\my_module\Service\ClickTracker
    arguments:
      - '@logger.channel.my_module'
      - '@database'

  my_module.custom_service:
    class: Drupal\my_module\Service\CustomService
    arguments:
      - '@current_user'

Common Extension Patterns

Add attributes, classes, or modify URLs:

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

  foreach ($elements as $delta => &$element) {
    if (isset($element['#url'])) {
      // Add tracking parameter
      $query = $element['#url']->getOption('query') ?? [];
      $query['utm_source'] = 'image_link';
      $element['#url']->setOption('query', $query);

      // Open in new window
      $element['#attributes']['target'] = '_blank';
    }
  }

  return $elements;
}

Only wrap images for certain users or conditions:

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

  // Only wrap if user has permission
  if (!$this->currentUser->hasPermission('access external links')) {
    // Remove link URLs
    foreach ($elements as &$element) {
      unset($element['#url']);
    }
  }

  return $elements;
}

Override link field discovery for dynamic field selection:

protected function getLinkFieldsOptions(): array {
  if ($this->imageLinkFieldsOptions === NULL) {
    $this->imageLinkFieldsOptions = [];

    // Custom logic to find link fields
    $entity_type = $this->fieldDefinition->getTargetEntityTypeId();
    $bundle = $this->fieldDefinition->getTargetBundle();

    // Query only fields matching your criteria
    $fields = $this->entityFieldManager
      ->getFieldDefinitions($entity_type, $bundle);

    foreach ($fields as $field_name => $field) {
      if ($field->getType() === 'link' && /* your custom condition */) {
        $this->imageLinkFieldsOptions[$field_name] = $field->getLabel();
      }
    }
  }

  return $this->imageLinkFieldsOptions;
}

4. Logging and Debugging

Log formatter operations for troubleshooting:

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

  $entity = $items->getEntity();
  $this->logger->debug(
    'Formatting @count images for @entity_type:@entity_id',
    [
      '@count' => count($elements),
      '@entity_type' => $entity->getEntityTypeId(),
      '@entity_id' => $entity->id(),
    ]
  );

  return $elements;
}

Testing Custom Formatters

Kernel Test Example

<?php

namespace Drupal\Tests\my_module\Kernel;

use Drupal\KernelTests\KernelTestBase;
use Drupal\entity_test\Entity\EntityTest;

/**
 * Tests for custom image link formatter.
 *
 * @group my_module
 */
class CustomImageLinkFormatterTest extends KernelTestBase {

  protected static $modules = [
    'system',
    'field',
    'image',
    'link',
    'image_link_formatter',
    'my_module',
  ];

  public function testTrackedImageFormatter() {
    // Create test entity with image and link fields
    $entity = EntityTest::create([
      'field_image' => ['target_id' => 1],
      'field_link' => ['uri' => 'https://example.com', 'title' => 'Example'],
    ]);

    // Render using custom formatter
    $formatter = $this->container
      ->get('plugin.manager.field.formatter')
      ->getInstance([
        'field_definition' => $entity->getFieldDefinition('field_image'),
        'view_mode' => 'default',
        'configuration' => [
          'type' => 'tracked_image_link_formatter',
          'image_link' => 'field_link',
          'track_clicks' => TRUE,
        ],
      ]);

    $elements = $formatter->viewElements($entity->field_image, 'en');

    // Assert link has tracking class
    $this->assertContains('tracked-link', $elements[0]['#attributes']['class']);
  }
}

Further Reading

Next Steps

API Reference