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
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 inevnm_eventsuse'published'.
Scale Design
- Cursor (keyset) pagination on
evnm_occurrencesandevnm_rsvps- 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.