NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Reviews for Laravel

JSON resource

ReviewResource (and ReviewCollection) expose a sensible public shape for API and mobile clients:

use RoundlyConsulting\Reviews\Http\Resources\ReviewResource;

return ReviewResource::collection(
    $product->approvedReviews()->with(['author', 'reviewable', 'responses'])->get(),
);
FieldValue
id / parent_idReview key; parent key for owner responses.
rating / status / verifiedScore (or null), status string, verified flag.
title / contentThe review text.
helpful_count / unhelpful_count / helpful_scoreVote tallies and their difference.
author / reviewableEach as { id, type } — type is the model’s class basename (User), never its class name; null once the related row is gone.
responsesNested ReviewResource items — only when the relation is loaded.
photosA list of { id, url, srcset }, signed when photos are private.
created_at / updated_at / approved_atISO 8601 strings.

meta is deliberately not exposed, so internal notes and rejection reasons stay private. Photo URLs follow the photo visibility — signed for private photos, never public.

Author and subject

author and reviewable never carry a PHP class name. Each renders through its own resource that wraps the related model — ReviewAuthorResource and ReviewableResource, both {id, type} by default. type is the model’s class basename (User, Product), with or without a morph map — never the morph alias or the class name (App\Models\User):

{
  "id": 12,
  "rating": 5,
  "author": { "id": 7, "type": "User" },
  "reviewable": { "id": 42, "type": "Product" },
  …
}

A related row that no longer exists renders null; both keys are always present. To add what your API needs — a name, an avatar, a title — point reviews.resources.author or reviews.resources.reviewable at a JsonResource of your own. It receives the related model:

// app/Http/Resources/ReviewerResource.php
final class ReviewerResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->resource->getKey(),
            'name' => $this->resource->name,
            'avatar' => $this->resource->avatar_url,
        ];
    }
}

// config/reviews.php
'resources' => [
    'author' => App\Http\Resources\ReviewerResource::class,
    'reviewable' => RoundlyConsulting\Reviews\Http\Resources\ReviewableResource::class,
],

A blank value takes the packaged resource; anything that is not a concrete JsonResource subclass throws InvalidConfigurationException naming the key. ReviewResource::authorOf($review, $request) and ReviewResource::reviewableOf($review, $request) return the same rendered references as plain arrays (or null).

Paginated collections

use RoundlyConsulting\Reviews\Http\Resources\ReviewCollection;

return new ReviewCollection(
    $product->approvedReviews()->latestFirst()->paginate(10),
);

ReviewCollection loads the authors and subjects of its reviews — and of the owner responses already loaded on them — in one query per morph type before rendering; it never loads responses itself. When you render reviews any other way (new ReviewResource($review), ReviewResource::collection()), eager-load author and reviewable, as above, to avoid a query per review.

Customising

Publish editable copies — App\Http\Resources\Reviews\ReviewResource and ReviewCollection in app/Http/Resources/Reviews, app-namespaced classes you own that start out rendering exactly what the package’s resource does:

# Translations → lang/vendor/reviews
php artisan vendor:publish --tag="reviews-translations"

# JSON resources → app/Http/Resources/Reviews
php artisan vendor:publish --tag="reviews-resources"

Edit them, and use them in your controllers instead of the package classes:

use App\Http\Resources\Reviews\ReviewResource;

return ReviewResource::collection(
    $product->approvedReviews()->with(['author', 'reviewable', 'responses'])->get(),
);

The published ReviewResource renders author and reviewable through the package’s helpers, so the reviews.resources.* swaps apply to your copy too:

// app/Http/Resources/Reviews/ReviewResource.php
use RoundlyConsulting\Reviews\Http\Resources\ReviewResource as PackageReviewResource;

'author' => PackageReviewResource::authorOf($review, $request),
'reviewable' => PackageReviewResource::reviewableOf($review, $request),

Copies published before 1.2.0 still render the stored author_type and reviewable_type — the full class name on an app without a morph map. Publish the resources again (--force overwrites your edits), or change those two keys in your copy to the lines above.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.