- Fixed z-index layering with !important rules for proper navigation stacking - Enhanced CSS file loading detection with multiple page identification methods - Added comprehensive header overlap prevention for all Astra theme header elements - Improved event edit page detection to handle URL patterns and page IDs - Verified all fixes working on staging with proper navigation visibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
637 lines
No EOL
22 KiB
PHP
637 lines
No EOL
22 KiB
PHP
<?php
|
|
/**
|
|
* HVAC Custom Event Edit Form Handler
|
|
*
|
|
* Memory-efficient, type-safe event editing using modern PHP patterns
|
|
* Replaces JavaScript-dependent TEC shortcode with server-side solution
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.1.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Custom event edit form handler with generator-based data loading
|
|
*/
|
|
final class HVAC_Custom_Event_Edit {
|
|
|
|
use HVAC_Singleton_Trait;
|
|
|
|
private const NONCE_ACTION = 'hvac_edit_event';
|
|
private const NONCE_FIELD = 'hvac_event_nonce';
|
|
private const CACHE_GROUP = 'hvac_events';
|
|
private const CACHE_TTL = 300; // 5 minutes
|
|
|
|
/**
|
|
* @var SplObjectStorage<WP_Post, ArrayObject> Event data cache
|
|
*/
|
|
private SplObjectStorage $eventCache;
|
|
|
|
/**
|
|
* @var ?int Current event ID being edited
|
|
*/
|
|
private ?int $currentEventId = null;
|
|
|
|
/**
|
|
* Constructor with property promotion
|
|
*/
|
|
private function __construct() {
|
|
$this->eventCache = new SplObjectStorage();
|
|
$this->initHooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress hooks
|
|
*/
|
|
private function initHooks(): void {
|
|
add_action('init', [$this, 'registerRewriteRules']);
|
|
add_action('template_redirect', [$this, 'handleFormSubmission']);
|
|
add_filter('query_vars', [$this, 'addQueryVars']);
|
|
|
|
// Single template_include hook at appropriate priority
|
|
add_filter('template_include', [$this, 'loadTemplate'], 1000);
|
|
|
|
add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
|
|
}
|
|
|
|
/**
|
|
* Register rewrite rules for edit URL
|
|
*/
|
|
public function registerRewriteRules(): void {
|
|
add_rewrite_rule(
|
|
'^trainer/event/edit/?$',
|
|
'index.php?hvac_event_edit=1',
|
|
'top'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add custom query vars
|
|
*/
|
|
public function addQueryVars(array $vars): array {
|
|
$vars[] = 'hvac_event_edit';
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* Load custom template for event edit page
|
|
*/
|
|
public function loadTemplate(string $template): string {
|
|
// Check if we're on the custom edit page
|
|
$is_edit_page = false;
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
|
|
|
// Method 1: Check by page ID (configuration-based approach)
|
|
if (defined('HVAC_EVENT_EDIT_PAGE_ID') && is_page(HVAC_EVENT_EDIT_PAGE_ID)) {
|
|
$is_edit_page = true;
|
|
}
|
|
// Method 2: Check URL path
|
|
elseif (strpos($request_uri, '/trainer/event/edit') !== false) {
|
|
$is_edit_page = true;
|
|
}
|
|
// Method 3: Check query var
|
|
elseif (get_query_var('hvac_event_edit') === '1') {
|
|
$is_edit_page = true;
|
|
}
|
|
|
|
if ($is_edit_page) {
|
|
$custom_template = HVAC_PLUGIN_DIR . 'templates/page-edit-event-custom.php';
|
|
if (file_exists($custom_template)) {
|
|
return $custom_template;
|
|
}
|
|
}
|
|
|
|
return $template;
|
|
}
|
|
|
|
/**
|
|
* Enqueue minimal required assets (no JavaScript!)
|
|
*/
|
|
public function enqueueAssets(): void {
|
|
if (!$this->isEditPage()) {
|
|
return;
|
|
}
|
|
|
|
wp_enqueue_style(
|
|
'hvac-event-edit-custom',
|
|
HVAC_PLUGIN_URL . 'assets/css/hvac-event-edit-custom.css',
|
|
[],
|
|
HVAC_PLUGIN_VERSION
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if current page is event edit page
|
|
*/
|
|
private function isEditPage(): bool {
|
|
// Check for the URL pattern /trainer/event/edit/
|
|
$is_edit_url = strpos($_SERVER['REQUEST_URI'], '/trainer/event/edit') !== false;
|
|
|
|
// Check for the specific page ID (6177) that handles edit events
|
|
$is_edit_page = is_page(6177);
|
|
|
|
// Check query var and template slug (fallback methods)
|
|
$has_query_var = get_query_var('hvac_event_edit') === '1';
|
|
$has_template = is_page() && get_page_template_slug() === 'templates/page-edit-event-custom.php';
|
|
|
|
return $is_edit_url || $is_edit_page || $has_query_var || $has_template;
|
|
}
|
|
|
|
/**
|
|
* Get event data using generator for memory efficiency
|
|
*
|
|
* @return Generator<string, mixed>
|
|
*/
|
|
public function getEventData(int $eventId): Generator {
|
|
// Check cache first
|
|
$cacheKey = "event_data_{$eventId}";
|
|
$cached = wp_cache_get($cacheKey, self::CACHE_GROUP);
|
|
|
|
if ($cached !== false && is_array($cached)) {
|
|
foreach ($cached as $key => $value) {
|
|
yield $key => $value;
|
|
}
|
|
return;
|
|
}
|
|
|
|
$event = get_post($eventId);
|
|
if (!$event || $event->post_type !== 'tribe_events') {
|
|
throw new InvalidArgumentException("Invalid event ID: {$eventId}");
|
|
}
|
|
|
|
// Core data
|
|
yield 'id' => $eventId;
|
|
yield 'title' => $event->post_title;
|
|
yield 'content' => $event->post_content;
|
|
yield 'excerpt' => $event->post_excerpt;
|
|
yield 'status' => $event->post_status;
|
|
yield 'author' => $event->post_author;
|
|
|
|
// Meta data - lazy load as needed
|
|
$metaKeys = $this->getEventMetaKeys();
|
|
foreach ($metaKeys as $key) {
|
|
yield $key => get_post_meta($eventId, $key, true);
|
|
}
|
|
|
|
// Venue data
|
|
$venueId = get_post_meta($eventId, '_EventVenueID', true);
|
|
if ($venueId) {
|
|
yield 'venue' => $this->getVenueData((int) $venueId);
|
|
}
|
|
|
|
// Organizer data
|
|
$organizerId = get_post_meta($eventId, '_EventOrganizerID', true);
|
|
if ($organizerId) {
|
|
yield 'organizer' => $this->getOrganizerData((int) $organizerId);
|
|
}
|
|
|
|
// Taxonomies
|
|
yield 'categories' => wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
|
|
yield 'tags' => wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
|
|
|
|
// Cache the data for future use (convert generator to array)
|
|
// Note: This is done after yielding to maintain generator efficiency
|
|
// The next call will use the cached version
|
|
$dataToCache = [];
|
|
$dataToCache['id'] = $eventId;
|
|
$dataToCache['title'] = $event->post_title;
|
|
$dataToCache['content'] = $event->post_content;
|
|
$dataToCache['excerpt'] = $event->post_excerpt;
|
|
$dataToCache['status'] = $event->post_status;
|
|
$dataToCache['author'] = $event->post_author;
|
|
foreach ($this->getEventMetaKeys() as $key) {
|
|
$dataToCache[$key] = get_post_meta($eventId, $key, true);
|
|
}
|
|
if ($venueId) {
|
|
$dataToCache['venue'] = $this->getVenueData((int) $venueId);
|
|
}
|
|
if ($organizerId) {
|
|
$dataToCache['organizer'] = $this->getOrganizerData((int) $organizerId);
|
|
}
|
|
$dataToCache['categories'] = wp_get_post_terms($eventId, 'tribe_events_cat', ['fields' => 'ids']);
|
|
$dataToCache['tags'] = wp_get_post_terms($eventId, 'post_tag', ['fields' => 'ids']);
|
|
|
|
wp_cache_set($cacheKey, $dataToCache, self::CACHE_GROUP, self::CACHE_TTL);
|
|
}
|
|
|
|
/**
|
|
* Get TEC event meta keys
|
|
*/
|
|
private function getEventMetaKeys(): array {
|
|
return [
|
|
'_EventStartDate',
|
|
'_EventEndDate',
|
|
'_EventStartDateUTC',
|
|
'_EventEndDateUTC',
|
|
'_EventTimezone',
|
|
'_EventTimezoneAbbr',
|
|
'_EventAllDay',
|
|
'_EventDuration',
|
|
'_EventCost',
|
|
'_EventCurrencySymbol',
|
|
'_EventCurrencyCode',
|
|
'_EventURL',
|
|
'_EventShowMap',
|
|
'_EventShowMapLink',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get venue data efficiently
|
|
*/
|
|
private function getVenueData(int $venueId): ArrayObject {
|
|
$venue = get_post($venueId);
|
|
if (!$venue || $venue->post_type !== 'tribe_venue') {
|
|
return new ArrayObject();
|
|
}
|
|
|
|
return new ArrayObject([
|
|
'id' => $venueId,
|
|
'title' => $venue->post_title,
|
|
'address' => get_post_meta($venueId, '_VenueAddress', true),
|
|
'city' => get_post_meta($venueId, '_VenueCity', true),
|
|
'state' => get_post_meta($venueId, '_VenueStateProvince', true),
|
|
'zip' => get_post_meta($venueId, '_VenueZip', true),
|
|
'country' => get_post_meta($venueId, '_VenueCountry', true),
|
|
'phone' => get_post_meta($venueId, '_VenuePhone', true),
|
|
'url' => get_post_meta($venueId, '_VenueURL', true),
|
|
], ArrayObject::ARRAY_AS_PROPS);
|
|
}
|
|
|
|
/**
|
|
* Get organizer data efficiently
|
|
*/
|
|
private function getOrganizerData(int $organizerId): ArrayObject {
|
|
$organizer = get_post($organizerId);
|
|
if (!$organizer || $organizer->post_type !== 'tribe_organizer') {
|
|
return new ArrayObject();
|
|
}
|
|
|
|
return new ArrayObject([
|
|
'id' => $organizerId,
|
|
'title' => $organizer->post_title,
|
|
'phone' => get_post_meta($organizerId, '_OrganizerPhone', true),
|
|
'website' => get_post_meta($organizerId, '_OrganizerWebsite', true),
|
|
'email' => get_post_meta($organizerId, '_OrganizerEmail', true),
|
|
], ArrayObject::ARRAY_AS_PROPS);
|
|
}
|
|
|
|
/**
|
|
* Handle form submission with comprehensive validation
|
|
*/
|
|
public function handleFormSubmission(): void {
|
|
if (!$this->isEditPage() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
return;
|
|
}
|
|
|
|
// Verify nonce
|
|
if (!isset($_POST[self::NONCE_FIELD]) ||
|
|
!wp_verify_nonce($_POST[self::NONCE_FIELD], self::NONCE_ACTION)) {
|
|
wp_die('Security check failed');
|
|
}
|
|
|
|
// Check capabilities
|
|
$eventId = isset($_POST['event_id']) ? (int) $_POST['event_id'] : 0;
|
|
if (!$this->canUserEditEvent($eventId)) {
|
|
wp_die('Insufficient permissions');
|
|
}
|
|
|
|
try {
|
|
$eventId = $this->saveEvent($_POST);
|
|
|
|
// Clear cache
|
|
wp_cache_delete("event_data_{$eventId}", self::CACHE_GROUP);
|
|
|
|
// Redirect with success message
|
|
wp_safe_redirect(add_query_arg([
|
|
'event_id' => $eventId,
|
|
'updated' => 'true'
|
|
], home_url('/trainer/event/edit/')));
|
|
exit;
|
|
|
|
} catch (Exception $e) {
|
|
// Log error and show message
|
|
error_log('Event save error: ' . $e->getMessage());
|
|
wp_die('Error saving event: ' . esc_html($e->getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save event with validation and sanitization
|
|
*/
|
|
private function saveEvent(array $data): int {
|
|
$eventId = isset($data['event_id']) ? (int) $data['event_id'] : 0;
|
|
|
|
// Prepare post data
|
|
$postData = [
|
|
'ID' => $eventId,
|
|
'post_type' => 'tribe_events',
|
|
'post_title' => sanitize_text_field($data['post_title'] ?? ''),
|
|
'post_content' => wp_kses_post($data['post_content'] ?? ''),
|
|
'post_excerpt' => sanitize_textarea_field($data['post_excerpt'] ?? ''),
|
|
'post_status' => in_array($data['post_status'] ?? '', ['publish', 'draft'])
|
|
? $data['post_status']
|
|
: 'draft',
|
|
];
|
|
|
|
// Validate required fields
|
|
if (empty($postData['post_title'])) {
|
|
throw new InvalidArgumentException('Event title is required');
|
|
}
|
|
|
|
// Insert or update
|
|
if ($eventId > 0) {
|
|
$result = wp_update_post($postData, true);
|
|
} else {
|
|
$postData['post_author'] = get_current_user_id();
|
|
$result = wp_insert_post($postData, true);
|
|
}
|
|
|
|
if (is_wp_error($result)) {
|
|
throw new RuntimeException($result->get_error_message());
|
|
}
|
|
|
|
$eventId = (int) $result;
|
|
|
|
// Update meta fields
|
|
$this->updateEventMeta($eventId, $data);
|
|
|
|
// Update venue
|
|
$this->updateVenue($eventId, $data);
|
|
|
|
// Update organizer
|
|
$this->updateOrganizer($eventId, $data);
|
|
|
|
// Update taxonomies
|
|
if (isset($data['event_categories'])) {
|
|
wp_set_post_terms($eventId, array_map('intval', $data['event_categories']), 'tribe_events_cat');
|
|
}
|
|
|
|
return $eventId;
|
|
}
|
|
|
|
/**
|
|
* Update event meta fields with validation
|
|
*/
|
|
private function updateEventMeta(int $eventId, array $data): void {
|
|
// Date/time fields
|
|
$startDate = $this->parseDateTime($data['EventStartDate'] ?? '', $data['EventStartTime'] ?? '');
|
|
$endDate = $this->parseDateTime($data['EventEndDate'] ?? '', $data['EventEndTime'] ?? '');
|
|
|
|
if ($startDate && $endDate) {
|
|
if ($endDate < $startDate) {
|
|
throw new InvalidArgumentException('End date must be after start date');
|
|
}
|
|
|
|
update_post_meta($eventId, '_EventStartDate', $startDate->format('Y-m-d H:i:s'));
|
|
update_post_meta($eventId, '_EventEndDate', $endDate->format('Y-m-d H:i:s'));
|
|
update_post_meta($eventId, '_EventStartDateUTC', get_gmt_from_date($startDate->format('Y-m-d H:i:s')));
|
|
update_post_meta($eventId, '_EventEndDateUTC', get_gmt_from_date($endDate->format('Y-m-d H:i:s')));
|
|
update_post_meta($eventId, '_EventDuration', $endDate->getTimestamp() - $startDate->getTimestamp());
|
|
}
|
|
|
|
// Other meta fields
|
|
$metaFields = [
|
|
'_EventAllDay' => isset($data['EventAllDay']) ? '1' : '0',
|
|
'_EventCost' => sanitize_text_field($data['EventCost'] ?? ''),
|
|
'_EventCurrencySymbol' => sanitize_text_field($data['EventCurrencySymbol'] ?? '$'),
|
|
'_EventURL' => esc_url_raw($data['EventURL'] ?? ''),
|
|
'_EventShowMap' => isset($data['EventShowMap']) ? '1' : '0',
|
|
'_EventShowMapLink' => isset($data['EventShowMapLink']) ? '1' : '0',
|
|
'_EventTimezone' => sanitize_text_field($data['EventTimezone'] ?? get_option('timezone_string')),
|
|
];
|
|
|
|
foreach ($metaFields as $key => $value) {
|
|
update_post_meta($eventId, $key, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse date and time into DateTime object
|
|
*/
|
|
private function parseDateTime(string $date, string $time): ?DateTime {
|
|
if (empty($date)) {
|
|
return null;
|
|
}
|
|
|
|
$dateTimeStr = $date;
|
|
if (!empty($time)) {
|
|
$dateTimeStr .= ' ' . $time;
|
|
} else {
|
|
$dateTimeStr .= ' 00:00:00';
|
|
}
|
|
|
|
try {
|
|
return new DateTime($dateTimeStr, wp_timezone());
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update or create venue
|
|
*/
|
|
private function updateVenue(int $eventId, array $data): void {
|
|
$venueId = isset($data['venue_id']) ? (int) $data['venue_id'] : 0;
|
|
|
|
// Use existing venue
|
|
if ($venueId > 0 && isset($data['use_existing_venue'])) {
|
|
update_post_meta($eventId, '_EventVenueID', $venueId);
|
|
return;
|
|
}
|
|
|
|
// Create new venue if data provided
|
|
if (!empty($data['venue_name'])) {
|
|
$venueData = [
|
|
'post_type' => 'tribe_venue',
|
|
'post_title' => sanitize_text_field($data['venue_name']),
|
|
'post_status' => 'publish',
|
|
];
|
|
|
|
if ($venueId > 0) {
|
|
$venueData['ID'] = $venueId;
|
|
$venueId = wp_update_post($venueData);
|
|
} else {
|
|
$venueId = wp_insert_post($venueData);
|
|
}
|
|
|
|
if ($venueId && !is_wp_error($venueId)) {
|
|
// Update venue meta
|
|
update_post_meta($venueId, '_VenueAddress', sanitize_text_field($data['venue_address'] ?? ''));
|
|
update_post_meta($venueId, '_VenueCity', sanitize_text_field($data['venue_city'] ?? ''));
|
|
update_post_meta($venueId, '_VenueStateProvince', sanitize_text_field($data['venue_state'] ?? ''));
|
|
update_post_meta($venueId, '_VenueZip', sanitize_text_field($data['venue_zip'] ?? ''));
|
|
update_post_meta($venueId, '_VenueCountry', sanitize_text_field($data['venue_country'] ?? ''));
|
|
update_post_meta($venueId, '_VenuePhone', sanitize_text_field($data['venue_phone'] ?? ''));
|
|
update_post_meta($venueId, '_VenueURL', esc_url_raw($data['venue_url'] ?? ''));
|
|
|
|
update_post_meta($eventId, '_EventVenueID', $venueId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update or create organizer
|
|
*/
|
|
private function updateOrganizer(int $eventId, array $data): void {
|
|
$organizerId = isset($data['organizer_id']) ? (int) $data['organizer_id'] : 0;
|
|
|
|
// Use existing organizer
|
|
if ($organizerId > 0 && isset($data['use_existing_organizer'])) {
|
|
update_post_meta($eventId, '_EventOrganizerID', $organizerId);
|
|
return;
|
|
}
|
|
|
|
// Create new organizer if data provided
|
|
if (!empty($data['organizer_name'])) {
|
|
$organizerData = [
|
|
'post_type' => 'tribe_organizer',
|
|
'post_title' => sanitize_text_field($data['organizer_name']),
|
|
'post_status' => 'publish',
|
|
];
|
|
|
|
if ($organizerId > 0) {
|
|
$organizerData['ID'] = $organizerId;
|
|
$organizerId = wp_update_post($organizerData);
|
|
} else {
|
|
$organizerId = wp_insert_post($organizerData);
|
|
}
|
|
|
|
if ($organizerId && !is_wp_error($organizerId)) {
|
|
// Update organizer meta
|
|
update_post_meta($organizerId, '_OrganizerPhone', sanitize_text_field($data['organizer_phone'] ?? ''));
|
|
update_post_meta($organizerId, '_OrganizerWebsite', esc_url_raw($data['organizer_website'] ?? ''));
|
|
update_post_meta($organizerId, '_OrganizerEmail', sanitize_email($data['organizer_email'] ?? ''));
|
|
|
|
update_post_meta($eventId, '_EventOrganizerID', $organizerId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user can edit event
|
|
*/
|
|
public function canUserEditEvent(int $eventId): bool {
|
|
if (!is_user_logged_in()) {
|
|
return false;
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
$userId = $user->ID;
|
|
|
|
// New event - check if user has the capability to create events
|
|
if ($eventId === 0) {
|
|
// Check for The Events Calendar capabilities or trainer role
|
|
return current_user_can('edit_tribe_events') ||
|
|
current_user_can('publish_tribe_events') ||
|
|
in_array('hvac_trainer', $user->roles) ||
|
|
in_array('hvac_master_trainer', $user->roles) ||
|
|
in_array('administrator', $user->roles);
|
|
}
|
|
|
|
// Existing event - validate ownership and permissions
|
|
$event = get_post($eventId);
|
|
if (!$event || $event->post_type !== 'tribe_events') {
|
|
return false;
|
|
}
|
|
|
|
// Check ownership FIRST - owners can always edit their own events
|
|
if ($event->post_author == $userId) {
|
|
return true;
|
|
}
|
|
|
|
// Administrators can edit any event
|
|
if (in_array('administrator', $user->roles)) {
|
|
return true;
|
|
}
|
|
|
|
// Master trainers can edit any event
|
|
if (in_array('hvac_master_trainer', $user->roles)) {
|
|
return true;
|
|
}
|
|
|
|
// Non-owners need special permissions to edit others' events
|
|
return current_user_can('edit_others_tribe_events') ||
|
|
current_user_can('edit_others_posts');
|
|
}
|
|
|
|
/**
|
|
* Get all venues for dropdown (using generator)
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getVenuesForDropdown(): Generator {
|
|
$venues = get_posts([
|
|
'post_type' => 'tribe_venue',
|
|
'posts_per_page' => 100, // Limit to prevent memory issues
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
'author' => get_current_user_id(),
|
|
]);
|
|
|
|
foreach ($venues as $venue) {
|
|
yield $venue->ID => $venue->post_title;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all organizers for dropdown (using generator)
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getOrganizersForDropdown(): Generator {
|
|
$organizers = get_posts([
|
|
'post_type' => 'tribe_organizer',
|
|
'posts_per_page' => 100, // Limit to prevent memory issues
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
'author' => get_current_user_id(),
|
|
]);
|
|
|
|
foreach ($organizers as $organizer) {
|
|
yield $organizer->ID => $organizer->post_title;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get event categories for checkboxes
|
|
*
|
|
* @return Generator<int, string>
|
|
*/
|
|
public function getCategoriesForCheckboxes(): Generator {
|
|
$categories = get_terms([
|
|
'taxonomy' => 'tribe_events_cat',
|
|
'hide_empty' => false,
|
|
]);
|
|
|
|
if (!is_wp_error($categories)) {
|
|
foreach ($categories as $category) {
|
|
yield $category->term_id => $category->name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Singleton trait for memory efficiency
|
|
*/
|
|
trait HVAC_Singleton_Trait {
|
|
private static ?self $instance = null;
|
|
|
|
public static function instance(): self {
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
private function __clone() {}
|
|
public function __wakeup() {
|
|
throw new Exception("Cannot unserialize singleton");
|
|
}
|
|
} |