beziworld / laravel-opinions
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.
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/bus: ^12.0|^13.0
- illuminate/cache: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- illuminate/view: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 19:28:57 UTC
README
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) withInvitationSentandInvitationFailedevents - 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()
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.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
- Security vulnerabilities: see SECURITY.md
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.