Skip to content

Field Formatter Development

When to Use

When creating custom display output for field values in view modes, requiring specialized rendering beyond core label/plain text formatters.

Decision

If you need... Use... Why
Simple output Extend FormatterBase Render field values in view modes
Linked output Build render arrays with #type => link Leverage render system for access control
Complex markup Use #theme render arrays Proper caching and preprocessing
External resources Add #attached libraries Load CSS/JS only when formatter used

Pattern

Custom formatter with PHP 8 attributes:

namespace Drupal\my_module\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;

#[FieldFormatter(
  id: "geolocation_map",
  label: new TranslatableMarkup("Map"),
  field_types: ["geolocation"],
)]
class GeolocationMapFormatter extends FormatterBase {

  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];

    foreach ($items as $delta => $item) {
      $elements[$delta] = [
        '#theme' => 'geolocation_map',
        '#lat' => $item->lat,
        '#lng' => $item->lng,
        '#attached' => [
          'library' => ['my_module/geolocation-map'],
        ],
      ];
    }

    return $elements;
  }
}

Template (templates/geolocation-map.html.twig):

<div class="geolocation-map" data-lat="{{ lat }}" data-lng="{{ lng }}">
  {{ lat }}, {{ lng }}
</div>

Place in src/Plugin/Field/FieldFormatter/GeolocationMapFormatter.php

Reference: /core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php

Common Mistakes

  • Wrong: Outputting raw HTML strings → Right: Use render arrays for proper caching and security
  • Wrong: Not sanitizing output → Right: ALWAYS use render arrays or Xss::filter() / Html::escape()
  • Wrong: Missing #attached libraries → Right: JavaScript/CSS won't load; use #attached, not drupal_add_library()
  • Wrong: Forgetting field_types → Right: Formatter won't appear as option
  • Wrong: Loading entities without access checks → Right: Use entity access API or build arrays from data only
  • Wrong: Not implementing settingsForm() → Right: No formatter configuration in Field UI

Security: - NEVER output raw user input. Use render arrays with #plain_text or #markup (filtered). - Check entity access before rendering referenced entities. - Sanitize URLs with UrlHelper::filterBadProtocol() before rendering links.

Performance: - Use #cache keys/tags/contexts on render arrays for granular caching. - Lazy load expensive resources (entity loads) only when actually rendering. - Use #lazy_builder for personalized/uncacheable content within otherwise cacheable fields.

Development Standards: - Return array of render arrays indexed by delta from viewElements() - Use dependency injection for services (inject via create()) - Implement settingsForm(), settingsSummary(), and defaultSettings() for configurable formatters

See Also