recurring_events-2.0.x-dev/src/Plugin/migrate/process/RecurringDate.php
src/Plugin/migrate/process/RecurringDate.php
<?php
declare(strict_types=1);
namespace Drupal\recurring_events\Plugin\migrate\process;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Transforms an array of dates to the format expected by recurring_events.
*
* The recurring_date plugin takes an array of event dates and converts it to
* the structure used by one of the RecurringEventsFieldTypeInterface fields
* (e.g., WeeklyRecurringDate), or to a 'daterange' field if the recurrence type
* could not be determined. This can then be imported into an eventseries (along
* with an appropriately set recur_type field) using the EntityEventSeries
* destination plugin via its 'recurring_date_field' configuration key.
*
* The $value is expected to be an array of arrays. Each child array represents
* an event instance and should contain one or two values representing the start
* and end datetimes, respectively. For example:
*
* @code
* $value = [
* [
* '20220719T120000',
* '20220719T130000',
* ]
* ]
* @endcode
*
* Alternatively, each child array can be associative and optionally include
* values for timezone and an iCalendar RRULE. For example:
*
* @code
* $value = [
* [
* 'start' => '20220719T120000',
* 'end' => '20220719T130000',
* 'rrule' => 'FREQ=WEEKLY;UNTIL=20220802T130000Z',
* 'timezone' => 'America/Detroit',
* ]
* ]
* @endcode
*
* In the above case, use the configuration keys (below) to tell the plugin
* which keys in the child arrays represent which values. Also, please note:
* - If the key specified by 'value_key' in the configuration is not found in
* the child array, the child array will be assumed to be non-associative.
* - Only the RRULE and timezone values on the first element in the $value
* array will be used.
* - The 'rrule' value can be an iCalendar RRULE string or an array as
* returned by RRuleHelper::parseRule.
*
* Available configuration keys:
* - value_key: The key in the source child arrays that represents the start
* date. Defaults to 'value'.
* - end_value_key: The key in the source child arrays that represents the end
* date. Defaults to 'end_value'.
* - rrule_key: The key in the source child arrays that represents an RRULE.
* Defaults to 'rrule'.
* - timezone_key: The key in the source child arrays that represents the
* timezone. Defaults to 'timezone'.
* - default_timezone: The timezone to use if none is provided for an event
* instance. Defaults to 'UTC'.
*
* Usage:
*
* @code
* process:
* recurring_date_field:
* plugin: recurring_date
* source: dates
* value_key: start
* end_value_key: end
* rrule_key: rrule
* timezone_key: timezone
* default_timezone: 'America/New_York'
* @endcode
*/
#[MigrateProcess(
id: "recurring_date",
handle_multiples: TRUE,
)]
class RecurringDate extends ProcessPluginBase {
/**
* Default event duration (in seconds).
*
* Used for events that have no end date in the source event.
*/
const DEFAULT_DURATION = 60 * 60;
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (!is_array($value)) {
throw new MigrateException('Input should be an array.');
}
return self::calculateRecurringDateValues($value, $this->configuration);
}
/**
* Converts date data including RRULE values to the format used by module.
*
* This method is provided as static so that it can be easily called by the
* EntityEventSeries destination plugin when migrating from a Drupal 7 date
* source field (via that plugin's 'source_date_field' option).
*
* @param array $source
* The date data.
* @param array $configuration
* The plugin configuration.
*
* @return array
* The data in recurring date field format.
*
* @throws \Drupal\migrate\MigrateException
* If the $source does not contain any values.
*/
public static function calculateRecurringDateValues(array $source, array $configuration): array {
// @todo Fix problem of specifying what months here.
// @todo Fix problem of no yearly pattern here.
if (!$num_dates = count($source)) {
throw new MigrateException('Date values are empty.');
}
$value_key = $configuration['value_key'] ?? 'value';
$end_value_key = $configuration['end_value_key'] ?? 'end_value';
$rrule_key = $configuration['rrule_key'] ?? 'rrule';
$timezone_key = $configuration['timezone_key'] ?? 'timezone';
$default_timezone = $configuration['default_timezone'] ?? 'UTC';
$first = $source[0];
$last = $source[$num_dates - 1];
// Take the timezone from the first element, if provided.
$timezone = empty($first[$timezone_key]) ? $default_timezone : $first[$timezone_key];
$source_timezone = new \DateTimeZone($timezone);
$storage_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
// Check whether the date data is non-associative.
if (!isset($first[$value_key])) {
$value_key = 0;
$end_value_key = 1;
}
// Get values for the first event in the series.
$start_event = new DrupalDateTime($first[$value_key], $source_timezone);
$end_key = (!empty($first[$end_value_key])) ? $end_value_key : $value_key;
$end_event = new DrupalDateTime($first[$end_key], $source_timezone);
if (!$duration = $end_event->getTimestamp() - $start_event->getTimestamp()) {
$duration = self::DEFAULT_DURATION;
$end_event->add(new \DateInterval('PT' . $duration . 'S'));
}
// Check for an RRULE.
$rule_in = empty($first[$rrule_key]) ? NULL : $first[$rrule_key];
if ($rule_in) {
$rrule = is_array($rule_in) ? $rule_in : RRuleHelper::parseRule($rule_in);
}
else {
$rrule = [];
}
if (empty($rrule['UNTIL'])) {
// Get values for the last event in the series.
$end_series = new DrupalDateTime($last[$value_key], $source_timezone);
}
else {
// Use the UNTIL value as the last event in the series.
$end_series = new DrupalDateTime($rrule['UNTIL'], $source_timezone);
}
// Adjust the timezone before storing values into the database.
$start_event->setTimezone($storage_timezone);
$end_event->setTimezone($storage_timezone);
$end_series->setTimezone($storage_timezone);
// Set the recurring date field data.
$recurrence_config = [];
$freq = $rrule['FREQ'] ?? NULL;
switch ($freq) {
// Set options for a weekly_recurring_date field.
case 'WEEKLY':
$recurrence_config = [
'value' => $start_event->format(RRuleHelper::DATETIME_FORMAT),
'end_value' => $end_series->format(RRuleHelper::DATETIME_FORMAT),
'time' => $start_event->format('h:i a'),
'end_time' => $end_event->format('h:i a'),
'duration' => $duration,
'duration_or_end_time' => 'end_time',
'days' => $rrule['BYDAY']['days'] ?? strtolower($start_event->format('l')),
];
break;
// Set options for a monthly_recurring_date field.
case 'MONTHLY':
$recurrence_config = [
'value' => $start_event->format(RRuleHelper::DATETIME_FORMAT),
'end_value' => $end_series->format(RRuleHelper::DATETIME_FORMAT),
'time' => $start_event->format('h:i a'),
'end_time' => $end_event->format('h:i a'),
'duration' => $duration,
'duration_or_end_time' => 'end_time',
'days' => $rrule['BYDAY']['days'] ?? strtolower($start_event->format('l')),
'type' => (!empty($rrule['BYMONTHDAY'])) ? 'monthday' : 'weekday',
'day_occurrence' => $rrule['BYDAY']['day_occurrence'] ?? NULL,
'day_of_month' => $rrule['BYMONTHDAY'] ?? NULL,
];
break;
// Set options for a custom_date field.
default:
foreach ($source as $date) {
$date_start = new DrupalDateTime($date[$value_key], $source_timezone);
$date_end = new DrupalDateTime($date[$end_value_key], $source_timezone);
$recurrence_config[] = [
'value' => $date_start->setTimezone($storage_timezone)->format(RRuleHelper::DATETIME_FORMAT),
'end_value' => $date_end->setTimezone($storage_timezone)->format(RRuleHelper::DATETIME_FORMAT),
];
}
}
return $recurrence_config;
}
}
