date_recur-8.x-2.2/src/DateRecurViewsHooks.php
src/DateRecurViewsHooks.php
<?php
declare(strict_types=1);
namespace Drupal\date_recur;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\datetime\DateTimeViewsHelper;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines Views hooks.
*/
class DateRecurViewsHooks implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* DateRecurViewsHooks constructor.
*
* @param \Drupal\Core\Database\Connection $database
* The active database connection.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* Module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager.
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
* The typed data manager.
*/
public function __construct(protected Connection $database,
protected ModuleHandlerInterface $moduleHandler,
protected EntityTypeManagerInterface $entityTypeManager,
protected EntityFieldManagerInterface $entityFieldManager,
protected TypedDataManagerInterface $typedDataManager,
protected ?DateTimeViewsHelper $dateTimeViewsHelper = NULL,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('module_handler'),
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('typed_data_manager'),
$container->has('datetime.views_helper') ? $container->get('datetime.views_helper') : NULL,
);
}
/**
* Implements hook_views_data().
*
* @see \hook_views_data()
* @see \date_recur_views_data()
*/
public function viewsData(): array {
$allFields = $this->getDateRecurFields();
$data = [];
foreach ($allFields as $entityTypeId => $fields) {
$entityType = $this->entityTypeManager->getDefinition($entityTypeId);
/** @var \Drupal\views\EntityViewsDataInterface $viewsData */
$viewsData = $this->entityTypeManager->getHandler($entityType->id(), 'views_data');
$entityViewsTable = $viewsData->getViewsTableForEntityType($entityType);
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $fields */
foreach ($fields as $fieldId => $field) {
$fieldLabel = $this->getFieldLabel($field->getTargetEntityTypeId(), $fieldId);
$fieldName = $field->getName();
$entityIdField = $entityType->getKey('id');
$entityRevisionField = $entityType->isRevisionable() ? $entityType->getKey('revision') : NULL;
$tArgs = [
'@field_name' => $fieldLabel,
'@entity_type' => $entityType->getLabel(),
];
// Occurrence filter.
$data[$entityViewsTable][$fieldName . '_occurrences']['filter'] = [
'id' => 'date_recur_occurrences_filter',
'title' => $this->t('Occurrences filter for @field_name', $tArgs),
// Instruct the filter to join the occurrence.entity_id field on
// base.entityId:
'field base entity_id' => $entityIdField,
'date recur field name' => $fieldName,
'entity_type' => $entityType->id(),
];
// Relationship from entity table to occurrence table.
$occurrenceTableName = DateRecurOccurrences::getOccurrenceCacheStorageTableName($field);
$data[$entityViewsTable][$fieldName . '_occurrences']['relationship'] = [
'id' => 'standard',
'base' => $occurrenceTableName,
'base field' => isset($entityRevisionField) ? 'revision_id' : 'entity_id',
'help' => $this->t('Get all occurrences for recurring date field @field_name', $tArgs),
'field' => $entityRevisionField ?? $entityIdField,
// Add new 'title'.
'title' => $this->t('Occurrences of @field_name', $tArgs),
// Default label for relationship in the UI.
'label' => $this->t('Occurrences of @field_name', $tArgs),
];
$dateField = [
'id' => 'date_recur_date',
'source date format' => $this->getFieldDateFormat($field),
'source time zone' => DateTimeItemInterface::STORAGE_TIMEZONE,
];
$data[$occurrenceTableName][$fieldName . '_value']['field'] = $dateField;
$data[$occurrenceTableName][$fieldName . '_end_value']['field'] = $dateField;
// Attached fields get automatic functionality provided by
// hook_field_views_data(). Add features here for base fields.
if ($field instanceof BaseFieldDefinition) {
$data[$occurrenceTableName]['table']['group'] = $this->t('Occurrences for @entity_type @field_name', [
'@entity_type' => $entityType->getLabel(),
'@field_name' => $fieldLabel,
]);
$startField = $fieldName . '_value';
$endField = $fieldName . '_end_value';
$data[$occurrenceTableName][$startField]['title'] = $this->t('Occurrence start date');
$data[$occurrenceTableName][$endField]['title'] = $this->t('Occurrence end date');
// Sort.
// DateTimeViewsHelper::buildViewsData() uses 'datetime', which relies
// on entity things.
$data[$occurrenceTableName][$startField]['sort']['id'] = 'date';
$data[$occurrenceTableName][$endField]['sort']['id'] = 'date';
}
}
}
return $data;
}
/**
* Implements hook_views_data_alter().
*
* @see \hook_views_data_alter()
* @see \date_recur_views_data_alter()
*/
public function viewsDataAlter(array &$data): void {
$removeFieldKeys = $this->getViewsPluginTypes();
$removeFieldKeys = array_flip($removeFieldKeys);
// Base fields don't yet have an option to provide defaults for their type,
// but entity views data still tries to add default views integration for
// the field primitives in
// \Drupal\views\EntityViewsData::mapSingleFieldViewsData
// Feature to add in: https://www.drupal.org/node/2489476.
// Remove the default plugins from entity views data since they are not
// something that should be supported. This also means adding plugins for
// date recur base fields cannot be added in hook_views_data or
// entity 'views_data' handlers..
// @todo Bring in all plugins from DateTimeViewsHelper::buildViewsData() if/when
// it supports base fields.
$allFields = $this->getDateRecurFields();
foreach ($allFields as $entityTypeId => $fields) {
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $entityStorage */
$entityStorage = $this->entityTypeManager->getStorage($entityTypeId);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $tableMapping */
$tableMapping = $entityStorage->getTableMapping($fields);
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $fields */
foreach ($fields as $fieldStorage) {
if (!$fieldStorage instanceof BaseFieldDefinition) {
continue;
}
if ($tableMapping->requiresDedicatedTableStorage($fieldStorage)) {
$fieldTable = $tableMapping->getDedicatedDataTableName($fieldStorage);
$fieldData = &$data[$fieldTable];
// Remove handler keys within each field. Keys like 'title', 'help'
// etc are ignored. Whereas 'argument', 'field', etc are removed.
foreach ($fieldData as &$field) {
$field = array_diff_key($field, $removeFieldKeys);
}
}
}
}
}
/**
* Implements hook_field_views_data().
*
* @see \hook_field_views_data()
* @see \date_recur_field_views_data()
*/
public function fieldViewsData(FieldStorageConfigInterface $fieldDefinition): array {
$data = [];
$entityTypeId = $fieldDefinition->getTargetEntityTypeId();
$entityType = $this->entityTypeManager->getDefinition($entityTypeId);
/** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $entityStorage */
$entityStorage = $this->entityTypeManager->getStorage($entityTypeId);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $tableMapping */
$tableMapping = $entityStorage->getTableMapping();
$fieldName = $fieldDefinition->getName();
// The field label, see also usage in views.views.inc.
$fieldLabel = $this->getFieldLabel($fieldDefinition->getTargetEntityTypeId(), $fieldDefinition->getName());
$fieldTableName = $tableMapping->getDedicatedDataTableName($fieldDefinition);
$parentData = $this->getParentFieldViewsData($fieldDefinition);
// If there is no views data then quit (does this happen?).
if (count($parentData) === 0) {
return $data;
}
$originalTable = $parentData[$fieldTableName];
$occurrenceTableName = DateRecurOccurrences::getOccurrenceCacheStorageTableName($fieldDefinition);
if ($this->database->schema()->tableExists($occurrenceTableName)) {
$occurrenceTable = $originalTable;
// Remove the automatic join, requires site builders to use relationship
// plugin.
unset($occurrenceTable['table']['join']);
// Unset some irrelevant fields.
foreach (array_keys($occurrenceTable) as $fieldId) {
$fieldId = (string) $fieldId;
if (($fieldId === 'table') || (str_contains($fieldId, $fieldName . '_value')) || (str_contains($fieldId, $fieldName . '_end_value'))) {
continue;
}
unset($occurrenceTable[$fieldId]);
}
// Update table name references.
$handlerTypes = $this->getViewsPluginTypes();
$recurTableGroup = $this->t('Occurrences for @entity_type @field_name', [
'@entity_type' => $entityType->getLabel(),
'@field_name' => $fieldLabel,
]);
foreach ($occurrenceTable as $fieldId => &$field) {
$field['group'] = $recurTableGroup;
foreach ($handlerTypes as $handlerType) {
if (isset($field[$handlerType]['table'])) {
$field[$handlerType]['table'] = $occurrenceTableName;
$field[$handlerType]['additional fields'] = [
$fieldName . '_value',
$fieldName . '_end_value',
'delta',
'field_delta',
];
}
}
}
$data[$occurrenceTableName] = $occurrenceTable;
}
$fieldTable = $originalTable;
// Change the title for all plugins provided by
// \datetime_range_field_views_data().
foreach ($fieldTable as $key => &$definitions) {
/** @var \Drupal\Core\StringTranslation\TranslatableMarkup|string|null $originalTitle */
$originalTitle = $definitions['title'] ?? '';
$tArgs = $originalTitle instanceof TranslatableMarkup ? $originalTitle->getArguments() : [];
$tArgs['@field_label'] = $fieldLabel;
if ($fieldName === $key) {
$definitions['title'] = isset($tArgs['@argument']) ?
$this->t('@field_label (@argument)', $tArgs) :
$this->t('@field_label', $tArgs);
}
elseif (str_contains($key, $fieldName . '_value')) {
$definitions['title'] = isset($tArgs['@argument']) ?
$this->t('@field_label: first occurrence start date (@argument)', $tArgs) :
$this->t('@field_label: first occurrence start date', $tArgs);
}
elseif (str_contains($key, $fieldName . '_end_value')) {
$definitions['title'] = isset($tArgs['@argument']) ?
$this->t('@field_label: first occurrence end date (@argument)', $tArgs) :
$this->t('@field_label: first occurrence end date', $tArgs);
}
elseif (str_contains($key, $fieldName . '_rrule')) {
$definitions['title'] = $this->t('@field_label: recurring rule (RRULE)', $tArgs);
}
elseif (str_contains($key, $fieldName . '_timezone')) {
$definitions['title'] = $this->t('@field_label: time zone', $tArgs);
}
elseif (str_contains($key, $fieldName . '_infinite')) {
$definitions['title'] = $this->t('@field_label: is infinite', $tArgs);
}
elseif ('delta' === $key) {
$definitions['title'] = $this->t('@field_label: field delta', $tArgs);
}
}
$data[$fieldTableName] = $fieldTable;
return $data;
}
/**
* Get date recur fields for entities supporting views.
*
* @return array
* An array of arrays of date recur fields keyed by entity type ID.
*/
protected function getDateRecurFields(): array {
// Date recur fields keyed by entity type id.
$fields = [];
// Get all date recur fields as base and attached fields.
foreach ($this->entityTypeManager->getDefinitions() as $entityType) {
// \Drupal\views\EntityViewsData class only allows entities with
// \Drupal\Core\Entity\Sql\SqlEntityStorageInterface.
// Only fieldable entities have base fields.
try {
if (
$this->entityTypeManager->getStorage($entityType->id()) instanceof SqlEntityStorageInterface &&
$entityType->hasHandlerClass('views_data') &&
$entityType->entityClassImplements(FieldableEntityInterface::class)) {
$fields[$entityType->id()] = array_filter(
$this->entityFieldManager->getFieldStorageDefinitions($entityType->id()),
function (FieldStorageDefinitionInterface $field): bool {
$typeDefinition = $this->typedDataManager->getDefinition('field_item:' . $field->getType());
// @see \Drupal\date_recur\DateRecurCachedHooks::fieldInfoAlter
return isset($typeDefinition[DateRecurOccurrences::IS_DATE_RECUR]);
},
);
}
}
catch (PluginNotFoundException $e) {
// This can occur when a content-entity is added during a config
// import that enables a module.
continue;
}
}
// Remove entity types with no date recur fields.
$fields = array_filter($fields);
return $fields;
}
/**
* Get the most popular label for a field storage.
*
* @param string $entityTypeId
* The entity type ID.
* @param string $fieldName
* The field.
*
* @return string
* The most popular label for a field storage.
*/
protected function getFieldLabel($entityTypeId, $fieldName): string {
return \views_entity_field_label($entityTypeId, $fieldName)[0];
}
/**
* Get the views definition for the field, as defined by datetime_range.
*
* @param \Drupal\field\FieldStorageConfigInterface $fieldDefinition
* A field storage definition.
*
* @return array
* The views data for a field.
*/
protected function getParentFieldViewsData(FieldStorageConfigInterface $fieldDefinition): array {
if ($this->dateTimeViewsHelper) {
$data = $this->dateTimeViewsHelper->buildViewsData($fieldDefinition, [], 'value');
$data = $this->dateTimeViewsHelper->buildViewsData($fieldDefinition, $data, 'end_value');
}
else {
$this->moduleHandler->loadInclude('datetime_range', 'inc', 'datetime_range.views');
$data = function_exists('datetime_range_field_views_data')
? \datetime_range_field_views_data($fieldDefinition)
: [];
}
// Expose rrule value to the views fields.
foreach ($data as $table_name => $table_data) {
$data[$table_name][$fieldDefinition->getName() . '_rrule']['field']['id'] = 'standard';
}
return $data;
}
/**
* Get an array of all views plugin types.
*
* @return array
* An array of all views plugin types.
*/
protected function getViewsPluginTypes(): array {
return Views::getPluginTypes();
}
/**
* Get date format of field storage.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $fieldDefinition
* A field definition.
*
* @return string
* A date format.
*/
protected function getFieldDateFormat(FieldStorageDefinitionInterface $fieldDefinition): string {
return $fieldDefinition->getSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE
? DateTimeItemInterface::DATE_STORAGE_FORMAT
: DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
}
}
