You are here

class SmartDateRule in Smart Date 3.4.x

Same name and namespace in other branches
  1. 8.2 modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule
  2. 3.x modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule
  3. 3.0.x modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule
  4. 3.1.x modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule
  5. 3.2.x modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule
  6. 3.3.x modules/smart_date_recur/src/Entity/SmartDateRule.php \Drupal\smart_date_recur\Entity\SmartDateRule

Defines the Smart date rule entity.

Plugin annotation


@ContentEntityType(
  id = "smart_date_rule",
  label = @Translation("Smart date recurring rule"),
  handlers = {
    "storage" = "Drupal\smart_date_recur\RuleStorage",
    "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
    "views_data" = "Drupal\views\EntityViewsData",

    "form" = {
      "remove" = "Drupal\smart_date_recur\Form\SmartDateRemoveInstanceForm",
    }
  },
  base_table = "smart_date_rule",
  data_table = "smart_date_rule_data",
  translatable = FALSE,
  entity_keys = {
    "id" = "rid",
    "label" = "rule",
    "uuid" = "uuid",
  },
  links = {
    "remove-form" = "/admin/content/smart_date_recur/{smart_date_rule}/instance/remove/{index}",
  },
)

Hierarchy

Expanded class hierarchy of SmartDateRule

8 files declare their use of SmartDateRule
Frequency.php in modules/smart_date_recur/src/Plugin/views/filter/Frequency.php
Instances.php in modules/smart_date_recur/src/Controller/Instances.php
RecurRuleUpdate.php in modules/smart_date_recur/src/Plugin/QueueWorker/RecurRuleUpdate.php
SmartDateDailyRangeFormatter.php in modules/smart_date_recur/src/Plugin/Field/FieldFormatter/SmartDateDailyRangeFormatter.php
SmartDateRecurrenceFormatter.php in modules/smart_date_recur/src/Plugin/Field/FieldFormatter/SmartDateRecurrenceFormatter.php

... See full list

File

modules/smart_date_recur/src/Entity/SmartDateRule.php, line 54

Namespace

Drupal\smart_date_recur\Entity
View source
class SmartDateRule extends ContentEntityBase {
  use EntityChangedTrait;
  use StringTranslationTrait;

  /**
   * The frequency of recurrence.
   *
   * @var string
   */
  protected $freq = '';

  /**
   * The limit to recurrence.
   *
   * @var string
   */
  protected $limit = '';

  /**
   * An imploded array of extra parameters, such as increment values.
   *
   * @var string
   */
  protected $parameters = '';

  /**
   * The assembled rule, as a string.
   *
   * @var string
   */
  protected $rule = '';

  /**
   * The timestamp for the first instance.
   *
   * @var int
   */
  protected $start = NULL;

  /**
   * The timezone for the field/rule.
   *
   * @var string
   */
  protected $timezone = NULL;

  /**
   * {@inheritdoc}
   */
  public function getRule() {
    return $this
      ->get('rule')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setRule($rule) {
    $this
      ->set('rule', $rule);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  private function makeRuleFromParts() {
    $repeat = $this
      ->get('freq')
      ->getString();
    if (empty($repeat)) {
      return FALSE;
    }
    $rule = new FormattableMarkup('RRULE:FREQ=:freq', [
      ':freq' => $repeat,
    ]);

    // Processing for extra parameters e.g. INCREMENT, BYMONTHDAY, etc.
    $params = $this
      ->get('parameters')
      ->getString();
    if (!empty($params)) {
      $rule .= ';' . $params;
    }

    // If a limit has been set, add it to the rule definition.
    $end = $this
      ->get('limit')
      ->getString();
    if (!empty($end)) {
      $rule .= ';' . $end;
      if (strpos($end, 'UNTIL') === 0 && strpos($end, 'T', 4) === FALSE) {

        // Add midnight to specify the end of the last day.
        $rule .= 'T235959';
      }
    }
    $this
      ->setRule($rule);
    return $rule;
  }

  /**
   * Retrieve all overrides created for this rule.
   */
  public function getRuleOverrides() {
    $result = \Drupal::entityQuery('smart_date_override')
      ->condition('rrule', $this
      ->id())
      ->execute();
    $overrides = [];
    if ($result && ($overrides_return = SmartDateOverride::loadMultiple($result))) {
      foreach ($overrides_return as $override) {
        $index = $override->rrule_index
          ->getString();
        $overrides[$index] = $override;
      }
    }
    return $overrides;
  }

  /**
   * Provide a formatted array of instances, with any overrides applied.
   */
  public function getRuleInstances($before = NULL, $after = NULL) {
    $instances = $this
      ->makeRuleInstances($before, $after)
      ->toArray();
    $overrides = $this
      ->getRuleOverrides();
    $formatted = [];
    foreach ($instances as $instance) {
      $index = $instance
        ->getIndex();

      // Check for an override.
      if (isset($overrides[$index])) {

        // Check for rescheduled, overridden, or cancelled
        // and don't use default value.
        $override = $overrides[$index];
        if ($override->entity_id
          ->getString()) {

          // Overridden, retrieve appropriate entity.
          $override_type = 'overridden';
          $override = $entity_storage
            ->load($override['entity_id']);
          $field = $override
            ->get($field_name);

          // TODO: drill down and retrieve, replace values.
        }
        elseif ($override->value
          ->getString()) {

          // Rescheduled, use values from override.
          $formatted[$index] = [
            'value' => $override->value
              ->getString(),
            'end_value' => $override->end_value
              ->getString(),
            'oid' => $override
              ->id(),
          ];
        }
        else {

          // Cancelled.
        }
        continue;
      }

      // Use the generated instance as-is.
      $formatted[$index] = [
        'value' => $instance
          ->getStart()
          ->getTimestamp(),
        'end_value' => $instance
          ->getEnd()
          ->getTimestamp(),
      ];
    }

    // Return the assembled array.
    return $formatted;
  }

  /**
   * Generate default instances based on rule structure.
   */
  public function getNewInstances() {
    $month_limit = $this
      ->getMonthsLimit($this);
    $before = strtotime('+' . (int) $month_limit . ' months');
    $instances = $this
      ->getStoredInstances();
    $last_instance = end($instances);
    $new_instances = $this
      ->makeRuleInstances($before, $last_instance['value']);
    return $new_instances;
  }

  /**
   * Helper function to parse instances from storage and return as an array.
   */
  public function getStoredInstances() {
    $instances = $this->instances
      ->getValue();
    if (is_array($instances)) {
      $instances = $instances[0]['data'];
    }
    return $instances;
  }

  /**
   * Generate default instances based on rule structure.
   */
  public function makeRuleInstances($before = NULL, $after = NULL) {
    $rrule = $this
      ->getAssembledRule();
    if (empty($rrule)) {

      // Required elements missing, so abort.
      return FALSE;
    }
    $constraint = NULL;
    if ($before && $after) {
      $constraint = new BetweenConstraint(new \DateTime('@' . $after), new \DateTime('@' . $before));
    }
    elseif ($before) {
      $constraint = new BeforeConstraint(new \DateTime('@' . $before));
    }
    elseif ($after) {
      $constraint = new AfterConstraint(new \DateTime('@' . $after));
    }
    $transformer = new ArrayTransformer();
    $instances = $transformer
      ->transform($rrule, $constraint);

    // TODO: Convert the generated instances into an array for later processing.
    return $instances;
  }

  /**
   * Retrieve the entity to which the rule is attached.
   */
  public function getParentEntity($id_only = FALSE) {

    // Retrieve the entity using the rule id.
    $rid = $this
      ->id();
    if (empty($rid)) {
      return FALSE;
    }
    $entity_type = $this->entity_type
      ->getString();
    $field_name = $this->field_name
      ->getString();
    $result = \Drupal::entityQuery($entity_type)
      ->condition($field_name . '.rrule', $rid)
      ->execute();
    $id = array_pop($result);
    if ($id_only) {
      return $id;
    }
    $entity_manager = \Drupal::entityTypeManager($entity_type);
    $entity_storage = $entity_manager
      ->getStorage($entity_type);
    $entity = $entity_storage
      ->load($id);
    return $entity;
  }

  /**
   * Get the RRule object.
   */
  public function getAssembledRule() {
    $rule = $this
      ->makeRuleFromParts();
    if (empty($rule)) {

      // Required elements missing, so abort.
      return FALSE;
    }

    // TODO: proper timezone handling, allowing for field override.
    $tz_string = \Drupal::config('system.date')
      ->get('timezone')['default'];
    $timezone = new \DateTimeZone($tz_string);
    $start = new \DateTime('@' . $this
      ->get('start')
      ->getString(), $timezone);
    $start
      ->setTimezone($timezone);
    $end = new \DateTime('@' . $this
      ->get('end')
      ->getString(), $timezone);
    $end
      ->setTimezone($timezone);
    $rrule = new Rule($rule, $start, $end);
    return $rrule;
  }

  /**
   * Use the transformer to get text output of the rule.
   */
  public function getTextRule() {
    $freq = $this
      ->get('freq')
      ->getString();
    $repeat = $freq;
    $params = $this
      ->getParametersArray();
    $day_labels = [
      'MO' => $this
        ->t('Monday'),
      'TU' => $this
        ->t('Tuesday'),
      'WE' => $this
        ->t('Wednesday'),
      'TH' => $this
        ->t('Thursday'),
      'FR' => $this
        ->t('Friday'),
      'SA' => $this
        ->t('Saturday'),
      'SU' => $this
        ->t('Sunday'),
    ];

    // Convert the stored repeat value to something human-readable.
    if ($params['interval'] && $params['interval'] > 1) {
      switch ($repeat) {
        case 'MINUTELY':
          $period = $this
            ->t('minutes');
          break;
        case 'HOURLY':
          $period = $this
            ->t('hours');
          break;
        case 'DAILY':
          $period = $this
            ->t('days');
          break;
        case 'WEEKLY':
          $period = $this
            ->t('weeks');
          break;
        case 'MONTHLY':
          $period = $this
            ->t('months');
          break;
        case 'YEARLY':
          $period = $this
            ->t('years');
          break;
      }
      $repeat = $this
        ->t('every :num :period', [
        ':num' => $params['interval'],
        ':period' => $period,
      ]);
    }
    else {
      $frequency_labels = static::getFrequencyLabels();
      $repeat = $frequency_labels[$repeat];
    }
    $start_ts = $this->start;

    // TODO: proper timezone handling, allowing for field override.
    $tz_string = \Drupal::config('system.date')
      ->get('timezone')['default'];
    $format = SmartDateFormat::load('time_only');
    $time_set = FALSE;

    // Add extra time parameters, if set.
    if ($params['byhour']) {
      $current_time = DrupalDateTime::createFromTimestamp($start_ts, $tz_string);
      $ranges = $this
        ->makeRanges($params['byhour']);
      $range_text = [];
      foreach ($ranges as $range) {
        $range_start = array_shift($range);
        $current_time
          ->setTime($range_start, 0);
        $range_start_ts = $current_time
          ->getTimestamp();
        if ($range) {
          $range_end = array_pop($range);
          $current_time
            ->setTime($range_end + 1, 0);
          $range_end_ts = $current_time
            ->getTimestamp();
        }
        else {
          $range_end_ts = $range_start_ts;
        }
        $range_text[] = SmartDateTrait::formatSmartDate($range_start_ts, $range_end_ts, $format
          ->getOptions(), $tz_string, 'string');
      }
      $repeat .= ' ' . $this
        ->t('within') . ' ' . implode(', ', $range_text);
      $time_set = TRUE;
    }
    if ($params['byminute']) {
      $ranges = $this
        ->makeRanges($params['byminute']);
      $range_text = [];
      foreach ($ranges as $range) {
        $range_start = array_shift($range);
        if ($range) {
          $range_end = array_pop($range);
          $range_text[] = $this
            ->t(':start to :end', [
            ':start' => $range_start,
            ':end' => $range_end,
          ], [
            'context' => 'Rule text',
          ]);
        }
        else {
          $range_text[] = $range_start;
        }
      }
      $repeat .= ' ' . $this
        ->t('at :ranges past the hour', [
        ':ranges' => implode(', ', $range_text),
      ], [
        'context' => 'Rule text',
      ]);
      $time_set = TRUE;
    }

    // Convert the stored day modifier to something human-readable.
    if ($params['which']) {
      switch ($params['which']) {
        case '1':
          $params['which'] = $this
            ->t('first');
          break;
        case '2':
          $params['which'] = $this
            ->t('second');
          break;
        case '3':
          $params['which'] = $this
            ->t('third');
          break;
        case '4':
          $params['which'] = $this
            ->t('fourth');
          break;
        case '5':
          $params['which'] = $this
            ->t('fifth');
          break;
        case '-1':
          $params['which'] = $this
            ->t('last');
          break;
      }
    }

    // Convert the stored day value to something human-readable.
    if (isset($params['day'])) {
      switch ($params['day']) {
        case 'SU':
        case 'MO':
        case 'TU':
        case 'WE':
        case 'TH':
        case 'FR':
        case 'SA':
          $params['day'] = $day_labels[$params['day']];
          break;
        case 'MO,TU,WE,TH,FR':
          $params['day'] = $this
            ->t('weekday');
          break;
        case 'SA,SU':
          $params['day'] = $this
            ->t('weekend day');
          break;
        case '':
          $params['day'] = $this
            ->t('day');
          break;
      }
    }

    // Format the day output.
    if (in_array($freq, [
      'MINUTELY',
      'HOURLY',
      'DAILY',
      'WEEKLY',
    ])) {
      if (!empty($params['byday']) && is_array($params['byday'])) {
        switch (count($params['byday'])) {
          case 1:
            $day_output = $day_labels[array_pop($params['byday'])];
            break;
          case 2:
            $day_output = $day_labels[$params['byday'][0]] . ' ' . $this
              ->t('and') . ' ' . $day_labels[$params['byday'][1]];
            break;
          default:
            $day_output = '';
            foreach ($params['byday'] as $key => $day) {
              if ($key === array_key_last($params['byday'])) {
                $day_output .= $this
                  ->t('and') . ' ' . $day_labels[$day];
              }
              else {
                $day_output .= $day_labels[$day] . ', ';
              }
            }
            break;
        }
      }
      else {

        // Default to getting the day from the start date.
        $day_labels_by_day_of_week = array_values($day_labels);
        $day_output = $day_labels_by_day_of_week[date('N', $start_ts) - 1];
      }
      $day = $this
        ->t('on :day', [
        ':day' => $day_output,
      ], [
        'context' => 'Rule text',
      ]);
    }
    else {
      $day = date('jS', $start_ts);
      if ($params['which']) {
        $day = $params['which'] . ' ' . $params['day'];
      }
      $day = $this
        ->t('on the :day', [
        ':day' => $day,
      ], [
        'context' => 'Rule text',
      ]);
    }

    // Format the month display, if needed.
    if ($freq == 'YEARLY') {
      $month = ' ' . $this
        ->t('of :month', [
        ':month' => date('F', $start_ts),
      ], [
        'context' => 'Rule text',
      ]);
    }
    else {
      $month = '';
    }
    if ($time_set) {
      $time = '';
    }
    else {

      // Format the time display.
      // Use the "Time Only" Smart Date Format to allow better formatting.
      $end_ts = $this->end
        ->getValue()[0]['value'];
      if (SmartDateTrait::isAllDay($start_ts, $end_ts, $tz_string)) {
        $time = SmartDateTrait::formatSmartDate($start_ts, $end_ts, $format
          ->getOptions(), $tz_string, 'string');
      }
      else {
        $time_string = SmartDateTrait::formatSmartDate($start_ts, $start_ts, $format
          ->getOptions(), $tz_string, 'string');
        $time = $this
          ->t('at :time', [
          ':time' => '',
        ], [
          'context' => 'Rule text',
        ]) . $time_string;
      }
    }

    // Process the limit value, if present.
    $limit = '';
    if ($this->limit) {
      list($limit_type, $limit_val) = explode('=', $this->limit);
      switch ($limit_type) {
        case 'UNTIL':
          $limit_ts = strtotime($limit_val);
          $format = SmartDateFormat::load('date_only');
          $date_string = SmartDateTrait::formatSmartDate($limit_ts, $limit_ts, $format
            ->getOptions(), $tz_string, 'string');
          $limit = ' ' . $this
            ->t('until :date', [
            ':date' => $date_string,
          ]);
          break;
        case 'COUNT':
          $limit = ' ' . $this
            ->t('for :num times', [
            ':num' => $limit_val,
          ]);
      }
    }
    return [
      '#theme' => 'smart_date_recurring_text_rule',
      '#repeat' => $repeat,
      '#day' => $day,
      '#month' => $month,
      '#time' => $time,
      '#limit' => $limit,
    ];
  }

  /**
   * Helper function to convert an array into ranges.
   *
   * @param array $array
   *   The array to convert.
   * @param int $offset
   *   The offset to use for comoparison.
   *
   * @return array
   *   An array of ranges.
   */
  private function makeRanges(array $array, $offset = 1) {
    $ranges = [];
    if (!$array || count($array) == 1) {
      return $array;
    }
    $start_item = array_shift($array);
    $range = [
      $start_item,
    ];
    foreach ($array as $value) {
      if ($value == $start_item + $offset) {

        // Add to the current range.
        $range[] = $value;
      }
      else {

        // Start a new range.
        $ranges[] = $range;
        $range = [
          $value,
        ];
      }
      $start_item = $value;
    }

    // Add the final range.
    $ranges[] = $range;
    return $ranges;
  }

  /**
   * Retrieve the months_limit value from the field definition.
   */
  public static function getThirdPartyFallback($field_def, $property, $default = NULL) {
    $value = $default;
    if (method_exists($field_def, 'getThirdPartySetting')) {

      // Works for field definitions and rule objects.
      $value = $field_def
        ->getThirdPartySetting('smart_date_recur', $property, $default);
    }
    elseif (method_exists($field_def, 'getSetting')) {

      // For custom entities, set value in your field definition.
      $value = $field_def
        ->getSetting($property);
    }
    return $value;
  }

  /**
   * Retrieve the months_limit value from the field definition.
   */
  public static function getMonthsLimit($field_def) {
    $month_limit = static::getThirdPartyFallback($field_def, 'month_limit', 12);
    return $month_limit;
  }

  /**
   * Return an array of frequency labels.
   */
  public static function getFrequencyLabels() {
    return [
      'MINUTELY' => t('By Minutes'),
      'HOURLY' => t('Hourly'),
      'DAILY' => t('Daily'),
      'WEEKLY' => t('Weekly'),
      'MONTHLY' => t('Monthly'),
      'YEARLY' => t('Annually'),
    ];
  }

  /**
   * Return an array of frequency labels.
   */
  public static function getFrequencyLabelsOrNull() {
    $values = [
      'none' => 'Not recurring',
    ];
    $labels = static::getFrequencyLabels();
    return array_merge($values, $labels);
  }

  /**
   * Retrieve a setting from the field config.
   */
  public function getFieldSettings($setting_name, $module = 'smart_date_recur') {
    $entity_type = $this->entity_type
      ->getString();
    $bundle = $this->bundle
      ->getString();
    $field_name = $this->field_name
      ->getString();
    $bundle_fields = \Drupal::getContainer()
      ->get('entity_field.manager')
      ->getFieldDefinitions($entity_type, $bundle);
    $field_def = $bundle_fields[$field_name];
    if ($field_def instanceof FieldConfigInterface) {
      $value = $field_def
        ->getThirdPartySetting($module, $setting_name);
    }
    elseif ($field_def instanceof BaseFieldDefinition) {

      // TODO: Document that for custom entities, you must enable recurring
      // functionality by adding ->setSetting('allow_recurring', TRUE)
      // to your field definition.
      $value = $field_def
        ->getSetting($setting_name);
    }
    else {

      // Not sure what other method we can provide to define this.
      $value = FALSE;
    }
    return $value;
  }

  /**
   * Convert the stored parameters into an array.
   */
  public function getParametersArray() {
    $params = $this
      ->get('parameters')
      ->getString();
    $return_array = [
      'interval' => NULL,
      'which' => '',
      'day' => '',
      'byday' => [],
      'byhour' => [],
      'byminute' => [],
    ];
    if ($params && ($params = explode(';', $params))) {
      foreach ($params as $param) {
        list($var_name, $var_value) = explode('=', $param);
        switch ($var_name) {
          case 'INTERVAL':
            $return_array['interval'] = (int) $var_value;
            break;
          case 'BYDAY':
            $arr = preg_split('/(?<=[-0-9])(?=[,A-Z]+)/i', $var_value);
            if ((int) $arr[0]) {

              // Starts with a number, so treat as a compound value.
              $return_array['which'] = $arr[0];
              $return_array['day'] = $arr[1];
            }
            else {

              // Assume this is a multi-day value.
              $freq = $this
                ->get('freq')
                ->getString();
              if (in_array($freq, [
                'MINUTELY',
                'HOURLY',
                'DAILY',
                'WEEKLY',
              ])) {

                // Split into an array before returning the value.
                $return_array['byday'] = explode(',', $arr[0]);
              }
              else {
                $return_array['day'] = $arr[0];
              }
            }
            break;
          case 'BYHOUR':
            $return_array['byhour'] = explode(',', $var_value);
            break;
          case 'BYMINUTE':
            $return_array['byminute'] = explode(',', $var_value);
            break;
          case 'BYMONTHDAY':
            $return_array['which'] = $var_value;
            break;
          case 'BYSETPOS':
            $return_array['which'] = $var_value;
            break;
        }
      }
    }
    return $return_array;
  }

  /**
   * Return an array of all rule properties.
   */
  public function getAllProperties() {
    $array = $this
      ->getParametersArray();
    $array['freq'] = $this
      ->get('freq')
      ->getString();
    $end = $this
      ->get('limit')
      ->getString();
    if (empty($end)) {
      $array['limit'] = NULL;
      $array['limit_val'] = NULL;
    }
    else {
      list($limit, $limit_val) = explode('=', $end);
      if ($limit == 'UNTIL') {

        // Add midnight to specify the end of the last day.
        $limit_val .= 'T235959';
      }
      $array['limit'] = $limit;
      $array['limit_val'] = $limit_val;
    }
    return $array;
  }

  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    parent::preSave($storage);
  }

  /**
   * {@inheritdoc}
   */
  public static function postDelete(EntityStorageInterface $storage, array $entities) {
    parent::postDelete($storage, $entities);
    foreach ($entities as $id => $rrule) {

      // Delete any child overrides when a rule is deleted.
      $overrides = $rrule
        ->getRuleOverrides();
      foreach ($overrides as $override) {
        $override
          ->delete();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);
    $fields['rule'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Rule'))
      ->setDescription(t('The Rule that will be used to generate instances.'))
      ->setSettings([
      'max_length' => 256,
      'text_processing' => 0,
    ])
      ->setDefaultValue('')
      ->setDisplayOptions('view', [
      'label' => 'hidden',
      'type' => 'string',
      'weight' => -4,
    ])
      ->setDisplayOptions('form', [
      'type' => 'string_textfield',
      'weight' => -4,
    ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE)
      ->setRevisionable(TRUE);

    // Separate storage for the frequency.
    $fields['freq'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Frequency'))
      ->setDescription(t('How often the date recurs.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', 8)
      ->setRequired(TRUE);

    // Separate storage for the limit.
    $fields['limit'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Limit'))
      ->setDescription(t('A constraint on how long to recur.'))
      ->setSetting('max_length', 25)
      ->setSetting('is_ascii', TRUE);

    // Separate storage for extra parameters such as INTERVAL or BYMONTHDAY.
    // NOTE: The intention is to store these semicolon-separated.
    $fields['parameters'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Parameters'))
      ->setDescription(t('Additional parameters to define the recurrence.'))
      ->setSetting('is_ascii', TRUE);

    // TODO: Decide if this field is necessary, given the presence of the Limit.
    $fields['unlimited'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Unlimited'))
      ->setDescription(t('Whether or not the rule has a limit or end.'))
      ->setDefaultValue(TRUE)
      ->setReadOnly(TRUE)
      ->setRevisionable(TRUE);
    $fields['entity_type'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Entity type'))
      ->setDescription(t('The entity type on which the date is set.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
    $fields['bundle'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Bundle'))
      ->setDescription(t('The bundle on which the date is set.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
    $fields['field_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Smart Date field name'))
      ->setDescription(t('The field name on which the date is set.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH);
    $fields['start'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Start timestamp value'))
      ->setRequired(TRUE);
    $fields['end'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('End timestamp value'))
      ->setRequired(TRUE);
    $fields['instances'] = BaseFieldDefinition::create('map')
      ->setLabel(t('Instances'))
      ->setDescription(t('A serialized array of the instances.'));
    $fields['uuid'] = BaseFieldDefinition::create('uuid')
      ->setLabel(t('UUID'))
      ->setDescription(t('The UUID of the SmartDateRule entity.'))
      ->setReadOnly(TRUE);
    return $fields;
  }

  /**
   * Validate recurring input, looking for values that will trigger a timeout.
   *
   * @param array $element
   *   An associative array containing the properties and children of the
   *   generic form element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param array $complete_form
   *   The complete form structure.
   */
  public static function validateRecurring(array &$element, FormStateInterface $form_state, array &$complete_form) {

    // Check that the value is set to recur.
    if (empty($element['repeat']['#value'])) {
      return;
    }

    // Only known issues are with DAILY recurring events and BYDAY values set.
    if ($element['repeat']['#value'] != 'DAILY' || empty($element['repeat-advanced']['byday']['#value'])) {
      return;
    }
    $start_time = $element['value']['#value']['object'];
    $end_time = $element['end_value']['#value']['object'];
    if (!$start_time instanceof DrupalDateTime || !$end_time instanceof DrupalDateTime) {

      // Unable to process if an invalid start or end.
      return;
    }

    // At this point, known issues involve provided BYDAY values that don't
    // include the start day.
    $start_day_num = $start_time
      ->format('N');
    $days_of_week = [
      1 => 'MO',
      2 => 'TU',
      3 => 'WE',
      4 => 'TH',
      5 => 'FR',
      6 => 'SA',
      7 => 'SU',
    ];
    $start_day = $days_of_week[$start_day_num];
    if (in_array($start_day, $element['repeat-advanced']['byday']['#value'])) {
      return;
    }

    // Daily repeats on a multiple of 7 where BYDAY doesn't include the start
    // day will cause the recurr library to time out, so check for this.
    if ($element['interval']['#value'] && $element['interval']['#value'] % 7 == 0) {
      $form_state
        ->setError($element, t('This recurrence pattern will yield zero instances.'));
    }

    // Daily repeats where BYDAY doesn't include the start day and the interval
    // is larger than the specified day range will create a rule with zero
    // instances, effectively creating an empty value and an orphaned rule.
    // Prevent this.
    if ($element['interval']['#value'] && $element['repeat-end']['#value'] == 'UNTIL' && !empty($element['repeat-end-date']['#value'])) {
      if ($element['repeat-end-date']['#value'] instanceof DrupalDateTime) {
        $stop_date = $element['repeat-end-date']['#value'];
      }
      else {
        $stop_date = new DrupalDateTime($element['repeat-end-date']['#value']);
      }
      $between = $start_time
        ->diff($stop_date, TRUE);
      if ($between->days < $element['interval']['#value']) {
        $form_state
          ->setError($element, t('This recurrence pattern will yield zero instances.'));
      }
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CacheableDependencyTrait::$cacheContexts protected property Cache contexts.
CacheableDependencyTrait::$cacheMaxAge protected property Cache max-age.
CacheableDependencyTrait::$cacheTags protected property Cache tags.
CacheableDependencyTrait::setCacheability protected function Sets cacheability; useful for value object constructors.
ContentEntityBase::$activeLangcode protected property Language code identifying the entity active language.
ContentEntityBase::$defaultLangcode protected property Local cache for the default language code.
ContentEntityBase::$defaultLangcodeKey protected property The default langcode entity key.
ContentEntityBase::$enforceRevisionTranslationAffected protected property Whether the revision translation affected flag has been enforced.
ContentEntityBase::$entityKeys protected property Holds untranslatable entity keys such as the ID, bundle, and revision ID.
ContentEntityBase::$fieldDefinitions protected property Local cache for field definitions.
ContentEntityBase::$fields protected property The array of fields, each being an instance of FieldItemListInterface.
ContentEntityBase::$fieldsToSkipFromTranslationChangesCheck protected static property Local cache for fields to skip from the checking for translation changes.
ContentEntityBase::$isDefaultRevision protected property Indicates whether this is the default revision.
ContentEntityBase::$langcodeKey protected property The language entity key.
ContentEntityBase::$languages protected property Local cache for the available language objects.
ContentEntityBase::$loadedRevisionId protected property The loaded revision ID before the new revision was set.
ContentEntityBase::$newRevision protected property Boolean indicating whether a new revision should be created on save.
ContentEntityBase::$revisionTranslationAffectedKey protected property The revision translation affected entity key.
ContentEntityBase::$translatableEntityKeys protected property Holds translatable entity keys such as the label.
ContentEntityBase::$translationInitialize protected property A flag indicating whether a translation object is being initialized.
ContentEntityBase::$translations protected property An array of entity translation metadata.
ContentEntityBase::$validated protected property Whether entity validation was performed.
ContentEntityBase::$validationRequired protected property Whether entity validation is required before saving the entity.
ContentEntityBase::$values protected property The plain data values of the contained fields.
ContentEntityBase::access public function Checks data value access. Overrides EntityBase::access 1
ContentEntityBase::addTranslation public function Adds a new translation to the translatable object. Overrides TranslatableInterface::addTranslation
ContentEntityBase::bundle public function Gets the bundle of the entity. Overrides EntityBase::bundle
ContentEntityBase::bundleFieldDefinitions public static function Provides field definitions for a specific bundle. Overrides FieldableEntityInterface::bundleFieldDefinitions 4
ContentEntityBase::clearTranslationCache protected function Clear entity translation object cache to remove stale references.
ContentEntityBase::createDuplicate public function Creates a duplicate of the entity. Overrides EntityBase::createDuplicate 1
ContentEntityBase::get public function Gets a field item list. Overrides FieldableEntityInterface::get
ContentEntityBase::getEntityKey protected function Gets the value of the given entity key, if defined. 1
ContentEntityBase::getFieldDefinition public function Gets the definition of a contained field. Overrides FieldableEntityInterface::getFieldDefinition
ContentEntityBase::getFieldDefinitions public function Gets an array of field definitions of all contained fields. Overrides FieldableEntityInterface::getFieldDefinitions
ContentEntityBase::getFields public function Gets an array of all field item lists. Overrides FieldableEntityInterface::getFields
ContentEntityBase::getFieldsToSkipFromTranslationChangesCheck protected function Returns an array of field names to skip in ::hasTranslationChanges. 1
ContentEntityBase::getIterator public function
ContentEntityBase::getLanguages protected function
ContentEntityBase::getLoadedRevisionId public function Gets the loaded Revision ID of the entity. Overrides RevisionableInterface::getLoadedRevisionId
ContentEntityBase::getRevisionId public function Gets the revision identifier of the entity. Overrides RevisionableInterface::getRevisionId
ContentEntityBase::getTranslatableFields public function Gets an array of field item lists for translatable fields. Overrides FieldableEntityInterface::getTranslatableFields
ContentEntityBase::getTranslatedField protected function Gets a translated field.
ContentEntityBase::getTranslation public function Gets a translation of the data. Overrides TranslatableInterface::getTranslation
ContentEntityBase::getTranslationLanguages public function Returns the languages the data is translated to. Overrides TranslatableInterface::getTranslationLanguages
ContentEntityBase::getTranslationStatus public function Returns the translation status. Overrides TranslationStatusInterface::getTranslationStatus
ContentEntityBase::getUntranslated public function Returns the translatable object referring to the original language. Overrides TranslatableInterface::getUntranslated
ContentEntityBase::hasField public function Determines whether the entity has a field with the given name. Overrides FieldableEntityInterface::hasField
ContentEntityBase::hasTranslation public function Checks there is a translation for the given language code. Overrides TranslatableInterface::hasTranslation
ContentEntityBase::hasTranslationChanges public function Determines if the current translation of the entity has unsaved changes. Overrides TranslatableInterface::hasTranslationChanges
ContentEntityBase::id public function Gets the identifier. Overrides EntityBase::id
ContentEntityBase::initializeTranslation protected function Instantiates a translation object for an existing translation.
ContentEntityBase::isDefaultRevision public function Checks if this entity is the default revision. Overrides RevisionableInterface::isDefaultRevision
ContentEntityBase::isDefaultTranslation public function Checks whether the translation is the default one. Overrides TranslatableInterface::isDefaultTranslation
ContentEntityBase::isDefaultTranslationAffectedOnly public function Checks if untranslatable fields should affect only the default translation. Overrides TranslatableRevisionableInterface::isDefaultTranslationAffectedOnly
ContentEntityBase::isLatestRevision public function Checks if this entity is the latest revision. Overrides RevisionableInterface::isLatestRevision
ContentEntityBase::isLatestTranslationAffectedRevision public function Checks whether this is the latest revision affecting this translation. Overrides TranslatableRevisionableInterface::isLatestTranslationAffectedRevision
ContentEntityBase::isNewRevision public function Determines whether a new revision should be created on save. Overrides RevisionableInterface::isNewRevision
ContentEntityBase::isNewTranslation public function Checks whether the translation is new. Overrides TranslatableInterface::isNewTranslation
ContentEntityBase::isRevisionTranslationAffected public function Checks whether the current translation is affected by the current revision. Overrides TranslatableRevisionableInterface::isRevisionTranslationAffected
ContentEntityBase::isRevisionTranslationAffectedEnforced public function Checks if the revision translation affected flag value has been enforced. Overrides TranslatableRevisionableInterface::isRevisionTranslationAffectedEnforced
ContentEntityBase::isTranslatable public function Returns the translation support status. Overrides TranslatableInterface::isTranslatable
ContentEntityBase::isValidationRequired public function Checks whether entity validation is required before saving the entity. Overrides FieldableEntityInterface::isValidationRequired
ContentEntityBase::label public function Gets the label of the entity. Overrides EntityBase::label 6
ContentEntityBase::language public function Gets the language of the entity. Overrides EntityBase::language
ContentEntityBase::onChange public function Reacts to changes to a field. Overrides FieldableEntityInterface::onChange
ContentEntityBase::postCreate public function Acts on a created entity before hooks are invoked. Overrides EntityBase::postCreate
ContentEntityBase::postSave public function Acts on a saved entity before the insert or update hook is invoked. Overrides EntityBase::postSave 9
ContentEntityBase::preSaveRevision public function Acts on a revision before it gets saved. Overrides RevisionableInterface::preSaveRevision 3
ContentEntityBase::referencedEntities public function Gets a list of entities referenced by this entity. Overrides EntityBase::referencedEntities 1
ContentEntityBase::removeTranslation public function Removes the translation identified by the given language code. Overrides TranslatableInterface::removeTranslation
ContentEntityBase::set public function Sets a field value. Overrides FieldableEntityInterface::set
ContentEntityBase::setDefaultLangcode protected function Populates the local cache for the default language code.
ContentEntityBase::setNewRevision public function Enforces an entity to be saved as a new revision. Overrides RevisionableInterface::setNewRevision
ContentEntityBase::setRevisionTranslationAffected public function Marks the current revision translation as affected. Overrides TranslatableRevisionableInterface::setRevisionTranslationAffected
ContentEntityBase::setRevisionTranslationAffectedEnforced public function Enforces the revision translation affected flag value. Overrides TranslatableRevisionableInterface::setRevisionTranslationAffectedEnforced
ContentEntityBase::setValidationRequired public function Sets whether entity validation is required before saving the entity. Overrides FieldableEntityInterface::setValidationRequired
ContentEntityBase::toArray public function Gets an array of all property values. Overrides EntityBase::toArray
ContentEntityBase::updateFieldLangcodes protected function Updates language for already instantiated fields.
ContentEntityBase::updateLoadedRevisionId public function Updates the loaded Revision ID with the revision ID. Overrides RevisionableInterface::updateLoadedRevisionId
ContentEntityBase::updateOriginalValues public function Updates the original values with the interim changes.
ContentEntityBase::uuid public function Gets the entity UUID (Universally Unique Identifier). Overrides EntityBase::uuid
ContentEntityBase::validate public function Validates the currently set values. Overrides FieldableEntityInterface::validate 1
ContentEntityBase::wasDefaultRevision public function Checks whether the entity object was a default revision when it was saved. Overrides RevisionableInterface::wasDefaultRevision
ContentEntityBase::__clone public function Magic method: Implements a deep clone.
ContentEntityBase::__construct public function Constructs an Entity object. Overrides EntityBase::__construct
ContentEntityBase::__get public function Implements the magic method for getting object properties.
ContentEntityBase::__isset public function Implements the magic method for isset().
ContentEntityBase::__set public function Implements the magic method for setting object properties.
ContentEntityBase::__sleep public function Overrides EntityBase::__sleep
ContentEntityBase::__unset public function Implements the magic method for unset().
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function Aliased as: traitSleep 2
DependencySerializationTrait::__wakeup public function 2
EntityBase::$enforceIsNew protected property Boolean indicating whether the entity should be forced to be new.
EntityBase::$entityTypeId protected property The entity type.
EntityBase::$typedData protected property A typed data object wrapping this entity.
EntityBase::create public static function Constructs a new entity object, without permanently saving it. Overrides EntityInterface::create
EntityBase::delete public function Deletes an entity permanently. Overrides EntityInterface::delete 2
EntityBase::enforceIsNew public function Enforces an entity to be new. Overrides EntityInterface::enforceIsNew
EntityBase::entityTypeBundleInfo protected function Gets the entity type bundle info service.
EntityBase::entityTypeManager protected function Gets the entity type manager.
EntityBase::getCacheContexts public function The cache contexts associated with this object. Overrides CacheableDependencyTrait::getCacheContexts
EntityBase::getCacheMaxAge public function The maximum age for which this object may be cached. Overrides CacheableDependencyTrait::getCacheMaxAge
EntityBase::getCacheTags public function The cache tags associated with this object. Overrides CacheableDependencyTrait::getCacheTags
EntityBase::getCacheTagsToInvalidate public function Returns the cache tags that should be used to invalidate caches. Overrides EntityInterface::getCacheTagsToInvalidate 4
EntityBase::getConfigDependencyKey public function Gets the key that is used to store configuration dependencies. Overrides EntityInterface::getConfigDependencyKey
EntityBase::getConfigDependencyName public function Gets the configuration dependency name. Overrides EntityInterface::getConfigDependencyName 1
EntityBase::getConfigTarget public function Gets the configuration target identifier for the entity. Overrides EntityInterface::getConfigTarget 1
EntityBase::getEntityType public function Gets the entity type definition. Overrides EntityInterface::getEntityType
EntityBase::getEntityTypeId public function Gets the ID of the type of the entity. Overrides EntityInterface::getEntityTypeId
EntityBase::getListCacheTagsToInvalidate protected function The list cache tags to invalidate for this entity.
EntityBase::getOriginalId public function Gets the original ID. Overrides EntityInterface::getOriginalId 1
EntityBase::getTypedData public function Gets a typed data object for this entity object. Overrides EntityInterface::getTypedData
EntityBase::hasLinkTemplate public function Indicates if a link template exists for a given key. Overrides EntityInterface::hasLinkTemplate
EntityBase::invalidateTagsOnDelete protected static function Invalidates an entity's cache tags upon delete. 1
EntityBase::invalidateTagsOnSave protected function Invalidates an entity's cache tags upon save. 1
EntityBase::isNew public function Determines whether the entity is new. Overrides EntityInterface::isNew 2
EntityBase::languageManager protected function Gets the language manager.
EntityBase::linkTemplates protected function Gets an array link templates. 1
EntityBase::load public static function Loads an entity. Overrides EntityInterface::load
EntityBase::loadMultiple public static function Loads one or more entities. Overrides EntityInterface::loadMultiple
EntityBase::postLoad public static function Acts on loaded entities. Overrides EntityInterface::postLoad 2
EntityBase::preCreate public static function Changes the values of an entity before it is created. Overrides EntityInterface::preCreate 7
EntityBase::preDelete public static function Acts on entities before they are deleted and before hooks are invoked. Overrides EntityInterface::preDelete 6
EntityBase::save public function Saves an entity permanently. Overrides EntityInterface::save 3
EntityBase::setOriginalId public function Sets the original ID. Overrides EntityInterface::setOriginalId 1
EntityBase::toLink public function Generates the HTML for a link to this entity. Overrides EntityInterface::toLink
EntityBase::toUrl public function Gets the URL object for the entity. Overrides EntityInterface::toUrl 2
EntityBase::uriRelationships public function Gets a list of URI relationships supported by this entity. Overrides EntityInterface::uriRelationships
EntityBase::urlRouteParameters protected function Gets an array of placeholders for this entity. 2
EntityBase::uuidGenerator protected function Gets the UUID generator.
EntityChangedTrait::getChangedTime public function Gets the timestamp of the last entity change for the current translation.
EntityChangedTrait::getChangedTimeAcrossTranslations public function Returns the timestamp of the last entity change across all translations.
EntityChangedTrait::setChangedTime public function Sets the timestamp of the last entity change for the current translation.
EntityChangesDetectionTrait::getFieldsToSkipFromTranslationChangesCheck protected function Returns an array of field names to skip when checking for changes. Aliased as: traitGetFieldsToSkipFromTranslationChangesCheck
RefinableCacheableDependencyTrait::addCacheableDependency public function 1
RefinableCacheableDependencyTrait::addCacheContexts public function
RefinableCacheableDependencyTrait::addCacheTags public function
RefinableCacheableDependencyTrait::mergeCacheMaxAge public function
SmartDateRule::$freq protected property The frequency of recurrence.
SmartDateRule::$limit protected property The limit to recurrence.
SmartDateRule::$parameters protected property An imploded array of extra parameters, such as increment values.
SmartDateRule::$rule protected property The assembled rule, as a string.
SmartDateRule::$start protected property The timestamp for the first instance.
SmartDateRule::$timezone protected property The timezone for the field/rule.
SmartDateRule::baseFieldDefinitions public static function Provides base field definitions for an entity type. Overrides ContentEntityBase::baseFieldDefinitions
SmartDateRule::getAllProperties public function Return an array of all rule properties.
SmartDateRule::getAssembledRule public function Get the RRule object.
SmartDateRule::getFieldSettings public function Retrieve a setting from the field config.
SmartDateRule::getFrequencyLabels public static function Return an array of frequency labels.
SmartDateRule::getFrequencyLabelsOrNull public static function Return an array of frequency labels.
SmartDateRule::getMonthsLimit public static function Retrieve the months_limit value from the field definition.
SmartDateRule::getNewInstances public function Generate default instances based on rule structure.
SmartDateRule::getParametersArray public function Convert the stored parameters into an array.
SmartDateRule::getParentEntity public function Retrieve the entity to which the rule is attached.
SmartDateRule::getRule public function
SmartDateRule::getRuleInstances public function Provide a formatted array of instances, with any overrides applied.
SmartDateRule::getRuleOverrides public function Retrieve all overrides created for this rule.
SmartDateRule::getStoredInstances public function Helper function to parse instances from storage and return as an array.
SmartDateRule::getTextRule public function Use the transformer to get text output of the rule.
SmartDateRule::getThirdPartyFallback public static function Retrieve the months_limit value from the field definition.
SmartDateRule::makeRanges private function Helper function to convert an array into ranges.
SmartDateRule::makeRuleFromParts private function
SmartDateRule::makeRuleInstances public function Generate default instances based on rule structure.
SmartDateRule::postDelete public static function Acts on deleted entities before the delete hook is invoked. Overrides EntityBase::postDelete
SmartDateRule::preSave public function Acts on an entity before the presave hook is invoked. Overrides ContentEntityBase::preSave
SmartDateRule::setRule public function
SmartDateRule::validateRecurring public static function Validate recurring input, looking for values that will trigger a timeout.
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
SynchronizableEntityTrait::$isSyncing protected property Whether this entity is being created, updated or deleted through a synchronization process.
SynchronizableEntityTrait::isSyncing public function
SynchronizableEntityTrait::setSyncing public function
TranslationStatusInterface::TRANSLATION_CREATED constant Status code identifying a newly created translation.
TranslationStatusInterface::TRANSLATION_EXISTING constant Status code identifying an existing translation.
TranslationStatusInterface::TRANSLATION_REMOVED constant Status code identifying a removed translation.