Skip to content

Implement proxy rendering engine with cache metadata handling #25

@e0ipso

Description

@e0ipso

Implement proxy rendering engine with cache metadata handling

Parent Issue: #22 - Add A/B Testing Proxy Block Plugin
Phase: Foundation
Depends On: #23 (plugin skeleton), #24 (configuration form)
Recommended Assignee: AI (systematic implementation following established patterns)

Objective

Implement the core rendering logic for the AbTestProxyBlock, including target block instantiation, render array construction, and proper cache metadata bubbling.

Tasks

1. Implement build() Method

Core Functionality:

  • Check render mode and handle accordingly
  • Instantiate target block plugin with configuration
  • Execute target block's build() method
  • Bubble up cache metadata properly
  • Return appropriate render array

2. Target Block Instantiation Logic

protected function createTargetBlock(): ?BlockPluginInterface {
  $config = $this->getConfiguration();
  $plugin_id = $config['target_block_plugin'] ?? '';
  $block_config = $config['target_block_config'] ?? [];
  
  if (empty($plugin_id)) {
    return NULL;
  }
  
  try {
    return $this->blockManager->createInstance($plugin_id, $block_config);
  }
  catch (PluginException $e) {
    \Drupal::logger('ab_blocks')->warning('Failed to create target block @plugin: @message', [
      '@plugin' => $plugin_id,
      '@message' => $e->getMessage(),
    ]);
    return NULL;
  }
}

3. Main Build Method Implementation

public function build(): array {
  $config = $this->getConfiguration();
  
  // Handle empty render mode
  if ($config['render_mode'] === 'empty') {
    return [
      '#markup' => '',
      '#cache' => [
        'contexts' => $this->getCacheContexts(),
        'tags' => $this->getCacheTags(),
        'max-age' => $this->getCacheMaxAge(),
      ],
    ];
  }
  
  // Create and render target block
  $target_block = $this->createTargetBlock();
  if (\!$target_block) {
    return $this->buildErrorState();
  }
  
  // Check target block access
  $access_result = $target_block->access(\Drupal::currentUser(), TRUE);
  if (\!$access_result->isAllowed()) {
    $build = [
      '#markup' => '',
      '#cache' => [
        'contexts' => $this->getCacheContexts(),
        'tags' => $this->getCacheTags(),
        'max-age' => $this->getCacheMaxAge(),
      ],
    ];
    CacheableMetadata::createFromObject($access_result)->applyTo($build);
    return $build;
  }
  
  // Build target block
  $build = $target_block->build();
  
  // Bubble up cache metadata from target block
  $this->bubbleTargetBlockCacheMetadata($build, $target_block);
  
  return $build;
}

4. Cache Metadata Handling

protected function bubbleTargetBlockCacheMetadata(array &$build, BlockPluginInterface $target_block): void {
  $cache_metadata = CacheableMetadata::createFromRenderArray($build);
  
  // Add target block's cache contexts, tags, and max-age
  $cache_metadata->addCacheContexts($target_block->getCacheContexts());
  $cache_metadata->addCacheTags($target_block->getCacheTags());
  $cache_metadata->setCacheMaxAge(
    Cache::mergeMaxAges($cache_metadata->getCacheMaxAge(), $target_block->getCacheMaxAge())
  );
  
  // Add proxy block's own cache metadata
  $cache_metadata->addCacheContexts($this->getCacheContexts());
  $cache_metadata->addCacheTags($this->getCacheTags());
  $cache_metadata->setCacheMaxAge(
    Cache::mergeMaxAges($cache_metadata->getCacheMaxAge(), $this->getCacheMaxAge())
  );
  
  $cache_metadata->applyTo($build);
}

5. Error State Handling

protected function buildErrorState(): array {
  return [
    '#markup' => $this->t('Block configuration error: Target block could not be loaded.'),
    '#cache' => [
      'contexts' => $this->getCacheContexts(),
      'tags' => $this->getCacheTags(),
      'max-age' => $this->getCacheMaxAge(),
    ],
  ];
}

6. Cache Method Overrides

public function getCacheContexts(): array {
  $cache_contexts = parent::getCacheContexts();
  
  // Add contexts based on configuration
  $config = $this->getConfiguration();
  if (\!empty($config['target_block_plugin'])) {
    $target_block = $this->createTargetBlock();
    if ($target_block) {
      $cache_contexts = Cache::mergeContexts($cache_contexts, $target_block->getCacheContexts());
    }
  }
  
  return $cache_contexts;
}

public function getCacheTags(): array {
  $cache_tags = parent::getCacheTags();
  
  $config = $this->getConfiguration();
  if (\!empty($config['target_block_plugin'])) {
    $target_block = $this->createTargetBlock();
    if ($target_block) {
      $cache_tags = Cache::mergeTags($cache_tags, $target_block->getCacheTags());
    }
  }
  
  // Add config-based cache tag
  $cache_tags[] = 'ab_test_proxy_block:' . $this->getPluginId();
  
  return $cache_tags;
}

public function getCacheMaxAge(): int {
  $max_age = parent::getCacheMaxAge();
  
  $config = $this->getConfiguration();
  if (\!empty($config['target_block_plugin'])) {
    $target_block = $this->createTargetBlock();
    if ($target_block) {
      $max_age = Cache::mergeMaxAges($max_age, $target_block->getCacheMaxAge());
    }
  }
  
  return $max_age;
}

Implementation Guidelines

Error Handling Strategy

  • Log errors for debugging without exposing to users
  • Graceful degradation when target blocks fail
  • Respect access control from target blocks
  • Return empty renders for invalid configurations

Performance Considerations

  • Cache target block instances when possible
  • Avoid recreating blocks unnecessarily
  • Proper cache invalidation triggers
  • Efficient cache metadata aggregation

Access Control

  • Respect target block access restrictions
  • Apply access results to cache metadata
  • Handle access-denied scenarios gracefully

Required Imports

use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Component\Plugin\Exception\PluginException;

Testing Guidelines

Unit Testing Scenarios

// Test empty render mode
public function testEmptyRenderMode() {
  // Set render_mode to 'empty'
  // Assert build() returns empty markup with proper cache metadata
}

// Test target block rendering  
public function testTargetBlockRendering() {
  // Mock target block with known output
  // Assert proxy block returns target block's output
  // Assert cache metadata is properly bubbled up
}

// Test invalid target block
public function testInvalidTargetBlock() {
  // Set non-existent target block plugin
  // Assert error state is returned
  // Assert error is logged
}

// Test access control
public function testAccessControl() {
  // Mock target block that denies access
  // Assert empty render with access metadata
}

Manual Testing

  • Place proxy block with different target blocks
  • Verify output matches target block's output
  • Check cache headers in browser dev tools
  • Test with access-restricted target blocks

Cache Behavior Requirements

Cache Contexts

  • Include all target block cache contexts
  • Add proxy-specific contexts if needed
  • Merge contexts properly using Cache::mergeContexts()

Cache Tags

  • Include all target block cache tags
  • Add proxy block configuration tag
  • Use Cache::mergeTags() for proper merging

Cache Max Age

  • Use most restrictive max age between proxy and target
  • Use Cache::mergeMaxAges() for proper calculation
  • Handle Cache::PERMANENT appropriately

Acceptance Criteria

  • Empty render mode returns empty markup with proper cache metadata
  • Target block instantiation works for valid plugins
  • Target block build() method executed and output returned
  • Cache metadata properly bubbled from target blocks
  • Access control respected from target blocks
  • Error states handled gracefully with logging
  • No PHP errors or warnings during rendering
  • Cache invalidation works correctly

Context for AI Implementation

This is a systematic implementation task that follows established Drupal patterns:

  1. Standard Block Rendering: Follow how other blocks handle target instantiation
  2. Cache Metadata Patterns: Study core blocks that proxy other plugins
  3. Error Handling: Use Drupal logging standards
  4. Access Control: Follow core access result handling patterns

Key References in Codebase:

  • Study existing block plugins in /core/modules/block/src/Plugin/Block/
  • Look at Layout Builder's block handling patterns
  • Reference cache metadata handling in core render systems

Testing Strategy:

  • Focus on systematic testing of all render paths
  • Mock dependencies appropriately for unit tests
  • Verify cache behavior matches expectations

Sequential Dependencies:

Metadata

Metadata

Assignees

No one assigned

    Labels

    🏷️ type:productionProduction codebase (src/, modules/, core functionality)📂 area:blocksLayout Builder block A/B testing features📂 area:coreCore plugin system, interfaces, managers, and base functionality🔥 priority:highImportant features, significant bugs affecting many users

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions