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.mdin 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
Free creates ten custom tables, all InnoDB / utf8mb4:
| Table | Contents |
|---|---|
evnm_events |
Core event rows. Hot-path filter/sort fields (status, author_id, featured, space_id, city) are real indexed columns, and "upcoming" sorts on the denormalized next_occurrence_utc pointer. |
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. One row per key, not many. |
evnm_venues |
Shared venue catalog with optional lat/lng. |
evnm_organizers |
Shared organizer catalog. |
evnm_webhook_events |
Gateway webhook ids already seen. The unique (gateway, event_id) is the replay-idempotency mechanism. |
evnm_notifications |
The in-app notification feed, also served to the mobile app. |
Pro adds five: evnm_follows (following), evnm_earnings + evnm_payouts + evnm_payout_debts (the commission ledger) and evnm_pro_feeds (calendar-sync subscriptions).
Note that there is no start_local and no category column on evnm_events. Start and end times live on evnm_occurrences (start_utc/start_local), and categories/tags are native WordPress taxonomies. Full column and index detail: Database Schema.
JSON columns (cold data only, never queried in WHERE): organizer, venue (snapshot on the event), recurrence, settings, response, meta, billing, early_bird, config. 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 inevnm_eventsuse'published'.
Scale Design
- Cursor (keyset) pagination on
evnm_occurrencesandevnm_rsvps, which avoidsOFFSETscans 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 throughevnm()- The service container's
tag()andbind()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.