View source  
  <?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\FieldStorageDefinitionEventSubscriberTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
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;
class DateRecurOccurrences implements EventSubscriberInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface {
  use EntityTypeEventSubscriberTrait;
  use FieldStorageDefinitionEventSubscriberTrait;
  
  public const IS_DATE_RECUR = 'is_date_recur';
  
  protected $database;
  
  protected $entityTypeManager;
  
  protected $entityFieldManager;
  
  protected $typedDataManager;
  
  public function __construct(Connection $database, EntityFieldManagerInterface $entityFieldManager, TypedDataManagerInterface $typedDataManager, EntityTypeManagerInterface $entityTypeManager) {
    $this->database = $database;
    $this->entityTypeManager = $entityTypeManager;
    $this->entityFieldManager = $entityFieldManager;
    $this->typedDataManager = $typedDataManager;
  }
  
  public function onSave(DateRecurValueEvent $event) : void {
    
    $list = $event
      ->getField();
    $fieldDefinition = $list
      ->getFieldDefinition();
    $tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition
      ->getFieldStorageDefinition());
    $isInsert = $event
      ->isInsert();
    if (!$isInsert) {
      
      
      $entityId = $list
        ->getEntity()
        ->id();
      $this->database
        ->delete($tableName)
        ->condition('entity_id', (string) $entityId)
        ->execute();
    }
    foreach ($list as $item) {
      $this
        ->saveItem($item, $tableName);
    }
  }
  
  protected function saveItem(DateRecurItem $item, string $tableName) : void {
    
    
    $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();
  }
  
  public function onEntityDelete(DateRecurValueEvent $event) : void {
    $list = $event
      ->getField();
    $fieldDefinition = $list
      ->getFieldDefinition();
    $tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition
      ->getFieldStorageDefinition());
    $delete = $this->database
      ->delete($tableName);
    
    $entityId = $list
      ->getEntity()
      ->id();
    $delete
      ->condition('entity_id', (string) $entityId);
    $delete
      ->execute();
  }
  
  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);
    
    $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();
  }
  
  public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $fieldStorageConfig) : void {
    if ($this
      ->isDateRecur($fieldStorageConfig)) {
      $this
        ->fieldStorageCreate($fieldStorageConfig);
    }
  }
  
  public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $fieldStorageConfig) : void {
    if ($this
      ->isDateRecur($fieldStorageConfig)) {
      $this
        ->fieldStorageDelete($fieldStorageConfig);
    }
  }
  
  public function onEntityTypeCreate(EntityTypeInterface $entity_type) : void {
    if (!$entity_type instanceof ContentEntityTypeInterface) {
      
      return;
    }
    foreach ($this
      ->getBaseFieldStorages($entity_type) as $baseFieldStorage) {
      $this
        ->fieldStorageCreate($baseFieldStorage);
    }
  }
  
  public function onEntityTypeDelete(EntityTypeInterface $entity_type) : void {
    if (!$entity_type instanceof ContentEntityTypeInterface) {
      
      return;
    }
    foreach ($this
      ->getBaseFieldStorages($entity_type) as $baseFieldStorage) {
      $this
        ->fieldStorageDelete($baseFieldStorage);
    }
  }
  
  protected function fieldStorageCreate(FieldStorageDefinitionInterface $fieldDefinition) : void {
    $this
      ->createOccurrenceTable($fieldDefinition);
  }
  
  protected function fieldStorageDelete(FieldStorageDefinitionInterface $fieldDefinition) : void {
    $tableName = static::getOccurrenceCacheStorageTableName($fieldDefinition);
    $this->database
      ->schema()
      ->dropTable($tableName);
  }
  
  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);
  }
  
  protected function createOccurrenceTable(FieldStorageDefinitionInterface $fieldDefinition) : void {
    $entityTypeId = $fieldDefinition
      ->getTargetEntityTypeId();
    $entityType = $this->entityTypeManager
      ->getDefinition($entityTypeId);
    $fieldName = $fieldDefinition
      ->getName();
    $entityFieldDefinitions = $this->entityFieldManager
      ->getFieldStorageDefinitions($entityTypeId);
    
    $idDefinition = $entityFieldDefinitions[$entityType
      ->getKey('id')];
    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',
    ];
    $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,
      'indexes' => [
        'value' => [
          'entity_id',
          $fieldName . '_value',
        ],
      ],
    ];
    $tableName = DateRecurOccurrences::getOccurrenceCacheStorageTableName($fieldDefinition);
    $this->database
      ->schema()
      ->createTable($tableName, $schema);
  }
  
  protected function massageDateValueForStorage(\DateTimeInterface $date, DateRecurItem $item) : string {
    
    $date
      ->setTimezone(new \DateTimeZone(DateRecurItem::STORAGE_TIMEZONE));
    
    $storageFormat = $item
      ->getDateStorageFormat();
    if ($storageFormat == DateRecurItem::DATE_STORAGE_FORMAT) {
      $date
        ->setTime(12, 0, 0);
    }
    return $date
      ->format($storageFormat);
  }
  
  protected function isDateRecur(FieldStorageDefinitionInterface $fieldDefinition) : bool {
    $typeDefinition = $this->typedDataManager
      ->getDefinition('field_item:' . $fieldDefinition
      ->getType());
    
    return isset($typeDefinition[DateRecurOccurrences::IS_DATE_RECUR]);
  }
  
  protected function getBaseFieldStorages(ContentEntityTypeInterface $entityType) : array {
    $baseFields = $this->entityFieldManager
      ->getBaseFieldDefinitions($entityType
      ->id());
    $baseFields = array_filter($baseFields, function (FieldDefinitionInterface $fieldDefinition) : bool {
      return $this
        ->isDateRecur($fieldDefinition
        ->getFieldStorageDefinition());
    });
    return array_map(function (FieldDefinitionInterface $baseField) : FieldStorageDefinitionInterface {
      return $baseField
        ->getFieldStorageDefinition();
    }, $baseFields);
  }
  
  public static function getOccurrenceCacheStorageTableName(FieldStorageDefinitionInterface $fieldDefinition) : string {
    return sprintf('date_recur__%s__%s', $fieldDefinition
      ->getTargetEntityTypeId(), $fieldDefinition
      ->getName());
  }
  
  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();
  }
}