Goal
By default, mam-reviews-manager reads and writes reviews as WordPress comments. Sibling plugins extend this — GeoDirectory listings and WooCommerce products each contribute their own provider so the app’s reviews come from the same native source the website reads from. This guide shows you how to do the same for any other source: implement the reviews provider contract, register your provider, and have the Reviews Manager resolve it for the post types you own.
When you finish, reviews for the post types your provider claims will flow end to end — the app’s reviews header, the reviews list, the “my review” lookup, the All Reviews list, and review submission — without touching core files in mam-reviews-manager.
A note on naming. This guide is titled “non-WooCommerce” because WooCommerce products are already handled by their own provider. The same steps apply to any source that is not plain WordPress comments: a third-party listings directory, a headless CMS, an external ratings API mirrored into WordPress, and so on.
How the provider layer works
Core never reads or writes review storage directly. For every post, it resolves exactly one provider through a registry and delegates all reads and writes to it. That single rule is what guarantees the app and the website stay in sync.
Resolution works like this:
- Providers are collected from the
mam_reviews_providersfilter. - Each provider declares a priority. The registry sorts them highest-priority-first.
- For a given post, the registry walks the sorted list and picks the first provider whose
supports()returnstrue. - The built-in WordPress-comments provider registers at priority
0and supports everything, so resolution always yields a provider. Your provider wins by registering at a higher priority and supporting only the posts it owns.
Your job is therefore narrow: tell the registry which posts you own (supports()), and answer the read/write methods for those posts.
Prerequisites
- MAM Suite installed with
mam-mainandmam-reviews-manageractive. The provider layer ships inmam-reviews-manager26.23.0+. - A plugin of your own to host the provider (a sibling MAM plugin, or any standard WordPress plugin loaded by
plugins_loaded). - PHP 8.1+ — the contract uses typed signatures.
- Comfort reading the reference implementation. The single best example is the default provider:
- Interface:
mam-reviews-manager/includes/interface-mam-reviews-provider.php - Shared base:
mam-reviews-manager/includes/abstract-mam-reviews-provider-base.php - Default provider:
mam-reviews-manager/includes/class-mam-reviews-default-provider.php - Registry + resolver:
mam-reviews-manager/includes/class-mam-reviews-provider-registry.php
- Interface:
The data shapes each method returns are defined in the contract spec referenced from the interface docblock (
claude-tasks/mam-reviews-refactoring/00-contract-spec.md, sections §4.1–§4.3 and §7). Where this guide describes a shape generically, defer to that spec and to the default provider’s output for the exact keys.
The contract at a glance
A provider implements MAM_Reviews_Provider. The seven methods, grouped by what they do:
| Method | Returns | Used for |
|---|---|---|
supports( int $post_id, string $post_type ): bool |
bool |
Claiming ownership of a post |
get_criteria( int $post_id ): array |
list of criterion descriptors | The rating inputs on the add-review form |
get_aggregates( int $post_id ): array |
aggregate stats | The reviews summary header |
get_reviews( int $post_id, array $args = [] ): array |
list of normalized reviews | The reviews list |
get_user_review( int $post_id, int $user_id ): ?array |
one normalized review or null |
The “you’ve already reviewed” / edit path |
save_review( array $submission ): array |
{ ok, comment_id, message } |
Persisting a submission |
get_all_reviews( array $args = [] ): array |
map of post_id => reviews |
The geo-filtered All Reviews list |
You will almost never implement all seven from scratch. Extend MAM_Reviews_Provider_Base — the abstract base supplies storage-agnostic helpers and sensible defaults for several methods.
What the base gives you for free:
get_criteria()returns a single “overall” criterion. Override only if your source is multi-criteria (like GeoDirectory’s Cleanliness / Food Quality dynamics).overall_criterion()builds that criterion from themam_reviews_rating_typeoption (starsvsnumeric).format_reviewer_name()renders “First L” from a WordPress user.load_photos()loads review photos fromattachment_idcomment meta.build_review()assembles the normalized review shape — including the legacystarsalias the current apps still read (the matching aggregate-levelaverage_ratingalias comes fromaggregate()) — so your output stays behavior-preserving.aggregate()computes average, total, star distribution, and latest-review date from a list of normalized reviews.get_all_reviews()has a default loop that calls yourget_reviews( 0, ... ). Override it if your source can answer “all posts I own” in one efficient query.supports()defaults totrue— you must override this, or you will claim every post.
Steps
1. Create the provider class
Create a class that extends the base. Put it in your own plugin’s includes/. At minimum, override supports() and implement the read/write methods the base does not.
<?php
/**
* Reviews provider backed by <your source>.
* Layer: Feature
*/
class my_source_reviews_provider extends MAM_Reviews_Provider_Base {
/**
* Claim only the post types this source owns.
*/
public function supports( int $post_id, string $post_type ): bool {
return in_array( $post_type, array( 'my_listing_cpt' ), true );
}
// ... read/write methods below ...
}
Match the base only on
supports()if your source stores ratings as WordPress comments and you simply need to claim a different post type. If your data lives elsewhere, you will override the read/write methods too.
2. Implement the read methods
These power what the user sees. Return the normalized shapes — do not invent your own keys, because the app and the All Reviews list key off the contract.
-
get_reviews( $post_id, $args )— return a list of normalized reviews for the post. When$post_idis0, return site-wide reviews, each carrying its ownpost_id(this is what the defaultget_all_reviews()loop relies on). Skip anonymous entries the way the default provider skips comments withuser_id <= 0. -
get_aggregates( $post_id )— returnoverall_average,total_ratings,distribution,per_criterion,latest_review_date, anddisplay_type. If your source already maintains native aggregates, read those. If not, the simplest correct implementation is to compute them from your own review list:public function get_aggregates( int $post_id ): array { return $this->aggregate( $this->get_reviews( $post_id ) ); } -
get_user_review( $post_id, $user_id )— return the user’s existing review (normalized) ornull. Guard against non-positive ids. -
get_criteria( $post_id )— only override if multi-criteria. Otherwise inherit the single overall criterion.
To build each normalized review from a WP_Comment, lean on the base’s build_review( $comment, $overall, $ratings, $current_user ) rather than hand-assembling the array. If your source is not comment-backed, you’ll construct the normalized array yourself — copy the key set the base emits so nothing downstream breaks.
3. Implement save_review() — write through the native path
This is the most important method to get right. The submission arrives normalized (see spec §7): post_id, post_type, user (a WP_User), review_text, ratings (criterion id => value, including MAM_Reviews_Provider_Base::OVERALL), photos (attachment ids), tip_amount, tip_level, and is_approved.
The contract’s rule: write so that your source’s native aggregates update. The default provider has no native aggregate table, so it inserts a comment directly and stores the overall rating in rating comment meta. Providers backed by a native engine must instead write through that engine’s own save path (for example, wp_new_comment() so the comment_post hook fires) — otherwise the website’s rating counts and the app’s aggregates will drift apart.
Return the result shape exactly:
return array(
'ok' => true, // false on failure
'comment_id' => (int) $id, // or null on failure
'message' => '', // user-facing error text on failure
);
Honor is_approved for the moderation/auto-approve state, and persist each entry in photos the way the default provider does (one attachment_id meta row per photo, so load_photos() can read them back).
4. Decide on get_all_reviews()
The base default calls get_reviews( 0, $args ) and buckets the results by post_id. That is correct as long as your get_reviews( 0 ) returns only the posts your provider owns. When it assembles the All Reviews list, the registry walks providers highest-priority-first and gives each post to the first provider that yields it, dropping any later provider’s reviews for that same post. So a higher-priority provider that returns posts it doesn’t actually own will shadow the true owner’s reviews for those posts. If a single query over your source is cheaper, override get_all_reviews() directly.
5. Register the provider on the mam_reviews_providers filter
Add a descriptor keyed by a unique slug. Give it a priority above 0 so it outranks the default fallback, and below any provider you must not displace.
add_filter( 'mam_reviews_providers', function ( array $providers ): array {
$providers['my_source'] = array(
'label' => __( 'My Source', 'your-textdomain' ),
'class' => 'my_source_reviews_provider',
'priority' => 20,
);
return $providers;
} );
The registry instantiates class once (it must class_exists and implement the interface), sorts by descending priority, and resolves the first provider whose supports() returns true. The default WordPress-comments provider sits at priority 0, so any positive priority wins for the post types you claim.
Load order matters. The registry boots lazily on first resolution, so your provider’s class file must be required and the filter added before the first review request. Registering inside your plugin’s
plugins_loadedbootstrap (asmam-reviews-managerdoes for its own default provider) is the safe pattern.
6. Verify resolution
Confirm the registry hands your provider the posts you expect. The global resolver is mam_reviews_provider_for( int $post_id ):
$provider = mam_reviews_provider_for( $my_post_id );
// Expect: instanceof my_source_reviews_provider for your post types,
// instanceof mam_reviews_default_provider for everything else.
Then exercise the app paths: open a reviewed item, check the header aggregates and the reviews list, submit a review, and confirm it appears on both the website’s native source and the app (this is the whole point — one source of truth). Finally, open the All Reviews list and confirm your posts appear without duplication.
Common pitfalls
- Forgetting to override
supports(). The base returnstrue, so an un-narrowed provider at a high priority will hijack reviews for every post type, including WooCommerce and GeoDirectory. - Inventing keys. The normalized review and aggregate shapes are a contract the app and the All Reviews list depend on, including legacy aliases like
starsandaverage_rating. Use the base helpers so you emit the full superset. - Writing a bare comment when your source has native aggregates. Reviews will display but native rating counts won’t update, and the app and website will disagree.
- Returning all posts from
get_reviews( 0 ). Only return the posts your provider owns. Because the registry gives each post to its highest-priority provider, a higher-priority provider that yields posts it doesn’t own will shadow the real owner’s reviews in the All Reviews list. - Registering too late. Add the filter before the first resolution.
Related
- Plugin Reference: mam-reviews-manager — feature overview, content sections, and the AJAX submission path.
- Explanation: How MAM resolves one provider per post — the priority +
supports()resolution model in depth. - Reference: The reviews provider contract — the exact normalized shapes for criteria, aggregates, reviews, and submissions (spec §4–§7).
Verification
This article was last verified against:
- Plugin:
mam-reviews-manager(provider layer,@since 26.23.0) - Source:
includes/interface-mam-reviews-provider.php - Source:
includes/abstract-mam-reviews-provider-base.php - Source:
includes/class-mam-reviews-provider-registry.php - Source:
includes/class-mam-reviews-default-provider.php - Source:
mam-reviews-manager.php(themam_reviews_providersregistration of the default provider)
Re-verify whenever the interface method signatures change, the normalized data shapes change, the mam_reviews_providers descriptor format changes, or the registry’s priority/resolution rules change.
