recurring_events-2.0.x-dev/src/Plugin/Field/FieldType/MonthlyRecurringDate.php
src/Plugin/Field/FieldType/MonthlyRecurringDate.php
<?php
declare(strict_types=1);
namespace Drupal\recurring_events\Plugin\Field\FieldType;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\recurring_events\Entity\EventSeries;
use Drupal\recurring_events\Plugin\RecurringEventsFieldTrait;
use Drupal\recurring_events\RecurringEventsFieldTypeInterface;
/**
* Plugin implementation of the 'monthly_recurring_date' field type.
*/
#[FieldType(
id: "monthly_recurring_date",
label: new TranslatableMarkup("Monthly Event"),
description: new TranslatableMarkup("Stores a monthly recurring date configuration"),
default_widget: "monthly_recurring_date",
default_formatter: "",
no_ui: TRUE,
)]
class MonthlyRecurringDate extends WeeklyRecurringDate implements RecurringEventsFieldTypeInterface {
use RecurringEventsFieldTrait;
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
$schema = parent::schema($field_definition);
$schema['columns']['type'] = [
'type' => 'varchar',
'length' => 20,
'not null' => TRUE,
];
$schema['columns']['day_occurrence'] = [
'type' => 'varchar',
'length' => 255,
];
$schema['columns']['days']['not null'] = FALSE;
$schema['columns']['day_of_month'] = [
'type' => 'varchar',
'length' => 255,
];
return $schema;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$type = $this->get('type')->getValue();
$occurrence = $this->get('day_occurrence')->getValue();
$day_of_month = $this->get('day_of_month')->getValue();
return parent::isEmpty() && empty($type) && empty($occurrence) && empty($day_of_month);
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
// Add our properties.
$properties['type'] = DataDefinition::create('string')
->setLabel(t('Event Recurrence Scheduling'))
->setDescription(t('Whether this event recurs based on weekdays, or days of the month'));
$properties['day_occurrence'] = DataDefinition::create('string')
->setLabel(t('Day Occurrence'))
->setDescription(t('Which occurrence of the day(s) of the week should event take place'));
$properties['day_of_month'] = DataDefinition::create('string')
->setLabel(t('Day of Month'))
->setDescription(t('The days of the month on which the event takes place'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function convertEntityConfigToArray(EventSeries $event) {
$config = [];
$config['start_date'] = $event->getMonthlyStartDate();
$config['end_date'] = $event->getMonthlyEndDate();
$config['time'] = strtoupper($event->getMonthlyStartTime());
$config['end_time'] = strtoupper($event->getMonthlyEndTime());
$config['duration'] = $event->getMonthlyDuration();
$config['duration_or_end_time'] = $event->getMonthlyDurationOrEndTime();
$config['monthly_type'] = $event->getMonthlyType();
switch ($event->getMonthlyType()) {
case 'weekday':
$config['day_occurrence'] = $event->getMonthlyDayOccurrences();
$config['days'] = $event->getMonthlyDays();
break;
case 'monthday':
$config['day_of_month'] = $event->getMonthlyDayOfMonth();
break;
}
return $config;
}
/**
* {@inheritdoc}
*/
public static function convertFormConfigToArray(FormStateInterface $form_state) {
$config = [];
$user_timezone = new \DateTimeZone(date_default_timezone_get());
$user_input = $form_state->getValues();
$time = $user_input['monthly_recurring_date'][0]['time'];
if (is_array($time)) {
$temp = DrupalDateTime::createFromFormat('H:i:s', $time['time']);
$time = $temp->format('h:i A');
}
$time_parts = static::convertTimeTo24hourFormat($time);
$timestamp = implode(':', $time_parts);
$user_input['monthly_recurring_date'][0]['value']->setTimezone($user_timezone);
$start_timestamp = $user_input['monthly_recurring_date'][0]['value']->format('Y-m-d') . 'T' . $timestamp;
$start_date = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $start_timestamp, $user_timezone);
$start_date->setTime(0, 0, 0);
$end_time = $user_input['monthly_recurring_date'][0]['end_time']['time'];
if (is_array($end_time)) {
$temp = DrupalDateTime::createFromFormat('H:i:s', $end_time['time']);
$end_time = $temp->format('h:i A');
}
$end_time_parts = static::convertTimeTo24hourFormat($end_time);
$end_timestamp = implode(':', $end_time_parts);
$user_input['monthly_recurring_date'][0]['end_value']->setTimezone($user_timezone);
$end_timestamp = $user_input['monthly_recurring_date'][0]['end_value']->format('Y-m-d') . 'T' . $end_timestamp;
$end_date = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $end_timestamp, $user_timezone);
$end_date->setTime(0, 0, 0);
$config['start_date'] = $start_date;
$config['end_date'] = $end_date;
$config['time'] = strtoupper($time);
$config['end_time'] = strtoupper($end_time);
$config['duration'] = $user_input['monthly_recurring_date'][0]['duration'];
$config['duration_or_end_time'] = $user_input['monthly_recurring_date'][0]['duration_or_end_time'];
$config['monthly_type'] = $user_input['monthly_recurring_date'][0]['type'];
switch ($config['monthly_type']) {
case 'weekday':
$config['day_occurrence'] = array_filter(array_values($user_input['monthly_recurring_date'][0]['day_occurrence']));
$config['days'] = array_filter(array_values($user_input['monthly_recurring_date'][0]['days']));
break;
case 'monthday':
$config['day_of_month'] = array_filter(array_values($user_input['monthly_recurring_date'][0]['day_of_month']));
break;
}
return $config;
}
/**
* {@inheritdoc}
*/
public static function buildDiffArray(array $entity_config, array $form_config) {
$diff = parent::buildDiffArray($entity_config, $form_config);
if ($entity_config['type'] === 'monthly_recurring_date') {
if (($entity_config['monthly_type'] ?? '') !== ($form_config['monthly_type'] ?? '')) {
$diff['monthly_type'] = [
'label' => t('Monthly Type'),
'stored' => $entity_config['monthly_type'] ?? '',
'override' => $form_config['monthly_type'] ?? '',
];
}
if ($entity_config['monthly_type'] === 'weekday') {
if (($entity_config['day_occurrence'] ?? []) !== ($form_config['day_occurrence'] ?? [])) {
$diff['day_occurrence'] = [
'label' => t('Day Occurrence'),
'stored' => implode(',', ($entity_config['day_occurrence'] ?? [])),
'override' => implode(',', ($form_config['day_occurrence'] ?? [])),
];
}
if (($entity_config['days'] ?? []) !== ($form_config['days'] ?? [])) {
$diff['days'] = [
'label' => t('Days'),
'stored' => implode(',', ($entity_config['days'] ?? [])),
'override' => implode(',', ($form_config['days'] ?? [])),
];
}
}
else {
if (($entity_config['day_of_month'] ?? []) !== ($form_config['day_of_month'] ?? [])) {
$diff['day_of_month'] = [
'label' => t('Day of the Month'),
'stored' => implode(',', ($entity_config['day_of_month'] ?? [])),
'override' => implode(',', ($form_config['day_of_month'] ?? [])),
];
}
}
}
return $diff;
}
/**
* {@inheritdoc}
*/
public static function calculateInstances(array $form_data) {
$dates = $events_to_create = [];
$time_parts = static::convertTimeTo24hourFormat($form_data['time']);
$utc_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
if (!empty($form_data['monthly_type'])) {
switch ($form_data['monthly_type']) {
case 'weekday':
// Loop through each weekday occurrence and weekday.
if (!empty($form_data['day_occurrence']) && !empty($form_data['days'])) {
foreach ($form_data['day_occurrence'] as $occurrence) {
foreach ($form_data['days'] as $weekday) {
// Find the occurrence of the specific weekdays within
// each month.
$day_occurrences = static::findWeekdayOccurrencesBetweenDates($occurrence, $weekday, $form_data['start_date'], $form_data['end_date']);
$dates = array_merge($dates, $day_occurrences);
}
}
}
break;
case 'monthday':
foreach ($form_data['day_of_month'] as $day_of_month) {
$days_of_month = static::findMonthDaysBetweenDates((int) $day_of_month, $form_data['start_date'], $form_data['end_date']);
$dates = array_merge($dates, $days_of_month);
}
break;
}
// If valid recurring dates were found.
if (!empty($dates)) {
foreach ($dates as $monthly_date) {
// Set the time of the start date to be the hours and
// minutes.
$monthly_date->setTime($time_parts[0], $time_parts[1]);
// Create a clone of this date.
$monthly_date_end = clone $monthly_date;
// Check whether we are using a duration or end time.
$duration_or_end_time = $form_data['duration_or_end_time'];
switch ($duration_or_end_time) {
case 'duration':
// Add the number of seconds specified in the duration field.
$monthly_date_end->modify('+' . $form_data['duration'] . ' seconds');
break;
case 'end_time':
// Set the time to be the end time.
$end_time_parts = static::convertTimeTo24hourFormat($form_data['end_time']);
if (!empty($end_time_parts)) {
$monthly_date_end->setTime($end_time_parts[0], $end_time_parts[1]);
}
break;
}
// Configure the timezone.
$monthly_date->setTimezone($utc_timezone);
$monthly_date_end->setTimezone($utc_timezone);
// Set this event to be created.
$events_to_create[$monthly_date->format('r')] = [
'start_date' => $monthly_date,
'end_date' => $monthly_date_end,
];
}
}
}
return $events_to_create;
}
/**
* Find all the day-of-month occurrences between two dates.
*
* @param int $day_of_month
* The day of the month.
* @param \Drupal\Core\Datetime\DrupalDateTime $start_date
* The start date.
* @param \Drupal\Core\Datetime\DrupalDateTime $end_date
* The end date.
*
* @return array
* An array of matching dates.
*/
public static function findMonthDaysBetweenDates(int $day_of_month, DrupalDateTime $start_date, DrupalDateTime $end_date): array {
$dates = [];
// Clone the date as we do not want to make changes to the original object.
$start = clone $start_date;
$end = clone $end_date;
// We want to create events up to and including the last day, so modify the
// end date to be midnight of the next day.
$end->modify('midnight next day');
// If the start date is after the end date then we have an invalid range so
// just return nothing.
if ($start->getTimestamp() > $end->getTimestamp()) {
return $dates;
}
$day_to_check = $day_of_month;
// If day of month is set to -1 that is the last day of the month, we need
// to calculate how many days a month has.
if ($day_of_month === -1) {
$day_to_check = (int) $start->format('t');
}
// If the day of the month is after the start date.
if ((int) $start->format('d') < $day_to_check) {
$new_date = clone $start;
$curr_month = (int) $new_date->format('m');
$curr_year = (int) $new_date->format('Y');
// Check to see if that date is a valid date.
if (!checkdate($curr_month, $day_to_check, $curr_year)) {
// If not, go find the next valid date.
$start = static::findNextMonthDay($day_of_month, $start);
}
else {
// This is a valid date, so let us start there.
$start->setDate($curr_year, $curr_month, $day_to_check);
}
}
// If the day of the month is in the past.
elseif ((int) $start->format('d') > $day_to_check) {
// Find the next valid start date.
$start = static::findNextMonthDay($day_of_month, $start);
}
// Loop through each month checking to see if the day of the month is a
// valid day, until the end date has been surpassed.
while ($start->getTimestamp() <= $end_date->getTimestamp()) {
// If we do not clone here we end up modifying the value of start in
// the array and get some funky dates returned.
$dates[] = clone $start;
// Find the next valid event date.
$start = static::findNextMonthDay($day_of_month, $start);
}
return $dates;
}
/**
* Find the next day-of-month occurrence.
*
* @param int $day_of_month
* The day of the month.
* @param \Drupal\Core\Datetime\DrupalDateTime $date
* The start date.
*
* @return \Drupal\Core\Datetime\DrupalDateTime
* The next occurrence of a specific day of the month.
*/
public static function findNextMonthDay(int $day_of_month, DrupalDateTime $date): DrupalDateTime {
$new_date = clone $date;
$curr_month = (int) $new_date->format('m');
$curr_year = (int) $new_date->format('Y');
$next_month = $curr_month;
$next_year = $curr_year;
do {
$next_month = ($next_month + 1) % 12 ?: 12;
$next_year = $next_month == 1 ? $next_year + 1 : $next_year;
// If the desired day of the month is the last day, calculate what that
// day is.
if ($day_of_month === -1) {
$new_date->setDate($next_year, $next_month, 1);
$day_of_month = (int) $new_date->format('t');
}
} while (checkdate($next_month, $day_of_month, $next_year) === FALSE);
$new_date->setDate($next_year, $next_month, $day_of_month);
return $new_date;
}
/**
* Find all the monthly occurrences of a specific weekday between two dates.
*
* @param string $occurrence
* Which occurrence of the weekday to find.
* @param string $weekday
* The name of the day of the week.
* @param \Drupal\Core\Datetime\DrupalDateTime $start_date
* The start date.
* @param \Drupal\Core\Datetime\DrupalDateTime $end_date
* The end date.
*
* @return array
* An array of matching dates.
*/
public static function findWeekdayOccurrencesBetweenDates($occurrence, $weekday, DrupalDateTime $start_date, DrupalDateTime $end_date) {
$dates = [];
// Clone the date as we do not want to make changes to the original object.
$start = clone $start_date;
// If the start date is after the end date then we have an invalid range so
// just return nothing.
if ($start->getTimestamp() > $end_date->getTimestamp()) {
return $dates;
}
// Grab the occurrence of the weekday we want for this current month.
$start->modify($occurrence . ' ' . $weekday . ' of this month');
// Make sure we didn't just go back in time.
if ($start < $start_date) {
// Go straight to next month.
$start->modify($occurrence . ' ' . $weekday . ' of next month');
}
// Loop through a week at a time, storing the date in the array to return
// until the end date is surpassed.
while ($start->getTimestamp() <= $end_date->getTimestamp()) {
// If we do not clone here we end up modifying the value of start in
// the array and get some funky dates returned.
$dates[] = clone $start;
$start->modify($occurrence . ' ' . $weekday . ' of next month');
}
return $dates;
}
}
