Code owners
Assign users and groups as approvers for specific file changes. Learn more.
EventCreationService.php 27.27 KiB
<?php
namespace Drupal\recurring_events;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\recurring_events\Entity\EventSeries;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\Messenger;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\Core\Field\FieldTypePluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\recurring_events\Entity\EventInstance;
use Drupal\field_inheritance\Entity\FieldInheritanceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a service with helper functions useful during event creation.
*/
class EventCreationService {
use StringTranslationTrait;
/**
* The translation interface.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
private $translation;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
private $database;
/**
* Logger Factory.
*
* @var \Drupal\Core\Logger\LoggerChannel
*/
protected $loggerChannel;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\Messenger
*/
protected $messenger;
/**
* The field type plugin manager.
*
* @var Drupal\Core\Field\FieldTypePluginManager
*/
protected $fieldTypePluginManager;
/**
* The entity field manager.
*
* @var Drupal\Core\Entity\EntityFieldManager
*/
protected $entityFieldManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The key value storage service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface
*/
protected $keyValueStore;
/**
* Class constructor.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The translation interface.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
* The logger factory.
* @param \Drupal\Core\Messenger\Messenger $messenger
* The messenger service.
* @param \Drupal\Core\Field\FieldTypePluginManager $field_type_plugin_manager
* The field type plugin manager.
* @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
* The key value storage service.
*/
public function __construct(TranslationInterface $translation, Connection $database, LoggerChannelFactoryInterface $logger, Messenger $messenger, FieldTypePluginManager $field_type_plugin_manager, EntityFieldManager $entity_field_manager, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager, KeyValueFactoryInterface $key_value) {
$this->translation = $translation;
$this->database = $database;
$this->loggerChannel = $logger->get('recurring_events');
$this->messenger = $messenger;
$this->fieldTypePluginManager = $field_type_plugin_manager;
$this->entityFieldManager = $entity_field_manager;
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->keyValueStore = $key_value;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('string_translation'),
$container->get('database'),
$container->get('logger.factory'),
$container->get('messenger'),
$container->get('plugin.manager.field.field_type'),
$container->get('entity_field.manager'),
$container->get('module_handler'),
$container->get('entity_type.manager'),
$container->get('keyvalue')
);
}
/**
* 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') {
$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'];
$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') {
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, without having to worry about order
* or casing discrepencies.
*
* @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 $form_state
* (Optional) The form state of an updated event series entity.
* @param Drupal\recurring_events\Entity\EventSeries $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') {
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();
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),
]));
}
// 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()]);
}
/**
* Create the event instances from the form state.
*
* @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']);
$this->configureDefaultInheritances($instance, $event->id());
$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']);
$this->configureDefaultInheritances($instance, $event->id());
$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) {
$entity->save();
}
else {
$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 $series_id
* The event series entity ID.
*/
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)) {
// 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)) {
$state_key = $entity_type . ':' . $instance->uuid();
$state = $this->keyValueStore->get('field_inheritance');
$state_values = $state->get($state_key);
if (empty($state_values)) {
$state_values = [
'enabled' => TRUE,
];
if (!empty($inherited_fields)) {
foreach ($inherited_fields as $inherited_field) {
$name = $inherited_field->idWithoutTypeAndBundle();
$state_values[$name] = [
'entity' => $series_id,
];
}
}
$state->set($state_key, $state_values);
}
}
}
}
/**
* 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) {
$state_key = 'eventinstance:' . $instance->uuid();
$state = $this->keyValueStore->get('field_inheritance');
$state_values = $state->get($state_key);
$name = $field_inheritance->idWithoutTypeAndBundle();
if (!empty($state_values[$name])) {
return;
}
$state_values[$name] = [
'entity' => $instance->eventseries_id->target_id,
];
$state->set($state_key, $state_values);
}
/**
* 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;
}
}