Skip to main content

Directory Theme class reference

18 min read

Restaurant Theme — Class reference

The 27 classes that make _rs behave differently from the shared Directory core. Generated from the theme source.

← Back to Restaurant Theme features

Cart Cart.php​

Shop cart storage — add/update/remove/list, backed by a dedicated DB table rather than a PHP session (session-based carts don't survive a session expiry or play well with page caching). Table pattern mirrors Ppt\_rs\ListingViews' versioned dbDelta-on-init install. A cart belongs to a cart_key: 'u' . user_id when signed in, otherwise a random id in a long-lived httponly cookie (merged into the user's cart on login so items survive signing in). Rows only store the qty/variant — everything else (title, image, live price, live stock) is read fresh from the listing on every request via items(), the same "meta is the source of truth" approach the old DT10 cart used, without reviving its $_SESSION storage.

API
register()enqueueAssets()headerIcon()table()install()ensureCookie()cartKey()mergeOnLogin()adopt()items()count()subtotal()isEmpty()needsShipping()appliedCoupon()totals()clearCoupon()add()updateQty()removeRow()clear()handlePost()handleCouponPost()handleAjax()
inc/_rs/Cart.php

Checkout Checkout.php​

Restaurant checkout (cloned from the Shop's). The diner chooses pickup or delivery, ASAP or a time slot, gives a name, phone and (for delivery) an address, and pays online or — pickup only — on collection. Those rules live in Ppt_rs\Ordering and Ppt_rs\Hours; handlePay() re-checks all of them. A pay-on-collection order never goes to a gateway: it is placed pending (unpaid) and the kitchen marks it paid. The rest is the Shop's multi-item cart checkout. A sibling to Ppt\Payments\CheckoutFlow (which only ever checks out ONE line — a listing plan or an ad zone), not a modification of it — CheckoutFlow.php is not touched by this class. Reuses the same shared machinery CheckoutFlow does: the ppt_payment_gateways registry and Stripe/PayPal plugins (Ppt\Admin\Payments), the exact ppt_gateway_start / ppt_gateway_verify filter contracts, the same ppt_orders CPT (Ppt\Admin\Orders), the same /checkout/ + /callback/ virtual pages (Ppt\Frontend\Pages), and the same receipt email templates (Ppt\Content\EmailTemplates). All price math (discount/shipping/tax) comes from Cart::totals(), which itself reuses CheckoutFlow::totals() for the tax portion — so the admin's Design ▸ Checkout settings (Coupons/Tax/Shipping/ Tracking) apply here exactly as they do to a listing-plan purchase, without duplicating that math. Order refs are prefixed FOOD- (vs CheckoutFlow's PP-) so Pages::content() can route an incoming /callback/ to whichever class actually owns that order, with no shared state between the two. Known limitation: gateway webhook handlers (e.g. ppt-stripe.php's webhook listener) call \Ppt\Payments\CheckoutFlow::markPaid() directly — they don't know about this class. Orders still complete correctly via the synchronous browser return to /callback/ (the common path — a gateway's hosted checkout redirects back here on success), which is what this build's Test-gateway and Stripe test-mode verification exercises. Async-only confirmation (the buyer's tab never returns) isn't wired for shop orders without also touching the gateway plugins, which this plan deliberately leaves untouched.

API
register()checkoutUrl()callbackUrl()isShopRef()handlePay()ajaxQty()orderItems()markPaid()renderCheckout()settleReturn()renderCallback()orderContext()
inc/_rs/Checkout.php

DemoAccounts DemoAccounts.php​

Shop — what the DEMO member walks into. A shop has one kind of member: a customer. Empty, that hub shows nothing worth looking at, so when the demo member is provisioned this gives them three past purchases — real ppt_orders rows with the same fields _rs\Checkout::createOrder() writes (line items snapshotted by name and price, totals, a delivery address) at three stages of the fulfilment tracker: delivered a fortnight ago, shipped last week, packed today. The items come from the active niche's curated sample products, so they match the shop the visitor has just been browsing; nothing needs a product row to exist. Orders are authored by the member and demo-marked, so DemoLogin's purge deletes them with the account. Gated to the shop profile by Theme::boot()'s fork-gating (Ppt_rs*).

API
register()furnish()
inc/_rs/DemoAccounts.php

Dish Dish.php​

Restaurant dish data — what makes a Shop product (Ppt_rs\Product) a dish on a menu: dietary tags, the 14 major allergens, a "special" flag, a sold-out switch and option groups (sizes, toppings, sides) that add to the price. Owner decisions (2026-09-30): - Dietary tags are a FIXED six (DIETS), so every design can draw the same chips and a menu can be filtered on them. - Allergens have three states, never two: allergens ticked → "Contains …"; the separate "contains none of the 14" tick → "None of the 14 major allergens"; neither → "Ask us about allergens". An empty list must never read as allergen-free. - Sold out is "today" (clears itself when the site's date moves on, so the kitchen never has to remember to switch it back) or "until switched back". No stock counts. Options: each group is "choose one" (a size) or "choose any" (toppings, optionally capped), required or not; each choice may add to the price. Group and choice ids are derived from their NAMES (idFor()), so a basket row keeps pointing at the same choice across edits that don't rename it, and a renamed or deleted choice makes that row fail resolve() instead of silently charging an old price.

API
diets()allergenList()diet()allergens()allergenState()number()special()soldOutMode()soldOut()today()options()needsChoice()fromPrice()idFor()normaliseOptions()resolve()saveFromRequest()chilli()chips()allergenText()optionFields()
inc/_rs/Dish.php

Expiry Expiry.php​

PPT — listing expiry / "Listing lifetime". The Settings ▸ Listings "Listing lifetime" (days) governs how long a listing stays live. 0 = never expires. A value > 0 stamps an expiry timestamp on each listing when it's published; a daily cron then runs the configured "On expiry" action (listings_expiry_action: nothing / draft / pending / trash) once the time is up. The expiry timestamp meta is SHARED with the pricing-plan expiry (PricingPlans::LISTING_EXPIRES_META). A listing that carries a pricing plan is governed by that plan's own duration (set in the editor / at checkout), so the global lifetime only applies to listings WITHOUT a plan. Either way, the cron here enforces whatever expiry timestamp a listing ends up with, and both editors show the time remaining.

API
register()metaKey()schedule()unschedule()onTransition()applyLifetime()runCheck()timestamp()remaining()dateLabel()
inc/_rs/Expiry.php

Single-listing gallery styles. The admin picks one of four layouts on the Design ▸ Listings tab (stored in the ppt_design option via Branding), and the single-listing template renders the chosen layout for the listing's images. standard — large hero photo + thumbnail strip (click to swap) [default] grid — all photos in a tiled grid (first one featured) carousel — a swipeable/scroll-snap slider with prev/next tall — full-width photos stacked vertically (good for portraits)

API
styles()locked()current()isFullSpan()render()css()
inc/_rs/Gallery.php

Geocoder Geocoder.php​

Bulk geocoder — fills in lat/lng for existing listings that have an address but no coordinates (e.g. listings created before the editor's map picker existed), so they get a precise map marker and the "Distance from me" feature. Uses the site's Maps provider (Settings ▸ API keys): Google / Mapbox geocoding APIs (their key), or OpenStreetMap Nominatim (keyless, rate-limited to ~1 req/sec with an identifying UA). A small box on the PPT Listings screen runs it in batches over AJAX. Listings that can't be geocoded are flagged (_ppt_geo_failed) so they aren't retried forever.

API
register()geocode()pendingCount()ajaxBatch()assets()box()
inc/_rs/Geocoder.php

Hours Hours.php​

The restaurant's opening hours — ONE site-wide weekly schedule (a restaurant is one place) plus holiday closures. Ordering reads it to switch itself on and off (Ppt_rs\Ordering), and phase 5's table reservations will read the same schedule. Each day is closed or open with up to three shifts ("12:00–14:30" and "17:00–22:30"). A shift whose end is at or before its start runs past midnight and belongs to the day it STARTED on, so Friday "18:00–02:00" is still open at 01:00 on Saturday. Closures are date ranges (site dates, inclusive); a closed date cancels every shift that starts on it. Stored in its own option, edited on the Settings hub's "Opening hours" tile. The option is only registered on the restaurant profile, because options.php writes every option registered in the group — registering it where the panel is not on the page would wipe it on the next save of any other tile. All times are the site's timezone (Settings ▸ General).

API
register()registerSetting()applies()days()defaults()all()sanitize()normalise()tz()now()isClosedDate()closureNote()shiftsOn()shiftAt()isOpenAt()isOpenNow()nextOpening()label()panel()
inc/_rs/Hours.php

ListingActions ListingActions.php​

Per-listing visitor actions shown on the single-listing section nav: - Add to favorites — toggles the listing in the member's saved list (user meta ppt_favorites); the account page shows them. Logged-in only; guests are routed to sign-in. - Report — flags the listing to the site admin. The reason is stored as a comment on the listing (type ppt_report, a custom approval status so it stays hidden from the front-end and the normal comment-moderation tabs) and also emailed to the admin. Reports are reviewed in the admin Comments screen's dedicated "Reports" tab. Open to guests and members.

API
register()favIds()favHas()favToggle()favListingIds()ajaxFav()ajaxReport()assets()favButton()reportButton()
inc/_rs/ListingActions.php

ListingCard ListingCard.php​

The canonical listing card — the single, theme-wide way a business/listing is shown as a card. Every directory surface (search results, single-page "related", and the live listing-grid blocks) renders through here, so a listing looks the same everywhere and the demo-vs-live data split lives in ONE place. Data-source agnostic: fromPost() maps a real listing_type record and fromSample() maps a curated demo row into the same normalized shape, which render() draws. The markup is built on the theme design tokens, so the one card automatically adopts each design's colours/fonts. Its CSS is enqueued site-wide (assets/css/listing-card.css) — the card only emits markup. Normalized shape: name, link, img, cat, rating, desc, desc_long, price, city, dist, featured (bool), highlighted (bool), badge (string), id, buyable (bool), sold_out (bool) badge is an optional ribbon label (e.g. "New") for callers that need one; when empty it falls back to "Featured" if featured is set, or "Sold out" when sold_out is set (which also overrides "Featured"). Every card on this fork carries the same footer: the price on the LEFT and an "Add to cart" pill on the RIGHT. Which of three shapes that pill takes is decided by buyable / sold_out: - buyable (a product price, no variant group, in stock) → a real <button> that adds to the cart in place (assets/js/shop-cart.js). - sold_out → the same pill, disabled, reading "Sold out". - anything else with a price (variants to choose, or a demo/preview card with no post behind it) → the pill as a <span>, so the click falls through to the card's own link and lands on the product page where options are picked. It can't be an <a>: the whole card already is one, and anchors can't nest. Only a listing with NO price at all falls back to the "View details" text CTA — a card offering to add a priceless thing to a cart would be a lie.

API
fromPost()fromSample()render()
inc/_rs/ListingCard.php

ListingEditor ListingEditor.php​

Listing editor — the shared brain behind the two listing-edit screens: - Admin : PPT-styled screen at ?page=ppt_listings&edit=<id> (Chrome shell), rendered by Admin\Listings when the edit param is present. - Member : front-end screen at /account/listing/<id>/ inside the Member Hub, rendered by Account\MembersPage for a listing the member owns. Both POST to admin-post.php (action ppt_listing_save) and run through the one save() below, so the field set, sanitising and persistence live in a single place. The mode (admin|member) decides which extras are honoured: admins get status, author, slug, featured/verified and the "edit only" custom fields; members get a friendly subset scoped to their own listing. Fields = core (title, description, category, tags, featured image, gallery, FAQ) + every field defined in the Custom Fields admin (Admin\Fields / option ppt_fields), stored as post meta keyed by the field key — which is what the single-listing template already reads.

API
register()newUrl()handleNew()submissionsOpen()memberCanAdd()canEdit()adminEditUrl()memberEditUrl()applicableFields()locationKeys()coordKeys()mapPickerReady()mapConfig()locationFields()socialNetworks()socialValue()socialIcon()hoursDays()hoursValue()normTime()bookingValue()bookingsEnabled()bookingProviderAllowed()currentCategoryId()
inc/_rs/ListingEditor.php

ListingFaq ListingFaq.php​

Single-listing FAQ. Reads per-listing FAQ items from the faq post meta (an array of ['q' => …, 'a' => …]). A live listing shows only its own saved FAQ — when it has none the section is omitted rather than filled with fabricated entries. Demo/preview mode supplies sample FAQ via DemoContent so a fresh design isn't empty. Populate a listing's own FAQ by saving faq meta, inject a site-wide default set via the ppt_listing_faq_default filter, or adjust the final list with the ppt_listing_faq filter.

API
items()
inc/_rs/ListingFaq.php

ListingHours ListingHours.php​

Business hours display for the single-listing sidebar. Reads the business_hours meta written by the listing editor (ListingEditor::HOURS_META) and renders a Google-Business-style day list with an "Open now / Closed now" badge computed in the site's timezone. Times are formatted with the site's time format.

API
has()isOpenNow()renderSidebar()
inc/_rs/ListingHours.php

ListingLocation ListingLocation.php​

Single-listing "Location" section — the richer location block: the full formatted address, an interactive map (ListingMap, precise when the listing has coordinates), a "Get directions" button, and a "Distance from you" control (browser geolocation → straight-line distance to the listing). Self-contained: it prints its own scoped CSS + JS once, so single.php just calls render().

API
address()coords()has()render()
inc/_rs/ListingLocation.php

ListingMap ListingMap.php​

The single-listing "Location" map. Uses the site's Maps provider setting (Settings ▸ API keys — the same ppt_maps_provider filter the search map reads). When the listing has stored coordinates (lat/lng, set by the editor's map picker) the map centres on them precisely and drops a named marker; otherwise it falls back to geocoding the address string: google / mapbox(fallback) → a keyless Google Maps embed osm → a Leaflet map (marker from coords, else Nominatim) Map libraries load from their CDNs — the owner-approved front-end map exception.

API
render()
inc/_rs/ListingMap.php

ListingSections ListingSections.php​

PPT — configurable single-listing sections. The admin (Design ▸ Listings ▸ "Listing page sections") gets a sortable list of on/off toggles for the optional feature blocks a listing page can carry — Booking, Business hours, Maps & Location, FAQ, Reviews. Turning one OFF removes it from BOTH the single listing page and the listing submission/edit form; the order of the list controls the order the (main-column) sections appear in. Config lives in the shared design option (Branding::OPTION = ppt_design) under the listing_sections key: { order:[keys…], enabled:{key:0|1} }. Anything not saved yet defaults to ON, in the catalog order — so existing sites are unchanged until the admin edits the list.

API
supported()catalog()config()order()enabled()orderedAmong()sanitize()
inc/_rs/ListingSections.php

ListingSpecs ListingSpecs.php​

Single-listing Specifications — the long-form spec copy a product page carries under its description (materials, sizing, care, what's in the box…), stored as sanitised HTML in the specifications post meta. Deliberately NOT the same thing as the custom-field "Details" list on the same page: that one is a structured label→value table defined site-wide in the Custom Fields admin, whereas this is free-form copy written per product, with its own sub-headings and bullet lists. They render as two separate accordion sections. Demo/preview mode falls back to DemoContent::specifications() so a fresh design isn't empty, exactly as ListingFaq does — nothing fabricated is ever written to the database, and a live product with no specs simply omits the section.

API
get()has()set()
inc/_rs/ListingSpecs.php

ListingViews ListingViews.php​

PPT listing analytics — records a "hit" every time a single listing page is viewed, and serves that data back to the listing owner as a graph in the member area (/account ▸ My listings ▸ the analytics icon). Each view stores the listing id, the timestamp (UTC), and — when the visitor is logged in — their user id + display name, so an owner can see who has been looking. Bots/crawlers are skipped so counts reflect real visitors. The owner (and admins) can read the analytics for a listing over the AJAX endpoint, which returns a daily series (for the chart), rolling-window totals (7/30/60/90 days) and a list of recent named viewers. Storage: a custom table {prefix}ppt_listing_views (dbDelta on init).

API
register()table()install()maybeRecord()total()countSince()series()recentViewers()ajaxData()
inc/_rs/ListingViews.php

Media Media.php​

Listing images — the single, WordPress-native resolver for the PPT image system. A listing's photos are its WP featured image (primary) plus any image attachments parented to it (the gallery). No legacy ?imgid refs, no image-URL meta strings — everything flows through the media library.

API
primary()gallery()videos()media()counts()
inc/_rs/Media.php

Ordering Ordering.php​

Restaurant ordering rules — how a basket becomes a pickup or delivery order. Owner decisions (2026-09-30): - Delivery = ONE flat fee, a minimum order, an optional "free over" amount and a list of postcodes/areas served (empty list = anywhere). - Pay on collection is offered for PICKUP only; delivery is always paid online. - Diners order ASAP or for a time slot, up to N days ahead (0 = today only, the default). - While the kitchen is closed the menu and basket still work, and checkout offers only slots from the next opening — "pre-order the next opening" — never ASAP. Settings live in the Checkout option (ppt_checkout['ordering']), on the Checkout ▸ Ordering tab drawn by adminPanel() and saved through sanitize(). Opening hours and closures come from Ppt_rs\Hours. Everything the checkout form posts is re-checked here on submit (fromPost()): the mode is switched on, the slot is still offered and not full, the kitchen is open for ASAP, the postcode is served and the minimum is met.

API
defaults()get()pickupOn()deliveryOn()modes()collectionOn()prep()step()fee()minimumShort()areas()delivers()norm()asap()slots()ordersPerSlot()fromPost()errorText()sanitize()adminPanel()
inc/_rs/Ordering.php

Product Product.php​

A dish's price and buy box — the Shop's product class (this fork is a clone of _sp) turned into a dish on a restaurant menu. Price, the struck-through "was" price and the On-sale flag work exactly as on the Shop; stock counts, weight, shipping and the Shopify-style variant matrix are gone. Dietary tags, allergens, the special flag, the sold-out switch and the priced option groups live in Ppt_rs\Dish. On save, the base price also mirrors into the searchable price meta (SearchPage::PRICE_META), so the price sort/filter and the card price keep working.

API
has()price()compareAtPrice()discounted()onSale()setOnSale()sku()barcode()tracksStock()stock()inventoryPolicy()requiresShipping()weight()variantGroups()hasVariants()variantCombos()combo()effectivePrice()inStock()stockLabel()render()
inc/_rs/Product.php

Reviews Reviews.php​

Listing reviews — built on native WordPress comments so they moderate and store like any comment, but stamped with the comment type "review" (the identifier that distinguishes them from ordinary blog comments in the admin) and a 1–5 star rating saved as comment meta. Reviews are always enabled on the listing CPT. Rendered in the single-listing "Reviews" (summary + breakdown + cards) and "Add Review" (star input + comment form) sections.

API
register()open()stampType()saveRating()onCommentChange()onStatusChange()enabledFor()setEnabledFor()syncPostRating()count()all()stats()stars()renderSummary()renderList()renderForm()
inc/_rs/Reviews.php

SearchPage SearchPage.php​

Server-rendered search / archive for the listing post type — the clean PPT rebuild of DT10's PPT\Custom\Search\SearchPage. Runs the normal WordPress main query (WP_Query) via official hooks (pre_get_posts + template_include) so it is SEO-friendly and every core filter applies. Contexts it owns: - keyword search (/?s=…) - the listing post-type archive (/listing/) - listing category / tag archives Filters (GET, all optional, combinable): s keyword tax-listing_category category term id price1 / price2 min / max price (numeric, price meta) sort featured | newest | oldest | rating | price_low | price_high | title paged pagination

API
register()shapeQuery()nearCoords()radiusKm()distanceFrom()distanceLabel()radiusClausesMain()radiusClauses()featuredOrderClauses()isListingContext()postType()isMapTakeover()categoryTax()terms()taxQueryFromRequest()listingTaxonomies()hasActiveFilters()matchingIds()termCounts()isOnSaleOnly()onSaleMetaQuery()priceMetaQuery()sortOptions()template()
inc/_rs/SearchPage.php

Shipping Shipping.php​

PPT Shop — shipping for the cart. The rate-resolution logic now lives in the shared Ppt\Checkout\Shipping (so the Shop cart and the Auction lot payment price shipping identically); this class is the Shop's thin façade over it, keeping the existing _rs\Shipping::quote() call sites working and adding the one Shop-specific piece the shared engine can't own — summing a cart's line weights from each product (Product::weight()). See Ppt\Checkout\Shipping for the full 1-6 resolution order (disabled → restricted-country block → live carrier API → manual per-country rate → weight bands → flat rate, then the "free over" threshold).

API
quote()cartWeight()
inc/_rs/Shipping.php

SingleListing SingleListing.php​

Single-listing page for the listing_type post type — the clean PPT rebuild of DT10's PPT\Custom\Single\SingleListing. Owns template_include for singular listings and renders its own token-styled template inside the normal main loop. No legacy $CORE / membership plumbing; standard WordPress access rules apply.

API
register()template()related()
inc/_rs/SingleListing.php

TermMeta TermMeta.php​

Per-term IMAGE + ICON for the listing taxonomies. Adds two custom fields to the native WordPress term add/edit screens (Categories, Tags and any custom listing taxonomy): a media-library image picker and a swatch picker over the theme icon library. The values are stored as term meta and consumed by the live-category blocks (via Blocks\Support\Categories) so an admin can give each category a branded photo and icon instead of the auto-derived listing photo / fallback glyph. Admin-only UI; the read helpers (image()/imageId()/icon()) are safe to call anywhere. Taxonomies are resolved live from the listing post type, so new custom taxonomies get the fields automatically.

API
register()hookTaxonomies()taxonomies()imageId()image()icon()assets()addFields()editFields()save()
inc/_rs/TermMeta.php

Uploads Uploads.php​

Listing media uploads — the AJAX backend for the Uppy uploader on the listing editor (replacing the WordPress media frame). Uppy's XHRUpload posts one file per request to ppt_listing_upload; each becomes a normal WordPress attachment (so Media::gallery()/Media::videos() and the single-listing template keep working). Files are left unattached (post_parent = 0) until the listing form is saved, which parents the final set (ListingEditor::save()). A companion ppt_listing_media_remove deletes a freshly uploaded, not-yet- saved attachment when the user removes it in the editor, so abandoned uploads don't pile up in the media library.

API
register()imageMimes()videoMimes()ajaxUpload()ajaxRemove()ajaxPoster()ajaxPersist()
inc/_rs/Uploads.php