Skip to content

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.

┌──────────────────────────────────────────────────────────────┐
│ 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.

  1. Admin SPA (src/admin/) — React via @wordpress/element. No bundle-size budget here; the Builder is admin-only.
  2. Storefront bundle (src/storefront/) — Preact via the preact/compat alias to stay under the storefront budget.
  3. REST API (includes/API/) — 13 controllers, ~55 routes under dino-discover/v1.
  4. Engines (includes/Engine/) — Question, Recommendation, Coverage, Statistics. Plus the Analytics surface (EventAggregator, SankeyBuilder, AttributionLinker).
  5. Repositories (includes/Repository/) — one per domain: Quiz, Question, Answer, Upvote, Session, Event, Lead, QuizSlug, Coverage.
  6. Cron (includes/Cron/) — Action Scheduler adapter + four jobs: EventFlush, EventPrune, CoverageRefresh, DailyRollup.
  7. Storefront PHP (includes/Storefront/) — Renderer, AssetLoader, Shortcode, TemplateLoader, DraftPreviewPage.
  8. Snapshot (includes/Snapshot/) — Snapshotter (publish + restore), QuizHydrator, DraftHydrator.

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.

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 single most load-bearing architectural property:

The storefront endpoints accept only published snapshots. Every public storefront route validates published_snapshot_id IS NOT NULL before 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.

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.

Dino Discover is HPOS-compatible:

  • The plugin declares HPOS compatibility via FeaturesUtil::declare_compatibility in includes/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.

Three layers, all keyed on a CacheVersionRegistry so publishes invalidate the right entries without a wipe:

  1. Snapshot read cache (WP object cache) — the full snapshot payload by ID.
  2. Product expansion cache (WP object cache + transient fallback) — the set of products an answer’s upvote target expands to, with a 24-hour TTL.
  3. Result cache (WP object cache) — the recommendation payload for the most recent recommend call per session, so GET /sessions/{sid}/results is a cache read.

The cache layer lives in includes/Engine/Cache/.

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.