date_recur-8.x-2.2/src/DateRecurOccurrences.php
src/DateRecurOccurrences.php
<?php
declare(strict_types=1);
namespace Drupal\date_recur;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeEventSubscriberTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeListenerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionEventSubscriberTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\date_recur\Event\DateRecurEvents;
use Drupal\date_recur\Event\DateRecurValueEvent;
use Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Manages occurrences tables and the data that populates them.
*
* - Generates occurrences for tables when entities are modified.
* - Manages tables when base or attached date recur fields are created,
* modified or deleted.
*/
class DateRecurOccurrences implements EventSubscriberInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
use EntityTypeEventSubscriberTrait;
use FieldStorageDefinitionEventSubscriberTrait;
/**
* The key in field definitions indicating whether field is date recur like.
*/
public const IS_DATE_RECUR = 'is_date_recur';
/**
* DateRecurOccurrences constructor.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
* The entity field manager.
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
* Manages data type plugins.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
*/
public function __construct(
protected Connection $database,
protected EntityFieldManagerInterface $entityFieldManager,
protected TypedDataManagerInterface $typedDataManager,
protected EntityTypeManagerInterface $entityTypeManager,
) {
}
/**
* Respond to a field value insertion or update.
*
* @param \Drupal\date_recur\Event\DateRecurValueEvent $event
* The date recur event.
*/
public function onSave(DateRecurValueEvent $event): void {
/** @var \Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem[]|\Drupal\date_recur\Plugin\Field\FieldType\DateRecurFieldItemList $list */
$list = $event->getField();
$fieldDefinition = $list->getFieldDefinition();
$tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition->getFieldStorageDefinition());
$isInsert = $event->isInsert();
if (!$isInsert) {
// Delete all existing values for entity and field combination.
/** @var string|int $entityId */
$entityId = $list->getEntity()->id();
$this->database->delete($tableName)
->condition('entity_id', (string) $entityId)
->execute();
}
foreach ($list as $item) {
$this->saveItem($item, $tableName);
}
}
/**
* Create table rows from occurrences for a single field value.
*
* @param \Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem $item
* Date recur field item.
* @param string $tableName
* The name of table to store occurrences.
*/
protected function saveItem(DateRecurItem $item, string $tableName): void {
// Type suggested, see https://www.drupal.org/project/drupal/issues/3094067.
/** @var string|int $fieldDelta */
$fieldDelta = $item->getName();
assert(is_int($fieldDelta));
$fieldName = $item->getFieldDefinition()->getName();
$entity = $item->getEntity();
$fields = [
'entity_id',
'field_delta',
'delta',
$fieldName . '_value',
$fieldName . '_end_value',
];
$baseRow = [
'entity_id' => $entity->id(),
'field_delta' => $fieldDelta,
];
if ($entity->getEntityType()->isRevisionable() && $entity instanceof RevisionableInterface) {
$fields[] = 'revision_id';
$baseRow['revision_id'] = $entity->getRevisionId();
}
$occurrences = $this->getOccurrencesForCacheStorage($item);
$rows = array_map(
function (DateRange $occurrence, $delta) use ($baseRow, $fieldName, $item): array {
$row = $baseRow;
$row['delta'] = $delta;
$row[$fieldName . '_value'] = $this->massageDateValueForStorage($occurrence->getStart(), $item);
$row[$fieldName . '_end_value'] = $this->massageDateValueForStorage($occurrence->getEnd(), $item);
return $row;
},
$occurrences,
array_keys($occurrences),
);
$insert = $this->database
->insert($tableName)
->fields($fields);
foreach ($rows as $row) {
$insert->values($row);
}
$insert->execute();
}
/**
* Respond to a entity deletion.
*
* @param \Drupal\date_recur\Event\DateRecurValueEvent $event
* The date recur event.
*/
public function onEntityDelete(DateRecurValueEvent $event): void {
$list = $event->getField();
$fieldDefinition = $list->getFieldDefinition();
$tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition->getFieldStorageDefinition());
$delete = $this->database
->delete($tableName);
/** @var string|int $entityId */
$entityId = $list->getEntity()->id();
$delete->condition('entity_id', (string) $entityId);
$delete->execute();
}
/**
* Respond to a entity revision deletion.
*
* @param \Drupal\date_recur\Event\DateRecurValueEvent $event
* The date recur event.
*/
public function onEntityRevisionDelete(DateRecurValueEvent $event): void {
$list = $event->getField();
$entity = $list->getEntity();
$fieldDefinition = $list->getFieldDefinition();
$tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition->getFieldStorageDefinition());
$delete = $this->database->delete($tableName);
/** @var string|int $entityId */
$entityId = $list->getEntity()->id();
$delete->condition('entity_id', (string) $entityId);
if ($entity->getEntityType()->isRevisionable() && $entity instanceof RevisionableInterface) {
$delete->condition('revision_id', $entity->getRevisionId());
}
$delete->execute();
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $fieldStorageConfig): void {
if ($this->isDateRecur($fieldStorageConfig)) {
$this->fieldStorageCreate($fieldStorageConfig);
}
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $fieldStorageConfig): void {
if ($this->isDateRecur($fieldStorageConfig)) {
$this->fieldStorageDelete($fieldStorageConfig);
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type): void {
if (!$entity_type instanceof ContentEntityTypeInterface) {
// Only add field for content entity types.
return;
}
foreach ($this->getBaseFieldStorages($entity_type) as $baseFieldStorage) {
$this->fieldStorageCreate($baseFieldStorage);
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type): void {
if (!$entity_type instanceof ContentEntityTypeInterface) {
// Only delete field for content entity types.
return;
}
foreach ($this->getBaseFieldStorages($entity_type) as $baseFieldStorage) {
$this->fieldStorageDelete($baseFieldStorage);
}
}
/**
* Reacts to field creation.
*/
protected function fieldStorageCreate(FieldStorageDefinitionInterface $fieldDefinition): void {
$this->createOccurrenceTable($fieldDefinition);
}
/**
* Reacts to field deletion.
*/
protected function fieldStorageDelete(FieldStorageDefinitionInterface $fieldDefinition): void {
$tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition);
if (!$this->database->schema()->tableExists($tableName)) {
return;
}
$this->database
->schema()
->dropTable($tableName);
}
/**
* Get all occurrences needing to be stored.
*
* @param \Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem $item
* The date recur field item.
*
* @return \Drupal\date_recur\DateRange[]
* Date range objects for storage.
*/
protected function getOccurrencesForCacheStorage(DateRecurItem $item): array {
$until = NULL;
if ($item->getHelper()->isInfinite()) {
$until = (new \DateTime('now'))
->add(new \DateInterval($item->getFieldDefinition()->getSetting('precreate')));
}
return $item->getHelper()->getOccurrences(NULL, $until);
}
/**
* Creates an occurrence table.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $fieldDefinition
* The field definition.
*/
protected function createOccurrenceTable(FieldStorageDefinitionInterface $fieldDefinition): void {
$entityTypeId = $fieldDefinition->getTargetEntityTypeId();
$entityType = $this->entityTypeManager->getDefinition($entityTypeId);
$fieldName = $fieldDefinition->getName();
$entityFieldDefinitions = $this->entityFieldManager->getFieldStorageDefinitions($entityTypeId);
// Logic taken from field tables: see \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getDedicatedTableSchema.
$idDefinition = $entityFieldDefinitions[$entityType->getKey('id')];
$fields = [];
if ($idDefinition->getType() === 'integer') {
$fields['entity_id'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
];
}
else {
$fields['entity_id'] = [
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
];
}
if ($entityType->isRevisionable()) {
$revisionDefinition = $entityFieldDefinitions[$entityType->getKey('revision')];
if ($revisionDefinition->getType() === 'integer') {
$fields['revision_id'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this data is attached to',
];
}
else {
$fields['revision_id'] = [
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'description' => 'The entity revision id this data is attached to',
];
}
}
$fields['field_delta'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The sequence number for this data item, used for multi-value fields',
];
$fields['delta'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The sequence number in generated occurrences for the RRULE',
];
$primaryKey = [
$entityType->isRevisionable() ? 'revision_id' : 'entity_id',
'field_delta',
'delta',
];
$fieldSchema = $fieldDefinition->getSchema();
$fields[$fieldName . '_value'] = $fieldSchema['columns']['value'];
$fields[$fieldName . '_end_value'] = $fieldSchema['columns']['end_value'];
$schema = [
'description' => sprintf('Occurrences cache for %s.%s', $fieldDefinition->getTargetEntityTypeId(), $fieldName),
'fields' => $fields,
'primary key' => $primaryKey,
'indexes' => [
'value' => ['entity_id', $fieldName . '_value'],
],
];
$tableName = DateRecurOccurrences::getOccurrenceCacheStorageTableName($fieldDefinition);
$this->database
->schema()
->createTable($tableName, $schema);
}
/**
* Convert date ready to be inserted into database column.
*
* @param \DateTimeInterface $date
* A date time object.
* @param \Drupal\date_recur\Plugin\Field\FieldType\DateRecurItem $item
* The date recur field item.
*
* @return string
* The date value for storage.
*/
protected function massageDateValueForStorage(\DateTimeInterface $date, DateRecurItem $item): string {
// Convert native timezone to UTC.
$date->setTimezone(new \DateTimeZone(DateRecurItem::STORAGE_TIMEZONE));
// If storage does not allow time, then reset to midday.
$storageFormat = $item->getDateStorageFormat();
if ($storageFormat == DateRecurItem::DATE_STORAGE_FORMAT) {
$date->setTime(12, 0, 0);
}
return $date->format($storageFormat);
}
/**
* Determines if a field is date recur or subclasses date recur.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $fieldDefinition
* A field definition.
*
* @return bool
* Whether field is date recur or subclasses date recur.
*/
protected function isDateRecur(FieldStorageDefinitionInterface $fieldDefinition): bool {
$typeDefinition = $this->typedDataManager->getDefinition('field_item:' . $fieldDefinition->getType());
// @see \Drupal\date_recur\DateRecurCachedHooks::fieldInfoAlter
return isset($typeDefinition[DateRecurOccurrences::IS_DATE_RECUR]);
}
/**
* Get field storage for date recur base fields for an entity type.
*
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $entityType
* An entity type.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
* An array of storage definitions for base fields for an entity type.
*/
protected function getBaseFieldStorages(ContentEntityTypeInterface $entityType): array {
$baseFields = $this->entityFieldManager->getBaseFieldDefinitions($entityType->id());
$baseFields = array_filter($baseFields,
fn (FieldDefinitionInterface $fieldDefinition): bool => $this->isDateRecur($fieldDefinition->getFieldStorageDefinition()),
);
return array_map(
fn (FieldDefinitionInterface $baseField): FieldStorageDefinitionInterface => $baseField->getFieldStorageDefinition(),
$baseFields,
);
}
/**
* Get the name of the table containing occurrences for a field.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $fieldDefinition
* The field definition.
*
* @return string
* A table name.
*/
public static function getOccurrenceCacheStorageTableName(FieldStorageDefinitionInterface $fieldDefinition): string {
return sprintf('date_recur__%s__%s', $fieldDefinition->getTargetEntityTypeId(), $fieldDefinition->getName());
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
DateRecurEvents::FIELD_VALUE_SAVE => ['onSave'],
DateRecurEvents::FIELD_ENTITY_DELETE => ['onEntityDelete'],
DateRecurEvents::FIELD_REVISION_DELETE => ['onEntityRevisionDelete'],
] + static::getEntityTypeEvents() + static::getFieldStorageDefinitionEvents();
}
}
