File "cleaner.php"
Full Path: /home/romayxjt/public_html/wp-content/plugins/vikbooking/admin/helpers/src/performance/cleaner.php
File size: 13.14 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @package VikBooking
* @subpackage core
* @author E4J s.r.l.
* @copyright Copyright (C) 2024 E4J s.r.l. All Rights Reserved.
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
* @link https://vikwp.com
*/
// No direct access
defined('ABSPATH') or die('No script kiddies please!');
/**
* Performance Cleaner implementation.
*
* @since 1.17.2 (J) - 1.7.2 (WP)
*/
final class VBOPerformanceCleaner
{
/**
* @var array
*/
private static $options = [];
/**
* Sets a list of given options to filter certain cleaning operations.
*
* @param array $options List of options to set.
*
* @return void
*/
public static function setOptions(array $options)
{
static::$options = $options;
}
/**
* Performs a global check on what needs to be done to clean up performances.
*
* @return int The number of database records that were cleaned up.
*/
public static function runCheck()
{
// list of operations that should be skipped
$skip_checks = VBOFactory::getConfig()->getArray('performance_cleaner_skip_list', []);
// number of records affected
$affected_records = 0;
if (!in_array('seasons', $skip_checks)) {
// clean up expired seasonal records
$affected_records += self::pricingAlterations();
// clean up ghost records
$affected_records += self::ghostRecords();
}
return $affected_records;
}
/**
* Cleans up expired season pricing records.
*
* @return int The number of rows affected.
*/
public static function pricingAlterations()
{
$dbo = JFactory::getDbo();
$affected = 0;
$nowinfo = getdate();
$year_base = mktime(0, 0, 0, 1, 1, $nowinfo['year']);
$midnight_base = ($nowinfo['hours'] * 3600) + ($nowinfo['minutes'] * 60) + $nowinfo['seconds'];
$season_secs = $nowinfo[0] - $year_base - $midnight_base;
$isleap = $nowinfo['year'] % 4 == 0 && ($nowinfo['year'] % 100 != 0 || $nowinfo['year'] % 400 == 0);
if ($isleap) {
$leapts = mktime(0, 0, 0, 2, 29, $nowinfo['year']);
if ($nowinfo[0] >= $leapts) {
$season_secs -= 86400;
}
}
// delete the records of the past year
$dbo->setQuery(
$dbo->getQuery(true)
->delete($dbo->qn('#__vikbooking_seasons'))
->where($dbo->qn('year') . ' = ' . $dbo->q(($nowinfo['year'] - 1)))
);
$dbo->execute();
$affected += (int) $dbo->getAffectedRows();
// delete the expired records for the current year
$dbo->setQuery(
$dbo->getQuery(true)
->delete($dbo->qn('#__vikbooking_seasons'))
->where($dbo->qn('from') . ' < ' . $season_secs)
->where($dbo->qn('to') . ' < ' . $season_secs)
->where($dbo->qn('from') . ' < ' . $dbo->qn('to'))
->where($dbo->qn('from') . ' > 0')
->where($dbo->qn('to') . ' > 0')
->where($dbo->qn('year') . ' = ' . $dbo->q($nowinfo['year']))
);
$dbo->execute();
$affected += (int) $dbo->getAffectedRows();
return $affected;
}
/**
* Identifies and cleans up ghost records occupying listings with non existing bookings.
* If the E4jConnect Channel Manager is available, an auto bulk-action is triggered.
*
* @return int The number of rows affected.
*
* @since 1.17.7 (J) - 1.7.7 (WP)
*/
public static function ghostRecords()
{
$dbo = JFactory::getDbo();
$affected = 0;
// identify the room-busy relations whose booking IDs are empty
$dbo->setQuery(
$dbo->getQuery(true)
->select($dbo->qn('idbusy'))
->from($dbo->qn('#__vikbooking_ordersbusy'))
->where(1)
->andWhere([
$dbo->qn('idorder') . ' = 0',
$dbo->qn('idorder') . ' IS NULL',
], 'OR')
);
// set involved busy record IDs, if any (column is `idbusy`)
$hanging_busy_ids = array_column($dbo->loadAssocList(), 'idbusy');
// identify the occupied records whose busy relations have empty booking IDs
$dbo->setQuery(
$dbo->getQuery(true)
->select($dbo->qn('b') . '.*')
->select($dbo->qn('ob.idorder'))
->select($dbo->qn('o.id', 'res_id'))
->from($dbo->qn('#__vikbooking_busy', 'b'))
->leftJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
->leftJoin($dbo->qn('#__vikbooking_orders', 'o') . ' ON ' . $dbo->qn('ob.idorder') . ' = ' . $dbo->qn('o.id'))
->where($dbo->qn('b.checkout') . ' >= ' . time())
->andWhere([
$dbo->qn('ob.idorder') . ' = 0',
$dbo->qn('ob.idorder') . ' IS NULL',
$dbo->qn('o.id') . ' IS NULL',
], 'OR')
);
$ghost_records = $dbo->loadAssocList();
// merge involved busy record IDs, if any (column is `id`)
$hanging_busy_ids = array_merge($hanging_busy_ids, array_column($ghost_records, 'id'));
// map to integer and filter involved busy record IDs for removal
$hanging_busy_ids = array_values(array_unique(array_filter(array_map('intval', $hanging_busy_ids))));
if ($hanging_busy_ids) {
// delete records involved
$dbo->setQuery(
$dbo->getQuery(true)
->delete($dbo->qn('#__vikbooking_busy'))
->where($dbo->qn('id') . ' IN (' . implode(', ', $hanging_busy_ids) . ')')
);
$dbo->execute();
$affected += (int) $dbo->getAffectedRows();
}
if ($ghost_records && class_exists('VikChannelManager')) {
// ghost records were removed, but OTAs may require a sync of availability
$listing_times_pool = [];
foreach ($ghost_records as $ghost_record) {
if (empty($ghost_record['idroom']) || empty($ghost_record['checkin']) || empty($ghost_record['checkout'])) {
continue;
}
if (!isset($listing_times_pool[$ghost_record['idroom']])) {
$listing_times_pool[$ghost_record['idroom']] = [];
}
// push listing check-in and check-out date times involved
$listing_times_pool[$ghost_record['idroom']][] = $ghost_record['checkin'];
$listing_times_pool[$ghost_record['idroom']][] = $ghost_record['checkout'];
}
$min_ts = 0;
$max_ts = 0;
foreach ($listing_times_pool as $listing_id => $listing_times) {
$min_ts = $min_ts ? min(min($listing_times), $min_ts) : min($listing_times);
$max_ts = $max_ts ? max(max($listing_times), $max_ts) : max($listing_times);
}
if ($min_ts && $max_ts) {
// no past dates allowed
$min_ts = $min_ts < time() ? time() : $min_ts;
// prepare the options for an auto bulk-action of type "availability"
VikChannelManager::autoBulkActions([
'from_date' => date('Y-m-d', $min_ts),
'to_date' => date('Y-m-d', $max_ts),
'forced_rooms' => array_keys($listing_times_pool),
'update' => 'availability',
]);
}
}
// return the number of affected records
return $affected;
}
/**
* Performs a pricing snapshot of a listing for a range of dates, by cleaning up
* all previous seasonal rates and by re-creating only the needed records. Useful
* for those listings who had hundreds of pricing alteration updates for the same
* calendar day. Strongly recommended only for those who use a SINGLE rate plan.
*
* @return array Seasons snapshot operation results.
*
* @throws Exception
*/
public static function listingSeasonSnapshot()
{
$dbo = JFactory::getDbo();
$listing_id = (int) (static::$options['listing_id'] ?? null);
$id_price = (int) (static::$options['id_price'] ?? null);
$from_date = static::$options['from_date'] ?? date('Y-m-d');
$to_date = static::$options['to_date'] ?? date('Y-m-d', strtotime('+3 months'));
if (!$listing_id) {
throw new Exception('Missing required listing ID.', 400);
}
if (!$from_date || !$to_date || strtotime($from_date) > strtotime($to_date)) {
throw new Exception('Invalid dates provided.', 400);
}
// obtain a pricing snapshot for the given listing and dates
$snapshot = VBOModelPricing::getInstance()->getRoomRates([
'from_date' => $from_date,
'to_date' => $to_date,
'id_room' => $listing_id,
'id_price' => $id_price,
'all_rplans' => false,
'restrictions' => false,
]);
// confirm the rate plan ID
foreach ($snapshot as $dayrate) {
$id_price = $dayrate['idprice'];
break;
}
// gather the list of season records to remove
$involved_seasons = [];
foreach ($snapshot as $dayrate) {
foreach ($dayrate['spids'] ?? [] as $sp_id) {
if (!in_array($sp_id, $involved_seasons)) {
$involved_seasons[] = $sp_id;
}
}
}
if (!$involved_seasons) {
throw new Exception('No seasonal records to clean for the given listing and dates.', 500);
}
// clean up database records
$dbo->setQuery(
$dbo->getQuery(true)
->delete($dbo->qn('#__vikbooking_seasons'))
->where($dbo->qn('id') . ' IN (' . implode(',', array_map('intval', $involved_seasons)) . ')')
);
$dbo->execute();
$records_removed = (int) $dbo->getAffectedRows();
// build new season intervals from snapshot
$all_days = array_keys($snapshot);
$season_intervals = [];
$firstind = 0;
$firstdaycost = $snapshot[$all_days[0]]['cost'];
$nextdaycost = false;
for ($i = 1; $i < count($all_days); $i++) {
$ind = $all_days[$i];
$nextdaycost = $snapshot[$ind]['cost'];
if ($firstdaycost != $nextdaycost) {
$interval = [
'from' => $all_days[$firstind],
'to' => $all_days[($i - 1)],
'cost' => $firstdaycost
];
$season_intervals[] = $interval;
$firstdaycost = $nextdaycost;
$firstind = $i;
}
}
if ($nextdaycost === false) {
$interval = [
'from' => $all_days[$firstind],
'to' => $all_days[$firstind],
'cost' => $firstdaycost
];
$season_intervals[] = $interval;
} elseif ($firstdaycost == $nextdaycost) {
$interval = [
'from' => $all_days[$firstind],
'to' => $all_days[($i - 1)],
'cost' => $firstdaycost
];
$season_intervals[] = $interval;
}
// list of errors occurred
$errors = [];
/**
* Attempt to preload and cache seasons for other dates, so that applying new rates
* will be faster. We cannot preload these dates, because seasons were just removed.
*/
VikBooking::preloadSeasonRecords([$listing_id], strtotime($to_date), strtotime('+1 month', strtotime($to_date)));
// scan all season intervals
foreach ($season_intervals as $season_snap) {
try {
// set the new rate for this calculated interval
VBOModelPricing::getInstance([
'from_date' => $season_snap['from'],
'to_date' => $season_snap['to'],
'id_room' => $listing_id,
'id_price' => $id_price,
'rate' => $season_snap['cost'],
'min_los' => 0,
'max_los' => 0,
'update_otas' => false,
])->modifyRateRestrictions();
} catch (Exception $e) {
// silently push the error
$errors[] = sprintf('%s - %s: %s', $season_snap['from'], $season_snap['to'], $e->getMessage());
}
}
// unset preloaded and cached seasons
VikBooking::preloadSeasonRecords([$listing_id], false);
// return values
return [
'listing_id' => $listing_id,
'new_intervals' => count($season_intervals),
'records_removed' => $records_removed,
'intervals' => $season_intervals,
'errors' => $errors,
];
}
}