Problem/Motivation

The entity_mesh module currently implements its own entity rendering logic in EntityRender::getFullEntityDom(), manually handling:

  • Theme switching with custom ThemeSwitcher service
  • Account switching for rendering as specific users
  • Language negotiation with custom LanguageNegotiatorSwitcher and StaticLanguageNegotiator services
  • View builder instantiation and render array construction
  • Drupal deprecation compatibility (renderInIsolation vs renderPlain)
  • Complex try-catch-finally blocks to ensure context restoration

The entity_render_context module was created based on patterns from entity_mesh itself and provides an identical service for rendering entities with configurable context (theme, user, language, view mode), including:

  • Built-in request-scoped caching to avoid redundant renders
  • Proper context restoration with guaranteed cleanup (via finally blocks)
  • Centralized deprecation handling for Drupal 10.3+ compatibility
  • Comprehensive error handling with logging
  • Reusable theme and language switching services

Integration benefits:

  • Eliminates ~300 lines of duplicate code
  • Simplifies getFullEntityDom() from 65 lines to ~25 lines
  • Improves performance through shared caching between entity_render_context's internal cache and entity_mesh's DOM cache
  • Reduces maintenance burden (rendering logic managed in one place)
  • Better separation of concerns (entity_mesh focuses on link analysis, entity_render_context handles rendering)
  • Ensures consistent rendering behavior across dependent modules (content_first, etc.)

Steps to reproduce

N/A - This is a refactoring to integrate an external service, not a bug fix.

To verify the integration after applying changes:

  1. Enable entity_render_context and entity_mesh modules
  2. Run database updates: drush updb (installs entity_render_context if upgrading)
  3. Clear cache: drush cr
  4. Navigate to the Entity Mesh UI (/admin/content/entity-mesh)
  5. Trigger entity mesh analysis on content
  6. Verify links are detected and analyzed correctly (no visual changes expected)
  7. Check that DOM parsing and link extraction work as before

Proposed resolution

Refactor entity_mesh to use entity_render_context.renderer service:

  1. Add module dependency: Declare entity_render_context as a dependency in entity_mesh.info.yml and composer.json
  2. Update service injection: Replace multiple services (@renderer, @account_switcher, @entity_mesh.theme_switcher, @entity_mesh.language_negotiator_switcher) with single
    @entity_render_context.renderer service in entity_mesh.services.yml
  3. Simplify EntityRender class:
    • Remove imports: DeprecationHelper, RendererInterface, AccountSwitcherInterface, LanguageNegotiatorSwitcher
    • Add import: EntityRenderContextInterface
    • Remove properties: $renderer, $accountSwitcher, $languageNegotiatorSwitcher, $themeSwitcher
    • Add property: $entityRenderContext with type EntityRenderContextInterface
    • Update constructor to accept EntityRenderContextInterface instead of individual rendering services
    • Simplify getFullEntityDom() method to use single service call
  4. Remove duplicate service classes:
    • Delete src/ThemeSwitcher.php
    • Delete src/ThemeSwitcherInterface.php
    • Delete src/Language/LanguageNegotiatorSwitcher.php
    • Delete src/Language/StaticLanguageNegotiator.php
    • Remove service definitions from entity_mesh.services.yml
  5. Add update hook: Create entity_mesh_update_10022() to install entity_render_context module for existing installations
  6. Update tests: Modify EntityRenderTest.php to use new constructor signature with mocked EntityRenderContextInterface

Before (EntityRender::getFullEntityDom):

protected function getFullEntityDom(EntityInterface $entity, string $langcode) {
    // Generate cache key based on entity type, ID, and language.
    $cache_key = $entity->getEntityTypeId() . ':' . $entity->id() . ':' . $langcode;

    // Check if DOM is already cached.
    if (isset($this->domCache[$cache_key])) {
      return $this->domCache[$cache_key];
    }
    // Switch to the default theme in case the admin theme is enabled.
    $previous_theme = $this->themeSwitcher->switchToDefault();

    $this->accountSwitcher->switchTo($this->entityMeshRepository->getMeshAccount());
    // Switch system to the entity language so the entity is fully rendered
    // in the specific language.
    $this->languageNegotiatorSwitcher->switchLanguage($entity->language());

    // Load all modules before rendering.
    if (!$this->moduleHandler->isLoaded()) {
      $this->moduleHandler->loadAll();
    }

    try {
      // Render entity HTML output:
      $view_mode = 'full';
      $view_builder = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId());
      $pre_render = $view_builder->view($entity, $view_mode, $langcode);

      $render_output = DeprecationHelper::backwardsCompatibleCall(
        currentVersion: \Drupal::VERSION,
        deprecatedVersion: '10.3',
        currentCallable: fn() => $this->renderer->renderInIsolation($pre_render),
        deprecatedCallable: fn() => $this->renderer->renderPlain($pre_render),
      );

      // Switches back to the current language:
      $this->languageNegotiatorSwitcher->switchBack();
      // Switch back to the current user:
      $this->accountSwitcher->switchBack();
      // Restore the original theme.
      $this->themeSwitcher->switchBack($previous_theme);

      // Parse the HTML.
      $dom = new \DOMDocument();
      @$dom->loadHTML('<?xml encoding="UTF-8">' . $render_output, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

      // Cache the DOM before returning.
      $this->domCache[$cache_key] = $dom;

      return $dom;
    }
    catch (\Exception $e) {
      // Ensure we switch everything back in case of an error.
      $this->languageNegotiatorSwitcher->switchBack();
      $this->accountSwitcher->switchBack();
      $this->themeSwitcher->switchBack($previous_theme);
      throw $e;
    }
  }
  

After (using entity_render_context):

protected function getFullEntityDom(EntityInterface $entity, string $langcode) {
    // Generate cache key based on entity type, ID, and language.
    $cache_key = $entity->getEntityTypeId() . ':' . $entity->id() . ':' . $langcode;

    // Check if DOM is already cached.
    if (isset($this->domCache[$cache_key])) {
      return $this->domCache[$cache_key];
    }

    // Use entity_render_context service to render the entity.
    // This handles theme switching, account switching, and language switching.
    $render_output = $this->entityRenderContext->renderEntity(
      $entity,
      'full',
      NULL,  // Use default theme
      $this->entityMeshRepository->getMeshAccount(),
      $langcode
    );

    if ($render_output === NULL) {
      throw new \Exception('Failed to render entity');
    }

    // Parse the HTML.
    $dom = new \DOMDocument();
    @$dom->loadHTML('<?xml encoding="UTF-8">' . $render_output, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);

    // Cache the DOM before returning.
    $this->domCache[$cache_key] = $dom;

    return $dom;
  }
  

Rendering context preserved:

  • Theme: NULL parameter uses default theme (matches existing behavior via switchToDefault())
  • User: Uses mesh account for rendering (preserves existing behavior)
  • Language: Explicit langcode parameter respects entity's language (matches existing behavior)
  • Context restoration: entity_render_context automatically restores all contexts via finally blocks

User interface changes

None. This is a backend refactoring. The entity_mesh module UI, functionality, and link analysis output remain unchanged.

API changes

Breaking changes (major version required):

  • Removed services:
    • entity_mesh.theme_switcher - Use entity_render_context.theme_switcher instead
    • entity_mesh.language_negotiator_switcher - Use entity_render_context.language_negotiator_switcher instead
    • entity_mesh.static_language_negotiator - Use entity_render_context.static_language_negotiator instead
  • Removed classes:
    • Drupal\entity_mesh\ThemeSwitcher
    • Drupal\entity_mesh\ThemeSwitcherInterface
    • Drupal\entity_mesh\Language\LanguageNegotiatorSwitcher
    • Drupal\entity_mesh\Language\StaticLanguageNegotiator
  • EntityRender constructor signature changed:
    • Removed parameters: RendererInterface $renderer, AccountSwitcherInterface $account_switcher, LanguageNegotiatorSwitcher $language_negotiator_switcher, ThemeSwitcher $theme_switcher
    • Added parameter: EntityRenderContextInterface $entity_render_context

Public API (behavioral compatibility maintained):

  • All public methods of EntityRender retain same signatures and behavior
  • getFullEntityDom() returns identical DOM structure
  • Link extraction logic unchanged

New module dependency:

  • entity_mesh now requires entity_render_context module to function

Upgrade path for custom code:

If you inject entity_mesh.theme_switcher, entity_mesh.language_negotiator_switcher, or entity_mesh.static_language_negotiator services, update to use equivalent services from entity_render_context module:

  • @entity_mesh.theme_switcher@entity_render_context.theme_switcher
  • @entity_mesh.language_negotiator_switcher@entity_render_context.language_negotiator_switcher
  • @entity_mesh.static_language_negotiator@entity_render_context.static_language_negotiator

If you extend EntityRender class, update your constructor to match the new signature.

Data model changes

None. This refactoring affects only the rendering service layer. No database schema, configuration, or entity definitions are changed. Existing entity mesh data remains compatible.

Upgrade path for existing installations:

  1. Update entity_mesh module code to new version
  2. Run database updates: drush updb
    • Update hook entity_mesh_update_10022() automatically installs entity_render_context module
  3. Clear cache: drush cr
  4. No manual intervention required
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

eduardo morales alberti’s picture

Status: Active » Needs review

Ready

eduardo morales alberti’s picture

Status: Needs review » Needs work
eduardo morales alberti’s picture

Status: Needs work » Needs review

Ready to review

eduardo morales alberti’s picture

Status: Needs review » Fixed

Merged, fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.