You are here

civicrm_entity.module in CiviCRM Entity 8.3

Same filename and directory in other branches
  1. 7.2 civicrm_entity.module
  2. 7 civicrm_entity.module

Module file for the CiviCRM Entity module.

File

civicrm_entity.module
View source
<?php

/**
 * @file
 * Module file for the CiviCRM Entity module.
 */
use Drupal\civicrm_entity\CivicrmEntityAccessHandler;
use Drupal\civicrm_entity\CivicrmEntityListBuilder;
use Drupal\civicrm_entity\CiviCrmEntityViewBuilder;
use Drupal\civicrm_entity\CivicrmEntityViewsData;
use Drupal\civicrm_entity\CiviEntityStorage;
use Drupal\civicrm_entity\Entity\CivicrmEntity;
use Drupal\civicrm_entity\Entity\Sql\CivicrmEntityStorageSchema;
use Drupal\civicrm_entity\Form\CivicrmEntityForm;
use Drupal\civicrm_entity\Routing\CiviCrmEntityRouteProvider;
use Drupal\civicrm_entity\SupportedEntities;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\query\QueryPluginBase;

/**
 * Implements hook_theme().
 */
function civicrm_entity_theme() {
  return [
    'civicrm_entity_entity_form' => [
      'render element' => 'form',
    ],
    'civicrm_entity' => [
      'render element' => 'elements',
    ],
  ];
}

/**
 * Implements hook_entity_type_build().
 *
 * Populates supported CiviCRM Entity definitions.
 */
function civicrm_entity_entity_type_build(array &$entity_types) {
  $logger = \Drupal::logger('civicrm-entity');
  $supported_entities = SupportedEntities::getInfo();
  $config = \Drupal::config('civicrm_entity.settings');
  $enabled_entity_types = $config
    ->get('enabled_entity_types') ?: [];
  foreach ($supported_entities as $entity_type_id => $civicrm_entity_info) {
    $clean_entity_type_id = str_replace('_', '-', $entity_type_id);
    $civicrm_entity_name = $civicrm_entity_info['civicrm entity name'];
    if (empty($civicrm_entity_info['label property'])) {
      $logger
        ->debug(sprintf('Missing label property: %s', $entity_type_id));
      continue;
    }
    $entity_type_info = [
      'provider' => 'civicrm_entity',
      'class' => CivicrmEntity::class,
      'originalClass' => CivicrmEntity::class,
      'id' => $entity_type_id,
      'component' => $civicrm_entity_info['component'] ?? NULL,
      'civicrm_entity' => $civicrm_entity_name,
      'civicrm_entity_ui_exposed' => in_array($entity_type_id, $enabled_entity_types),
      'label' => new TranslatableMarkup('CiviCRM :name', [
        ':name' => $civicrm_entity_info['civicrm entity label'],
      ]),
      // @todo add label_singular
      // @todo add label_plural
      // @todo add label_count
      'entity_keys' => [
        'id' => 'id',
        'label' => $civicrm_entity_info['label property'],
      ],
      'base_table' => $entity_type_id,
      'admin_permission' => 'administer civicrm entity',
      'permission_granularity' => 'entity_type',
      'handlers' => [
        'storage' => CiviEntityStorage::class,
        'access' => CivicrmEntityAccessHandler::class,
        'views_data' => CivicrmEntityViewsData::class,
        'storage_schema' => CivicrmEntityStorageSchema::class,
      ],
    ];
    if (in_array($entity_type_id, $enabled_entity_types)) {
      $entity_type_info = array_merge_recursive($entity_type_info, [
        'handlers' => [
          'list_builder' => CivicrmEntityListBuilder::class,
          'view_builder' => CiviCrmEntityViewBuilder::class,
          'route_provider' => [
            'default' => CiviCrmEntityRouteProvider::class,
          ],
          'form' => [
            'default' => CivicrmEntityForm::class,
            'add' => CivicrmEntityForm::class,
            'edit' => CivicrmEntityForm::class,
            'delete' => ContentEntityDeleteForm::class,
          ],
        ],
        // Generate route paths.
        'links' => [
          'canonical' => sprintf('/%s/{%s}', $clean_entity_type_id, $entity_type_id),
          'delete-form' => sprintf('/%s/{%s}/delete', $clean_entity_type_id, $entity_type_id),
          'edit-form' => sprintf('/%s/{%s}/edit', $clean_entity_type_id, $entity_type_id),
          'add-form' => sprintf('/%s/add', $clean_entity_type_id, $entity_type_id),
          'collection' => sprintf('/admin/structure/civicrm-entity/%s', $clean_entity_type_id),
        ],
        'field_ui_base_route' => "entity.{$entity_type_id}.collection",
      ]);
    }

    // If this entity has bundle support, we define the bundle field as "bundle"
    // and will use the "bundle property" as the field to fetch field options
    // from CiviCRM with.
    //
    // @see civicrm_entity_entity_bundle_info()
    // @see \Drupal\civicrm_entity\Entity\CivicrmEntity::baseFieldDefinitions()
    if (!empty($civicrm_entity_info['bundle property'])) {
      $entity_type_info['entity_keys']['bundle'] = 'bundle';
      $entity_type_info['civicrm_bundle_property'] = $civicrm_entity_info['bundle property'];
      if (isset($entity_type_info['links'])) {

        // For entities with bundles that are exposed, add the `bundle` key to
        // the add-form route. In CiviCrmEntityRouteProvider::getAddFormRoute
        // we default the value, so that it isn't actually required in the URL.
        $entity_type_info['links']['add-form'] = sprintf('%s/{%s}', $entity_type_info['links']['add-form'], $entity_type_info['entity_keys']['bundle']);
      }
    }
    $entity_types[$entity_type_id] = new ContentEntityType($entity_type_info);
  }
}

/**
 * Implements hook_entity_bundle_info().
 */
function civicrm_entity_entity_bundle_info() {
  $transliteration = \Drupal::transliteration();

  /** @var \Drupal\civicrm_entity\CiviCrmApiInterface $civicrm_api */
  $civicrm_api = \Drupal::service('civicrm_entity.api');
  $bundles = [];
  $entity_types_with_bundles = array_filter(SupportedEntities::getInfo(), static function (array $civicrm_entity_info) {
    return !empty($civicrm_entity_info['bundle property']);
  });
  foreach ($entity_types_with_bundles as $entity_type_id => $civicrm_entity_info) {

    // We keep a bundle that is the same as the entity type ID. This allows us
    // to create fields as if this entity has no bundles.
    $bundles[$entity_type_id] = [
      $entity_type_id => [
        'label' => $civicrm_entity_info['civicrm entity label'],
      ],
    ];
    $options = $civicrm_api
      ->getOptions($civicrm_entity_info['civicrm entity name'], $civicrm_entity_info['bundle property']);
    foreach ($options as $option) {
      $machine_name = SupportedEntities::optionToMachineName($option, $transliteration);
      $bundles[$entity_type_id][$machine_name]['label'] = $option;
    }
  }
  return $bundles;
}

/**
 * Implements hook_entity_bundle_field_info().
 *
 * This ensures CiviCRM Entity entity types have their field config instances
 * across all bundles. It's a copy of the Field module's logic, but clones
 * field config definitions.
 *
 * @see field_entity_bundle_field_info().
 */
function civicrm_entity_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  $result = [];
  if ($entity_type
    ->get('civicrm_entity_ui_exposed') && $entity_type
    ->hasKey('bundle')) {

    // Query by filtering on the ID as this is more efficient than filtering
    // on the entity_type property directly.
    $ids = \Drupal::entityQuery('field_config')
      ->condition('id', $entity_type
      ->id() . '.', 'STARTS_WITH')
      ->execute();

    // Fetch all fields and key them by field name.
    $field_configs = FieldConfig::loadMultiple($ids);

    // Clone the field configs, so that we can modify them and change the
    // target bundle type without manipulating the statically cached entries
    // in the entity storage;
    $cloned_field_configs = array_map(static function (FieldConfigInterface $field) use ($bundle) {
      $cloned = clone $field;
      $cloned
        ->set('bundle', $bundle);
      return $cloned;
    }, $field_configs);
    foreach ($cloned_field_configs as $field_instance) {
      $result[$field_instance
        ->getName()] = $field_instance;
    }
  }
  return $result;
}

/**
 * Implements hook_entity_view_display_alter().
 *
 * There is no way to handle this in the entity type's view builder.
 */
function civicrm_entity_entity_view_display_alter(EntityViewDisplayInterface $display, array $context) {
  $entity_type = \Drupal::entityTypeManager()
    ->getDefinition($context['entity_type']);
  assert($entity_type !== NULL);
  if ($entity_type
    ->get('civicrm_entity') && $entity_type
    ->hasKey('bundle')) {
    $entity_display_repository = \Drupal::service('entity_display.repository');
    assert($entity_display_repository instanceof EntityDisplayRepositoryInterface);
    $root_display = $entity_display_repository
      ->getViewDisplay($entity_type
      ->id(), $entity_type
      ->id());
    $display
      ->set('content', $root_display
      ->get('content'));
    $display
      ->set('hidden', $root_display
      ->get('hidden'));
    if ($root_display instanceof LayoutBuilderEntityViewDisplay) {
      $layout_builder_settings = $root_display
        ->getThirdPartySettings('layout_builder');
      foreach ($layout_builder_settings as $setting_key => $setting) {
        $display
          ->setThirdPartySetting('layout_builder', $setting_key, $setting);
      }
    }
  }
}

/**
 * Implements callback_allowed_values_function().
 *
 * Provides the pseudoconstant values for CiviCRM entity fields.
 *
 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
 *   The field storage definition.
 * @param \Drupal\Core\Entity\FieldableEntityInterface|NULL $entity
 *   The entity.
 * @param bool $cacheable
 *   If the options are cacheable.
 *
 * @return array
 *   The array of field options.
 */
function civicrm_entity_pseudoconstant_options(FieldStorageDefinitionInterface $definition, FieldableEntityInterface $entity = NULL, &$cacheable = NULL) {

  /** @var \Drupal\civicrm_entity\CiviCrmApiInterface $civicrm_api */
  $civicrm_api = \Drupal::service('civicrm_entity.api');
  $entity_type = \Drupal::entityTypeManager()
    ->getDefinition($definition
    ->getTargetEntityTypeId());
  $options = $civicrm_api
    ->getOptions($entity_type
    ->get('civicrm_entity'), $definition
    ->getName());
  return $options;
}

/**
 * Implements hook_preprocess().
 */
function template_preprocess_civicrm_entity(&$variables) {

  // Add fields as content to template.
  $variables += [
    'content' => [],
  ];
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Add the view_mode to the template.
  $variables['view_mode'] = $variables['elements']['#view_mode'];

  // Add the bundle to the template.
  $variables['entity_type'] = _civicrm_entity_get_entity_type_from_elements($variables['elements']);
}

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function civicrm_entity_theme_suggestions_civicrm_entity_alter(array &$suggestions, array $variables) {
  $view_mode = $variables['elements']['#view_mode'];
  $hook = $variables['theme_hook_original'];

  // Add a suggestion based on the entity type.
  if ($entity_type = _civicrm_entity_get_entity_type_from_elements($variables['elements'])) {
    $suggestions[] = $hook . '__' . $entity_type;

    // Add a suggestion based on the view mode.
    $suggestions[] = $hook . '__' . $entity_type . '__' . $view_mode;
  }
}

/**
 * Helper to find the entity type from $variables['elements'].
 */
function _civicrm_entity_get_entity_type_from_elements($elements) {
  if (isset($elements['#entity_type'])) {
    return $elements['#entity_type'];
  }

  // Find the CivicrmEntity from elements if #entity_type is not set.
  foreach ($elements as $element) {
    if ($element instanceof CivicrmEntity) {

      /** @var CivicrmEntity $element */
      return $element
        ->getEntityTypeId();
    }
  }
  return NULL;
}

/**
 * For stashing deleted entities until we need them later.
 *
 * @todo Convert hooks to a service and keep this in a member variable
 *
 * @param string $objectName
 *   The CiviCRM entity type.
 * @param int $id
 *   The id.
 * @param \Drupal\Core\Entity\EntityInterface|null|string $entity
 *   The entity.
 *
 * @return void|\Drupal\Core\Entity\EntityInterface
 */
function _civicrm_entity_stash($objectName, $id, $entity = NULL) {
  $cache =& drupal_static(__FUNCTION__, []);
  if (empty($entity)) {
    return isset($cache[$objectName][$id]) ? $cache[$objectName][$id] : NULL;
  }
  elseif ($entity === 'clear') {
    unset($cache[$objectName][$id]);
  }
  else {
    $cache[$objectName][$id] = $entity;
  }
}

/**
 * Implements hook_civicrm_pre().
 */
function civicrm_entity_civicrm_pre($op, $objectName, $id, &$params) {
  if (\Drupal::config('civicrm_entity.settings')
    ->get('disable_hooks')) {
    return;
  }
  $operations = [
    'create',
    'edit',
    'delete',
    'restore',
  ];
  if (!in_array($op, $operations)) {
    return;
  }
  $entityType = SupportedEntities::getEntityType($objectName);

  // Check valid entity type.
  if (!$entityType) {
    return;
  }

  /** @var \Drupal\civicrm_entity\CiviEntityStorage $storage */
  $storage = \Drupal::entityTypeManager()
    ->getStorage($entityType);
  if ($op == 'create') {
    $entity = $storage
      ->create($params);
  }
  elseif (empty($id)) {

    // Sometimes 'delete' is called with an $id of NULL, but we can't really do
    // anything with that in this context, so return.
    return;
  }
  else {

    // Special handling for EntityTag objects.
    if ($objectName == 'EntityTag') {
      $id = $storage
        ->getEntityTagEntityId($params[0][0], $params[1]);
    }
    $entity = $storage
      ->load($id);
  }
  if (!$entity) {
    return;
  }
  if ($entity
    ->id()) {
    $entity->original = $storage
      ->loadUnchanged($entity
      ->id());
  }
  switch ($op) {
    case 'create':
      $storage
        ->civiPreSave($entity);
      break;
    case 'delete':
      $storage
        ->civiPreDelete($entity);
      _civicrm_entity_stash($objectName, $id, $entity);
      break;
    case 'restore':
      $storage
        ->civiPreSave($entity);
      break;
    case 'edit':
      $storage
        ->civiPreSave($entity);
      break;
  }
}

/**
 * Implements hook_civicrm_post().
 */
function civicrm_entity_civicrm_post($op, $objectName, $objectId, &$objectRef) {
  if (\Drupal::config('civicrm_entity.settings')
    ->get('disable_hooks')) {
    return;
  }
  $operations = [
    'create',
    'edit',
    'delete',
    'restore',
  ];
  if (!in_array($op, $operations)) {
    return;
  }
  $entityType = SupportedEntities::getEntityType($objectName);

  // Check valid entity type.
  if (!$entityType) {
    return;
  }

  /** @var \Drupal\civicrm_entity\CiviEntityStorage $storage */
  $storage = \Drupal::entityTypeManager()
    ->getStorage($entityType);

  // Fix because $objectId is not set for participant payments, possibly other
  // entity types.
  if (!$objectId) {

    // If we cannot determine the id, bail.
    if (empty($objectRef->id)) {
      return;
    }
    $objectId = $objectRef->id;
  }
  if ($op == 'delete') {
    $entity = _civicrm_entity_stash($objectName, $objectId);
  }
  else {

    // Special handling for EntityTag objects.
    if ($entityType == 'civicrm_entity_tag') {
      foreach ($objectRef[0] as $entityTag) {
        $object = new CRM_Core_BAO_EntityTag();
        $object->entity_id = $entityTag;
        $object->entity_table = 'civicrm_contact';
        $object->tag_id = $objectId;
        if ($object
          ->find(TRUE)) {
          $entity = $storage
            ->load($object->id);
          $entity->original = $storage
            ->loadUnchanged($entity
            ->id());
          _civicrm_entity_post_invoke($op, $storage, $entity);
        }
      }
      return;
    }
    if ($entity = $storage
      ->load($objectId)) {
      $entity->original = $storage
        ->loadUnchanged($entity
        ->id());
    }
  }
  if ($entity) {
    _civicrm_entity_post_invoke($op, $storage, $entity);
  }
  _civicrm_entity_stash($objectName, $objectId, 'clear');
}

/**
 * Invokes the post save hooks for a CiviCRM entity.
 *
 * @param string $op
 *   The operation being performed.
 * @param \Drupal\civicrm_entity\CiviEntityStorage $storage
 *   The entity storage.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity.
 */
function _civicrm_entity_post_invoke($op, $storage, $entity) {
  switch ($op) {
    case 'create':
      $storage
        ->civiPostSave($entity, FALSE);
      break;
    case 'delete':
      $storage
        ->civiPostDelete($entity);
      break;
    case 'restore':
      $storage
        ->civiPostSave($entity, TRUE);
      break;
    case 'edit':
      $storage
        ->civiPostSave($entity, TRUE);
      break;
  }
}
function civicrm_entity_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  \Drupal::service('civicrm')
    ->initialize();
  $multilingual = \CRM_Core_I18n::isMultilingual();
  if ($multilingual) {
    global $dbLocale;
    $columns = CRM_Core_I18n_SchemaStructure::columns();
    $affectedColumns = [];
    foreach ($columns as $table => $hash) {
      foreach (array_keys($hash) as $column) {
        $affectedColumns[] = "{$table}.{$column}";
      }
    }
    foreach ($query->where as &$condition_group) {
      foreach ($condition_group['conditions'] as &$condition) {
        if (!is_object($condition['field'])) {
          foreach ($affectedColumns as $aff_column) {
            if (strpos($aff_column, $condition['field']) !== FALSE) {
              $condition['field'] = str_replace($aff_column, $aff_column . $dbLocale, $condition['field']);
            }
          }
        }
      }
    }
    foreach ($query->fields as &$field) {
      if (array_key_exists($field['table'], $columns) && array_key_exists($field['field'], $columns[$field['table']])) {
        $field['field'] .= $dbLocale;
      }
    }
  }
}

/**
 * Implements hook_theme_registry_alter().
 */
function civicrm_entity_theme_registry_alter(&$theme_registry) {
  $theme_registry['civicrm_entity']['preprocess functions'][] = 'field_group_build_entity_groups';
}

/**
 * Implements hook_rebuild().
 *
 * This resets the field storage and entity type definitions for civicrm_entity
 * according to the active definitions to avoid mismatches since the definitions
 * are not necessary to be updated.
 */
function civicrm_entity_rebuild() {

  /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_repository */
  $entity_last_installed_repository = \Drupal::service('entity.last_installed_schema.repository');

  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager */
  $entity_field_manager = \Drupal::service('entity_field.manager');

  /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
  $entity_type_manager = \Drupal::service('entity_type.manager');
  $supported_entities = SupportedEntities::getInfo();
  foreach (array_keys($supported_entities) as $entity_type_id) {

    // Reset field storage definitions.
    $field_storage_definitions = $entity_field_manager
      ->getFieldStorageDefinitions($entity_type_id);
    $entity_last_installed_repository
      ->setLastInstalledFieldStorageDefinitions($entity_type_id, $field_storage_definitions);

    // Reset entity type definition.
    $definition = $entity_type_manager
      ->getDefinition($entity_type_id);
    $entity_last_installed_repository
      ->setLastInstalledDefinition($definition);
  }
}

/**
 * Implements hook_rules_action_info_alter().
 */
function civicrm_entity_rules_action_info_alter(&$rules_actions) {
  $rules_actions['civicrm_entity_user_create']['context']['format']
    ->setDescription(t('Format of the username. Use <a href="@url">Twig style</a> tokens for using the available data.', [
    '@url' => 'https://www.drupal.org/docs/8/modules/typed-data-api-enhancements/typeddata-tokens',
  ]));
}