recurring_events-2.0.x-dev/modules/recurring_events_registration/src/RegistrationCreationService.php
modules/recurring_events_registration/src/RegistrationCreationService.php
<?php
declare(strict_types=1);
namespace Drupal\recurring_events_registration;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Utility\Token;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\recurring_events\Entity\EventInstance;
use Drupal\recurring_events\Entity\EventSeries;
use Drupal\recurring_events_registration\Enum\RegistrationType;
/**
* Provides a service with helper functions for registration creation.
*/
class RegistrationCreationService {
/**
* Event instance entity.
*
* @var \Drupal\recurring_events\Entity\EventInstance
*/
protected EventInstance $eventInstance;
/**
* Event series entity.
*
* @var \Drupal\recurring_events\Entity\EventSeries
*/
protected EventSeries $eventSeries;
public function __construct(
protected readonly TranslationInterface $translation,
protected readonly Connection $database,
protected readonly LoggerChannelFactoryInterface $loggerFactory,
protected readonly MessengerInterface $messenger,
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly ModuleHandlerInterface $moduleHandler,
protected readonly Token $token,
) {}
/**
* Sets the event entities from the given event instance.
*
* The service depends on having an event series set, so make sure to pass in
* a fully populated event instance that references an event series.
*
* @param \Drupal\recurring_events\Entity\EventInstance $event_instance
* The event instance.
*/
public function setEventInstance(EventInstance $event_instance) {
$this->eventInstance = $event_instance;
$event_series = $event_instance->getEventSeries();
if (empty($event_series)) {
throw new \InvalidArgumentException('No event series set on event instance.');
}
$this->eventSeries = $event_series;
}
/**
* Set the event series, helpful to get a fresh copy of the entity.
*
* @param \Drupal\recurring_events\Entity\EventSeries $event_series
* The event series.
*/
public function setEventSeries(EventSeries $event_series) {
$this->eventSeries = $event_series;
}
/**
* Get the event instance.
*
* @return \Drupal\recurring_events\Entity\EventInstance
* The event instance.
*/
public function getEventInstance() {
return $this->eventInstance ?? NULL;
}
/**
* Get the event series.
*
* @return \Drupal\recurring_events\Entity\EventSeries
* The event series.
*/
public function getEventSeries() {
return $this->eventSeries ?? NULL;
}
/**
* Retrieve all registered parties.
*
* @param bool $include_nonwaitlisted
* Whether or not to include non-waitlisted registrants.
* @param bool $include_waitlisted
* Whether or not to include waitlisted registrants.
* @param int|null $uid
* The user ID for whom to retrieve registrants. If NULL, all registrants
* will be retrieved.
*
* @return \Drupal\recurring_events_registration\Entity\RegistrantInterface[]
* An array of registrants.
*/
public function retrieveRegisteredParties($include_nonwaitlisted = TRUE, $include_waitlisted = TRUE, $uid = FALSE) {
$properties = [];
if ($include_nonwaitlisted && !$include_waitlisted) {
$properties['waitlist'] = 0;
}
elseif (!$include_nonwaitlisted && $include_waitlisted) {
$properties['waitlist'] = 1;
}
if (!$include_waitlisted) {
$properties['waitlist'] = 0;
}
if ($uid) {
$properties['user_id'] = $uid;
}
switch ($this->getRegistrationType()) {
case RegistrationType::Series:
if (!empty($this->eventSeries->id())) {
$properties['eventseries_id'] = $this->eventSeries->id();
}
break;
default:
if (!empty($this->eventInstance->id())) {
$properties['eventinstance_id'] = $this->eventInstance->id();
}
break;
}
return $this->entityTypeManager->getStorage('registrant')->loadByProperties($properties);
}
/**
* Retrieves the count of all registered parties.
*
* @param bool $include_nonwaitlisted
* Whether or not to include non-waitlisted registrants.
* @param bool $include_waitlisted
* Whether or not to include waitlisted registrants.
* @param int $uid
* The user ID for whom to retrieve registrants.
*
* @return int
* The count of registrants.
*/
public function retrieveRegisteredPartiesCount($include_nonwaitlisted = TRUE, $include_waitlisted = TRUE, $uid = FALSE) {
$query = $this->entityTypeManager->getStorage('registrant')->getQuery();
if ($include_nonwaitlisted && !$include_waitlisted) {
$query->condition('waitlist', 0);
}
elseif (!$include_nonwaitlisted && $include_waitlisted) {
$query->condition('waitlist', 1);
}
if (!$include_waitlisted) {
$query->condition('waitlist', 0);
}
if ($uid) {
$query->condition('user_id', $uid);
}
switch ($this->getRegistrationType()) {
case RegistrationType::Series:
if (!empty($this->eventSeries->id())) {
$query->condition('eventseries_id', $this->eventSeries->id());
}
break;
default:
if (!empty($this->eventInstance->id())) {
$query->condition('eventinstance_id', $this->eventInstance->id());
}
break;
}
$query->accessCheck(TRUE);
$result = $query->count()->execute();
return $result;
}
/**
* Retrieve all registered parties for a series.
*
* @param bool $future_only
* Whether to only return registered parties for future events.
*
* @return array
* An array of registrants.
*/
public function retrieveAllSeriesRegisteredParties($future_only = FALSE) {
$parties = [];
if (($future_only && $this->eventSeriesHasFutureInstances()) || !$future_only) {
$properties = [
'eventseries_id' => $this->eventSeries->id(),
];
$results = $this->entityTypeManager->getStorage('registrant')->loadByProperties($properties);
if (!empty($results)) {
$parties = $results;
}
}
return $parties;
}
/**
* Check if this event series has events in the future.
*
* @return bool
* Whether the event series has future instances or not.
*/
public function eventSeriesHasFutureInstances() {
$future_instances = FALSE;
$instances = $this->eventSeries->event_instances->referencedEntities();
if (!empty($instances)) {
foreach ($instances as $instance) {
$end_date = $instance->date->end_date->getTimestamp();
if ($end_date > time()) {
return TRUE;
}
}
}
return $future_instances;
}
/**
* Check if this event instance is a future event.
*
* @return bool
* Whether the event instance is in the future.
*/
public function eventInstanceIsInFuture() {
return $this->eventInstance->date->end_date->getTimestamp() > time();
}
/**
* Checks if event instance has any availability.
*
* @return bool
* Whether registration spaces exist.
*/
public function hasAvailability() {
$availability = $this->retrieveAvailability();
if ($availability == -1 || $availability > 0) {
return TRUE;
}
return FALSE;
}
/**
* Get registration availability.
*
* @return int
* The number of spaces available for registration.
*/
public function retrieveAvailability() {
$availability = 0;
$parties_count = $this->retrieveRegisteredPartiesCount(TRUE, FALSE);
$capacity = $this->eventSeries->event_registration->capacity;
if (empty($capacity)) {
// Set capacity to unlimited if no capacity is specified.
return -1;
}
$availability = $capacity - $parties_count;
if ($availability < 0) {
$availability = 0;
}
return $availability;
}
/**
* Get whether this event has a waitlist.
*
* @return bool
* Whether or not there is a waitlist for this event.
*/
public function hasWaitlist() {
$waitlist = FALSE;
if (!empty($this->eventSeries->event_registration->waitlist)) {
$waitlist = (bool) $this->eventSeries->event_registration->waitlist;
}
return $waitlist;
}
/**
* Get whether this event has registration.
*
* @return bool
* Whether or not registration is open for this event.
*/
public function hasRegistration() {
$registration = FALSE;
if (!empty($this->eventSeries->event_registration->registration)) {
$registration = (bool) $this->eventSeries->event_registration->registration;
}
return $registration;
}
/**
* Get registration date range.
*
* @return array
* The registration date range array.
*/
public function getRegistrationDateRange() {
$date_range = [];
$value = $this->eventSeries->event_registration->getValue();
if (!empty($value)) {
$date_range['value'] = $value[array_key_first($value)]['value'];
$date_range['end_value'] = $value[array_key_first($value)]['end_value'];
}
return $date_range;
}
/**
* Has the user registered for this event before.
*
* @param int $uid
* The ID of the user.
*
* @return bool
* Whether this user has already registered for this event.
*/
public function hasUserRegisteredById($uid) {
$registrants = $this->retrieveRegisteredParties(TRUE, TRUE, $uid);
return !empty($registrants);
}
/**
* Retrieve all waitlisted users.
*
* @return array
* An array of Drupal\recurring_events_registration\Entity\Registrant users.
*/
public function retrieveWaitlistedParties() {
$parties = [];
$registrants = $this->retrieveRegisteredParties(FALSE, TRUE);
if (!empty($registrants)) {
$parties = $registrants;
}
return $parties;
}
/**
* Retrieve first user on the waitlist.
*
* @return \Drupal\recurring_events_registration\Entity\RegistrantInterface
* A fully loaded registrant entity.
*/
public function retrieveFirstWaitlistParty() {
$waitlisted_users = $this->retrieveWaitlistedParties();
if (!empty($waitlisted_users)) {
/** @var Drupal\recurring_events_registration\Entity\RegistrantInterface */
$first = reset($waitlisted_users);
$this->moduleHandler->alter('recurring_events_registration_first_waitlist', $first);
return $first;
}
return NULL;
}
/**
* Get registration type.
*
* @return \Drupal\recurring_events_registration\Enum\RegistrationType
* The type of registration: series, or instance.
*/
public function getRegistrationType(): RegistrationType {
return RegistrationType::tryFrom($this->eventSeries->event_registration->registration_type ?? '') ?? RegistrationType::defaultValue();
}
/**
* Get instance registration open schedule type.
*
* @return string
* The type of open registration schedule: now, start, or custom.
*/
public function getInstanceRegistrationOpenScheduleType() {
$type = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_open)) {
$type = $this->eventSeries->event_registration->instance_schedule_open;
}
return $type;
}
/**
* Get instance registration close schedule type.
*
* @return string
* The type of close registration schedule: start, end, or custom.
*/
public function getInstanceRegistrationCloseScheduleType() {
$type = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_close)) {
$type = $this->eventSeries->event_registration->instance_schedule_close;
}
return $type;
}
/**
* Get registration dates type.
*
* @return string
* The type of registration dates: open, or scheduled.
*/
public function getRegistrationDatesType() {
$type = FALSE;
if (!empty($this->eventSeries->event_registration->registration_dates)) {
$type = $this->eventSeries->event_registration->registration_dates;
}
return $type;
}
/**
* Get instance registration open time modifier.
*
* @return string
* The modifier for the opening time relative to the event start.
*/
public function getInstanceRegistrationOpenTimeModifier() {
$modifier = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_open_amount) && !empty($this->getInstanceRegistrationOpenTimeUnit())) {
$modifier = $this->eventSeries->event_registration->instance_schedule_open_amount . ' ' . $this->getInstanceRegistrationOpenTimeUnit();
$modifier = '- ' . $modifier;
}
return $modifier;
}
/**
* Get instance registration open time unit.
*
* @return string
* The unit used to define the open registration time.
*/
public function getInstanceRegistrationOpenTimeUnit() {
$unit = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_open_units)) {
$unit = $this->eventSeries->event_registration->instance_schedule_open_units;
}
return $unit;
}
/**
* Get instance registration close time modifier.
*
* @return string
* The modifier for the closing time relative to the event start.
*/
public function getInstanceRegistrationCloseTimeModifier() {
$modifier = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_close_amount) && !empty($this->getInstanceRegistrationCloseTimeUnit())) {
$modifier = $this->eventSeries->event_registration->instance_schedule_close_amount . ' ' . $this->getInstanceRegistrationCloseTimeUnit();
switch ($this->eventSeries->event_registration->instance_schedule_close_type) {
case 'after':
$modifier = '+ ' . $modifier;
break;
case 'before':
default:
$modifier = '- ' . $modifier;
break;
}
}
return $modifier;
}
/**
* Get instance registration close time unit.
*
* @return string
* The unit used to define the close registration time.
*/
public function getInstanceRegistrationCloseTimeUnit() {
$unit = FALSE;
if (!empty($this->eventSeries->event_registration->instance_schedule_close_units)) {
$unit = $this->eventSeries->event_registration->instance_schedule_close_units;
}
return $unit;
}
/**
* Is registration open for this event?
*
* @return bool
* Whether or not registration is open for this event.
*/
public function registrationIsOpen() {
$registration = FALSE;
if (!$this->hasRegistration()) {
return $registration;
}
$now = new DrupalDateTime();
$reg_open_close_dates = $this->registrationOpeningClosingTime();
if (empty($reg_open_close_dates) || empty($reg_open_close_dates['reg_open']) || empty($reg_open_close_dates['reg_close'])) {
return $registration;
}
$registration = (
$now->getTimestamp() >= $reg_open_close_dates['reg_open']->getTimestamp()
&& $now->getTimestamp() < $reg_open_close_dates['reg_close']->getTimestamp()
);
return $registration;
}
/**
* Get registration opening date and time.
*
* @return array
* An array of drupal date time objects for when registration opens/closes.
*/
public function registrationOpeningClosingTime() {
$reg_dates = FALSE;
// Does this event even have registration?
if ($this->hasRegistration()) {
// Grab the type of registration and the type of dates.
$reg_type = $this->getRegistrationType();
$reg_dates_type = $this->getRegistrationDatesType();
$timezone = new \DateTimeZone(date_default_timezone_get());
$utc_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
$now = new DrupalDateTime();
switch ($reg_dates_type) {
case 'open':
// For series, the event registration should close when the first
// event in that series begins. For instance registration the event
// registration should close when that instance begins.
$event_date = match ($reg_type) {
RegistrationType::Series => $this->eventSeries->getSeriesStart(),
default => $this->eventInstance->date->start_date,
};
$event_date->setTimezone($timezone);
$reg_dates = [
'reg_open' => $now,
'reg_close' => $event_date,
];
break;
case 'scheduled':
// The two registration types are 'series' or 'instance'.
switch ($reg_type) {
case RegistrationType::Series:
$reg_start = NULL;
$reg_end = NULL;
$reg_date_range = $this->getRegistrationDateRange();
if (!empty($reg_date_range)) {
if (!empty($reg_date_range['value'])) {
$reg_start = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $reg_date_range['value'], $utc_timezone);
$reg_start->setTimezone($timezone);
}
if (!empty($reg_date_range['end_value'])) {
$reg_end = DrupalDateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $reg_date_range['end_value'], $utc_timezone);
$reg_end->setTimezone($timezone);
}
}
break;
case RegistrationType::Instance:
$event_start_date = $this->eventInstance->date->start_date;
$event_end_date = $this->eventInstance->date->end_date;
// Calculate registration opening time.
switch ($this->getInstanceRegistrationOpenScheduleType()) {
case 'now':
$reg_start = new DrupalDateTime();
break;
case 'start':
$reg_start = clone $event_start_date;
break;
case 'custom':
$open_time_modifier = $this->getInstanceRegistrationOpenTimeModifier();
$reg_start = clone $event_start_date;
$reg_start->modify($open_time_modifier);
break;
}
// Calculate registration closing time.
switch ($this->getInstanceRegistrationCloseScheduleType()) {
case 'start':
$reg_end = clone $event_start_date;
break;
case 'end':
$reg_end = clone $event_end_date;
break;
case 'custom':
$close_time_modifier = $this->getInstanceRegistrationCloseTimeModifier();
$reg_end = clone $event_start_date;
$reg_end->modify($close_time_modifier);
break;
}
break;
}
$reg_dates = [
'reg_open' => $reg_start,
'reg_close' => $reg_end,
];
break;
}
}
return $reg_dates;
}
/**
* Promote a registrant from the waitlist.
*/
public function promoteFromWaitlist() {
if (!$this->hasWaitlist()) {
return;
}
if ($this->hasAvailability()) {
$first_waitlist = $this->retrieveFirstWaitlistParty();
if (!empty($first_waitlist)) {
$first_waitlist->setWaitlist(FALSE);
$first_waitlist->save();
$key = 'promotion_notification';
$reg_type = $this->getRegistrationType();
$future_event = match ($reg_type) {
RegistrationType::Series => $this->eventSeriesHasFutureInstances(),
default => $this->eventInstanceIsInFuture(),
};
if ($future_event) {
recurring_events_registration_send_notification($key, $first_waitlist);
}
}
}
}
/**
* Retrieve the tokens available for a registrant.
*/
public function getAvailableTokens($relevant_tokens = ['registrant']) {
if ($this->moduleHandler->moduleExists('token')) {
$token_help = [
'#theme' => 'token_tree_link',
'#token_types' => $relevant_tokens,
];
}
else {
$all_tokens = $this->token->getInfo();
$tokens = [];
foreach ($relevant_tokens as $token_prefix) {
if (!empty($all_tokens['tokens'][$token_prefix])) {
foreach ($all_tokens['tokens'][$token_prefix] as $token_key => $value) {
$tokens[] = '[' . $token_prefix . ':' . $token_key . ']';
}
}
}
$token_text = $this->translation->translate('Available tokens are: @tokens', [
'@tokens' => implode(', ', $tokens),
]);
$token_help = [
'#type' => 'markup',
'#markup' => $token_text->render(),
];
}
return $token_help;
}
/**
* Has the email address registered for this event before.
*
* @param string $email
* The email address of the user.
* @param int $ignored_registrant_id
* The ID of the registrant to ignore during duplicate email checks.
*
* @return bool|int
* Return the registration ID if it exists otherwise return FALSE.
*/
public function hasUserRegisteredByEmail($email, $ignored_registrant_id = NULL) {
// Look through this series' registrations.
$existing_registration_id = FALSE;
$current_series_registrations = $this->retrieveAllSeriesRegisteredParties();
foreach ($current_series_registrations as $registration_id => $registration_record) {
if ($registration_id === $ignored_registrant_id) {
continue;
}
if ($this->cleanEmailAddress($email) == $this->cleanEmailAddress($registration_record->get('email')->value)) {
if ($this->getRegistrationType() === RegistrationType::Instance) {
// Compare the event instance ID and email address.
if (($this->eventInstance->id() == $registration_record->get('eventinstance_id')->target_id)) {
// Remember the existing registration ID and stop looking.
$existing_registration_id = $registration_id;
break;
}
}
else {
$existing_registration_id = $registration_id;
break;
}
}
}
return $existing_registration_id;
}
/**
* Clean the email address to handle plus- and dot-addressing.
*
* @param string $email
* The email address of the user.
*
* @return string
* The cleaned email address
*/
public function cleanEmailAddress($email) {
$email_address_parts = (isset($email) ? explode('@', $email) : ['', '']);
$email_address_clean = str_replace('.', '', $email_address_parts[0]) . '@' . $email_address_parts[1];
$email_address_clean = preg_replace('/\+.*@/', '@', $email_address_clean);
return (isset($email) ? strtolower($email_address_clean) : NULL);
}
/**
* Do email addresses have to be unique for this event?
*
* @return bool
* Whether or not unique email addresses are enforced for this event.
*/
public function registrationUniqueEmailAddress() {
return $this->eventSeries->event_registration->unique_email_address;
}
/**
* Which roles are allowed to register for this event? Comma delimited.
*
* @return array
* An array of roles that are allowed to register for this event.
*/
public function registrationPermittedRoles() {
$permitted_roles_string = $this->eventSeries->event_registration->permitted_roles;
$permitted_roles = [];
if (empty($permitted_roles_string)) {
return $permitted_roles;
}
// Remove extra spaces from the list of roles.
$permitted_roles_string = str_replace(' ', '', $permitted_roles_string);
// Convert the string into an array of roles.
if (strpos($permitted_roles_string, ',')) {
$permitted_roles = explode(',', $permitted_roles_string);
}
else {
$permitted_roles[] = $permitted_roles_string;
}
return $permitted_roles;
}
}
