Eventonomy

Architecture

Eventonomy is built in seven well-separated layers. Understanding them tells you exactly where to put code, how to resolve dependencies, and why certain patterns are required.

Full reference: docs/ARCHITECTURE.md in the plugin root. This page is a navigable summary; the source file is the authority.

What You Will Learn

  • The seven layers and what belongs in each one
  • How auto-discovery keeps parallel development conflict-free
  • The data model: tables, indexes, and JSON columns
  • How the Free / Pro seam works

The Seven Layers

Layer Location Rule
Bootstrap eventonomy.php, Core/Plugin Wiring only - no logic
Container Core/ServiceContainer bind() (swap one) / tag() (add to collection) / get() / tagged()
Repository Repository/* + Contracts/*Interface The only SQL layer; prepared statements; query-args filters; cache incrementor
Services Services/* Business logic; returns arrays or WP_Error; fires before/after hooks; never echo
Surface REST/Controller/*, CLI/*, Blocks/*, src/blocks/* Thin - validate → service → format
Templates templates/* Presentation only; zero DB; $view_data via evnm_get_view_data()
Admin Admin/* Custom UI; REST/Settings-API backed; no alert/confirm

The container accessor: evnm( ContractClass::class ) is the public API for resolving any service. Always resolve a contract, never new a concrete class.

Auto-Discovery (The Backbone)

Auto-discovery keeps parallel feature branches conflict-free - you never edit a shared registrar to add something new.

What you want to add How to add it
A new service or repository Add one *Provider.php to includes/Providers/
A new REST endpoint Add one *Controller.php to includes/REST/Controller/
A new Gutenberg block Add a src/blocks/*/block.json directory
A new admin page Add one *Page.php implementing PageInterface to includes/Admin/

The registrars (RestRegistrar, BlockRegistrar, AdminMenu, ServiceContainer::load_providers()) scan those paths automatically.

Data Model

Eight custom tables, all InnoDB / utf8mb4:

Table Contents
evnm_events Core event rows. Hot-path filter/sort fields (start_local, status, author_id, category) are real indexed columns.
evnm_occurrences One row per event instance. Recurring events generate many rows here via OccurrenceMaterializer. Keyset-paginated on (id).
evnm_rsvps Attendee registrations. Cursor-paginated (the largest table on busy sites).
evnm_tickets Ticket types per event.
evnm_orders Registration orders - $0 (Free) and paid (Pro).
evnm_meta Key/value metadata for any resource type.
evnm_venues Shared venue catalog with optional lat/lng.
evnm_organizers Shared organizer catalog.

Pro adds evnm_follows for organizer following.

JSON columns (cold data only - never queried in WHERE): organizer, venue (snapshot on the event), recurrence, settings, response. Do not add WHERE column->'$.key' queries - keep indexes on real columns.

Categories and tags use native WordPress taxonomies bound to the evnm_event object-type string - not a CPT, just the taxonomy registration.

Event Status Values

Eventonomy uses its own status strings - not WordPress's publish:

draft | pending | published | cancelled | private

Important: Use 'published', not 'publish'. WordPress pages that Eventonomy creates use 'publish' because they are standard WP pages - but the event rows in evnm_events use 'published'.

Scale Design

  • Cursor (keyset) pagination on evnm_occurrences and evnm_rsvps - avoids OFFSET scans on large tables.
  • Cache incrementor invalidation - wp_cache_set_last_changed() keeps shared data fresh without a per-row flush strategy.
  • Atomic ticket increment - TicketRepository::increment_sold() uses an atomic SQL increment to prevent oversell under concurrent load.
  • Background fan-out - notifications and occurrence expansion run via cron / Action Scheduler, not inline in the request.

The Free / Pro Seam

Pro extends Free only through the public extension surface:

  • evnm_* hooks (actions and filters)
  • Eventonomy\Contracts\* interfaces resolved through evnm()
  • The service container's tag() and bind() methods

Pro never imports Free concrete classes. The payment seam is apply_filters('evnm_process_payment', null, $order, $request) - Free returns WP_Error(402) for paid orders; Pro hooks in to capture payment and returns a result.

This means your own add-ons can extend Eventonomy with the same depth that Pro does.

What's Next?

Browse the full REST API contract.

REST API Reference →