- Added missing template_include filter to HVAC_Custom_Event_Edit class - Added loadTemplate method to handle template loading for rewrite rules - Added support for hierarchical URL /trainer/event/edit in load_custom_templates - Created WordPress page at /trainer/event/edit/ for custom form - Assigned page-edit-event-custom.php template to the page This ensures the custom event edit form loads correctly without JavaScript dependencies
558 lines
No EOL
18 KiB
PHP
558 lines
No EOL
18 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']);
|
|
add_filter('template_include', [$this, 'loadTemplate']);
|
|
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 {
|
|
if (get_query_var('hvac_event_edit') === '1') {
|
|
$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 {
|
|
return get_query_var('hvac_event_edit') === '1'
|
|
|| (is_page() && get_page_template_slug() === 'templates/page-edit-event-custom.php');
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
yield from $cached;
|
|
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']);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
// New event
|
|
if ($eventId === 0) {
|
|
return current_user_can('edit_posts');
|
|
}
|
|
|
|
// Existing event
|
|
$event = get_post($eventId);
|
|
if (!$event || $event->post_type !== 'tribe_events') {
|
|
return false;
|
|
}
|
|
|
|
// Check ownership or admin capability
|
|
$userId = get_current_user_id();
|
|
return $event->post_author === $userId || 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' => -1,
|
|
'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' => -1,
|
|
'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");
|
|
}
|
|
} |