Architecture
This page is the architectural overview of Dino Discover for developers about to extend the plugin or read the codebase. It maps the moving parts and calls out the design invariants you should know before you start patching.
The canonical, fully-detailed architecture document lives in the
repo as dino-discover-technical-design.md. This page is the
public-docs distillation.
High-level systems map
Section titled “High-level systems map”┌──────────────────────────────────────────────────────────────┐│ WordPress Admin ││ ┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐ ││ │ Builder SPA │ │ Dashboard │ │ Coverage / Settings │ ││ │ (@wp/element)│ │ │ │ │ ││ └──────┬───────┘ └──────┬───────┘ └─────────┬───────────┘ ││ └──────── REST: /wp-json/dino-discover/v1 ────────┐ │└──────────────────────────────────────────────────────────┼────┘ │┌──────────────────────────────────────────────────────────▼────┐│ PHP (includes/) ││ ┌─────────────┐ ┌─────────────────────────────┐ ││ │ Controllers │──▶│ Engines │ ││ │ (API/) │ │ Question, Recommendation, │ ││ └─────────────┘ │ Coverage, Statistics │ ││ │ └────────────┬────────────────┘ ││ ▼ ▼ ││ ┌─────────────┐ ┌─────────────────┐ ││ │Repositories │ │ Snapshot │ ││ │(Repository/)│ │ Hydrator │ ││ └──────┬──────┘ └─────────┬───────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────────────────────────────┐ ││ │ dd_* tables (drafts, snapshots, │ ││ │ sessions, events, leads, coverage) │ ││ └─────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐│ Storefront (Preact via preact/compat) ││ bootstrap ──▶ session ──▶ events ──▶ recommend ──▶ lead │└────────────────────────────────────────────────────────────────┘The two SPAs — Builder (React) and Storefront quiz (Preact) — both
talk to the same PHP layer through the REST namespace
/wp-json/dino-discover/v1/. Engines do the work; repositories own
database access; the snapshot layer is the bridge between the
mutable draft and the immutable published version.
Module layers
Section titled “Module layers”- Admin SPA (
src/admin/) — React via@wordpress/element. No bundle-size budget here; the Builder is admin-only. - Storefront bundle (
src/storefront/) — Preact via thepreact/compatalias to stay under the storefront budget. - REST API (
includes/API/) — 13 controllers, ~55 routes underdino-discover/v1. - Engines (
includes/Engine/) — Question, Recommendation, Coverage, Statistics. Plus the Analytics surface (EventAggregator,SankeyBuilder,AttributionLinker). - Repositories (
includes/Repository/) — one per domain: Quiz, Question, Answer, Upvote, Session, Event, Lead, QuizSlug, Coverage. - Cron (
includes/Cron/) — Action Scheduler adapter + four jobs: EventFlush, EventPrune, CoverageRefresh, DailyRollup. - Storefront PHP (
includes/Storefront/) — Renderer, AssetLoader, Shortcode, TemplateLoader, DraftPreviewPage. - Snapshot (
includes/Snapshot/) — Snapshotter (publish + restore), QuizHydrator, DraftHydrator.
The container
Section titled “The container”The plugin uses PHP-DI 7 as its container. The composition root
is includes/Plugin.php. Every engine, repository, and controller is
registered through the container; bindings can be replaced through
WordPress filters at boot time:
add_filter( 'dino_discover_recommendation_engine', function( $default ) { return new \MyAgency\AiRecommendationEngine();} );The global dino_discover_<name>_engine filter is what’s emitted
today. A per-quiz variant (dino_discover_<name>_engine_for_quiz) is
named in the design doc as a v2 affordance, but no callsite exists in
includes/ yet — depend on the global filter only.
The four engines in one diagram
Section titled “The four engines in one diagram”Storefront session calls: Admin calls: POST /sessions ──┐ POST /publish ──┐ POST /events │ │ POST /recommend │ ┌───────────────┐ │ └─────▶│ QuestionEngine│ │ └──────┬────────┘ │ ▼ next_node(...) │ next question │ │ ┌───────────────────┐ │ │ RecommendationEng │◀──┐ │ └──────┬────────────┘ │ │ ▼ recommend(...) │ │ RecommendationSet │ │ │ │ Nightly + on publish + on demand: │ ▼ ┌────────────────────┐ │ ┌───────────────┐ │ CoverageEngine │◀─────────────────────────┴───────│ Snapshotter │ │ .refresh(...) │ └───────────────┘ └────────────────────┘ Admin Metrics tab: ┌───────────────┐ │StatisticsEng │ └───────────────┘Each engine is one PHP interface in includes/Engine/. The
boundaries between engines are deliberate — adding a new ranking
strategy means implementing one interface, not patching the
storefront and the REST layer.
The published-only invariant
Section titled “The published-only invariant”The single most load-bearing architectural property:
The storefront endpoints accept only published snapshots. Every public storefront route validates
published_snapshot_id IS NOT NULLbefore serving data.
The exception is the admin draft-preview tab, which lives behind an admin session + a one-shot nonce + a 30-minute expiry + audit logging. It’s the single endpoint allowed to read draft state, and it lives in a separate controller file from the public routes.
The invariant is enforced mechanically by a custom PHPStan rule
(includes/PHPStan/Rules/PublishedSnapshotOnlyRule.php). If a
contributor accidentally adds a storefront endpoint that reads a
draft, the static analyzer fails the build.
Optimistic locking
Section titled “Optimistic locking”Every admin write to a quiz carries an expected_draft_version. The
server compares it to the current draft version; mismatch returns
409 dd_draft_version_mismatch. This protects against two-admin
edits clobbering each other.
The dd_quizzes schema also carries draft_lock_user_id and
draft_lock_acquired_at columns reserved for a future advisory
“X is editing this quiz” banner. The columns are unused in 0.1.0 —
no code path reads or writes them yet.
HPOS compatibility
Section titled “HPOS compatibility”Dino Discover is HPOS-compatible:
- The plugin declares HPOS compatibility via
FeaturesUtil::declare_compatibilityinincludes/Plugin.php. - Every order read goes through
wc_get_orders(). - A custom PHPStan rule (
NoLegacyOrderTableReadRule) refuses code paths that reach the legacy posts table directly.
The plugin works on both HPOS-enabled and legacy-storage stores. The recommended configuration is HPOS-enabled because that’s where WooCommerce is going.
Caching layers
Section titled “Caching layers”Three layers, all keyed on a CacheVersionRegistry so publishes
invalidate the right entries without a wipe:
- Snapshot read cache (WP object cache) — the full snapshot payload by ID.
- Product expansion cache (WP object cache + transient fallback) — the set of products an answer’s upvote target expands to, with a 24-hour TTL.
- Result cache (WP object cache) — the recommendation payload
for the most recent recommend call per session, so
GET /sessions/{sid}/resultsis a cache read.
The cache layer lives in includes/Engine/Cache/.
Bundle budget
Section titled “Bundle budget”The storefront JavaScript has a hard CI gate on its gzipped size,
enforced by scripts/check-storefront-bundle-size.sh. Long-term
target ≤12 KB; current tripwire ≤18 KB (transiently bumped
on 2026-05-22 to land the results-page and lead-capture surfaces,
with a trim follow-up tracked openly).
Bootstrap response is variant-agnostic and immutable for one year;
the heavy storefront bundle only loads on first interaction.
What’s next
Section titled “What’s next”- Engine Interfaces — the four PHP contracts in detail.
- REST API Reference — the endpoint catalogue.
- Hook Reference — every named hook with its file:line.
- Performance Monitoring — the user-facing side of the bundle budget.