rinvex/laravel-testimonials
Rinvex Laravel Testimonials helps you add customer testimonials to Laravel apps with ready-made models, migrations, and a simple API for creating, managing, and displaying testimonials. Integrate and customize with minimal setup.
1. Check compatibility before you composer require. This is a 2018–2020-era Rinvex package and it is archived. Open its composer.json on GitHub first and look at the illuminate/* and rinvex/laravel-support constraints. On Laravel 10/11 you will almost certainly get a dependency conflict. Realistic options, in order of preference:
src/Models/Testimonial.php and the migration into app/Models and database/migrations, drop the provider, and you own it.--ignore-platform-reqs — only as a last resort; it will "install" but you inherit untested internals.2. If you do install it, publish and migrate:
composer require rinvex/laravel-testimonials
php artisan vendor:publish --tag=rinvex-testimonials-config
php artisan vendor:publish --tag=rinvex-testimonials-migrations
php artisan migrate
Rinvex packages of this generation read config from config/testimonials.php under the rinvex.testimonials key, and table/model class names are usually configurable there.
3. Read these four files first, in this order:
| File | Why |
|---|---|
src/Models/Testimonial.php |
$fillable, $casts, traits (HasSlug, SoftDeletes, translatable, cacheable), scopes |
database/migrations/*_create_testimonials_table.php |
Actual columns — this defines what you can build |
config/testimonials.php |
Table name, model class binding, feature toggles |
src/Providers/TestimonialsServiceProvider.php |
What it auto-registers (binds, migrations, translations) — expect no routes and no controllers |
4. First use case — the approval-gated testimonial wall. The package's whole reason for existing is the is_approved boolean. The minimal loop is: public form → Testimonial row with is_approved = false → admin screen lists unapproved → admin toggles → public page renders approved only. Build the form/controller/routes yourself; the package deliberately stops at the model layer.
The package does not ship a controller or route, so you own validation and abuse prevention:
$data = $request->validate([
'title' => ['required', 'string', 'max:150'],
'body' => ['required', 'string', 'max:5000'],
'rating' => ['nullable', 'integer', 'between:1,5'],
]);
$testimonial = new \Rinvex\Testimonials\Models\Testimonial();
$testimonial->fill($data);
$testimonial->is_approved = false; // set explicitly, see Gotchas
$testimonial->customer_name = $request->user()?->name;
$testimonial->save();
Add honeypot + RateLimiter middleware on the POST route — a public write endpoint with no captcha is spam bait.
Always scope; never trust the default query:
$testimonials = Testimonial::query()
->where('is_approved', true)
->latest()
->paginate(12);
If the model ships a scope (e.g. ->approved()), use it and check its definition — archived packages sometimes have scopes that forgot soft deletes.
Two patterns work:
// Simple boolean toggle
Testimonial::where('is_approved', false)->latest()->paginate(25);
// If the model exposes approve/disapprove methods, prefer them —
// they may also fire events and/or flush the model cache.
$testimonial->approve();
Either way, wrap it in a policy or gate, and prefer a named route like POST /admin/testimonials/{testimonial}/approve over a generic update so approval is auditable.
Rinvex commonly uses spatie/laravel-translatable for title/body. If so:
// Read (falls back to app locale)
$testimonial->title;
$testimonial->getTranslation('title', 'ar');
// Write
$testimonial->setTranslations('title', ['en' => 'Great', 'ar' => 'رائع']);
$testimonial->save();
Check the $translatable array on the model — if the fields are plain text columns instead, ignore all of this.
Approved testimonials change rarely, so cache them, but bust on approval:
// In an observer on saved/deleted
Cache::forget("testimonials.approved.{$testimonial->getKey()}");
Cache::forget('testimonials.wall');
If the package already pulls in a cacheable trait layer, clear that too — approval toggles appearing stale for minutes is the #1 "bug" report on these packages.
Wrap it in your own resource rather than exposing the model:
class TestimonialResource extends JsonResource {
public function toArray($request): array {
return [
'id' => $this->id,
'title' => $this->title,
'body' => strip_tags($this->body), // or an HTML sanitizer
'rating' => $this->rating,
];
}
}
Attach testimonials to your own domain objects with a follow-up migration (user_id, product_id, or a testimonialable morph) rather than fighting the package. If you're vendoring the model anyway, add the columns directly.
It's abandoned — treat it as a starting schema, not a dependency. No Laravel 10/11 support, no security patches, 13 stars means very few eyes on the code. Budget half a day to read the model and migration end-to-end before shipping. The vendoring path is genuinely cheap here.
Non-fillable is_approved silently does nothing. If is_approved isn't in $fillable, Testimonial::create(['is_approved' => false]) throws no error in classic Laravel behaviour — the attribute is just dropped and the DB default applies. If the default is true, you've just published every spam submission. Set the flag as a property (as above) or use forceFill().
Cast the boolean. Confirm protected $casts = ['is_approved' => 'boolean']. Without it, $testimonial->is_approved is the string '0', which is truthy in Blade @if checks in some paths.
Config-named tables bite when you rename. Rinvex tables are typically referenced via config('rinvex.testimonials.tables.testimonials'). If you rename the table in config but not in the published migration (or vice versa), you get "table not found" at runtime. Same for config('rinvex.testimonials.models.testimonial') — that's your extension point for swapping in a subclass with extra relations.
Migration class style. Migrations from this era are named classes (class CreateTestimonialsTable), not return new class. On Laravel 9+ they still load, but if you re-publish into an existing set, watch for duplicate class-name collisions. Rename or convert when vendoring.
HTML in body — sanitize on output. Store raw, render escaped ({{ }}) unless you explicitly support rich text, in which case run it through HTMLPurifier or strip_tags($html, '<p><br><strong><em>'). A public submission field rendered with {!! !!} is stored XSS.
Ratings are usually tinyint nullable. Guard against null in your average-rating aggregate — AVG(rating) returns null and your round() blows up. COALESCE(AVG(rating), 0).
Seeding order and foreign keys. If the migration adds a customer_id FK to your users table (and assumes it's called users), a custom auth table name will break it. Check config('auth.providers.users.table') and adjust when vendoring.
Events are optional. Look for src/Events/. If there are none, wire an Observer for saved/deleted — that's where cache invalidation, moderation notifications, and re-indexing belong.
Testing. RefreshDatabase plus Cache::flush() in setUp() if any cacheable trait is active; otherwise your approval test asserts stale data and you spend an hour chasing ghosts.
Spam is your problem, not the package's. Rate limit the POST, require auth or email verification for submissions you'll actually display, and consider defaulting is_approved to false at the database level (->default(false)) so any code path that forgets the flag fails closed.
Where to extend, not modify. If you keep the package installed: bind a model subclass via the config key, register an observer in AppServiceProvider::boot(), and add columns via your own migration. Never edit vendor/ — you'll lose it on the next composer update, and there will be no upstream to merge from.
How can I help you explore Laravel packages today?