Appearance
Database Models
Introduction
WP Social Ninja's WPFluent framework provides a beautiful, simple ActiveRecord implementation for working with the database. Each wpsr_ table has a corresponding Model used to query and persist records — an Eloquent-like API that will feel familiar if you have used Laravel.
NOTE
WP Social Ninja also exposes global helper functions (like wpsrGetReviews() and wpsrDb()) for common data access — prefer those for simple cases. The models are documented here for internal use and advanced third-party integrations. See Global Helpers.
Built-in models
Models live in the core plugin at app/Models/. Each extends the app Model (WPSocialReviews\App\Models\Model), which extends the framework WPSocialReviews\Framework\Database\Orm\Model and guards ['id', 'ID'] from mass assignment.
| Model | Table | Notes |
|---|---|---|
Review | wpsr_reviews | rating cast to int, fields cast to json; SearchableScope |
ReviewCategory 4.4.0 | wpsr_review_categories | id cast to int; fillable name, slug |
ReviewCategoryRelationship 4.4.0 | wpsr_review_category_relationships | Pivot; bulk attach/detach helpers |
ReviewForm | wpsr_review_forms | Native/Pro review forms |
Cache | wpsr_caches | Feed / API response cache |
OptimizeImage | wpsr_optimize_images | Optimized media records |
Template | wp_posts | Template post type; SearchableScope |
Widget | wp_posts | Chat widget post type; SearchableScope |
Notification | wp_posts | Notification popups; SearchableScope |
Post | wp_posts | Generic post access; SearchableScope |
Throughout this page we use WPSocialReviews\App\Models\Review as the example.
Retrieving Models
Think of each model as a powerful query builder for its table. all() returns every row:
php
use WPSocialReviews\App\Models\Review;
$reviews = Review::all();
foreach ($reviews as $review) {
echo $review->reviewer_name;
}Adding constraints
Because each model serves as a query builder, add constraints and call get():
php
$reviews = Review::where('platform_name', 'google')
->where('review_approved', 1)
->orderBy('id', 'DESC')
->limit(10)
->skip(5)
->get();Retrieving Single Models / Aggregates
php
// By primary key
$review = Review::find(1);
// First matching the constraints
$review = Review::where('platform_name', 'facebook')->first();
// Multiple by primary key
$reviews = Review::find([1, 2, 3]);Aggregate helpers return a scalar:
php
$count = Review::where('platform_name', 'google')->count();
$max = Review::where('platform_name', 'google')->max('rating');Available aggregates include count, max, min, avg, and sum.
Inserting & Updating
Inserts
php
$review = Review::create([
'platform_name' => 'custom',
'reviewer_name' => 'Jane Doe',
'reviewer_email' => '[email protected]',
'rating' => 5,
'reviewer_text' => 'Fantastic!',
'review_approved' => 1,
'fields' => ['source' => 'my_addon'], // encoded to JSON via the cast
]);Updates
Assign attributes and save(), or update with an array:
php
$review = Review::find(1);
$review->rating = 4;
$review->save();
// or
Review::find(1)->update([
'rating' => 4,
'review_approved' => 1,
]);Accessing Attributes
Access columns as properties. Casts apply automatically:
php
$review = Review::find(1);
$rating = $review->rating; // int (cast)
$fields = $review->fields; // array (json cast)
$name = $review->reviewer_name;Deleting Models
php
// A single instance
Review::find(1)->delete();
// By query (mass delete — no model events fired)
Review::where('review_approved', 0)->delete();Query Scopes
Scopes let you reuse common constraints. WP Social Ninja ships a SearchableScope trait (app/Models/Traits/SearchableScope.php) that adds a searchBy scope. A model using it declares a $searchable array of columns, then you call the scope with the small first letter:
php
// scopeSearchBy(...) → called as searchBy(...)
$reviews = Review::searchBy('great service')->get();Internally the scope builds an OR LIKE across the model's $searchable fields — handy for list-screen search boxes.
Relationships
The framework ORM supports Eloquent-style relationships (belongsTo, hasMany, …). WP Social Ninja's models are largely self-contained (a single wpsr_reviews table keyed by platform_name / source_id rather than normalized joins), so cross-model relations are few — but the capability is there if you extend the models in your own code.
Review categories 4.4.0
The one normalized relation in core is many-to-many, in both directions:
php
use WPSocialReviews\App\Models\Review;
use WPSocialReviews\App\Models\ReviewCategory;
$review->categories(); // belongsToMany(ReviewCategory, …, 'review_id', 'category_id')
$category->reviews(); // belongsToMany(Review, …, 'category_id', 'review_id')Deleting a ReviewCategory detaches it from every related row — the model's boot() registers a deleting listener that calls $category->reviews()->detach(). That listener only fires on model deletes; a mass ReviewCategory::where(...)->delete() leaves pivot rows behind.
Resolving names to IDs
Names arrive from the admin, CSV import, and Fluent Forms submissions, so the model owns the normalization rather than each call site:
php
// Case- and whitespace-insensitive; "Support" and "support" never fragment into two rows.
$category = ReviewCategory::findOrCreateByName('Customer Support');
// A save payload's ids and new names, resolved to confirmed IDs.
$ids = ReviewCategory::resolveIds([3, 4], ['Delivery']);
// Lookups in both directions.
$ids = ReviewCategory::idsForNames(['Delivery', 'Support']);
$names = ReviewCategory::namesForIds([3, 4]);
// Listings — with or without relationship counts.
$all = ReviewCategory::allOrdered(); // alphabetical, names only
$withCount = ReviewCategory::allOrderedWithCounts(); // adds `reviews_count`resolveIds() takes ids and names as two separate lists because they cannot be told apart by value — a category named 2024 looks exactly like ID 2024. Ids are confirmed against the table, so a stale one is dropped rather than linked.
Both name and slug are capped at ReviewCategory::MAX_LENGTH (191). The cap is applied in a setNameAttribute() mutator with mb_substr(), so no write path can miss it and a multi-byte character is never split in half.
findOrCreateByName() is not atomic
The lookup and the insert are separate statements, so concurrent imports can both reach the insert with the same slug. The UNIQUE index on slug rejects the loser, and the method re-reads whatever the winner created — but if you write categories yourself, expect that race and handle the duplicate-key failure.
Bulk pivot writes
ReviewCategoryRelationship exposes static helpers for the bulk paths, chunked at ReviewCategoryRelationship::BATCH_SIZE:
php
use WPSocialReviews\App\Models\ReviewCategoryRelationship;
ReviewCategoryRelationship::bulkAttach([12, 13], $categoryId);
ReviewCategoryRelationship::bulkDetach([12, 13], $categoryId);
ReviewCategoryRelationship::deleteForReviews([12, 13]); // all categories, e.g. on review deletePer-review work goes through the relation instead — $review->categories()->sync($ids). The helpers above exist for the set-based operations spanning many reviews at once, which a relation would turn into one query per review. BATCH_SIZE is 500, because a "select all" makes these id lists as long as the site's review count and the statements must stay under max_allowed_packet.
bulkAttach() uses insertOrIgnore(), so the UNIQUE (review_id, category_id) key is the dedupe mechanism — re-attaching an existing link is a no-op rather than an error.
Deleting reviews does not clear pivot rows
Review deletes are query-builder mass deletes, so no model event fires and nothing else unlinks the categories. Call deleteForReviews() wherever you delete reviews. It no-ops safely when the tables do not exist yet — it is reachable from cron, which never runs the migration (that happens when the plugin's admin page renders).
The pivot has no foreign keys, so validate that $categoryId resolves to a real category before calling; see Schema.
Using the query builder directly
When you don't need a full model, reach for the framework query builder via wpsrDb():
php
$rows = wpsrDb()
->table('wpsr_reviews')
->where('platform_name', 'yelp')
->get();See the Query Builder page for the complete method reference.
Validate before you query
Per the project conventions, validate IDs and inputs before building a query — early return on empty values and never assume an ID is present. See Coding Conventions.

