Search by

beziworld / laravel-opinions

kamcw

Customer review services for Laravel: Trusted Shops (eTrusted) invitations, reviews and cached ratings, Ceneo Zaufane Opinie transaction script, unified data objects and a rating badge.

Package info

github.com/Beziworld/laravel-opinions

Homepage

Issues

pkg:composer/beziworld/laravel-opinions

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 19:28 UTC

This package is auto-updated.

Last update: 2026-11-04 19:28:57 UTC


README

tests Latest Version License

Customer review services for Laravel shops behind one driver API:

  • Trusted Shops (eTrusted API): review invitations after an order, reviews of the shop and its products, aggregated rating
  • Ceneo Zaufane Opinie: the transaction script for the order confirmation page, which makes Ceneo e-mail its review survey
  • unified data objects: Invitation, Review, ReviewPage, Rating
  • queued invitations (SendReviewInvitation, never retried automatically) with InvitationSent and InvitationFailed events
  • cached ratings with a stale-on-error fallback, a scheduler command and a <x-opinions::rating-badge /> Blade component (English and Polish)
  • many shops (brands, store fronts, tenants), runtime credentials and custom drivers with Opinions::extend()

Polska wersja dokumentacji

Supported services

Service Invitations Reviews Rating Browser script
Trusted Shops (trustedshops) yes, POST /events yes, GET /reviews yes, channel aggregate no
Ceneo Zaufane Opinie (ceneo) no server API no API no API yes
Opineo not included not included not included not included

Ceneo's public API (Swagger) covers offers, bidding and orders but has no methods for reviews or surveys; Zaufane Opinie works through a script on the order confirmation page. Opineo publishes no API documentation (integration instructions are available only in the Opineo business panel), so the package ships no Opineo driver. If you have the instructions, register your own driver with Opinions::extend().

Why this package

Existing PHP packages cover one service each: antistatique/trustedshops-php-sdk targets the legacy Trusted Shops API that Trusted Shops now marks as outdated, Ceneo and Opineo packages are Magento modules or small clients of undocumented endpoints. This package uses the current eTrusted API, adds Laravel queues, caching, Blade components and multi-shop configuration, and gives every service the same data objects.

Installation

composer require beziworld/laravel-opinions
php artisan vendor:publish --tag=opinions-config   # optional
OPINIONS_DRIVER=trustedshops

TRUSTEDSHOPS_CLIENT_ID=...           # eTrusted Control Center: Settings > API credentials
TRUSTEDSHOPS_CLIENT_SECRET=...
TRUSTEDSHOPS_CHANNEL_ID=chl-...      # channel of the shop
TRUSTEDSHOPS_EVENT_TYPE=checkout     # event type configured in eTrusted
TRUSTEDSHOPS_LOCALE=pl_PL

CENEO_ACCOUNT_GUID=...               # GUID assigned when joining Zaufane Opinie

OPINIONS_QUEUE=reviews               # optional queue for invitations
OPINIONS_INVITATION_DELAY=0          # minutes

Invitations after an order

use Beziworld\Opinions\Data\Invitation;
use Beziworld\Opinions\Data\InvitationProduct;
use Beziworld\Opinions\Facades\Opinions;

$invitation = new Invitation(
    orderReference: $order->number,
    email: $order->email,
    firstName: $order->first_name,
    lastName: $order->last_name,
    orderedAt: $order->created_at,
    products: $order->items->map(fn ($item) => new InvitationProduct(
        sku: $item->sku,
        name: $item->name,
        url: route('product', $item->product),     // required by Trusted Shops for product reviews
        price: $item->price,                       // "49.90"
        quantity: $item->quantity,
        gtin: $item->ean,
    ))->all(),
);

if ($order->review_consent) {
    Opinions::queueInvitation($invitation);      // one job per driver in opinions.invitations.drivers
}

Send invitations only to customers who opted in. Trusted Shops turns the event into an invitation according to the questionnaire and schedule configured for the event type in eTrusted. Opinions::sendInvitation($invitation) sends synchronously and returns InvitationResult objects (reference is the eTrusted evt-... id). Failed jobs dispatch InvitationFailed and are not retried, so the customer never receives two invitations.

Ceneo Zaufane Opinie

{{-- layout head --}}
<x-opinions::ceneo-script />

{{-- order confirmation page --}}
<x-opinions::ceneo-transaction :invitation="$invitation" :consent="$order->review_consent" />

The components render the loader https://ssl.ceneo.pl/ct/v5/script.js?accountGuid=... and the call _ceneo('transaction', {client_email, order_id, shop_products, amount}), as described in the Shoper integration guide; Ceneo's own instructions are available in the Ceneo panel. Without consent the e-mail address is left empty. All values are JSON-encoded with HTML-safe escaping.

Reviews

use Beziworld\Opinions\Data\ReviewQuery;
use Beziworld\Opinions\Enums\ReviewType;

$page = Opinions::driver('trustedshops')->reviews(ReviewQuery::approved(20));

foreach ($page->items as $review) {
    $review->rating;          // 4.0 (1 to 5)
    $review->title;
    $review->comment;
    $review->authorName;
    $review->verified;
    $review->reply;           // shop reply, if any
    $review->submittedAt;     // CarbonImmutable in the application time zone
}

$older = Opinions::driver()->reviews(ReviewQuery::approved(20)->withAfter($page->nextCursor));

// product reviews for given SKUs, iterating over every page lazily
Opinions::driver()->cursor(new ReviewQuery(type: ReviewType::Product, skus: ['SKU-1'], statuses: ['APPROVED']));

Trusted Shops recommends hiding REJECTED reviews in custom widgets; ReviewQuery::approved() does that.

Rating badge

<x-opinions::rating-badge driver="trustedshops" url="https://www.trustedshops.pl/..." class="mt-4" />
Opinions::rating();                          // cached Rating: average, count, distribution, period, trend
Opinions::rating('trustedshops', 'outlet');
Opinions::refreshRating();                   // fetch now and store

// routes/console.php
Schedule::command('opinions:refresh-ratings')->hourly();

The badge shows stars, 4,8/5 and the number of reviews (Polish plural forms included) and renders nothing when the shop has no reviews. Ratings are cached for OPINIONS_CACHE_TTL seconds (6 hours by default); when the service fails, the last known rating is kept. Trusted Shops recommends the 365-day aggregate for public display, which is the default (TRUSTEDSHOPS_RATING_PERIOD). Publish the views with --tag=opinions-views to change the markup.

Many shops

// config/opinions.php: 'shops' => ['default' => [...], 'outlet' => ['trustedshops' => [...]]]
Opinions::shop('outlet')->queueInvitation($invitation);
Opinions::shop('outlet')->driver('trustedshops')->reviews();

// credentials stored per tenant
Opinions::using('trustedshops', $tenant->trustedShopsCredentials(), shop: $tenant->key)->invite($invitation);

// custom driver
Opinions::extend('opineo', fn (array $config, string $shop) => new OpineoDriver($config, $shop));

Errors

Exception When
ApiException the service rejected the request or could not be reached: driver(), httpStatus(), payload(), isRateLimited(), retryAfter()
UnsupportedOperation the driver's service does not offer the operation (e.g. reviews from Ceneo)
InvalidConfiguration unknown shop or driver, missing credentials, invalid option

All extend OpinionsException. Invitations are sent once; reviews and ratings are retried on connection errors and HTTP 5xx (TRUSTEDSHOPS_RETRIES). An expired token is refreshed once after HTTP 401.

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need an integration? Get in touch through beziworld.eu.

License

MIT. See LICENSE.md. This package is not affiliated with Trusted Shops, Ceneo or Opineo.