Skip to content

Annotations Context — Developer Reference

Developer-focused reference. For module overview, feature list, and permissions see README.md.


ContextAssembler (annotations_context.assembler)

The central service. Builds a structured PHP array from annotation data. All other features in this module and its consumers derive from this payload.

use Drupal\annotations_context\ContextAssembler;

$payload = $assembler->assemble();                                 // all targets, all types
$payload = $assembler->assemble(['entity_type' => 'node']);        // one entity type
$payload = $assembler->assemble(['target_id' => 'node__article']); // one target
$payload = $assembler->assemble(['types' => ['editorial']]);       // explicit type filter
$payload = $assembler->assemble(['ref_depth' => 1]);               // follow ER fields one hop
$payload = $assembler->assemble(['account' => $currentUser]);      // filter by user permissions
$payload = $assembler->assemble(['role' => 'editor']);             // simulate a role
$payload = $assembler->assemble(['inc_refs' => TRUE]);             // add reverse ER sources

All options are optional and combine freely.

Options

Option Type Default Description
entity_type string\|null null Limit to targets of this entity type.
target_id string\|null null Limit to a single target by machine name (e.g. node__article).
types string[]\|null null (all) Explicit list of annotation type IDs to include.
ref_depth int 0 Entity-reference traversal depth. 0 = no traversal; 12 follows linked targets.
role string\|null null Simulate context as this Drupal role — only types that role can consume are included. Takes precedence over account.
account AccountInterface\|null null Filter to types the given account can view via its combined role permissions. Accounts with administer annotations bypass filtering.
inc_meta bool false Add type, cardinality, and description to each field entry. Useful for AI context; noisy for human review.
inc_refs bool false Add an incoming_refs key to each target listing annotation targets that reference it via entity-reference fields. Flat only — no recursive reverse traversal.

Role and account filtering

Use role to simulate what a role sees without impersonating a user — useful for previews and testing:

$payload = $assembler->assemble(['role' => 'content_editor']);

Use account for real current-user context in live features. Accounts with administer annotations bypass all type filtering:

$payload = $assembler->assemble(['account' => $this->currentUser]);

role takes precedence when both are supplied.

Payload structure

[
  'groups' => [
    'node' => [
      'entity_type' => 'node',
      'label'       => 'Content types',
      'targets'     => [
        'node__article' => [
          'id'          => 'node__article',
          'label'       => 'Article',
          'entity_type' => 'node',
          'bundle'      => 'article',
          'annotations' => [
            'editorial' => ['label' => 'Editorial', 'value' => '...'],
            'rules'     => ['label' => 'Rules',     'value' => '...'],
          ],
          'fields' => [
            'body' => [
              'label'       => 'Body',
              'annotations' => ['editorial' => ['label' => 'Editorial', 'value' => '...']],
              // 'meta' key present when inc_meta = TRUE:
              'meta' => ['type' => 'text_long', 'cardinality' => 'single value', 'description' => '...'],
            ],
          ],
          'references'    => [...], // only present when ref_depth > 0
          'incoming_refs' => [      // only present when inc_refs = TRUE
            'media__image' => [
              'label'      => 'Image',
              'via_fields' => ['field_featured_image'],
            ],
          ],
        ],
      ],
    ],
  ],
  'meta' => [
    'generated_at' => '2026-04-20T12:00:00+00:00',
    'ref_depth'    => 0,
    'inc_refs'     => FALSE,
    'target_count' => 12,
  ],
]

Only non-empty annotation values are included. Targets with no matching annotations are omitted when type-filtering is active.

HTML normalization: All string values in the payload are passed through flattenHtml() before being added. This strips markup, preserves links as text (url), decodes HTML entities, and collapses whitespace. Normalization happens at read time so every consumer — the HTML preview and the markdown export — receives clean text.

Cache metadata from alter implementations

If your code produces a cacheable page from an assembled payload, merge alter-contributed cache requirements:

$payload = $assembler->assemble($options);
$assembler->getLastCacheableMetadata()->applyTo($build);

ContextPreviewController does this automatically.


ContextRenderer (annotations_context.renderer)

Renders the payload to a UTF-8 markdown string. Stateless — no Drupal services involved. Safe for file download; values are not HTML-escaped (markdown is plain text).

$markdown = $renderer->render($payload);

ContextHtmlRenderer (annotations_context.html_renderer)

Renders the payload to a Drupal render array. All annotation values are escaped via Html::escape(). Uses details/summary collapsible cards.

$build = $htmlRenderer->render($payload);
// Return directly from a controller.

Writing a custom renderer

A renderer just consumes the plain PHP array — no base class required.

class MyJsonRenderer {
  public function render(array $payload): string {
    $output = [];
    foreach ($payload['groups'] as $group) {
      foreach ($group['targets'] as $target) {
        $output[] = [
          'id'          => $target['id'],
          'annotations' => $target['annotations'],
          'fields'      => $target['fields'],
        ];
      }
    }
    return json_encode($output, JSON_PRETTY_PRINT);
  }
}

Security: Always escape annotation values when producing HTML output. Although ContextAssembler strips HTML markup from values at read time, the resulting plain text must still be escaped (via Html::escape() or #plain_text) before insertion into HTML.


Entity reference traversal

Set ref_depth to follow entity reference fields into referenced targets:

  • 0 (default) — no traversal; only the directly annotated target
  • 1 — one hop (e.g. Article → referenced Media)
  • 2 — two hops (recommended maximum; depth 3+ rarely adds useful signal and can produce very large payloads)

Each referenced target is assembled in full and nested under references → field name → target ID. Cycle detection prevents the same target appearing twice in a payload.

Incoming references

Set inc_refs => TRUE (or ?inc_refs=1 on HTTP endpoints) to surface reverse relationships — which annotation targets reference a given target. Useful for leaf entities.

$payload = $assembler->assemble([
  'target_id' => 'media__image',
  'inc_refs'  => TRUE,
]);

Each target entry gains an incoming_refs key:

'incoming_refs' => [
  'node__article' => [
    'label'      => 'Article',
    'via_fields' => ['field_featured_image'],
  ],
  'node__landing_page' => [
    'label'      => 'Landing page',
    'via_fields' => ['field_hero_media', 'field_gallery'],
  ],
],

via_fields is always an array. Only ER fields in the source target's annotation scope are considered, matching the forward traversal behavior. Reverse traversal is flat — incoming sources are not themselves expanded.


Extending the payload

ContextAssembler::assemble() invokes hook_annotations_context_alter() at the end of every assembly call. Use it to append, remove, or reshape payload sections.

use Drupal\Core\Cache\CacheableMetadata;

function mymodule_annotations_context_alter(array &$payload, array $options, CacheableMetadata &$cacheableMetadata): void {
  // Any top-level key not named 'groups' or 'meta' is yours.
  $payload['my_section'] = [
    'setting_a' => 'value',
    'setting_b' => TRUE,
  ];

  $cacheableMetadata->addCacheTags(['mymodule_data_list']);
  $cacheableMetadata->addCacheContexts(['user.roles']);
}

The $options argument is the same array passed to assemble() — use it to conditionally modify the payload based on filters the caller applied.

Callers that produce cacheable output must merge the metadata:

$payload = $assembler->assemble($options);
$assembler->getLastCacheableMetadata()->applyTo($build);