Skip to content

Hooks Reference#

Entity Builder provides hooks for extending functionality without creating full extension plugins.

Available Hooks#

hook_eb_ui_grid_provider_info#

Registers a grid provider for the Entity Builder UI.

Location: eb_ui module

Purpose: Register alternative UI providers for definition editing.

Example:

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Hook implementations for my_module.
 */
class MyModuleHooks {

  use StringTranslationTrait;

  /**
   * Implements hook_eb_ui_grid_provider_info().
   */
  #[Hook('eb_ui_grid_provider_info')]
  public function gridProviderInfo(): array {
    return [
      'my_grid' => [
        'id' => 'my_grid',
        'label' => $this->t('My Custom Grid'),
        'form_class' => '\Drupal\my_module\Form\MyGridForm',
        'library' => 'my_module/grid',
        'theme_class' => 'my-theme-class',
      ],
    ];
  }

}

Provider Properties:

Property Type Required Description
id string Yes Unique provider ID
label string Yes Human-readable label
form_class string Yes Form class implementing the UI
library string Yes Library to attach
theme_class string No CSS class for theming

Using Hooks with PHP Attributes#

Drupal 11 uses PHP attributes for hook registration:

<?php

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;

class MyModuleHooks {

  #[Hook('eb_ui_grid_provider_info')]
  public function gridProviderInfo(): array {
    // Implementation
  }

}

Register the hook class in your services file:

1
2
3
4
5
# my_module.services.yml
services:
  Drupal\my_module\Hook\MyModuleHooks:
    class: Drupal\my_module\Hook\MyModuleHooks
    autowire: true

Creating a Custom Grid Provider#

Step 1: Implement the Hook#

#[Hook('eb_ui_grid_provider_info')]
public function gridProviderInfo(): array {
  return [
    'my_provider' => [
      'id' => 'my_provider',
      'label' => $this->t('My Custom Provider'),
      'form_class' => '\Drupal\my_module\Form\MyProviderForm',
      'library' => 'my_module/provider',
    ],
  ];
}

Step 2: Create the Form Class#

<?php

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\eb\Entity\EbDefinition;

/**
 * Custom grid provider form.
 */
class MyProviderForm extends FormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'my_provider_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state, ?EbDefinition $eb_definition = NULL): array {
    // Get current definition data
    $data = $eb_definition ? [
      'bundle_definitions' => $eb_definition->get('bundle_definitions') ?? [],
      'field_definitions' => $eb_definition->get('field_definitions') ?? [],
    ] : [];

    // Build your custom interface
    $form['my_interface'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'my-provider-container'],
    ];

    // Hidden field to store data
    $form['definition_data'] = [
      '#type' => 'hidden',
      '#default_value' => json_encode($data),
    ];

    // Attach your JavaScript library
    $form['#attached']['library'][] = 'my_module/provider';
    $form['#attached']['drupalSettings']['myProvider'] = [
      'data' => $data,
    ];

    $form['actions'] = [
      '#type' => 'actions',
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save'),
      '#button_type' => 'primary',
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    // Process submitted data
    $data = json_decode($form_state->getValue('definition_data'), TRUE);

    // Update the definition entity
    $definition = $form_state->get('eb_definition');
    if ($definition) {
      $definition->set('bundle_definitions', $data['bundle_definitions'] ?? []);
      $definition->set('field_definitions', $data['field_definitions'] ?? []);
      $definition->save();

      $this->messenger()->addStatus($this->t('Definition saved.'));
    }
  }

}

Step 3: Create the JavaScript Library#

# my_module.libraries.yml
provider:
  version: VERSION
  js:
    js/my-provider.js: {}
  css:
    theme:
      css/my-provider.css: {}
  dependencies:
    - core/drupal
    - core/drupalSettings
    - core/once
// js/my-provider.js
(function (Drupal, drupalSettings, once) {
  'use strict';

  Drupal.behaviors.myProvider = {
    attach: function (context, settings) {
      once('my-provider', '#my-provider-container', context).forEach(function (container) {
        // Initialize your custom interface
        const data = drupalSettings.myProvider?.data || {};

        // Build your UI and update hidden field when data changes.
        function updateData(newData) {
          const hiddenField = document.querySelector('[name="definition_data"]');
          if (hiddenField) {
            hiddenField.value = JSON.stringify(newData);
          }
        }
      });
    }
  };
})(Drupal, drupalSettings, once);

Step 4: Add Route Priority (Optional)#

If your provider needs route priority:

<?php

namespace Drupal\my_module\Access;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\eb_ui\Service\GridProviderManager;

/**
 * Access checker for my provider routes.
 */
class MyProviderAccess {

  /**
   * Constructor.
   */
  public function __construct(
    protected GridProviderManager $providerManager,
  ) {}

  /**
   * Checks access.
   */
  public function access(AccountInterface $account): AccessResultInterface {
    $provider = $this->providerManager->getActiveProvider();

    // Allow if our provider is active
    return AccessResult::allowedIf(
      $provider && $provider['id'] === 'my_provider'
    )->addCacheContexts(['user.permissions']);
  }

}

Using eb_ui's Shared API#

Grid providers can use eb_ui's shared API endpoints:

Validation Endpoint#

async function validateData(data) {
  const response = await fetch('/eb/api/validate', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': drupalSettings.csrfToken,
      'X-Requested-With': 'XMLHttpRequest',
    },
    body: JSON.stringify(data),
  });

  return response.json();
}

Preview Endpoint#

async function previewOperations(data) {
  const response = await fetch('/eb/api/preview', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': drupalSettings.csrfToken,
      'X-Requested-With': 'XMLHttpRequest',
    },
    body: JSON.stringify(data),
  });

  return response.json();
}

Entity Discovery Endpoints#

// Get bundles for entity type
async function getBundles(entityType) {
  const response = await fetch(`/eb/api/bundles/${entityType}`, {
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
  });
  return response.json();
}

// Get field configuration
async function getEntityConfig(entityType, bundle) {
  const response = await fetch(`/eb/api/entity-config/${entityType}/${bundle}`, {
    headers: { 'X-Requested-With': 'XMLHttpRequest' },
  });
  return response.json();
}

Best Practices#

  1. Use shared endpoints - Leverage eb_ui's validation and preview APIs
  2. Follow Drupal patterns - Use Drupal behaviors and once()
  3. Handle errors gracefully - Show user-friendly error messages
  4. Maintain data consistency - Keep hidden field in sync with UI state
  5. Add accessibility - Follow WCAG guidelines for custom interfaces