Recurring Events Architecture
The system supports recurring events using a hybrid virtual/materialized instance pattern:
- Recurring Parent: An event with
evt_recurrence_typeset (not null). Holds the recurrence pattern. Hidden from public listings. - Virtual Instance: Computed in-memory (stdClass) from the parent's pattern. No database row. Registration closed.
- Materialized Instance: A real
evt_eventsrow created by admin action. Fully independent after creation.
Key checks
$event->is_recurring_parent(); // evt_recurrence_type IS NOT NULL
$event->is_instance(); // evt_parent_event_id IS NOT NULLMaterialization
Materialization is admin-initiated only (via admin event detail page). Virtual instances become materialized instances when admin clicks "Materialize" or "Cancel".
URL routing
/event/{slug}/{date} for recurring event instances (e.g., /event/weekly-class/2025-03-05). The slug identifies the parent, the date selects the occurrence.
Public listings
Use MultiEvent::getWithRepeatingEvents() to get a merged, deduplicated, sorted array of standalone events plus expanded recurring instances:
// Get upcoming events with recurring series expanded (default 6-month range)
$events = MultiEvent::getWithRepeatingEvents(
['upcoming' => true, 'deleted' => false, 'visibility' => 1],
null, // range_end (default: +6 months)
20 // limit (optional)
);Returns a mixed array of Event objects and virtual stdClass instances. Handles all deduplication between materialized instances and virtual instances automatically.
Do NOT manually query with exclude_recurring_parents and merge get_instances_for_range() — use getWithRepeatingEvents() instead to avoid duplicate materialized instance bugs.
Key model methods
$parent->get_instances_for_range($start, $end)— returns mixed array of Event and stdClass objects$parent->materialize_instance($date)— creates DB row, returns Event$parent->compute_occurrence_dates($from, $count)— pure date math$parent->get_recurrence_description()— human-readable pattern text