recurring_events-2.0.x-dev/src/EventCreationService.php

src/EventCreationService.php
<?php

declare(strict_types=1);

namespace Drupal\recurring_events;

use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\field_inheritance\Entity\FieldInheritanceInterface;
use Drupal\recurring_events\Entity\EventInstance;
use Drupal\recurring_events\Entity\EventSeries;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Provides a service with helper functions useful during event creation.
 */
class EventCreationService {

  use StringTranslationTrait;

  /**
   * Logger Factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannel
   */
  protected LoggerChannelInterface $loggerChannel;

  public function __construct(
    protected readonly TranslationInterface $translation,
    protected readonly Connection $database,
    protected readonly LoggerChannelFactoryInterface $logger,
    protected readonly MessengerInterface $messenger,
    protected readonly FieldTypePluginManagerInterface $fieldTypePluginManager,
    protected readonly EntityFieldManagerInterface $entityFieldManager,
    protected readonly ModuleHandlerInterface $moduleHandler,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    #[Autowire(service: 'keyvalue')]
    protected readonly KeyValueFactoryInterface $keyValueStore,
  ) {
    $this->loggerChannel = $logger->get('recurring_events');
  }

  /**
   * Check whether there have been form recurring configuration changes.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of an updated event series entity.
   *
   * @return bool
   *   TRUE if recurring config changes, FALSE otherwise.
   */
  public function checkForFormRecurConfigChanges(EventSeries $event, FormStateInterface $form_state) {
    $entity_config = $this->convertArrayLowercaseSorted(
      (array) $this->convertEntityConfigToArray($event));
    $form_config = $this->convertArrayLowercaseSorted(
      (array) $this->convertFormConfigToArray($form_state));
    return !(serialize($entity_config) === serialize($form_config));
  }

  /**
   * Check whether there have been original recurring configuration changes.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   * @param \Drupal\recurring_events\Entity\EventSeries $original
   *   The original stored event series entity.
   *
   * @return bool
   *   TRUE if recurring config changes, FALSE otherwise.
   */
  public function checkForOriginalRecurConfigChanges(EventSeries $event, EventSeries $original) {
    $entity_config = $this->convertArrayLowercaseSorted(
      (array) $this->convertEntityConfigToArray($event));
    $original_config = $this->convertArrayLowercaseSorted(
      (array) $this->convertEntityConfigToArray($original));
    return !(serialize($entity_config) === serialize($original_config));
  }

  /**
   * Converts an EventSeries entity's recurring configuration to an array.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   *
   * @return array
   *   The recurring configuration as an array.
   */
  public function convertEntityConfigToArray(EventSeries $event) {
    $config = [];
    $config['type'] = $event->getRecurType();
    $config['excluded_dates'] = $event->getExcludedDates();
    $config['included_dates'] = $event->getIncludedDates();

    if ($config['type'] === 'custom') {
      if ($custom_dates = $event->getCustomDates()) {
        $config['custom_dates'] = $event->getCustomDates();
      }
    }
    else {
      $field_definition = $this->fieldTypePluginManager->getDefinition($config['type']);
      $field_class = $field_definition['class'];
      $config += $field_class::convertEntityConfigToArray($event);
    }

    $this->moduleHandler->alter('recurring_events_entity_config_array', $config);

    return $config;
  }

  /**
   * Converts a form state object's recurring configuration to an array.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of an updated event series entity.
   *
   * @return array
   *   The recurring configuration as an array.
   */
  public function convertFormConfigToArray(FormStateInterface $form_state) {
    $config = [];

    $utc_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
    $user_input = $form_state->getValues();

    $config['type'] = $user_input['recur_type'][0]['value'] ?? 'custom';

    $config['excluded_dates'] = [];
    if (!empty($user_input['excluded_dates'])) {
      $config['excluded_dates'] = $this->getDatesFromForm($user_input['excluded_dates']);
    }

    $config['included_dates'] = [];
    if (!empty($user_input['included_dates'])) {
      $config['included_dates'] = $this->getDatesFromForm($user_input['included_dates']);
    }

    if ($config['type'] === 'custom') {
      $user_input['custom_date'] = $user_input['custom_date'] ?? [];
      foreach ($user_input['custom_date'] as $key => $custom_date) {
        if (!is_numeric($key)) {
          continue;
        }
        $start_date = $end_date = NULL;

        if (!empty($custom_date['value']) && !empty($custom_date['end_value'])) {

          $start_timestamp = $custom_date['value']->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
          $end_timestamp = $custom_date['end_value']->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
          $start_date = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $start_timestamp, $utc_timezone);
          $end_date = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $end_timestamp, $utc_timezone);

          $config['custom_dates'][] = [
            'start_date' => $start_date,
            'end_date' => $end_date,
          ];
        }
      }
    }
    else {
      $field_definition = $this->fieldTypePluginManager->getDefinition($config['type']);
      $field_class = $field_definition['class'];
      $config += $field_class::convertFormConfigToArray($form_state);
    }

    $this->moduleHandler->alter('recurring_events_form_config_array', $config);

    return $config;
  }

  /**
   * Normalize an array for equality checks.
   *
   * Do not worry about order or casing discrepancies.
   *
   * @param array $input
   *   The array to clean and sort.
   *
   * @return array
   *   A cleaned array.
   */
  public static function convertArrayLowercaseSorted(array $input) {
    foreach ($input as $key => $val) {
      if (is_object($val)) {
        $input[$key] = self::convertArrayLowercaseSorted((array) $val);
      }
      if (is_array($val)) {
        $input[$key] = self::convertArrayLowercaseSorted($val);
      }
      if (is_string($val)) {
        $input[$key] = strtolower($val);
      }
    }
    uksort($input, 'strcmp');
    return $input;
  }

  /**
   * Build diff array between stored entity and form state.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   * @param \Drupal\Core\Form\FormStateInterface|null $form_state
   *   (Optional) The form state of an updated event series entity.
   * @param \Drupal\recurring_events\Entity\EventSeries|null $edited
   *   (Optional) The edited event series entity.
   *
   * @return array
   *   An array of differences.
   */
  public function buildDiffArray(EventSeries $event, ?FormStateInterface $form_state = NULL, ?EventSeries $edited = NULL) {
    $diff = [];

    $entity_config = $this->convertEntityConfigToArray($event);
    $form_config = [];

    if (!is_null($form_state)) {
      $form_config = $this->convertFormConfigToArray($form_state);
    }
    if (!is_null($edited)) {
      $form_config = $this->convertEntityConfigToArray($edited);
    }

    if (empty($form_config)) {
      return $diff;
    }

    if ($entity_config['type'] !== $form_config['type']) {
      $diff['type'] = [
        'label' => $this->translation->translate('Recur Type'),
        'stored' => $entity_config['type'],
        'override' => $form_config['type'],
      ];
    }
    else {
      if ($entity_config['excluded_dates'] !== $form_config['excluded_dates']) {
        $entity_dates = $this->buildDateString($entity_config['excluded_dates']);
        $config_dates = $this->buildDateString($form_config['excluded_dates']);
        $diff['excluded_dates'] = [
          'label' => $this->translation->translate('Excluded Dates'),
          'stored' => $entity_dates,
          'override' => $config_dates,
        ];
      }
      if ($entity_config['included_dates'] !== $form_config['included_dates']) {
        $entity_dates = $this->buildDateString($entity_config['included_dates']);
        $config_dates = $this->buildDateString($form_config['included_dates']);
        $diff['included_dates'] = [
          'label' => $this->translation->translate('Included Dates'),
          'stored' => $entity_dates,
          'override' => $config_dates,
        ];
      }

      if ($entity_config['type'] === 'custom') {
        $form_config['custom_dates'] = $form_config['custom_dates'] ?? [];
        if ($entity_config['custom_dates'] !== $form_config['custom_dates']) {
          $stored_start_ends = $overridden_start_ends = [];

          $user_timezone = new \DateTimeZone(date_default_timezone_get());

          foreach ($entity_config['custom_dates'] as $date) {
            if (!empty($date['start_date']) && !empty($date['end_date'])) {
              $date['start_date']->setTimezone($user_timezone);
              $date['end_date']->setTimezone($user_timezone);
              $stored_start_ends[] = $date['start_date']->format('Y-m-d h:ia') . ' - ' . $date['end_date']->format('Y-m-d h:ia');
            }
          }

          foreach ($form_config['custom_dates'] as $date) {
            if (!empty($date['start_date']) && !empty($date['end_date'])) {
              $date['start_date']->setTimezone($user_timezone);
              $date['end_date']->setTimezone($user_timezone);
              $overridden_start_ends[] = $date['start_date']->format('Y-m-d h:ia') . ' - ' . $date['end_date']->format('Y-m-d h:ia');
            }
          }

          $diff['custom_dates'] = [
            'label' => $this->translation->translate('Custom Dates'),
            'stored' => implode(', ', $stored_start_ends),
            'override' => implode(', ', $overridden_start_ends),
          ];
        }
      }
      else {
        $field_definition = $this->fieldTypePluginManager->getDefinition($entity_config['type']);
        $field_class = $field_definition['class'];
        $diff += $field_class::buildDiffArray($entity_config, $form_config);
      }
    }

    $this->moduleHandler->alter('recurring_events_diff_array', $diff);

    return $diff;
  }

  /**
   * Clear out existing event instances.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The event series entity.
   */
  public function clearEventInstances(EventSeries $event) {
    // Allow other modules to react prior to the deletion of all instances.
    $this->moduleHandler->invokeAll('recurring_events_save_pre_instances_deletion', [
      $event,
    ]);

    // Find all the instances and delete them.
    $instances = $event->event_instances->referencedEntities();
    $initial_instance_count = count($instances);

    $this->moduleHandler->invokeAll('recurring_events_save_pre_instances_deletion_alter', [&$instances]);

    if (!empty($instances)) {
      foreach ($instances as $instance) {
        // Allow other modules to react prior to deleting a specific
        // instance after a date configuration change.
        $this->moduleHandler->invokeAll('recurring_events_save_pre_instance_deletion', [
          $event,
          $instance,
        ]);

        $instance->delete();

        // Allow other modules to react after deleting a specific instance
        // after a date configuration change.
        $this->moduleHandler->invokeAll('recurring_events_save_post_instance_deletion', [
          $event,
          $instance,
        ]);
      }

      $this->messenger->addStatus($this->translation->translate('A total of %count existing event instances were removed', [
        '%count' => count($instances),
      ]));

      $final_instance_count = count($instances);
      if ($initial_instance_count > $final_instance_count) {
        $this->messenger->addStatus($this->translation->translate('%count existing event instances were skipped.', [
          '%count' => $initial_instance_count - $final_instance_count,
        ]));
      }
    }

    // Allow other modules to react after the deletion of all instances.
    $this->moduleHandler->invokeAll('recurring_events_save_post_instances_deletion', [
      $event,
    ]);

    $this->entityTypeManager->getStorage('eventseries')->resetCache([$event->id()]);
  }

  /**
   * Creates the event instances from the form state.
   *
   * This is intended to be called on newly created event series entities. When
   * an existing event series entity needs to have the instances recreated, make
   * sure to clear the existing instances first using ::clearEventInstances().
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   *
   * @return \Drupal\recurring_events\Entity\EventInstance[]
   *   An array of event instances created for the series.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function createInstances(EventSeries $event): array {
    $form_data = $this->convertEntityConfigToArray($event);
    $event_instances = [];

    if (!empty($form_data['type'])) {
      if ($form_data['type'] === 'custom') {
        if (!empty($form_data['custom_dates'])) {
          $events_to_create = [];
          foreach ($form_data['custom_dates'] as $date_range) {
            // Set this event to be created.
            $events_to_create[$date_range['start_date']->format('r')] = [
              'start_date' => $date_range['start_date'],
              'end_date' => $date_range['end_date'],
            ];
          }

          // Allow modules to alter the array of event instances before they
          // get created.
          $this->moduleHandler->alter('recurring_events_event_instances_pre_create', $events_to_create, $event);

          if (!empty($events_to_create)) {
            foreach ($events_to_create as $custom_event) {
              $instance = $this->createEventInstance($event, $custom_event['start_date'], $custom_event['end_date']);
              if ($instance) {
                $this->configureDefaultInheritances($instance, (int) $event->id());
                $instance->save();
                $event_instances[] = $instance;
              }
            }
          }
        }
      }
      else {
        $field_definition = $this->fieldTypePluginManager->getDefinition($form_data['type']);
        $field_class = $field_definition['class'];
        $events_to_create = $field_class::calculateInstances($form_data);

        // Allow modules to alter the array of event instances before they
        // get created.
        $this->moduleHandler->alter('recurring_events_event_instances_pre_create', $events_to_create, $event);

        if (!empty($events_to_create)) {
          foreach ($events_to_create as $event_to_create) {
            $instance = $this->createEventInstance($event, $event_to_create['start_date'], $event_to_create['end_date']);
            if ($instance) {
              $this->configureDefaultInheritances($instance, (int) $event->id());
              $instance->save();
              $event_instances[] = $instance;
            }
          }
        }
      }
    }

    // Create a message to indicate how many instances were changed.
    $this->messenger->addMessage($this->translation->translate('A total of %items event instances were created as part of this event series.', [
      '%items' => count($event_instances),
    ]));

    return $event_instances;
  }

  /**
   * Create an event instance from an event series.
   *
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The stored event series entity.
   * @param \Drupal\Core\Datetime\DrupalDateTime $start_date
   *   The start date and time of the event.
   * @param \Drupal\Core\Datetime\DrupalDateTime $end_date
   *   The end date and time of the event.
   *
   * @return \Drupal\recurring_events\Entity\EventInstance
   *   The created event instance entity object.
   */
  public function createEventInstance(EventSeries $event, DrupalDateTime $start_date, DrupalDateTime $end_date) {
    $data = [
      'eventseries_id' => $event->id(),
      'date' => [
        'value' => $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
        'end_value' => $end_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
      ],
      'type' => $event->getType(),
      'uid' => $event->getOwnerId(),
    ];

    $this->moduleHandler->alter('recurring_events_event_instance', $data);

    $storage = $this->entityTypeManager->getStorage('eventinstance');
    if ($event->isDefaultTranslation()) {
      $entity = $storage->create($data);
    }
    else {
      // Grab the untranslated event series.
      $original = $event->getUntranslated();
      // Find the corresponding default language event instance that matches
      // the date and time of the version we wish to translate, so that we are
      // mapping the translations from default language to translated language
      // appropriately.
      $entity_ids = $storage->getQuery()
        ->condition('date__value', $data['date']['value'])
        ->condition('date__end_value', $data['date']['end_value'])
        ->condition('eventseries_id', $data['eventseries_id'])
        ->condition('type', $data['type'])
        ->condition('langcode', $original->language()->getId())
        ->accessCheck(FALSE)
        ->execute();

      if (!empty($entity_ids)) {
        // Load the default language version of the event instance.
        $entity = $storage->load(reset($entity_ids));
        // Only add a translation if we do not already have one.
        if (!$entity->hasTranslation($event->language()->getId())) {
          $entity->addTranslation($event->language()->getId(), $data);
        }
      }
    }

    if (!$entity) {
      $this->loggerChannel->warning('Missing event instance in default language. Translation could not be created');
    }

    return $entity;
  }

  /**
   * Configure the default field inheritances for event instances.
   *
   * @param \Drupal\recurring_events\Entity\EventInstance $instance
   *   The event instance.
   * @param int|null $series_id
   *   Optional event series entity ID. If omitted, the event series ID will be
   *   taken from the instance.
   */
  public function configureDefaultInheritances(EventInstance $instance, ?int $series_id = NULL) {
    if (is_null($series_id)) {
      $series_id = $instance->eventseries_id->target_id;
    }

    if (empty($series_id)) {
      return;
    }

    // Configure the field inheritances for this instance.
    $entity_type = $instance->getEntityTypeId();
    $bundle = $instance->bundle();

    $inherited_fields = $this->entityTypeManager->getStorage('field_inheritance')->loadByProperties([
      'sourceEntityType' => 'eventseries',
      'destinationEntityType' => $entity_type,
      'destinationEntityBundle' => $bundle,
    ]);

    if (empty($inherited_fields)) {
      return;
    }

    $inherited_field = reset($inherited_fields);

    $field = $instance->get('field_inheritance')->first()?->getValue() ?? [
      'enabled' => TRUE,
      'fields' => [],
      'entities' => [
        $inherited_field->sourceIdentifier() => ['entity' => $series_id],
      ],
    ];

    foreach ($inherited_fields as $inherited_field) {
      $name = $inherited_field->idWithoutTypeAndBundle();
      $field['fields'][$name] = [
        'entity' => $series_id,
        'skip' => FALSE,
      ];
    }

    $instance->set('field_inheritance', $field);
  }

  /**
   * When adding a new field inheritance, add the default values for it.
   *
   * @param \Drupal\recurring_events\Entity\EventInstance $instance
   *   The event instance for which to configure default inheritance values.
   * @param \Drupal\field_inheritance\Entity\FieldInheritanceInterface $field_inheritance
   *   The field inheritance being created or updated.
   */
  public function addNewDefaultInheritance(EventInstance $instance, FieldInheritanceInterface $field_inheritance) {
    $field = $instance->get('field_inheritance')->first()?->getValue() ?? [
      'enabled' => TRUE,
      'fields' => [],
    ];
    $name = $field_inheritance->idWithoutTypeAndBundle();
    $field['fields'][$name] = [
      'entity' => $instance->eventseries_id->target_id,
      'skip' => FALSE,
    ];
  }

  /**
   * Get exclude/include dates from form.
   *
   * @param array $field
   *   The field from which to retrieve the dates.
   *
   * @return array
   *   An array of dates.
   */
  private function getDatesFromForm(array $field) {
    $dates = [];

    if (!empty($field)) {
      foreach ($field as $key => $date) {
        if (!is_numeric($key)) {
          continue;
        }
        if (!empty($date['value']) && !empty($date['end_value'])) {
          $dates[] = [
            'value' => $date['value']->format('Y-m-d'),
            'end_value' => $date['end_value']->format('Y-m-d'),
          ];
        }
      }
    }
    return $dates;
  }

  /**
   * Build a string from excluded or included date ranges.
   *
   * @var array $config
   *   The configuration from which to build a string.
   *
   * @return string
   *   The formatted date string.
   */
  private function buildDateString(array $config) {
    $string = '';

    $string_parts = [];
    if (!empty($config)) {
      foreach ($config as $date) {
        $range = $this->translation->translate('@start_date to @end_date', [
          '@start_date' => $date['value'],
          '@end_date' => $date['end_value'],
        ]);
        $string_parts[] = '(' . $range . ')';
      }

      $string = implode(', ', $string_parts);
    }
    return $string;
  }

  /**
   * Retrieve the recur field types.
   *
   * @param bool $allow_alter
   *   Allow altering of the field types.
   *
   * @return array
   *   An array of field types.
   */
  public function getRecurFieldTypes($allow_alter = TRUE) {
    // Build an array of recur type field options based on FieldTypes that
    // implement the Drupal\recurring_events\RecurringEventsFieldTypeInterface
    // interface. Allow for other modules to customize this list with an alter
    // hook.
    $recur_fields = [];
    $fields = $this->entityFieldManager->getBaseFieldDefinitions('eventseries');
    foreach ($fields as $field) {
      $field_definition = $this->fieldTypePluginManager->getDefinition($field->getType());
      $class = new \ReflectionClass($field_definition['class']);
      if ($class->implementsInterface('\Drupal\recurring_events\RecurringEventsFieldTypeInterface')) {
        $recur_fields[$field->getName()] = $field->getLabel();
      }
    }

    $recur_fields['custom'] = $this->t('Custom/Single Event');
    if ($allow_alter) {
      $this->moduleHandler->alter('recurring_events_recur_field_types', $recur_fields);
    }
    return $recur_fields;
  }

  /**
   * Update instance status.
   *
   * @param \Drupal\recurring_events\Entity\EventInstance $instance
   *   The event instance for which to update the status.
   * @param \Drupal\recurring_events\Entity\EventSeries $event
   *   The event series entity.
   */
  public function updateInstanceStatus(EventInstance $instance, EventSeries $event) {
    $original_event = $event->original;
    $field_name = 'status';

    if ($this->moduleHandler->moduleExists('workflows')) {
      if ($event->hasField('moderation_state') && $instance->hasField('moderation_state')) {
        $series_query = $this->entityTypeManager->getStorage('workflow')->getQuery()->accessCheck(FALSE);
        $series_query->condition('type_settings.entity_types.eventseries.*', $event->bundle());
        $series_workflows = $series_query->accessCheck(FALSE)->execute();
        $series_workflows = array_keys($series_workflows);
        $series_workflow = reset($series_workflows);

        $instance_query = $this->entityTypeManager->getStorage('workflow')->getQuery()->accessCheck(FALSE);
        $instance_query->condition('type_settings.entity_types.eventinstance.*', $instance->bundle());
        $instance_workflows = $instance_query->accessCheck(FALSE)->execute();
        $instance_workflows = array_keys($instance_workflows);
        $instance_workflow = reset($instance_workflows);

        // We only want to mimic moderation state if the series and instance use
        // the same workflows, otherwise we cannot guarantee the states match.
        if ($instance_workflow === $series_workflow) {
          $field_name = 'moderation_state';
        }
        else {
          return FALSE;
        }
      }
    }

    $new_state = $event->get($field_name)->getValue();
    $instance_state = $instance->get($field_name)->getValue();

    if (!empty($original_event)) {
      $original_state = $original_event->get($field_name)->getValue();
    }
    else {
      $instance->set($field_name, $new_state);
      return TRUE;
    }

    // If the instance state matches the original state of the series we want
    // to also update the instance state.
    if ($instance_state === $original_state) {
      $instance->set($field_name, $new_state);
      return TRUE;
    }

    return FALSE;

  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc