Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Review Bundle Laravel Package

sylius/review-bundle

Sylius Review Bundle adds customer product reviews to Sylius stores, with ratings, review submission, moderation, and average rating display. Integrate it to let shoppers share feedback and help others make informed buying decisions.

View on GitHub
Deep Wiki
Context7

Getting Started

Do not composer require this. It is a [READ-ONLY] subtree split of the Sylius monorepo targeting Symfony 2.x and PHP 5.x-era runtimes. It will not resolve against a current Laravel composer.json (Illuminate constraints, symfony/* v2 pins, and EOL PHP requirements all collide). Your first action is to clone the repository read-only and treat it as a domain specification written in code, not a dependency.

Read these files first, in this order (paths relative to the bundle root):

  1. Model/ProductReview.php and Model/ProductReviewInterface.php — the canonical column set: title, rating, comment, author (name/email), status, timestamps.
  2. Model/ReviewableInterface.php + the ReviewableTrait — how a subject (Product) exposes its reviews and its average rating. This is the polymorphic contract you will port.
  3. The state machine config under Resources/config/ (look for state_machine / ReviewTransitions) — the new → accepted | rejected workflow and its guards.
  4. Resources/config/doctrine/*.xml — the actual column types, nullable flags, and indexes. Translate these literally before you improve anything.
  5. The manager/repository classes (e.g. ProductReviewManager, ProductReviewRepository) — where averaging and eligibility checks live.

Timebox a one-to-two day spike whose only deliverable is a written spec: the reviews table, the status enum, the transitions, and the aggregation trigger. Do not attempt an install "just to see."

First use case to ship: the schema plus the moderation workflow, with the storefront form stubbed. It is the highest-parity, lowest-risk slice and unblocks the admin queue and the product-page average at the same time.

// database/migrations/xxxx_create_reviews_table.php
Schema::create('reviews', function (Blueprint $table) {
    $table->id();
    $table->morphs('reviewable');            // reviewable_type + reviewable_id
    $table->foreignId('customer_id')->nullable()->constrained();
    $table->string('author_name')->nullable();
    $table->string('author_email')->nullable();
    $table->string('title')->nullable();
    $table->unsignedTinyInteger('rating');   // 1–5, Sylius treats it as required
    $table->text('comment')->nullable();
    $table->string('status')->default('new'); // new | accepted | rejected
    $table->timestamps();

    $table->index(['reviewable_type', 'reviewable_id', 'status'], 'reviews_subject_status_idx');
});

Implementation Patterns

Polymorphic subject, not a foreign key to products. Sylius models the review against a review subject rather than the entity directly, which is what makes it reusable across reviewable types. Port that with a Reviewable trait:

trait Reviewable
{
    public function reviews(): MorphMany
    {
        return $this->morphMany(Review::class, 'reviewable');
    }

    public function acceptedReviews(): MorphMany
    {
        return $this->reviews()->where('status', ReviewStatus::Accepted);
    }

    public function recomputeRating(): void
    {
        $stats = $this->reviews()
            ->where('status', ReviewStatus::Accepted)
            ->selectRaw('COUNT(*) as c, AVG(rating) as avg')
            ->first();

        $this->forceFill([
            'rating_average' => round((float) $stats->avg, 2),
            'rating_count'   => (int) $stats->c,
        ])->save();
    }
}

Encode moderation as an enum with explicit transitions. Sylius's state machine is the single most valuable thing to copy; the failure mode it prevents is $review->status = 'accepted' scattered through controllers.

enum ReviewStatus: string {
    case New = 'new';
    case Accepted = 'accepted';
    case Rejected = 'rejected';
}

// Review model
public function accept(): void
{
    $this->assertTransition(ReviewStatus::New, ReviewStatus::Accepted);
    $this->status = ReviewStatus::Accepted;
    $this->save();
    $this->reviewable->recomputeRating();   // or dispatch an event
}

Guard the attribute with protected $guarded = ['status'] and keep fillable status-free so mass assignment cannot bypass the transitions.

Aggregation on transition, not on read. The product page reads rating_average / rating_count off the product row (or a cache key), and every accept/reject/delete calls recomputeRating(). Recompute-on-write with a full re-AVG beats incremental +1 counters here because moderation makes rejections and reversals routine.

Admin moderation queue. Rebuild the Sylius grid as a filtered, paginated index — status filter defaulting to new, bulk accept/reject, and a subject column. Whatever the stack (Filament, Nova, or a plain Blade table), the requirements from the original grid are: filter by status, filter by product, search by author, and row actions for each transition.

Storefront submission. A FormRequest mirroring the bundle's validation, plus rate limiting per IP/customer, a honeypot, and ->middleware('throttle:reviews'). Decide guest reviews up front — the original allows them with name/email captured.

Keep the port behind an interface. Wrap it in a ReviewService contract. If a maintained Laravel package appears, or reviews become a growth channel and you buy a vendor, you swap the implementation without touching controllers or views.

Gotchas and Tips

  • Doctrine lifecycle callbacks will not translate silently. Sylius's averaging and cascade behaviour hang off Doctrine events. In Eloquent the equivalents are observers and foreign keys; verify each callback's intent in the original before reproducing it, because a dropped onDelete cascade is invisible until a product is deleted.
  • morphTo cannot have a database foreign key. Nothing stops a review from orphaning when its subject is deleted. Add a scheduled prune job, or soft-delete subjects, and decide deliberately which.
  • Index from day one. The composite (reviewable_type, reviewable_id, status) index is not optional on a real catalog; polymorphic lookups without it degrade badly, and the moderation dashboard needs status in the same index to paginate.
  • Average-rating drift is the classic bug. Two simultaneous accepts compute stale averages. Wrap the transition and the recompute in a transaction with lockForUpdate on the subject row, or push recompute to a queued job with a unique key per subject.
  • Rating semantics. The original treats rating as required 1–5. If you allow nullable ratings (comment-only reviews), AVG(rating) silently shifts and your star display needs a separate count. Decide before the migration.
  • Configuration does not port. Sylius's sylius_review.* settings (author-display behaviour, eligibility rules) are Symfony tree config. Re-express them as config/reviews.php with env() defaults: show_customer_name, allow_guest_reviews, require_approval, min_length.
  • The bundled feature set is the floor, not the target. Absent from the box and therefore on you: verified-purchase linkage, photo/video attachments, multi-criteria ratings, helpfulness voting, review-solicitation emails, spam classification, and AggregateRating structured data for SEO. Budget each as its own task rather than assuming a config flag.
  • Guest authors are a privacy surface. Storing author_email on an unmoderated public write endpoint means you need a retention/anonymisation policy and a way to honour deletion requests; the Symfony original offers nothing here.
  • Anti-abuse is the real operational risk. Manual accept/reject does not scale, and the bundle has no spam detection. Add a honeypot plus a simple classifier or third-party check before the queue becomes unmanageable.
  • Don't vendor any of the code. Every hour spent coaxing Symfony2 service definitions or Twig templates to run is wasted; read for intent, rewrite against Laravel primitives. The legitimate exception is small value objects where the logic is self-contained.
  • Extension points in the original — resource overrides, form extensions, event listeners — map to Laravel as: a ReviewService interface, FormRequest + custom validation rules, model observers, domain events (ReviewAccepted, ReviewRejected), and policies for who may moderate. Design to those seams now so the swap-out later is cheap.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package