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¶
- Extends, not overrides — Inherits from core
ImageFormatterandResponsiveImageFormatter - Trait-based — Common logic in
ImageLinkFormatterTraitfor reusability - Dependency injection — Services injected via the
create()factory method - 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.
Step 2: Link Fields Discovery¶
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).
Step 3: Render with Link Wrapping¶
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¶
Caching Link Fields¶
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;
}
}
Altering Link Behavior¶
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;
}