Files
Cloud-CMS/lib/Entity/Schedule.php

2191 lines
77 KiB
PHP
Raw Normal View History

2025-12-02 10:32:59 -05:00
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Entity;
use Carbon\Carbon;
use Respect\Validation\Validator as v;
use Stash\Interfaces\PoolInterface;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\ScheduleCriteriaFactory;
use Xibo\Factory\ScheduleExclusionFactory;
use Xibo\Factory\ScheduleReminderFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Translate;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Class Schedule
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Schedule implements \JsonSerializable
{
use EntityTrait;
public static $LAYOUT_EVENT = 1;
public static $COMMAND_EVENT = 2;
public static $OVERLAY_EVENT = 3;
public static $INTERRUPT_EVENT = 4;
public static $CAMPAIGN_EVENT = 5;
public static $ACTION_EVENT = 6;
public static $MEDIA_EVENT = 7;
public static $PLAYLIST_EVENT = 8;
public static $SYNC_EVENT = 9;
public static $DATA_CONNECTOR_EVENT = 10;
public static $DATE_MIN = 0;
public static $DATE_MAX = 2147483647;
/**
* @SWG\Property(
* description="The ID of this Event"
* )
* @var int
*/
public $eventId;
/**
* @SWG\Property(
* description="The Event Type ID"
* )
* @var int
*/
public $eventTypeId;
/**
* @SWG\Property(
* description="The CampaignID this event is for"
* )
* @var int
*/
public $campaignId;
/**
* @SWG\Property(
* description="The CommandId this event is for"
* )
* @var int
*/
public $commandId;
/**
* @SWG\Property(
* description="Display Groups assigned to this Scheduled Event.",
* type="array",
* @SWG\Items(ref="#/definitions/DisplayGroup")
* )
* @var DisplayGroup[]
*/
public $displayGroups = [];
/**
* @SWG\Property(
* description="Schedule Reminders assigned to this Scheduled Event.",
* type="array",
* @SWG\Items(ref="#/definitions/ScheduleReminder")
* )
* @var ScheduleReminder[]
*/
public $scheduleReminders = [];
/**
* @SWG\Property(
* description="Schedule Criteria assigned to this Scheduled Event.",
* type="array",
* @SWG\Items(ref="#/definitions/ScheduleCriteria")
* )
* @var ScheduleCriteria[]
*/
public $criteria = [];
/**
* @SWG\Property(
* description="The userId that owns this event."
* )
* @var int
*/
public $userId;
/**
* @SWG\Property(
* description="A Unix timestamp representing the from date of this event in CMS time."
* )
* @var int
*/
public $fromDt;
/**
* @SWG\Property(
* description="A Unix timestamp representing the to date of this event in CMS time."
* )
* @var int
*/
public $toDt;
/**
* @SWG\Property(
* description="Integer indicating the event priority."
* )
* @var int
*/
public $isPriority;
/**
* @SWG\Property(
* description="The display order for this event."
* )
* @var int
*/
public $displayOrder;
/**
* @SWG\Property(
* description="If this event recurs when what is the recurrence period.",
* enum={"None", "Minute", "Hour", "Day", "Week", "Month", "Year"}
* )
* @var string
*/
public $recurrenceType;
/**
* @SWG\Property(
* description="If this event recurs when what is the recurrence frequency.",
* )
* @var int
*/
public $recurrenceDetail;
/**
* @SWG\Property(
* description="A Unix timestamp indicating the end time of the recurring events."
* )
* @var int
*/
public $recurrenceRange;
/**
* @SWG\Property(description="Recurrence repeats on days - 0 to 7 where 0 is a monday")
* @var string
*/
public $recurrenceRepeatsOn;
/**
* @SWG\Property(description="Recurrence monthly repeats on - 0 is day of month, 1 is weekday of week")
* @var int
*/
public $recurrenceMonthlyRepeatsOn;
/**
* @SWG\Property(
* description="The Campaign/Layout Name",
* readOnly=true
* )
* @var string
*/
public $campaign;
/**
* @SWG\Property(
* description="The Command Name",
* readOnly=true
* )
* @var string
*/
public $command;
/**
* @SWG\Property(
* description="The Day Part Id"
* )
* @var int
*/
public $dayPartId;
/**
* @SWG\Property(description="Is this event an always on event?")
* @var int
*/
public $isAlways;
/**
* @SWG\Property(description="Does this event have custom from/to date times?")
* @var int
*/
public $isCustom;
/**
* Last Recurrence Watermark
* @var int
*/
public $lastRecurrenceWatermark;
/**
* @SWG\Property(description="Flag indicating whether the event should be synchronised across displays")
* @var int
*/
public $syncEvent = 0;
/**
* @SWG\Property(description="Flag indicating whether the event will sync to the Display timezone")
* @var int
*/
public $syncTimezone;
/**
* @SWG\Property(description="Seconds (0-3600) of each full hour that is scheduled that this Layout should occupy")
* @var int
*/
public $shareOfVoice;
/**
* @SWG\Property(description="The maximum number of plays per hour per display for this event")
* @var int
*/
public $maxPlaysPerHour;
/**
* @SWG\Property(description="Flag (0-1), whether this event is using Geo Location")
* @var int
*/
public $isGeoAware;
/**
* @SWG\Property(description="Geo JSON representing the area of this event")
* @var string
*/
public $geoLocation;
/**
* @SWG\Property(description="For Action event type, Action trigger code")
* @var string
*/
public $actionTriggerCode;
/**
* @SWG\Property(description="For Action event type, the type of the Action (navigate to Layout or Command)")
* @var string
*/
public $actionType;
/**
* @SWG\Property(description="For Action event type and navigate to Layout Action type, the Layout code")
* @var string
*/
public $actionLayoutCode;
/**
* @SWG\Property(description="If the schedule should be considered part of a larger campaign")
* @var int
*/
public $parentCampaignId;
/**
* @SWG\Property(description="For sync events, the id the the sync group")
* @var int
*/
public $syncGroupId;
/**
* @SWG\Property(description="For data connector events, the dataSetId")
* @var int
*/
public $dataSetId;
/**
* @SWG\Property(description="For data connector events, the data set parameters")
* @var int
*/
public $dataSetParams;
/**
* @SWG\Property(description="The userId of the user that last modified this Schedule")
* @var int
*/
public $modifiedBy;
public $modifiedByName;
/**
* @SWG\Property(description="The Date this Schedule was created on")
* @var string
*/
public $createdOn;
/**
* @SWG\Property(description="The Date this Schedule was las updated on")
* @var string
*/
public $updatedOn;
/**
* @SWG\Property(description="The name of this Scheduled Event")
* @var string
*/
public $name;
/**
* @SWG\Property(description="The resolutionId of this Fullscreen Scheduled Event")
* @var int
*/
public int $resolutionId;
/**
* @SWG\Property(description="The duration of this Fullscreen Scheduled Event")
* @var int
*/
public int $layoutDuration;
/**
* @SWG\Property(description="The background color of this Fullscreen Scheduled Event")
* @var string
*/
public string $backgroundColor;
/**
* @var ScheduleEvent[]
*/
private $scheduleEvents = [];
private $datesToFormat = ['toDt', 'fromDt'];
private $dayPart = null;
/**
* @var ConfigServiceInterface
*/
private $config;
/** @var PoolInterface */
private $pool;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
/** @var DayPartFactory */
private $dayPartFactory;
/** @var CampaignFactory */
private $campaignFactory;
/** @var ScheduleReminderFactory */
private $scheduleReminderFactory;
/** @var ScheduleExclusionFactory */
private $scheduleExclusionFactory;
/**
* @var UserFactory
*/
private $userFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @param ConfigServiceInterface $config
* @param PoolInterface $pool
* @param DisplayGroupFactory $displayGroupFactory
* @param DayPartFactory $dayPartFactory
* @param UserFactory $userFactory
* @param ScheduleReminderFactory $scheduleReminderFactory
* @param ScheduleExclusionFactory $scheduleExclusionFactory
*/
public function __construct(
$store,
$log,
$dispatcher,
$config,
$pool,
$displayGroupFactory,
$dayPartFactory,
$userFactory,
$scheduleReminderFactory,
$scheduleExclusionFactory,
private readonly ScheduleCriteriaFactory $scheduleCriteriaFactory
) {
$this->setCommonDependencies($store, $log, $dispatcher);
$this->config = $config;
$this->pool = $pool;
$this->displayGroupFactory = $displayGroupFactory;
$this->dayPartFactory = $dayPartFactory;
$this->userFactory = $userFactory;
$this->scheduleReminderFactory = $scheduleReminderFactory;
$this->scheduleExclusionFactory = $scheduleExclusionFactory;
$this->excludeProperty('lastRecurrenceWatermark');
}
/**
* @param CampaignFactory $campaignFactory
* @return $this
*/
public function setCampaignFactory($campaignFactory)
{
$this->campaignFactory = $campaignFactory;
return $this;
}
public function __clone()
{
$this->eventId = null;
}
/**
* @param DisplayNotifyServiceInterface $displayNotifyService
* @return $this
*/
public function setDisplayNotifyService($displayNotifyService)
{
$this->displayNotifyService = $displayNotifyService;
return $this;
}
/**
* Get the Display Notify Service
* @return DisplayNotifyServiceInterface
*/
public function getDisplayNotifyService(): DisplayNotifyServiceInterface
{
return $this->displayNotifyService->init();
}
/**
* @return int
*/
public function getId()
{
return $this->eventId;
}
/**
* @return int
*/
public function getOwnerId()
{
return $this->userId;
}
/**
* Sets the Owner
* @param int $ownerId
*/
public function setOwner($ownerId)
{
$this->userId = $ownerId;
}
/**
* @param ScheduleCriteria $criteria
* @param int|null $id
* @return $this
*/
public function addOrUpdateCriteria(ScheduleCriteria $criteria, ?int $id = null): Schedule
{
// set empty array as the default value if original value is empty/null
$originalValue = $this->getOriginalValue('criteria') ?? [];
// Does this already exist?
foreach ($originalValue as $existing) {
if ($id !== null && $existing->id === $id) {
$this->criteria[] = $criteria;
return $this;
}
}
// We didn't find it.
$this->criteria[] = $criteria;
return $this;
}
/**
* Are the provided dates within the schedule look ahead
* @return bool
* @throws GeneralException
*/
private function inScheduleLookAhead(): bool
{
if ($this->isAlwaysDayPart()) {
return true;
}
// From Date and To Date are in UNIX format
$currentDate = Carbon::now();
$rfLookAhead = clone $currentDate;
$rfLookAhead->addSeconds(intval($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD')));
// Dial current date back to the start of the day
$currentDate->startOfDay();
// Is the event updated to a past date?
$isEventUpdatedToPastDate = ($this->toDt <= $this->getOriginalValue('toDt'));
// Test dates
if ($this->recurrenceType != '') {
// A recurring event
$this->getLog()->debug('Checking look ahead based on recurrence');
// we should check whether the event from date is before the lookahead (i.e. the event has recurred once)
// we should also check whether the recurrence range is still valid
// (i.e. we've not stopped recurring and we don't recur forever)
return (
$this->fromDt <= $rfLookAhead->format('U')
&& ($this->recurrenceRange == 0 || $this->recurrenceRange > $currentDate->format('U'))
);
} else if (!$this->isCustomDayPart() || $this->eventTypeId == self::$COMMAND_EVENT) {
// Day parting event (non recurring) or command event
// only test the from date.
$this->getLog()->debug('Checking look ahead based from date ' . $currentDate->toRssString());
return ($this->fromDt >= $currentDate->format('U') && $this->fromDt <= $rfLookAhead->format('U'));
} else if ($isEventUpdatedToPastDate) {
// Check if the event was updated to a past date
// We only need to check the toDt
$this->getLog()->debug('Checking look ahead based based on previous event details');
return ($this->getOriginalValue('toDt') >= $currentDate->format('U'));
} else {
// Compare the event dates
$this->getLog()->debug(
'Checking look ahead based event dates '
. $currentDate->toRssString() . ' / ' . $rfLookAhead->toRssString()
);
return ($this->fromDt <= $rfLookAhead->format('U') && $this->toDt >= $currentDate->format('U'));
}
}
/**
* Load
* @param array $options
* @throws NotFoundException
*/
public function load($options = [])
{
$options = array_merge([
'loadDisplayGroups' => true,
'loadScheduleReminders' => false,
'loadScheduleCriteria' => true,
], $options);
// If we are already loaded, then don't do it again
if ($this->loaded || $this->eventId == null || $this->eventId == 0) {
return;
}
// Load display groups
if ($options['loadDisplayGroups']) {
$this->displayGroups = $this->displayGroupFactory->getByEventId($this->eventId);
}
// Load schedule reminders
if ($options['loadScheduleReminders']) {
$this->scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId'=> $this->eventId]);
}
// Load schedule criteria
if ($options['loadScheduleCriteria']) {
$this->criteria = $this->scheduleCriteriaFactory->getByEventId($this->eventId);
}
// Set the original values now that we're loaded.
$this->setOriginals();
// We are fully loaded
$this->loaded = true;
}
/**
* Assign DisplayGroup
* @param DisplayGroup $displayGroup
* @throws NotFoundException
*/
public function assignDisplayGroup($displayGroup)
{
$this->load();
if (!in_array($displayGroup, $this->displayGroups)) {
$this->displayGroups[] = $displayGroup;
}
}
/**
* Unassign DisplayGroup
* @param DisplayGroup $displayGroup
* @throws NotFoundException
*/
public function unassignDisplayGroup($displayGroup)
{
$this->load();
$this->displayGroups = array_udiff($this->displayGroups, [$displayGroup], function ($a, $b) {
/**
* @var DisplayGroup $a
* @var DisplayGroup $b
*/
return $a->getId() - $b->getId();
});
}
/**
* Validate
* @throws GeneralException
*/
public function validate()
{
if (count($this->displayGroups) <= 0 && $this->eventTypeId !== Schedule::$SYNC_EVENT) {
throw new InvalidArgumentException(__('No display groups selected'), 'displayGroups');
}
$this->getLog()->debug('EventTypeId: ' . $this->eventTypeId
. '. DayPartId: ' . $this->dayPartId
. ', CampaignId: ' . $this->campaignId
. ', CommandId: ' . $this->commandId);
// If we are a custom day part, make sure we don't have a fromDt which is way in the past
if ($this->isCustomDayPart() && $this->fromDt < Carbon::now()->subYears(10)->format('U')) {
throw new InvalidArgumentException(__('The from date is too far in the past.'), 'fromDt');
}
if (!empty($this->name) && strlen($this->name) > 50) {
throw new InvalidArgumentException(
__('Name cannot be longer than 50 characters.'),
'name'
);
}
if ($this->eventTypeId == Schedule::$LAYOUT_EVENT ||
$this->eventTypeId == Schedule::$CAMPAIGN_EVENT ||
$this->eventTypeId == Schedule::$OVERLAY_EVENT ||
$this->eventTypeId == Schedule::$INTERRUPT_EVENT ||
$this->eventTypeId == Schedule::$MEDIA_EVENT ||
$this->eventTypeId == Schedule::$PLAYLIST_EVENT
) {
// Validate layout
if (!v::intType()->notEmpty()->validate($this->campaignId)) {
throw new InvalidArgumentException(__('Please select a Campaign/Layout for this event.'), 'campaignId');
}
if ($this->isCustomDayPart()) {
// validate the dates
if ($this->toDt <= $this->fromDt) {
throw new InvalidArgumentException(
__('Can not have an end time earlier than your start time'),
'start/end'
);
}
}
$this->commandId = null;
// additional validation for Interrupt Layout event type
if ($this->eventTypeId == Schedule::$INTERRUPT_EVENT) {
if (!v::intType()->notEmpty()->min(0)->max(3600)->validate($this->shareOfVoice)) {
throw new InvalidArgumentException(
__('Share of Voice must be a whole number between 0 and 3600'),
'shareOfVoice'
);
}
}
} else if ($this->eventTypeId == Schedule::$COMMAND_EVENT) {
// Validate command
if (!v::intType()->notEmpty()->validate($this->commandId)) {
throw new InvalidArgumentException(__('Please select a Command for this event.'), 'command');
}
$this->campaignId = null;
$this->toDt = null;
} elseif ($this->eventTypeId == Schedule::$ACTION_EVENT) {
if (!v::stringType()->notEmpty()->validate($this->actionType)) {
throw new InvalidArgumentException(__('Please select a Action Type for this event.'), 'actionType');
}
if (!v::stringType()->notEmpty()->validate($this->actionTriggerCode)) {
throw new InvalidArgumentException(
__('Please select a Action trigger code for this event.'),
'actionTriggerCode'
);
}
if ($this->isCustomDayPart()) {
// validate the dates
if ($this->toDt <= $this->fromDt) {
throw new InvalidArgumentException(
__('Can not have an end time earlier than your start time'),
'start/end'
);
}
}
if ($this->actionType === 'command') {
if (!v::intType()->notEmpty()->validate($this->commandId)) {
throw new InvalidArgumentException(__('Please select a Command for this event.'), 'commandId');
}
} elseif ($this->actionType === 'navLayout') {
if (!v::stringType()->notEmpty()->validate($this->actionLayoutCode)) {
throw new InvalidArgumentException(
__('Please select a Layout code for this event.'),
'actionLayoutCode'
);
}
$this->commandId = null;
}
$this->campaignId = null;
} else if ($this->eventTypeId === Schedule::$SYNC_EVENT) {
if (!v::intType()->notEmpty()->validate($this->syncGroupId)) {
throw new InvalidArgumentException(__('Please select a Sync Group for this event.'), 'syncGroupId');
}
if ($this->isCustomDayPart()) {
// validate the dates
if ($this->toDt <= $this->fromDt) {
throw new InvalidArgumentException(
__('Can not have an end time earlier than your start time'),
'start/end'
);
}
}
} else if ($this->eventTypeId === Schedule::$DATA_CONNECTOR_EVENT) {
if (!v::intType()->notEmpty()->validate($this->dataSetId)) {
throw new InvalidArgumentException(__('Please select a DataSet for this event.'), 'dataSetId');
}
if ($this->isCustomDayPart()) {
// validate the dates
if ($this->toDt <= $this->fromDt) {
throw new InvalidArgumentException(
__('Can not have an end time earlier than your start time'),
'start/end'
);
}
}
$this->campaignId = null;
} else {
// No event type selected
throw new InvalidArgumentException(__('Please select the Event Type'), 'eventTypeId');
}
// Make sure we have a sensible recurrence setting
if (!$this->isCustomDayPart() && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) {
throw new InvalidArgumentException(
__('Repeats selection is invalid for Always or Daypart events'),
'recurrencyType'
);
}
// Check display order is positive
if ($this->displayOrder < 0) {
throw new InvalidArgumentException(__('Display Order must be 0 or a positive number'), 'displayOrder');
}
// Check priority is positive
if ($this->isPriority < 0) {
throw new InvalidArgumentException(__('Priority must be 0 or a positive number'), 'isPriority');
}
// Check max plays per hour is positive
if ($this->maxPlaysPerHour < 0) {
throw new InvalidArgumentException(__('Maximum plays per hour must be 0 or a positive number'), 'maxPlaysPerHour');
}
// Run some additional validation if we have a recurrence type set.
if (!empty($this->recurrenceType)) {
// Check recurrenceDetail every is positive
if ($this->recurrenceDetail === null || $this->recurrenceDetail <= 0) {
throw new InvalidArgumentException(__('Repeat every must be a positive number'), 'recurrenceDetail');
}
// Make sure that we don't repeat more frequently than the duration of the event as this is a common
// misconfiguration which results in overlapping repeats
if ($this->eventTypeId !== Schedule::$COMMAND_EVENT) {
$eventDuration = $this->toDt - $this->fromDt;
// Determine the number of seconds our repeat type/interval represents
switch ($this->recurrenceType) {
case 'Minute':
$repeatDuration = $this->recurrenceDetail * 60;
break;
case 'Hour':
$repeatDuration = $this->recurrenceDetail * 3600;
break;
case 'Day':
$repeatDuration = $this->recurrenceDetail * 86400;
break;
case 'Week':
$repeatDuration = $this->recurrenceDetail * 86400 * 7;
break;
case 'Month':
$repeatDuration = $this->recurrenceDetail * 86400 * 30;
break;
case 'Year':
$repeatDuration = $this->recurrenceDetail * 86400 * 365;
break;
default:
throw new InvalidArgumentException(__('Unknown repeat type'), 'recurrenceType');
}
if ($repeatDuration < $eventDuration) {
throw new InvalidArgumentException(
__('An event cannot repeat more often than the interval between its start and end date'),
'recurrenceDetail',
$eventDuration . ' seconds'
);
}
}
}
}
/**
* Save
* @param array $options
* @throws GeneralException
*/
public function save($options = [])
{
$options = array_merge([
'validate' => true,
'audit' => true,
'deleteOrphaned' => false,
'notify' => true
], $options);
if ($options['validate']) {
$this->validate();
}
// Handle "always" day parts
if ($this->isAlwaysDayPart()) {
$this->fromDt = self::$DATE_MIN;
$this->toDt = self::$DATE_MAX;
}
if ($this->eventId == null || $this->eventId == 0) {
$this->add();
$auditMessage = 'Added';
$this->loaded = true;
$isEdit = false;
} else {
// If this save action means there aren't any display groups assigned
// and if we're set to deleteOrphaned, then delete
if ($options['deleteOrphaned'] && count($this->displayGroups) <= 0) {
$this->delete();
return;
} else {
$this->edit();
$auditMessage = 'Saved';
}
$isEdit = true;
}
// Manage display assignments
if ($this->loaded) {
// Manage assignments
$this->manageAssignments($isEdit && $options['notify']);
}
// Update schedule criteria
$criteriaIds = [];
foreach ($this->criteria as $criteria) {
$criteria->eventId = $this->eventId;
$criteria->save();
$criteriaIds[] = $criteria->id;
}
// Remove records that no longer exist.
if (count($criteriaIds) > 0) {
// There are still criteria left
$this->getStore()->update('
DELETE FROM `schedule_criteria`
WHERE `id` NOT IN (' . implode(',', $criteriaIds) . ')
AND `eventId` = :eventId
', [
'eventId' => $this->eventId,
]);
} else {
// No criteria left at all (or never was any)
$this->getStore()->update('DELETE FROM `schedule_criteria` WHERE `eventId` = :eventId', [
'eventId' => $this->eventId,
]);
}
// Notify
if ($options['notify']) {
// Only if the schedule effects the immediate future - i.e. within the RF Look Ahead
// Or if the scheduled event was updated to a past date
if ($this->inScheduleLookAhead()) {
$this->getLog()->debug(
'Schedule changing is within the schedule look ahead, will notify '
. count($this->displayGroups) . ' display groups'
);
foreach ($this->displayGroups as $displayGroup) {
/* @var DisplayGroup $displayGroup */
$this
->getDisplayNotifyService()
->collectNow()
->notifyByDisplayGroupId($displayGroup->displayGroupId);
}
} else {
$this->getLog()->debug('Schedule changing is not within the schedule look ahead');
}
}
if ($options['audit']) {
$this->audit($this->getId(), $auditMessage, null, true);
}
// Drop the cache for this event
$this->dropEventCache();
}
/**
* Delete this Schedule Event
*/
public function delete($options = [])
{
$this->load();
$options = array_merge([
'notify' => true
], $options);
// Notify display groups
$notify = $this->displayGroups;
// Audit
$this->audit($this->getId(), 'Deleted', $this->toArray(true));
// Delete display group assignments
$this->displayGroups = [];
$this->unlinkDisplayGroups();
// Delete schedule exclusions
$scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]);
foreach ($scheduleExclusions as $exclusion) {
$exclusion->delete();
}
// Delete schedule reminders
if ($this->scheduleReminderFactory !== null) {
$scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $this->eventId]);
foreach ($scheduleReminders as $reminder) {
$reminder->delete();
}
}
// Delete schedule criteria
$this->getStore()->update('DELETE FROM `schedule_criteria` WHERE `eventId` = :eventId', [
'eventId' => $this->eventId,
]);
if ($this->eventTypeId === self::$SYNC_EVENT) {
$this->getStore()->update('DELETE FROM `schedule_sync` WHERE eventId = :eventId', [
'eventId' => $this->eventId
]);
}
// Delete the event itself
$this->getStore()->update('DELETE FROM `schedule` WHERE eventId = :eventId', ['eventId' => $this->eventId]);
// Notify
if ($options['notify']) {
// Only if the schedule effects the immediate future - i.e. within the RF Look Ahead
if ($this->inScheduleLookAhead() && $this->displayNotifyService !== null) {
$this->getLog()->debug(
'Schedule changing is within the schedule look ahead, will notify '
. count($notify) . ' display groups'
);
foreach ($notify as $displayGroup) {
/* @var DisplayGroup $displayGroup */
$this
->getDisplayNotifyService()
->collectNow()
->notifyByDisplayGroupId($displayGroup->displayGroupId);
}
} else if ($this->displayNotifyService === null) {
$this->getLog()->info('Notify disabled, dependencies not set');
}
} else {
$this->getLog()->debug('Event delete: Notify disabled, option set to false');
}
// Drop the cache for this event
$this->dropEventCache();
}
/**
* Add
*/
private function add()
{
$this->eventId = $this->getStore()->insert('
INSERT INTO `schedule` (
eventTypeId,
CampaignId,
commandId,
userID,
is_priority,
FromDT,
ToDT,
DisplayOrder,
recurrence_type,
recurrence_detail,
recurrence_range,
`recurrenceRepeatsOn`,
`recurrenceMonthlyRepeatsOn`,
`dayPartId`,
`syncTimezone`,
`syncEvent`,
`shareOfVoice`,
`isGeoAware`,
`geoLocation`,
`actionType`,
`actionTriggerCode`,
`actionLayoutCode`,
`maxPlaysPerHour`,
`parentCampaignId`,
`syncGroupId`,
`modifiedBy`,
`createdOn`,
`updatedOn`,
`name`,
`dataSetId`,
`dataSetParams`
)
VALUES (
:eventTypeId,
:campaignId,
:commandId,
:userId,
:isPriority,
:fromDt,
:toDt,
:displayOrder,
:recurrenceType,
:recurrenceDetail,
:recurrenceRange,
:recurrenceRepeatsOn,
:recurrenceMonthlyRepeatsOn,
:dayPartId,
:syncTimezone,
:syncEvent,
:shareOfVoice,
:isGeoAware,
:geoLocation,
:actionType,
:actionTriggerCode,
:actionLayoutCode,
:maxPlaysPerHour,
:parentCampaignId,
:syncGroupId,
:modifiedBy,
:createdOn,
:updatedOn,
:name,
:dataSetId,
:dataSetParams
)
', [
'eventTypeId' => $this->eventTypeId,
'campaignId' => $this->campaignId,
'commandId' => $this->commandId,
'userId' => $this->userId,
'isPriority' => $this->isPriority,
'fromDt' => $this->fromDt,
'toDt' => $this->toDt,
'displayOrder' => $this->displayOrder,
'recurrenceType' => empty($this->recurrenceType) ? null : $this->recurrenceType,
'recurrenceDetail' => $this->recurrenceDetail,
'recurrenceRange' => $this->recurrenceRange,
'recurrenceRepeatsOn' => $this->recurrenceRepeatsOn,
'recurrenceMonthlyRepeatsOn' => ($this->recurrenceMonthlyRepeatsOn == null)
? 0 :
$this->recurrenceMonthlyRepeatsOn,
'dayPartId' => $this->dayPartId,
'syncTimezone' => $this->syncTimezone,
'syncEvent' => $this->syncEvent,
'shareOfVoice' => $this->shareOfVoice,
'isGeoAware' => $this->isGeoAware,
'geoLocation' => $this->geoLocation,
'actionType' => $this->actionType,
'actionTriggerCode' => $this->actionTriggerCode,
'actionLayoutCode' => $this->actionLayoutCode,
'maxPlaysPerHour' => $this->maxPlaysPerHour,
'parentCampaignId' => $this->parentCampaignId == 0 ? null : $this->parentCampaignId,
'syncGroupId' => $this->syncGroupId == 0 ? null : $this->syncGroupId,
'modifiedBy' => null,
'createdOn' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'updatedOn' => null,
'name' => !empty($this->name) ? $this->name : null,
'dataSetId' => !empty($this->dataSetId) ? $this->dataSetId : null,
'dataSetParams' => !empty($this->dataSetParams) ? $this->dataSetParams : null,
]);
}
/**
* Edit
*/
private function edit()
{
$this->getStore()->update('
UPDATE `schedule` SET
eventTypeId = :eventTypeId,
campaignId = :campaignId,
commandId = :commandId,
is_priority = :isPriority,
userId = :userId,
fromDt = :fromDt,
toDt = :toDt,
displayOrder = :displayOrder,
recurrence_type = :recurrenceType,
recurrence_detail = :recurrenceDetail,
recurrence_range = :recurrenceRange,
`recurrenceRepeatsOn` = :recurrenceRepeatsOn,
`recurrenceMonthlyRepeatsOn` = :recurrenceMonthlyRepeatsOn,
`dayPartId` = :dayPartId,
`syncTimezone` = :syncTimezone,
`syncEvent` = :syncEvent,
`shareOfVoice` = :shareOfVoice,
`isGeoAware` = :isGeoAware,
`geoLocation` = :geoLocation,
`actionType` = :actionType,
`actionTriggerCode` = :actionTriggerCode,
`actionLayoutCode` = :actionLayoutCode,
`maxPlaysPerHour` = :maxPlaysPerHour,
`parentCampaignId` = :parentCampaignId,
`syncGroupId` = :syncGroupId,
`modifiedBy` = :modifiedBy,
`updatedOn` = :updatedOn,
`name` = :name,
`dataSetId` = :dataSetId,
`dataSetParams` = :dataSetParams
WHERE eventId = :eventId
', [
'eventTypeId' => $this->eventTypeId,
'campaignId' => ($this->campaignId !== 0) ? $this->campaignId : null,
'commandId' => $this->commandId,
'userId' => $this->userId,
'isPriority' => $this->isPriority,
'fromDt' => $this->fromDt,
'toDt' => $this->toDt,
'displayOrder' => $this->displayOrder,
'recurrenceType' => empty($this->recurrenceType) ? null : $this->recurrenceType,
'recurrenceDetail' => $this->recurrenceDetail,
'recurrenceRange' => $this->recurrenceRange,
'recurrenceRepeatsOn' => $this->recurrenceRepeatsOn,
'recurrenceMonthlyRepeatsOn' => $this->recurrenceMonthlyRepeatsOn,
'dayPartId' => $this->dayPartId,
'syncTimezone' => $this->syncTimezone,
'syncEvent' => $this->syncEvent,
'shareOfVoice' => $this->shareOfVoice,
'isGeoAware' => $this->isGeoAware,
'geoLocation' => $this->geoLocation,
'actionType' => $this->actionType,
'actionTriggerCode' => $this->actionTriggerCode,
'actionLayoutCode' => $this->actionLayoutCode,
'maxPlaysPerHour' => $this->maxPlaysPerHour,
'parentCampaignId' => $this->parentCampaignId == 0 ? null : $this->parentCampaignId,
'syncGroupId' => $this->syncGroupId == 0 ? null : $this->syncGroupId,
'modifiedBy' => $this->modifiedBy,
'updatedOn' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
'name' => $this->name,
'dataSetId' => !empty($this->dataSetId) ? $this->dataSetId : null,
'dataSetParams' => !empty($this->dataSetParams) ? $this->dataSetParams : null,
'eventId' => $this->eventId,
]);
}
/**
* Get events between the provided dates.
* @param Carbon $fromDt
* @param Carbon $toDt
* @return ScheduleEvent[]
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function getEvents($fromDt, $toDt)
{
// Events scheduled "always" will return one event
if ($this->isAlwaysDayPart()) {
// Create events with min/max dates
$this->addDetail(Schedule::$DATE_MIN, Schedule::$DATE_MAX);
return $this->scheduleEvents;
}
// Copy the dates as we are going to be operating on them.
$fromDt = $fromDt->copy();
$toDt = $toDt->copy();
if ($this->pool == null) {
throw new ConfigurationException(__('Cache pool not available'));
}
if ($this->eventId == null) {
throw new InvalidArgumentException(__('Unable to generate schedule, unknown event'), 'eventId');
}
// What if we are requesting a single point in time?
if ($fromDt == $toDt) {
$this->log->debug(
'Requesting event for a single point in time: '
. $fromDt->format(DateFormatHelper::getSystemFormat())
);
}
$events = [];
$fromTimeStamp = $fromDt->format('U');
$toTimeStamp = $toDt->format('U');
// Rewind the from date to the start of the month
$fromDt->startOfMonth();
if ($fromDt == $toDt) {
$this->log->debug(
'From and To Dates are the same after rewinding 1 month,
the date is the 1st of the month, adding a month to toDate.'
);
$toDt->addMonth();
}
// Load the dates into a date object for parsing
$eventStart = Carbon::createFromTimestamp($this->fromDt);
$eventEnd = ($this->toDt == null) ? $eventStart->copy() : Carbon::createFromTimestamp($this->toDt);
// Does the original event go over the month boundary?
if ($eventStart->month !== $eventEnd->month) {
// We expect some residual events to spill out into the month we are generating
// wind back the generate from date
$fromDt->subMonth();
$this->getLog()->debug(
'Expecting events from the prior month to spill over into this one,
pulled back the generate from dt to ' . $fromDt->toRssString()
);
} else {
$this->getLog()->debug('The main event has a start and end date within the month, no need to pull it in from the prior month. [eventId:' . $this->eventId . ']');
}
// Keep a cache of schedule exclusions, so we look them up by eventId only one time per event
$scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]);
// Request month cache
while ($fromDt < $toDt) {
// Empty scheduleEvents as we are looping through each month
// we dont want to save previous month events
$this->scheduleEvents = [];
// Events for the month.
$this->generateMonth($fromDt, $eventStart, $eventEnd);
$this->getLog()->debug(
'Filtering Events: ' . json_encode($this->scheduleEvents, JSON_PRETTY_PRINT)
. '. fromTimeStamp: ' . $fromTimeStamp . ', toTimeStamp: ' . $toTimeStamp
);
foreach ($this->scheduleEvents as $scheduleEvent) {
// Find the excluded recurring events
$exclude = false;
foreach ($scheduleExclusions as $exclusion) {
if ($scheduleEvent->fromDt == $exclusion->fromDt &&
$scheduleEvent->toDt == $exclusion->toDt) {
$exclude = true;
continue;
}
}
if ($exclude) {
continue;
}
if (in_array($scheduleEvent, $events)) {
continue;
}
if ($scheduleEvent->toDt == null) {
if ($scheduleEvent->fromDt >= $fromTimeStamp && $scheduleEvent->toDt < $toTimeStamp) {
$events[] = $scheduleEvent;
}
} else {
if ($scheduleEvent->fromDt <= $toTimeStamp && $scheduleEvent->toDt > $fromTimeStamp) {
$events[] = $scheduleEvent;
}
}
}
// Move the month forwards
$fromDt->addMonth();
}
// Clear our cache of schedule exclusions
$scheduleExclusions = null;
$this->getLog()->debug(
'Filtered ' . count($this->scheduleEvents) . ' to ' . count($events)
. ', events: ' . json_encode($events, JSON_PRETTY_PRINT)
);
return $events;
}
/**
* Generate Instances
* @param Carbon $generateFromDt
* @param Carbon $start
* @param Carbon $end
* @throws GeneralException
*/
private function generateMonth($generateFromDt, $start, $end)
{
// Operate on copies of the dates passed.
$start = $start->copy();
$end = $end->copy();
$generateFromDt->copy()->startOfMonth();
$generateToDt = $generateFromDt->copy()->addMonth();
$this->getLog()->debug(
'Request for schedule events on eventId ' . $this->eventId
. ' from: ' . Carbon::createFromTimestamp($generateFromDt->format(DateFormatHelper::getSystemFormat()))
. ' to: ' . Carbon::createFromTimestamp($generateToDt->format(DateFormatHelper::getSystemFormat()))
. ' [eventId:' . $this->eventId . ']'
);
// If we are a daypart event, look up the start/end times for the event
$this->calculateDayPartTimes($start, $end);
// Does the original event fall into this window?
if ($start <= $generateToDt && $end > $generateFromDt) {
// Add the detail for the main event (this is the event that originally triggered the generation)
$this->getLog()->debug('Adding original event: ' . $start->toAtomString() . ' - ' . $end->toAtomString());
$this->addDetail($start->format('U'), $end->format('U'));
}
// If we don't have any recurrence, we are done
if (empty($this->recurrenceType) || empty($this->recurrenceDetail)) {
return;
}
// Detect invalid recurrences and quit early
if (!$this->isCustomDayPart() && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) {
return;
}
// Check the cache
$item = $this->pool->getItem('schedule/' . $this->eventId . '/' . $generateFromDt->format('Y-m'));
if ($item->isHit()) {
$this->scheduleEvents = $item->get();
$this->getLog()->debug('Returning from cache! [eventId:' . $this->eventId . ']');
return;
}
$this->getLog()->debug('Cache miss! [eventId:' . $this->eventId . ']');
// vv anything below here means that the event window requested is not in the cache vv
// WE ARE NOT IN THE CACHE
// this means we need to always walk the tree from the last watermark
// if the last watermark is after the from window, then we need to walk from the beginning
// Handle recurrence
$originalStart = $start->copy();
$lastWatermark = ($this->lastRecurrenceWatermark != 0)
? Carbon::createFromTimestamp($this->lastRecurrenceWatermark)
: Carbon::createFromTimestamp(self::$DATE_MIN);
$this->getLog()->debug(
'Recurrence calculation required - last water mark is set to: ' . $lastWatermark->toRssString()
. '. Event dates: ' . $start->toRssString() . ' - '
. $end->toRssString() . ' [eventId:' . $this->eventId . ']'
);
// Set the temp starts
// the start date should be the latest of the event start date and the last recurrence date
if ($lastWatermark > $start && $lastWatermark < $generateFromDt) {
$this->getLog()->debug(
'The last watermark is later than the event start date and the generate from dt,
using the watermark for forward population [eventId:' . $this->eventId . ']'
);
// Need to set the toDt based on the original event duration and the watermark start date
$eventDuration = $start->diffInSeconds($end, true);
/** @var Carbon $start */
$start = $lastWatermark->copy();
$end = $start->copy()->addSeconds($eventDuration);
if ($start <= $generateToDt && $end >= $generateFromDt) {
$this->getLog()->debug('The event start/end is inside the month');
// If we're a weekly repeat, check that the start date is on a selected day
if ($this->recurrenceType !== 'Week'
|| (!empty($this->recurrenceRepeatsOn)
&& in_array($start->dayOfWeekIso, explode(',', $this->recurrenceRepeatsOn)))
) {
$this->addDetail($start->format('U'), $end->format('U'));
}
}
}
// range should be the smallest of the recurrence range and the generate window todt
// the start/end date should be the the first recurrence in the current window
if ($this->recurrenceRange != 0) {
$range = Carbon::createFromTimestamp($this->recurrenceRange);
// Override the range to be within the period we are looking
$range = ($range < $generateToDt) ? $range : $generateToDt->copy();
} else {
$range = $generateToDt->copy();
}
$this->getLog()->debug(
'[' . $generateFromDt->toRssString() . ' - ' . $generateToDt->toRssString()
. '] Looping from ' . $start->toRssString()
. ' to ' . $range->toRssString() . ' [eventId:' . $this->eventId . ']'
);
// loop until we have added the recurring events for the schedule
while ($start < $range) {
$this->getLog()->debug(
'Loop: ' . $start->toRssString() . ' to ' . $range->toRssString()
. ' [eventId:' . $this->eventId . ', end: ' . $end->toRssString() . ']'
);
// add the appropriate time to the start and end
switch ($this->recurrenceType) {
case 'Minute':
$start->minute($start->minute + $this->recurrenceDetail);
$end->minute($end->minute + $this->recurrenceDetail);
break;
case 'Hour':
$start->hour($start->hour + $this->recurrenceDetail);
$end->hour($end->hour + $this->recurrenceDetail);
break;
case 'Day':
$start->day($start->day + $this->recurrenceDetail);
$end->day($end->day + $this->recurrenceDetail);
break;
case 'Week':
// recurrenceRepeatsOn will contain an array we can use to determine which days it should repeat
// on. Roll forward 7 days, adding each day we hit
// if we go over the start of the week, then jump forward by the recurrence range
if (!empty($this->recurrenceRepeatsOn)) {
// Parse days selected and add the necessary events
$daysSelected = explode(',', $this->recurrenceRepeatsOn);
// Are we on the start day of this week already?
$onStartOfWeek = ($start->copy()->setTimeFromTimeString('00:00:00') == $start->copy()->locale(Translate::GetLocale())->startOfWeek()->setTimeFromTimeString('00:00:00'));
// What is the end of this week
$endOfWeek = $start->copy()->locale(Translate::GetLocale())->endOfWeek();
$this->getLog()->debug(
'Days selected: ' . $this->recurrenceRepeatsOn . '. End of week = ' . $endOfWeek
. ' start date ' . $start . ' [eventId:' . $this->eventId . ']'
);
for ($i = 1; $i <= 7; $i++) {
// Add a day to the start dates
// after the first pass, we will already be on the first day of the week
if ($i > 1 || !$onStartOfWeek) {
$start->day($start->day + 1);
$end->day($end->day + 1);
}
$this->getLog()->debug('Assessing start date ' . $start->toAtomString()
. ', isoDayOfWeek is ' . $start->dayOfWeekIso . ' [eventId:' . $this->eventId . ']');
// If we go over the recurrence range, stop
// if we go over the start of the week, stop
if ($start > $range || $start > $endOfWeek) {
break;
}
// Is this day set?
if (!in_array($start->dayOfWeekIso, $daysSelected)) {
continue;
}
if ($end > $generateFromDt && $start < $generateToDt) {
$this->getLog()->debug(
'Adding detail for ' . $start->toAtomString() . ' to ' . $end->toAtomString()
);
if ($this->eventTypeId == self::$COMMAND_EVENT) {
$this->addDetail($start->format('U'), null);
} else {
// If we are a daypart event, look up the start/end times for the event
$this->calculateDayPartTimes($start, $end);
$this->addDetail($start->format('U'), $end->format('U'));
}
} else {
$this->getLog()->debug('Event is outside range');
}
}
$this->getLog()->debug(
'Finished 7 day roll forward, start date is ' . $start . ' [eventId:' . $this->eventId . ']'
);
// If we haven't passed the end of the week, roll forward
if ($start < $endOfWeek) {
$start->day($start->day + 1);
$end->day($end->day + 1);
}
// Wind back a week and then add our recurrence detail
$start->day($start->day - 7);
$end->day($end->day - 7);
$this->getLog()->debug(
'Resetting start date to ' . $start . ' [eventId:' . $this->eventId . ']'
);
}
// Jump forward a week from the original start date (when we entered this loop)
$start->day($start->day + ($this->recurrenceDetail * 7));
$end->day($end->day + ($this->recurrenceDetail * 7));
break;
case 'Month':
// We use the difference to set the end date
$difference = $end->diffInSeconds($start);
// Are we repeating on the day of the month, or the day of the week
if ($this->recurrenceMonthlyRepeatsOn == 1) {
// Week day repeat
// Work out the position in the month of this day and the ordinal
$ordinals = ['first', 'second', 'third', 'fourth', 'last'];
$ordinal = $ordinals[ceil($originalStart->day / 7) - 1];
// Move forwards to the start of the appropriate month
for ($i = 0; $i < $this->recurrenceDetail; $i++) {
$start->endOfMonth()->addSecond();
}
// Set to the right day
$start->modify($ordinal . ' ' . $originalStart->format('l') . ' of ' . $start->format('F Y'));
$start->setTimeFrom($originalStart);
$this->getLog()->debug('Monthly repeats every ' . $this->recurrenceDetail . ' months on '
. $ordinal . ' ' . $start->format('l') . ' of ' . $start->format('F Y'));
} else {
// Day repeat
$startTest = $start->copy()->addDays(28 * $this->recurrenceDetail);
if ($originalStart->day > intval($startTest->format('t'))) {
// The next month has fewer days than the current month
$start = $startTest->endOfMonth()->setTimeFrom($originalStart);
} else {
$start->addMonth()->day($originalStart->day);
}
$this->getLog()->debug('Monthly repeats every ' . $this->recurrenceDetail . ' months '
. ' on a specific day ' . $originalStart->day . ' days this month ' . $start->format('t')
. ' set to ' . $start->format('Y-m-d'));
}
// Base the end on the start + difference
$end = $start->copy()->addSeconds($difference);
break;
case 'Year':
$start->year($start->year + $this->recurrenceDetail);
$end->year($end->year + $this->recurrenceDetail);
break;
default:
throw new InvalidArgumentException(__('Invalid recurrence type'), 'recurrenceType');
}
// after we have added the appropriate amount, are we still valid
if ($start > $range) {
$this->getLog()->debug(
'Breaking mid loop because we\'ve exceeded the range. Start: ' . $start->toRssString()
. ', range: ' . $range->toRssString() . ' [eventId:' . $this->eventId . ']'
);
break;
}
// Push the watermark
$lastWatermark = $start->copy();
// Don't add if we are weekly recurrency (handles it's own adding)
if ($this->recurrenceType == 'Week' && !empty($this->recurrenceRepeatsOn)) {
continue;
}
if ($start <= $generateToDt && $end >= $generateFromDt) {
if ($this->eventTypeId == self::$COMMAND_EVENT) {
$this->addDetail($start->format('U'), null);
} else {
// If we are a daypart event, look up the start/end times for the event
$this->calculateDayPartTimes($start, $end);
$this->addDetail($start->format('U'), $end->format('U'));
}
}
}
$this->getLog()->debug(
'Our last recurrence watermark is: ' . $lastWatermark->toRssString()
. '[eventId:' . $this->eventId . ']'
);
// Update our schedule with the new last watermark
$lastWatermarkTimeStamp = $lastWatermark->format('U');
if ($lastWatermarkTimeStamp != $this->lastRecurrenceWatermark) {
$this->lastRecurrenceWatermark = $lastWatermarkTimeStamp;
$this->getStore()->update('UPDATE `schedule` SET lastRecurrenceWatermark = :lastRecurrenceWatermark WHERE eventId = :eventId', [
'eventId' => $this->eventId,
'lastRecurrenceWatermark' => $this->lastRecurrenceWatermark
]);
}
// Update the cache
$item->set($this->scheduleEvents);
$item->expiresAt(Carbon::now()->addMonths(2));
$this->pool->saveDeferred($item);
return;
}
/**
* Drop the event cache
* @param $key
*/
private function dropEventCache($key = null)
{
$compKey = 'schedule/' . $this->eventId;
if ($key !== null) {
$compKey .= '/' . $key;
}
$this->pool->deleteItem($compKey);
}
/**
* Calculate the DayPart times
* @param Carbon $start
* @param Carbon $end
* @throws GeneralException
*/
private function calculateDayPartTimes($start, $end)
{
$dayOfWeekLookup = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
if (!$this->isAlwaysDayPart() && !$this->isCustomDayPart()) {
// TODO: replace with $dayPart->adjustForDate()?
// End is always based on Start
$end->setTimestamp($start->format('U'));
// Get the day part
$dayPart = $this->getDayPart();
$this->getLog()->debug(
'Start and end time for dayPart is ' . $dayPart->startTime . ' - ' . $dayPart->endTime
);
// What day of the week does this start date represent?
// dayOfWeek is 0 for Sunday to 6 for Saturday
$found = false;
foreach ($dayPart->exceptions as $exception) {
// Is there an exception for this day of the week?
if ($exception['day'] == $dayOfWeekLookup[$start->dayOfWeek]) {
$start->setTimeFromTimeString($exception['start']);
$end->setTimeFromTimeString($exception['end']);
if ($start >= $end) {
$end->addDay();
}
$this->getLog()->debug(
'Found exception Start and end time for dayPart exception is '
. $exception['start'] . ' - ' . $exception['end']
);
$found = true;
break;
}
}
if (!$found) {
// Set the time section of our dates based on the daypart date
$start->setTimeFromTimeString($dayPart->startTime);
$end->setTimeFromTimeString($dayPart->endTime);
if ($start >= $end) {
$this->getLog()->debug('Start is ahead of end - adding a day to the end date');
$end->addDay();
}
}
}
}
/**
* Add Detail
* @param int $fromDt
* @param int $toDt
*/
private function addDetail($fromDt, $toDt)
{
$this->scheduleEvents[] = new ScheduleEvent($fromDt, $toDt);
}
/**
* Manage the assignments
* @param bool $notify should we notify or not?
* @throws \Xibo\Exception\XiboException
*/
private function manageAssignments($notify)
{
$this->linkDisplayGroups();
$this->unlinkDisplayGroups();
$this->getLog()->debug('manageAssignments: Assessing whether we need to notify');
$originalDisplayGroups = $this->getOriginalValue('displayGroups');
// Get the difference between the original display groups assigned and the new display groups assigned
if ($notify && $originalDisplayGroups !== null && $this->inScheduleLookAhead()) {
$diff = [];
foreach ($originalDisplayGroups as $element) {
/** @var \Xibo\Entity\DisplayGroup $element */
$diff[$element->getId()] = $element;
}
if (count($diff) > 0) {
$this->getLog()->debug(
'manageAssignments: There are ' . count($diff) . ' existing DisplayGroups on this Event'
);
$ids = array_map(function ($element) {
return $element->getId();
}, $this->displayGroups);
$except = array_diff(array_keys($diff), $ids);
if (count($except) > 0) {
foreach ($except as $item) {
$this->getLog()->debug(
'manageAssignments: calling notify on displayGroupId '
. $diff[$item]->getId()
);
$this->getDisplayNotifyService()->collectNow()->notifyByDisplayGroupId($diff[$item]->getId());
}
} else {
$this->getLog()->debug('manageAssignments: No need to notify');
}
} else {
$this->getLog()->debug('manageAssignments: No change to DisplayGroup assignments');
}
} else {
$this->getLog()->debug('manageAssignments: Not in look-ahead');
}
}
/**
* Link Layout
*/
private function linkDisplayGroups()
{
// TODO: Make this more efficient by storing the prepared SQL statement
$sql = 'INSERT INTO `lkscheduledisplaygroup` (eventId, displayGroupId) VALUES (:eventId, :displayGroupId) ON DUPLICATE KEY UPDATE displayGroupId = displayGroupId';
$i = 0;
foreach ($this->displayGroups as $displayGroup) {
$i++;
$this->getStore()->insert($sql, array(
'eventId' => $this->eventId,
'displayGroupId' => $displayGroup->displayGroupId
));
}
}
/**
* Unlink Layout
*/
private function unlinkDisplayGroups()
{
// Unlink any layouts that are NOT in the collection
$params = ['eventId' => $this->eventId];
$sql = 'DELETE FROM `lkscheduledisplaygroup` WHERE eventId = :eventId AND displayGroupId NOT IN (0';
$i = 0;
foreach ($this->displayGroups as $displayGroup) {
$i++;
$sql .= ',:displayGroupId' . $i;
$params['displayGroupId' . $i] = $displayGroup->displayGroupId;
}
$sql .= ')';
$this->getStore()->update($sql, $params);
}
/**
* @return \Xibo\Entity\DayPart
* @throws \Xibo\Exception\NotFoundException
*/
private function getDayPart()
{
if ($this->dayPart === null) {
$this->dayPart = $this->dayPartFactory->getById($this->dayPartId);
}
return $this->dayPart;
}
/**
* Is this event an always daypart event
* @return bool
* @throws NotFoundException
*/
public function isAlwaysDayPart()
{
return $this->getDayPart()->isAlways === 1;
}
/**
* Is this event a custom daypart event
* @return bool
* @throws NotFoundException
*/
public function isCustomDayPart()
{
return $this->getDayPart()->isCustom === 1;
}
/**
* Get next reminder date
* @param Carbon $now
* @param ScheduleReminder $reminder
* @param int $remindSeconds
* @return int|null
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function getNextReminderDate($now, $reminder, $remindSeconds)
{
// Determine toDt so that we don't getEvents which never ends
// adding the recurrencedetail at the end (minute/hour/week) to make sure we get at least 2 next events
$toDt = $now->copy();
// For a future event we need to forward now to event fromDt
$fromDt = Carbon::createFromTimestamp($this->fromDt);
if ($fromDt > $toDt) {
$toDt = $fromDt;
}
switch ($this->recurrenceType) {
case 'Minute':
$toDt->minute(($toDt->minute + $this->recurrenceDetail) + $this->recurrenceDetail);
break;
case 'Hour':
$toDt->hour(($toDt->hour + $this->recurrenceDetail) + $this->recurrenceDetail);
break;
case 'Day':
$toDt->day(($toDt->day + $this->recurrenceDetail) + $this->recurrenceDetail);
break;
case 'Week':
$toDt->day(($toDt->day + $this->recurrenceDetail * 7 ) + $this->recurrenceDetail);
break;
case 'Month':
$toDt->month(($toDt->month + $this->recurrenceDetail ) + $this->recurrenceDetail);
break;
case 'Year':
$toDt->year(($toDt->year + $this->recurrenceDetail ) + $this->recurrenceDetail);
break;
default:
throw new InvalidArgumentException(__('Invalid recurrence type'), 'recurrenceType');
}
// toDt is set so that we get two next events from now
$scheduleEvents = $this->getEvents($now, $toDt);
foreach ($scheduleEvents as $event) {
if ($reminder->option == ScheduleReminder::$OPTION_BEFORE_START) {
$reminderDt = $event->fromDt - $remindSeconds;
if ($reminderDt >= $now->format('U')) {
return $reminderDt;
}
} elseif ($reminder->option == ScheduleReminder::$OPTION_AFTER_START) {
$reminderDt = $event->fromDt + $remindSeconds;
if ($reminderDt >= $now->format('U')) {
return $reminderDt;
}
} elseif ($reminder->option == ScheduleReminder::$OPTION_BEFORE_END) {
$reminderDt = $event->toDt - $remindSeconds;
if ($reminderDt >= $now->format('U')) {
return $reminderDt;
}
} elseif ($reminder->option == ScheduleReminder::$OPTION_AFTER_END) {
$reminderDt = $event->toDt + $remindSeconds;
if ($reminderDt >= $now->format('U')) {
return $reminderDt;
}
}
}
// No next event exist
throw new NotFoundException(__('reminderDt not found as next event does not exist'));
}
/**
* Get event title
* @return string
* @throws GeneralException
*/
public function getEventTitle()
{
// Setting for whether we show Layouts with out permissions
$showLayoutName = ($this->config->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);
// Load the display groups
$this->load();
$displayGroupList = '';
if (count($this->displayGroups) >= 0) {
$array = array_map(function ($object) {
return $object->displayGroup;
}, $this->displayGroups);
$displayGroupList = implode(', ', $array);
}
$user = $this->userFactory->getById($this->userId);
// Event Title
if ($this->campaignId == 0) {
// Command
$title = __('%s scheduled on %s', $this->command, $displayGroupList);
} else {
// Should we show the Layout name, or not (depending on permission)
// Make sure we only run the below code if we have to, its quite expensive
if (!$showLayoutName && !$user->isSuperAdmin()) {
// Campaign
$campaign = $this->campaignFactory->getById($this->campaignId);
if (!$user->checkViewable($campaign)) {
$this->campaign = __('Private Item');
}
}
$title = __('%s scheduled on %s', $this->campaign, $displayGroupList);
}
return $title;
}
private function toArray($jsonEncodeArrays = false)
{
$objectAsJson = $this->jsonSerialize();
foreach ($objectAsJson as $key => $value) {
$displayGroups = [];
if (is_array($value) && $jsonEncodeArrays) {
if ($key === 'displayGroups') {
foreach ($value as $index => $displayGroup) {
/** @var DisplayGroup $displayGroup */
$displayGroups[$index] = $displayGroup->jsonForAudit();
}
$objectAsJson[$key] = json_encode($displayGroups);
} else {
$objectAsJson[$key] = json_encode($value);
}
}
if (in_array($key, $this->datesToFormat)) {
$objectAsJson[$key] = !empty($value)
? Carbon::createFromTimestamp($value)->format(DateFormatHelper::getSystemFormat())
: $value;
}
if ($key === 'campaignId' && isset($this->campaignFactory)) {
$campaign = $this->campaignFactory->getById($value);
$objectAsJson['campaign'] = $campaign->campaign;
}
}
return $objectAsJson;
}
/**
* Get all changed properties for this entity
* @param bool $jsonEncodeArrays
* @return array
* @throws NotFoundException
*/
public function getChangedProperties($jsonEncodeArrays = false)
{
$changedProperties = [];
foreach ($this->jsonSerialize() as $key => $value) {
if (!is_array($value)
&& !is_object($value)
&& $this->propertyOriginallyExisted($key)
&& $this->hasPropertyChanged($key)
) {
if (in_array($key, $this->datesToFormat)) {
$original = empty($this->getOriginalValue($key))
? $this->getOriginalValue($key)
: Carbon::createFromTimestamp($this->getOriginalValue($key))
->format(DateFormatHelper::getSystemFormat());
$new = empty($value)
? $value
: Carbon::createFromTimestamp($value)
->format(DateFormatHelper::getSystemFormat());
$changedProperties[$key] = $original . ' > ' . $new;
} else {
$changedProperties[$key] = $this->getOriginalValue($key) . ' > ' . $value;
if ($key === 'campaignId' && isset($this->campaignFactory)) {
$campaign = $this->campaignFactory->getById($value);
$changedProperties['campaign'] =
$this->getOriginalValue('campaign') . ' > ' . $campaign->campaign;
}
}
}
if (is_array($value)
&& $jsonEncodeArrays
&& $this->propertyOriginallyExisted($key)
&& $this->hasPropertyChanged($key)
) {
if ($key === 'displayGroups') {
$displayGroups = [];
$originalDisplayGroups = [];
foreach ($this->getOriginalValue($key) as $index => $displayGroup) {
/** @var DisplayGroup $displayGroup */
$originalDisplayGroups[$index] = $displayGroup->jsonForAudit();
}
foreach ($value as $index => $displayGroup) {
$displayGroups[$index] = $displayGroup->jsonForAudit();
}
$changedProperties[$key] =
json_encode($originalDisplayGroups) . ' > ' . json_encode($displayGroups);
} else {
$changedProperties[$key] =
json_encode($this->getOriginalValue($key)) . ' > ' . json_encode($value);
}
}
}
return $changedProperties;
}
/**
* Get an array of event types
* @param array $exclude Event type IDs to exclude
* @return array
*/
public static function getEventTypes(array $exclude = []): array
{
$eventTypes = [
['eventTypeId' => self::$LAYOUT_EVENT, 'eventTypeName' => __('Layout')],
['eventTypeId' => self::$COMMAND_EVENT, 'eventTypeName' => __('Command')],
['eventTypeId' => self::$OVERLAY_EVENT, 'eventTypeName' => __('Overlay Layout')],
['eventTypeId' => self::$INTERRUPT_EVENT, 'eventTypeName' => __('Interrupt Layout')],
['eventTypeId' => self::$CAMPAIGN_EVENT, 'eventTypeName' => __('Campaign')],
['eventTypeId' => self::$ACTION_EVENT, 'eventTypeName' => __('Action')],
['eventTypeId' => self::$MEDIA_EVENT, 'eventTypeName' => __('Video/Image')],
['eventTypeId' => self::$PLAYLIST_EVENT, 'eventTypeName' => __('Playlist')],
['eventTypeId' => self::$SYNC_EVENT, 'eventTypeName' => __('Synchronised Event')],
['eventTypeId' => self::$DATA_CONNECTOR_EVENT, 'eventTypeName' => __('Data Connector')],
];
if (!empty($exclude)) {
$eventTypes = array_filter(
$eventTypes,
fn($type) => !in_array($type['eventTypeId'], $exclude, true)
);
}
return array_values($eventTypes);
}
/**
* @return string
*/
public function getSyncTypeForEvent(): string
{
$layouts = $this->getStore()->select(
'SELECT `schedule_sync`.layoutId FROM `schedule_sync` WHERE `schedule_sync`.eventId = :eventId',
['eventId' => $this->eventId]
);
return (count(array_unique($layouts, SORT_REGULAR)) === 1)
? __('Synchronised Mirrored Content')
: __('Synchronised Content');
}
/**
* @param SyncGroup $syncGroup
* @param SanitizerInterface $sanitizer
* @return void
* @throws NotFoundException
*/
public function updateSyncLinks(SyncGroup $syncGroup, SanitizerInterface $sanitizer): void
{
foreach ($syncGroup->getSyncGroupMembers() as $display) {
$this->getStore()->insert('INSERT INTO `schedule_sync` (`eventId`, `displayId`, `layoutId`)
VALUES(:eventId, :displayId, :layoutId) ON DUPLICATE KEY UPDATE layoutId = :layoutId', [
'eventId' => $this->eventId,
'displayId' => $display->displayId,
'layoutId' => $sanitizer->getInt('layoutId_' . $display->displayId)
]);
}
}
}