Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * Minasa XML Export (events + schedules + locations + organizers + dataProviders)
- * Optimized:
- * - Eligible event IDs are computed once and cached (transient + in-request memo)
- * - Generator loops IDs directly (no heavy WP_Query object loop)
- * - Uses site timezone, fixes date-only noon nudge, aligns schedules to <duration>
- * - Excludes ACF true/false remove_from_export
- */
- if (!defined('ABSPATH')) exit;
- /*------------------------------------------------------------------------------
- | CRON: daily generation + only-when-needed
- ------------------------------------------------------------------------------*/
- add_action('init', function () {
- if (!wp_next_scheduled('generate_azizi_xml_daily')) {
- wp_schedule_event(time(), 'daily', 'generate_azizi_xml_daily');
- }
- });
- add_action('generate_azizi_xml_daily', 'maybe_generate_azizi_xml');
- function minasa_get_latest_event_modified_gmt(): int {
- $q = new WP_Query([
- 'post_type' => 'offbeat-event',
- 'post_status' => 'publish',
- 'posts_per_page' => 1,
- 'orderby' => 'modified',
- 'order' => 'DESC',
- 'fields' => 'ids',
- 'no_found_rows' => true,
- 'update_post_term_cache' => false,
- 'update_post_meta_cache' => false,
- ]);
- if (!empty($q->posts)) {
- return (int) get_post_modified_time('U', true, $q->posts[0]); // GMT epoch
- }
- return 0;
- }
- function maybe_generate_azizi_xml() {
- $file_path = ABSPATH . 'azizi.xml';
- $last_run = file_exists($file_path) ? filemtime($file_path) : 0;
- $latest_mod = minasa_get_latest_event_modified_gmt();
- if ($latest_mod > $last_run) {
- generate_azizi_xml($file_path);
- }
- }
- /*------------------------------------------------------------------------------
- | CACHED ID LIST: compute once, cache, then reuse
- ------------------------------------------------------------------------------*/
- function minasa_get_export_event_ids(bool $force = false): array {
- static $memo = null; // in-request memoization
- if (!$force && is_array($memo)) {
- return $memo;
- }
- $todayYmd = date('Ymd'); // local date; matches how your ACF Ymd is compared elsewhere
- $latest_mod = minasa_get_latest_event_modified_gmt();
- // Transient key includes the day + latest modified GMT → auto-invalidates on change
- $t_key = "minasa_export_ids_{$todayYmd}_{$latest_mod}";
- if (!$force) {
- $cached = get_transient($t_key);
- if (is_array($cached)) {
- return $memo = $cached;
- }
- }
- // Build a lean ID-only query that excludes remove_from_export = true
- $args = [
- 'post_type' => 'offbeat-event',
- 'post_status' => 'publish',
- 'posts_per_page' => -1,
- 'fields' => 'ids',
- 'no_found_rows' => true,
- 'update_post_term_cache' => false,
- 'update_post_meta_cache' => false,
- 'meta_query' => [
- 'relation' => 'AND',
- // date-window inclusion
- [
- 'relation' => 'OR',
- [
- 'key' => 'edgtf_event_end_date',
- 'value' => $todayYmd,
- 'compare' => '>=',
- 'type' => 'NUMERIC',
- ],
- [
- 'relation' => 'AND',
- [
- 'key' => 'edgtf_event_end_date',
- 'value' => '',
- 'compare' => '=',
- ],
- [
- 'key' => 'edgtf_event_start_date',
- 'value' => $todayYmd,
- 'compare' => '>=',
- 'type' => 'NUMERIC',
- ],
- ],
- ],
- // exclude remove_from_export = true
- [
- 'relation' => 'OR',
- [
- 'key' => 'remove_from_export',
- 'compare' => 'NOT EXISTS',
- ],
- [
- 'key' => 'remove_from_export',
- 'value' => '1',
- 'compare' => '!=',
- ],
- ],
- ],
- ];
- $q = new WP_Query($args);
- $ids = $q->posts ?: [];
- // Cache (24h TTL; key auto-changes when content changes, so we don’t need to delete)
- set_transient($t_key, $ids, DAY_IN_SECONDS);
- return $memo = $ids;
- }
- // Optional: purge helper if you want to invalidate eagerly on edits.
- function minasa_purge_export_id_cache(): void {
- // No-op: keys are content-addressed. You can still delete all 'minasa_export_ids_*' if you track last key.
- }
- // Example eager purge on edits to relevant meta (optional)
- add_action('updated_post_meta', function($meta_id, $post_id, $meta_key) {
- if (get_post_type($post_id) !== 'offbeat-event') return;
- if (in_array($meta_key, ['edgtf_event_start_date','edgtf_event_end_date','remove_from_export'], true)) {
- minasa_purge_export_id_cache();
- }
- }, 10, 3);
- /*------------------------------------------------------------------------------
- | Helpers: date parsing + interval building + organizers/locations payloads
- ------------------------------------------------------------------------------*/
- function minasa_parse_date_v2($date_str, DateTimeZone $tz = null) {
- $date_str = trim((string) $date_str);
- if ($date_str === '') return null;
- $tz = $tz ?: wp_timezone();
- $norm = str_replace(['.', '/'], '-', $date_str);
- $candidates = array_unique([$date_str, $norm]);
- $formats = ['Ymd','Y-m-d','Y/m/d','d.m.Y','d/m/Y','m/d/Y','d-m-Y','m-d-Y'];
- foreach ($candidates as $cand) {
- foreach ($formats as $fmt) {
- $dt = DateTime::createFromFormat($fmt, $cand, $tz);
- if ($dt instanceof DateTime) {
- $err = DateTime::getLastErrors();
- if (($err['warning_count'] ?? 0) === 0 && ($err['error_count'] ?? 0) === 0) {
- $dt->setTime(0, 0, 0);
- return $dt;
- }
- }
- }
- }
- $ts = strtotime($norm);
- if ($ts !== false) {
- $dt = new DateTime('@' . $ts);
- $dt->setTimezone($tz);
- $dt->setTime(0, 0, 0);
- return $dt;
- }
- error_log("[Minasa] Failed to parse date '{$date_str}'");
- return null;
- }
- function minasa_build_interval_v2($start_date, $end_date, $start_time, $end_time) {
- $tz = wp_timezone();
- $sd = minasa_parse_date_v2($start_date, $tz);
- if (!$sd) return [null, null];
- $start_time = trim((string)$start_time);
- if ($start_time === '') {
- $sd->setTime(12, 0, 0); // noon to avoid UTC rollback on date-only
- } else {
- [$sh, $sm] = array_pad(explode(':', $start_time, 2), 2, '00');
- $sd->setTime((int)$sh, (int)$sm, 0);
- }
- if ($end_date || $end_time) {
- $ed = minasa_parse_date_v2($end_date ?: $start_date, $tz) ?: clone $sd;
- $end_time = trim((string)$end_time);
- if ($end_time === '') {
- $ed = clone $sd;
- $ed->add(new DateInterval('PT3H'));
- } else {
- [$eh, $em] = array_pad(explode(':', $end_time, 2), 2, '00');
- $ed->setTime((int)$eh, (int)$em, 0);
- if ($ed < $sd) {
- $ed = clone $sd;
- $ed->add(new DateInterval('PT1H'));
- }
- }
- } else {
- $ed = clone $sd;
- $ed->add(new DateInterval('PT3H'));
- }
- return [$sd, $ed];
- }
- function collect_location_term_payload($term, $data_provider_uuid) {
- $tid = (int) $term->term_id;
- $organizer = resolve_default_organizer_record($data_provider_uuid);
- return [
- 'organizerUuid7' => $organizer['uuid'],
- 'dataProviderUuid7' => $data_provider_uuid,
- 'created' => current_time(DATE_ATOM),
- 'modified' => current_time(DATE_ATOM),
- 'providerReference' => ['id' => (string)$tid, 'url' => get_term_link($term)],
- 'imageUrl' => '',
- 'title' => $term->name,
- 'street' => (string) get_field('street', "event_location_{$tid}"),
- 'zip' => (string) get_field('zip', "event_location_{$tid}"),
- 'city' => (string) get_field('city', "event_location_{$tid}"),
- 'country' => 'CH',
- 'phone' => (string) get_field('phone', "event_location_{$tid}"),
- 'email' => (string) get_field('email', "event_location_{$tid}"),
- 'url' => (string) get_field('web_url', "event_location_{$tid}"),
- ];
- }
- function resolve_organizer_for_post($post_id, $data_provider_uuid) {
- $name = trim((string) get_field('organizer_name', $post_id));
- $city = trim((string) get_field('organizer_city', $post_id));
- $zip = trim((string) get_field('organizer_zip', $post_id));
- $email = trim((string) get_field('organizer_email', $post_id));
- $url = trim((string) get_field('organizer_url', $post_id));
- if ($name !== '') {
- $key = sanitize_title($name);
- $uuid = wp_generate_uuid4();
- return [
- 'key' => $key,
- 'uuid' => $uuid,
- 'data' => [
- 'dataProviderUuid7' => $data_provider_uuid,
- 'created' => current_time(DATE_ATOM),
- 'modified' => current_time(DATE_ATOM),
- 'imageUrl' => '',
- 'providerReference' => ['id' => $key, 'url' => home_url('/organizer/' . $key)],
- 'title' => $name,
- 'zip' => $zip,
- 'city' => $city,
- 'country' => 'CH',
- 'email' => $email,
- 'url' => $url,
- ],
- ];
- }
- return resolve_default_organizer_record($data_provider_uuid);
- }
- function resolve_default_organizer_record($data_provider_uuid) {
- $site_name = get_bloginfo('name') ?: 'Site Organizer';
- $key = sanitize_title($site_name);
- $seed = md5(home_url() . '|organizer|' . $key);
- $uuid = sprintf('%08s-%04s-%04s-%04s-%12s',
- substr($seed, 0, 8), substr($seed, 8, 4), substr($seed, 12, 4),
- substr($seed, 16, 4), substr($seed, 20, 12)
- );
- return [
- 'key' => $key,
- 'uuid' => $uuid,
- 'data' => [
- 'dataProviderUuid7' => $data_provider_uuid,
- 'created' => current_time(DATE_ATOM),
- 'modified' => current_time(DATE_ATOM),
- 'imageUrl' => '',
- 'providerReference' => ['id' => 'organizer-' . $key, 'url' => home_url()],
- 'title' => $site_name,
- 'zip' => '',
- 'city' => '',
- 'country' => 'CH',
- 'email' => get_option('admin_email'),
- 'url' => home_url(),
- ],
- ];
- }
- /*------------------------------------------------------------------------------
- | Generator: loops cached IDs, primes caches, writes XML
- ------------------------------------------------------------------------------*/
- function generate_azizi_xml($file_path) {
- $site_tz = wp_timezone();
- // Data provider identity
- $data_provider_uuid = apply_filters('minasa_dataprovider_uuid7', '73308c2c-dbf4-710d-b896-19cc49673998');
- $data_provider_name = apply_filters('minasa_dataprovider_name', preg_replace('~https?://~', '', home_url()));
- // Get and cache eligible event IDs once
- $event_ids = minasa_get_export_event_ids();
- // Prime caches to reduce DB hits in the loop
- if ($event_ids) {
- // Prime post object + meta (private core function but stable), then term cache
- if (function_exists('_prime_post_caches')) {
- _prime_post_caches($event_ids, false, true);
- } else {
- update_meta_cache('post', $event_ids);
- }
- update_object_term_cache($event_ids, 'offbeat-event');
- }
- $writer = new XMLWriter();
- $writer->openURI($file_path);
- $writer->startDocument('1.0', 'UTF-8');
- $writer->setIndent(true);
- $writer->setIndentString(' ');
- $writer->startElement('minasa');
- $writer->writeAttribute('xsi:schemaLocation', 'https://minasa-demo.ch/services/export/azizi/1.0?schema');
- $writer->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
- $writer->writeAttribute('xmlns', 'https://minasa.ch/schema/v1');
- // Maps for referenced entities
- $location_map = [];
- $organizer_map = [];
- $touched_locations = [];
- $touched_organizers = [];
- /*----------------------------------
- | <events>
- ----------------------------------*/
- $writer->startElement('events');
- foreach ($event_ids as $event_id) {
- // Defensive skip if meta somehow says remove
- $remove_flag = get_post_meta($event_id, 'remove_from_export', true);
- if ($remove_flag === '1' || $remove_flag === 1) continue;
- $event_uuid = wp_generate_uuid4();
- // Post fields
- $title = get_the_title($event_id) ?: '';
- $content = apply_filters('the_content', (string) get_post_field('post_content', $event_id));
- $image = get_the_post_thumbnail_url($event_id, 'full');
- $ticket = (string) get_field('edgtf_tickets', $event_id);
- // Categories (adjust as needed)
- $main_cat = 'Musik';
- $subcats = get_the_terms($event_id, 'offbeat-event-category');
- $subcat_name = ($subcats && !is_wp_error($subcats)) ? $subcats[0]->name : '';
- // ACF date/time
- $start_date = get_field('edgtf_event_start_date', $event_id);
- $end_date = get_field('edgtf_event_end_date', $event_id);
- $start_time = get_field('edgtf_event_start_time', $event_id);
- $end_time = get_field('edgtf_event_end_time', $event_id);
- // Interval
- [$start_dt, $end_dt] = minasa_build_interval_v2($start_date, $end_date, $start_time, $end_time);
- // Location
- $loc_terms = get_the_terms($event_id, 'event_location');
- $loc_term = ($loc_terms && !is_wp_error($loc_terms)) ? $loc_terms[0] : null;
- $location_uuid = null;
- if ($loc_term) {
- $tid = (int) $loc_term->term_id;
- if (!isset($location_map[$tid])) {
- $location_map[$tid] = [
- 'uuid' => wp_generate_uuid4(),
- 'data' => collect_location_term_payload($loc_term, $data_provider_uuid),
- ];
- }
- $location_uuid = $location_map[$tid]['uuid'];
- $touched_locations[$location_uuid] = true;
- }
- // Organizer
- $org_payload = resolve_organizer_for_post($event_id, $data_provider_uuid);
- $org_key = $org_payload['key'];
- if (!isset($organizer_map[$org_key])) {
- $organizer_map[$org_key] = [
- 'uuid' => $org_payload['uuid'],
- 'data' => $org_payload['data'],
- ];
- }
- $organizer_uuid = $organizer_map[$org_key]['uuid'];
- $touched_organizers[$organizer_uuid] = true;
- // <event>
- $writer->startElement('event');
- $writer->writeElement('uuid7', $event_uuid);
- if ($location_uuid) $writer->writeElement('locationUuid7', $location_uuid);
- $writer->writeElement('organizerUuid7', $organizer_uuid);
- $writer->writeElement('dataProviderUuid7', $data_provider_uuid);
- $writer->writeElement('created', get_the_date(DATE_ATOM, $event_id));
- $writer->writeElement('modified', get_post_modified_time(DATE_ATOM, false, $event_id));
- $writer->writeElement('published', get_the_date(DATE_ATOM, $event_id));
- $writer->writeElement('eventClass', 'event');
- $writer->writeElement('eventMode', $location_uuid ? 'offline' : 'online');
- $writer->startElement('category');
- $writer->writeElement('mainCategory', $main_cat);
- // if ($subcat_name) $writer->writeElement('secondaryCategory', $subcat_name);
- $writer->endElement();
- if ($image) $writer->writeElement('imageUrl', esc_url_raw($image));
- $writer->writeElement('title', wp_strip_all_tags($title));
- $writer->startElement('description');
- $writer->writeCData($content);
- $writer->endElement();
- if (!empty($ticket)) $writer->writeElement('presaleUrl', esc_url_raw($ticket));
- // duration (dates only)
- $writer->startElement('duration');
- $dur_from = null;
- $dur_to = null;
- if ($start_date) {
- $dur_from = preg_replace('/(\d{4})[-\/.]?(\d{2})[-\/.]?(\d{2})/', '$1-$2-$3', $start_date);
- $writer->writeElement('from', $dur_from);
- }
- if ($end_date ?: $start_date) {
- $to = $end_date ?: $start_date;
- $dur_to = preg_replace('/(\d{4})[-\/.]?(\d{2})[-\/.]?(\d{2})/', '$1-$2-$3', $to);
- $writer->writeElement('to', $dur_to);
- }
- $writer->endElement(); // duration
- // schedules
- if ($start_dt) {
- // Align schedules' calendar dates with duration (keep time)
- if (!empty($dur_from)) {
- $from_dt = DateTime::createFromFormat('Y-m-d', $dur_from, $site_tz);
- if ($from_dt instanceof DateTime && $start_dt->format('Y-m-d') !== $from_dt->format('Y-m-d')) {
- $start_dt->setDate((int)$from_dt->format('Y'), (int)$from_dt->format('m'), (int)$from_dt->format('d'));
- }
- }
- if (!empty($dur_to) && $end_dt instanceof DateTime && empty($end_time)) {
- $to_dt = DateTime::createFromFormat('Y-m-d', $dur_to, $site_tz);
- if ($to_dt instanceof DateTime && $end_dt->format('Y-m-d') !== $to_dt->format('Y-m-d')) {
- $end_dt->setDate((int)$to_dt->format('Y'), (int)$to_dt->format('m'), (int)$to_dt->format('d'));
- }
- }
- $writer->startElement('schedules');
- $writer->startElement('schedule');
- $writer->writeElement('uuid7', wp_generate_uuid4());
- if ($location_uuid) $writer->writeElement('locationUuid7', $location_uuid);
- $writer->writeElement('created', get_the_date(DATE_ATOM, $event_id));
- $writer->writeElement('modified', get_post_modified_time(DATE_ATOM, false, $event_id));
- $writer->writeElement('eventStatus', 'scheduled');
- $writer->writeElement('start', (clone $start_dt)->setTimezone($site_tz)->format(DATE_ATOM));
- $writer->writeElement('end', (clone $end_dt)->setTimezone($site_tz)->format(DATE_ATOM));
- $writer->startElement('recurrence');
- $writer->writeElement('frequency', 'single');
- $writer->endElement();
- $writer->writeElement('soldOut', 'false');
- $writer->endElement(); // schedule
- $writer->endElement(); // schedules
- } else {
- error_log("[Minasa] Skipped schedules for event #{$event_id} — no parsable start date/time");
- }
- // providerReference
- $writer->startElement('providerReference');
- $writer->writeElement('id', (string)$event_id);
- $writer->writeElement('url', get_permalink($event_id));
- $writer->endElement();
- $writer->endElement(); // </event>
- }
- $writer->endElement(); // </events>
- /*----------------------------------
- | <locations> (only those referenced)
- ----------------------------------*/
- $writer->startElement('locations');
- foreach ($location_map as $term_id => $payload) {
- $uuid = $payload['uuid'];
- if (empty($touched_locations[$uuid])) continue;
- $d = $payload['data'];
- $writer->startElement('location');
- $writer->writeElement('uuid7', $uuid);
- if (!empty($d['organizerUuid7'])) $writer->writeElement('organizerUuid7', $d['organizerUuid7']);
- if (!empty($d['dataProviderUuid7'])) $writer->writeElement('dataProviderUuid7', $d['dataProviderUuid7']);
- $writer->writeElement('created', $d['created']);
- $writer->writeElement('modified', $d['modified']);
- if (!empty($d['providerReference'])) {
- $writer->startElement('providerReference');
- if (!empty($d['providerReference']['id'])) $writer->writeElement('id', $d['providerReference']['id']);
- if (!empty($d['providerReference']['url'])) $writer->writeElement('url', $d['providerReference']['url']);
- $writer->endElement();
- }
- if (!empty($d['imageUrl'])) $writer->writeElement('imageUrl', $d['imageUrl']);
- $writer->startElement('address');
- $writer->writeElement('legalType', 'organization');
- if (!empty($d['title'])) $writer->writeElement('title', $d['title']);
- if (!empty($d['street'])) $writer->writeElement('street', $d['street']);
- if (!empty($d['zip'])) $writer->writeElement('zip', $d['zip']);
- if (!empty($d['city'])) $writer->writeElement('city', $d['city']);
- $writer->writeElement('country', $d['country'] ?: 'CH');
- if (!empty($d['phone'])) $writer->writeElement('phone', $d['phone']);
- if (!empty($d['email'])) $writer->writeElement('email', $d['email']);
- if (!empty($d['url'])) $writer->writeElement('url', $d['url']);
- $writer->endElement(); // address
- $writer->startElement('contacts'); $writer->endElement();
- $writer->endElement(); // location
- }
- $writer->endElement(); // </locations>
- /*----------------------------------
- | <organizers> (only those referenced)
- ----------------------------------*/
- $writer->startElement('organizers');
- foreach ($organizer_map as $key => $payload) {
- $uuid = $payload['uuid'];
- if (empty($touched_organizers[$uuid])) continue;
- $d = $payload['data'];
- $writer->startElement('organizer');
- $writer->writeElement('uuid7', $uuid);
- $writer->writeElement('dataProviderUuid7', $d['dataProviderUuid7']);
- $writer->writeElement('created', $d['created']);
- $writer->writeElement('modified', $d['modified']);
- if (!empty($d['imageUrl'])) $writer->writeElement('imageUrl', $d['imageUrl']);
- if (!empty($d['providerReference'])) {
- $writer->startElement('providerReference');
- if (!empty($d['providerReference']['id'])) $writer->writeElement('id', $d['providerReference']['id']);
- if (!empty($d['providerReference']['url'])) $writer->writeElement('url', $d['providerReference']['url']);
- $writer->endElement();
- }
- $writer->startElement('address');
- $writer->writeElement('legalType', 'organization');
- if (!empty($d['title'])) $writer->writeElement('title', $d['title']);
- if (!empty($d['zip'])) $writer->writeElement('zip', $d['zip']);
- if (!empty($d['city'])) $writer->writeElement('city', $d['city']);
- $writer->writeElement('country', $d['country'] ?: 'CH');
- if (!empty($d['email'])) $writer->writeElement('email', $d['email']);
- if (!empty($d['url'])) $writer->writeElement('url', $d['url']);
- $writer->endElement(); // address
- $writer->startElement('contacts'); $writer->endElement();
- $writer->endElement(); // organizer
- }
- $writer->endElement(); // </organizers>
- /*----------------------------------
- | <dataProviders>
- ----------------------------------*/
- $writer->startElement('dataProviders');
- $writer->startElement('dataProvider');
- $writer->writeElement('uuid7', $data_provider_uuid);
- $writer->writeElement('provider', $data_provider_name);
- $writer->endElement();
- $writer->endElement(); // </dataProviders>
- $writer->endElement(); // </minasa>
- $writer->endDocument();
- $writer->flush();
- }
- add_action('admin_init', function () {
- if (isset($_GET['regen_azizi_xml']) && current_user_can('manage_options')) {
- generate_azizi_xml(ABSPATH . 'azizi.xml');
- wp_die('Azizi XML regenerated at ' . ABSPATH . 'azizi.xml');
- }
- });
Advertisement
Add Comment
Please, Sign In to add comment