-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
π·οΈ type:productionProduction codebase (src/, modules/, core functionality)Production codebase (src/, modules/, core functionality)π area:blocksLayout Builder block A/B testing featuresLayout Builder block A/B testing featuresπ area:coreCore plugin system, interfaces, managers, and base functionalityCore plugin system, interfaces, managers, and base functionalityπ₯ priority:highImportant features, significant bugs affecting many usersImportant features, significant bugs affecting many users
Description
Add comprehensive error handling and edge case management
Parent Issue: #22 - Add A/B Testing Proxy Block Plugin
Phase: Polish
Depends On: #23 (skeleton), #24 (configuration), #25 (rendering)
Recommended Assignee: AI (systematic error condition identification)
Objective
Implement comprehensive error handling throughout the AbTestProxyBlock to ensure graceful degradation and proper debugging information for all edge cases.
Error Scenarios to Handle
1. Plugin Instantiation Errors
Scenarios:
- Target block plugin doesn't exist
- Target block plugin exists but fails to instantiate
- Plugin configuration is malformed
- Plugin dependencies missing
Implementation:
protected function createTargetBlock(): ?BlockPluginInterface {
$config = $this->getConfiguration();
$plugin_id = $config['target_block_plugin'] ?? '';
if (empty($plugin_id)) {
$this->logError('Empty target block plugin ID', ['config' => $config]);
return NULL;
}
if (\!$this->blockManager->hasDefinition($plugin_id)) {
$this->logError('Target block plugin not found: @plugin', ['@plugin' => $plugin_id]);
return NULL;
}
try {
$block_config = $config['target_block_config'] ?? [];
return $this->blockManager->createInstance($plugin_id, $block_config);
}
catch (PluginException $e) {
$this->logError('Failed to instantiate target block @plugin: @message', [
'@plugin' => $plugin_id,
'@message' => $e->getMessage(),
]);
return NULL;
}
catch (\Exception $e) {
$this->logError('Unexpected error instantiating target block @plugin: @message', [
'@plugin' => $plugin_id,
'@message' => $e->getMessage(),
]);
return NULL;
}
}2. Configuration Validation Errors
Scenarios:
- Invalid render mode values
- Malformed target block configuration
- Missing required configuration keys
- Configuration from different Drupal version
Implementation:
protected function validateConfiguration(): array {
$config = $this->getConfiguration();
$errors = [];
// Validate render mode
$valid_modes = ['block', 'empty'];
if (\!in_array($config['render_mode'] ?? '', $valid_modes)) {
$errors[] = 'Invalid render mode: ' . ($config['render_mode'] ?? 'NULL');
}
// Validate target block configuration when in block mode
if (($config['render_mode'] ?? '') === 'block') {
if (empty($config['target_block_plugin'])) {
$errors[] = 'Target block plugin required when render mode is "block"';
}
if (isset($config['target_block_config']) && \!is_array($config['target_block_config'])) {
$errors[] = 'Target block configuration must be an array';
}
}
return $errors;
}
public function build(): array {
$validation_errors = $this->validateConfiguration();
if (\!empty($validation_errors)) {
foreach ($validation_errors as $error) {
$this->logError('Configuration validation error: @error', ['@error' => $error]);
}
return $this->buildConfigurationErrorState($validation_errors);
}
// Continue with normal build process...
}3. Target Block Runtime Errors
Scenarios:
- Target block build() method throws exception
- Target block returns invalid render array
- Target block access check fails
- Target block has circular dependencies
Implementation:
protected function buildTargetBlock(BlockPluginInterface $target_block): array {
try {
$build = $target_block->build();
if (\!is_array($build)) {
$this->logError('Target block returned non-array build: @type', [
'@type' => gettype($build),
]);
return $this->buildRuntimeErrorState('Target block returned invalid build result');
}
return $build;
}
catch (\Exception $e) {
$this->logError('Target block build() failed: @message', [
'@message' => $e->getMessage(),
'@plugin' => $target_block->getPluginId(),
]);
return $this->buildRuntimeErrorState('Target block failed to render');
}
}4. Form Handling Errors
Scenarios:
- Form state corruption during AJAX requests
- Invalid form submissions
- Missing form values
- Form tampering/security issues
Implementation in blockValidate():
public function blockValidate($form, FormStateInterface $form_state) {
$values = $form_state->getValues();
try {
// Validate render mode
if (\!isset($values['render_mode']) || \!in_array($values['render_mode'], ['block', 'empty'])) {
$form_state->setErrorByName('render_mode', $this->t('Invalid render mode selected.'));
return;
}
// Validate block mode requirements
if ($values['render_mode'] === 'block') {
if (empty($values['target_block_plugin'])) {
$form_state->setErrorByName('target_block_plugin',
$this->t('You must select a target block when render mode is "block".'));
return;
}
// Security: Validate plugin ID format
if (\!preg_match('/^[a-z0-9_]+$/', $values['target_block_plugin'])) {
$form_state->setErrorByName('target_block_plugin',
$this->t('Invalid block plugin ID format.'));
return;
}
// Validate plugin exists
if (\!$this->blockManager->hasDefinition($values['target_block_plugin'])) {
$form_state->setErrorByName('target_block_plugin',
$this->t('The selected block plugin does not exist.'));
return;
}
}
}
catch (\Exception $e) {
$this->logError('Form validation error: @message', ['@message' => $e->getMessage()]);
$form_state->setError($form, $this->t('A validation error occurred. Please try again.'));
}
}5. Cache-Related Errors
Scenarios:
- Target block returns invalid cache metadata
- Cache context calculation fails
- Cache tag generation errors
- Memory issues with large cache metadata
Implementation:
protected function bubbleTargetBlockCacheMetadata(array &$build, BlockPluginInterface $target_block): void {
try {
$cache_metadata = CacheableMetadata::createFromRenderArray($build);
// Safely get cache contexts
try {
$target_contexts = $target_block->getCacheContexts();
if (is_array($target_contexts)) {
$cache_metadata->addCacheContexts($target_contexts);
}
}
catch (\Exception $e) {
$this->logError('Failed to get cache contexts from target block: @message', ['@message' => $e->getMessage()]);
}
// Safely get cache tags
try {
$target_tags = $target_block->getCacheTags();
if (is_array($target_tags)) {
$cache_metadata->addCacheTags($target_tags);
}
}
catch (\Exception $e) {
$this->logError('Failed to get cache tags from target block: @message', ['@message' => $e->getMessage()]);
}
// Safely get cache max age
try {
$target_max_age = $target_block->getCacheMaxAge();
if (is_int($target_max_age)) {
$cache_metadata->setCacheMaxAge(
Cache::mergeMaxAges($cache_metadata->getCacheMaxAge(), $target_max_age)
);
}
}
catch (\Exception $e) {
$this->logError('Failed to get cache max age from target block: @message', ['@message' => $e->getMessage()]);
}
$cache_metadata->applyTo($build);
}
catch (\Exception $e) {
$this->logError('Failed to apply cache metadata: @message', ['@message' => $e->getMessage()]);
// Apply basic cache metadata as fallback
$build['#cache'] = [
'contexts' => ['url'],
'tags' => [$this->getCacheTagsToInvalidate()],
'max-age' => 0, // Disable caching on error
];
}
}Helper Methods for Error Handling
1. Centralized Logging
protected function logError(string $message, array $context = []): void {
\Drupal::logger('ab_blocks')->error('[AbTestProxyBlock] ' . $message, $context + [
'plugin_id' => $this->getPluginId(),
'block_id' => $this->getConfiguration()['id'] ?? 'unknown',
]);
}
protected function logWarning(string $message, array $context = []): void {
\Drupal::logger('ab_blocks')->warning('[AbTestProxyBlock] ' . $message, $context + [
'plugin_id' => $this->getPluginId(),
'block_id' => $this->getConfiguration()['id'] ?? 'unknown',
]);
}2. Error State Builders
protected function buildConfigurationErrorState(array $errors): array {
return [
'#markup' => $this->t('Block configuration errors: @errors', [
'@errors' => implode(', ', $errors),
]),
'#cache' => ['max-age' => 0], // Don't cache error states
];
}
protected function buildRuntimeErrorState(string $message): array {
return [
'#markup' => $this->t('Block error: @message', ['@message' => $message]),
'#cache' => ['max-age' => 0],
];
}
protected function buildAccessDeniedState(): array {
return [
'#markup' => '',
'#cache' => [
'contexts' => $this->getCacheContexts(),
'tags' => $this->getCacheTags(),
'max-age' => $this->getCacheMaxAge(),
],
];
}3. Configuration Recovery
protected function recoverConfiguration(): array {
$config = $this->getConfiguration();
$defaults = $this->defaultConfiguration();
// Merge with defaults to fix missing keys
$recovered_config = $config + $defaults;
// Validate and fix invalid values
if (\!in_array($recovered_config['render_mode'], ['block', 'empty'])) {
$recovered_config['render_mode'] = 'empty';
$this->logWarning('Invalid render mode recovered to "empty"');
}
if (\!is_array($recovered_config['target_block_config'])) {
$recovered_config['target_block_config'] = [];
$this->logWarning('Invalid target block config recovered to empty array');
}
return $recovered_config;
}Debug Mode Support
Add development-friendly debug information:
protected function addDebugInformation(array &$build): void {
if (\Drupal::service('kernel')->getEnvironment() === 'local') {
$config = $this->getConfiguration();
$build['#prefix'] = '<\!-- AbTestProxyBlock Debug: ' .
'Mode=' . ($config['render_mode'] ?? 'unknown') . ', ' .
'Target=' . ($config['target_block_plugin'] ?? 'none') . ' -->';
}
}Testing Requirements
Error Condition Tests
public function testMissingTargetBlockPlugin() {
// Set non-existent plugin ID
// Assert error state returned
// Assert error logged
}
public function testInvalidRenderMode() {
// Set invalid render mode
// Assert configuration error
// Assert fallback behavior
}
public function testTargetBlockException() {
// Mock target block that throws exception
// Assert runtime error state
// Assert error logged
}
public function testMalformedConfiguration() {
// Set invalid configuration structure
// Assert recovery mechanisms work
// Assert validation catches issues
}Edge Case Tests
- Circular dependency prevention
- Memory limit handling with large configurations
- Concurrent access scenarios
- Database connection failures
Acceptance Criteria
- All error scenarios handled gracefully without fatal errors
- Comprehensive logging for debugging
- User-friendly error messages (no technical details exposed)
- Configuration validation prevents invalid states
- Error states don't break page rendering
- Fallback mechanisms work correctly
- Debug information available in development
- Performance doesn't degrade significantly with error handling
Context for AI Implementation
This is a systematic task focusing on:
- Comprehensive Error Identification: Think through all possible failure points
- Graceful Degradation: Never break the page, always return valid render array
- Debugging Support: Log useful information for developers
- Security: Validate all inputs, sanitize error messages for users
- Performance: Error handling shouldn't impact normal operation significantly
Implementation Strategy:
- Add error handling around every external dependency call
- Use try-catch blocks judiciously (not everywhere, but around risky operations)
- Provide fallback render arrays for all error conditions
- Log errors for debugging without exposing technical details to users
- Test error conditions systematically
Key Patterns:
- Always return valid render arrays
- Use Drupal's logger service consistently
- Apply proper cache metadata even in error states
- Validate configurations early and often
Metadata
Metadata
Assignees
Labels
π·οΈ type:productionProduction codebase (src/, modules/, core functionality)Production codebase (src/, modules/, core functionality)π area:blocksLayout Builder block A/B testing featuresLayout Builder block A/B testing featuresπ area:coreCore plugin system, interfaces, managers, and base functionalityCore plugin system, interfaces, managers, and base functionalityπ₯ priority:highImportant features, significant bugs affecting many usersImportant features, significant bugs affecting many users