depa/sulu-google-reviews-bundle
Symfony bundle for Sulu CMS 3 that imports Google Places reviews via the Places API, stores reviews rated 4 stars or higher in a dedicated table, adds a native admin interface for moderation, and renders them on the frontend through a flexible Sulu block.
Symfony bundle for Sulu CMS 3 that fetches Google Places reviews via the Places API, stores them filtered (≥ 4 stars) in a dedicated database table, and provides a native Sulu admin interface for moderation. Frontend output is rendered through a flexible Sulu block.
Available Languages: 🇩🇪 Deutsch
composer require depa/sulu-google-reviews-bundle
With Symfony Flex the bundle is registered in config/bundles.php automatically on composer require. If you don't use Flex, add it manually:
Depa\SuluGoogleReviewsBundle\DepaSuluGoogleReviewsBundle::class => ['all' => true],
Add to config/routes/routes_admin.yaml:
DepaSuluGoogleReviewsBundle:
resource: "@DepaSuluGoogleReviewsBundle/Resources/config/routes_admin.yaml"
prefix: /admin/api
The bundle ships empty defaults for both variables, so no .env entry is required — the application boots without them. On the server, set the real values in .env.local (never commit):
GOOGLE_PLACES_API_KEY=AIzaSy...
GOOGLE_PLACE_ID=ChIJ...
Find your Place ID on Google Maps Platform. If the values are empty, the import command aborts with a hint.
Alternatively, set the values (or different env vars) via the bundle configuration:
# config/packages/depa_sulu_google_reviews.yaml
depa_sulu_google_reviews:
api_key: '%env(MY_KEY_VAR)%'
place_id: '%env(MY_PLACE_VAR)%'
bin/adminconsole doctrine:schema:update --force
Creates the depa_googlereviews_reviews table.
bin/adminconsole cache:clear
The admin detail view uses read-only React field types whose source ships as an npm package inside the bundle (Resources/js). Wire it into the project's admin build like any other Sulu admin JS package:
Reference the package in assets/admin/package.json:
"dependencies": {
"sulu-google-reviews-bundle": "file:../../vendor/depa/sulu-google-reviews-bundle/Resources/js"
}
Import it in the admin entry file (assets/admin/app.js):
import 'sulu-google-reviews-bundle';
Install dependencies and build the admin assets:
cd assets/admin
npm install
npm run build
Note: Sulu cannot compile a bundle's admin JS on its own — registration and build always run through the project. This step is therefore required once on the project side.
Add to config/templates/blocks/block--section.xml under <types>:
<type ref="block--google-reviews"/>
The "Google Reviews" block will now appear in the Sulu admin editor when adding sub-blocks within a section.
| Field | Description |
|---|---|
| Title | Optional heading above the reviews |
| Limit | Number of reviews to display (default: 3) |
| Sort | Sort mode (see below) |
| Empty Text | Message shown when no reviews are available (optional; falls back to a default) |
| Value | Behavior |
|---|---|
By Date |
Newest reviews first |
By Rating |
Highest star rating first |
Custom Order |
Sorted by the "Sort Order" field of individual reviews |
bin/adminconsole sulu:google-reviews:fetch
0 3 * * * /path/to/project/bin/adminconsole sulu:google-reviews:fetch
The command imports new reviews (≥ 4 stars) and, for existing entries, updates text, rating, profile photo and relative time. Manual moderation fields (blocked, custom sort order) are preserved.
By default the command fetches the reviews Google ranks as most relevant. The modern Places API (New) offers no sort option, so it always returns that set. To fetch the newest reviews instead, pass --newest:
bin/adminconsole sulu:google-reviews:fetch --newest
This uses the Legacy Places API (reviews_sort=newest) — the only Google endpoint that can sort reviews. It requires:
If the legacy API is unavailable, the run logs a warning for the affected locale and imports nothing for it. Without --newest the behaviour is unchanged.
The command fetches reviews per webspace locale (resolved automatically via Sulu's WebspaceManager). For each configured language, Google's translated version of the review text and the localized relative time are stored — all in the same database row. A review therefore stays a single entry in the admin regardless of the number of languages.
Because Google only returns the latest ~5 reviews, older reviews do not get a translation from the fetch alone when a newly added language appears. A separate command translates the stored original text into all missing webspace languages via a translation service:
bin/adminconsole sulu:google-reviews:translate-missing
robole/sulu-ai-translator-bundle (DeepL) is installed, the bundle wires the matching adapter automatically (via a compiler pass) — no project configuration required. If the bundle is missing, the command aborts with a hint.Depa\SuluGoogleReviewsBundle\Translation\ReviewTranslatorInterface to your own implementation in the project; it then takes precedence over the DeepL auto-wiring.places.googleapis.com/v1/places/{placeId}) with the API key in the header (X-Goog-Api-Key) and a FieldMask (X-Goog-FieldMask: reviews). The old "Places API (Legacy)" is not used, as it can no longer be enabled for new Google Cloud projects. The "Places API (New)" must be enabled in the Google Cloud Console.reviews is the billable Enterprise SKU). With e.g. three languages the number of requests per fetch run triples.After installation, the menu item "Google Reviews" appears in the Sulu backend (icon: comment).
Shows all imported reviews with author, star rating, date, block status, and sort order.
| Section | Fields | Editable |
|---|---|---|
| Review (imported from Google) | read-only display: author, stars, date, original language and the review text per webspace language | No |
| Moderation & Display | Block review, sort order | Yes |
The review itself is rendered through the read-only admin field type google_review_display (see Installation, step 7).
Blocked reviews won't appear on the frontend and are excluded from get_stored_google_reviews() results.
The Sort Order field is used when the block's sort mode is set to "Custom Order".
0 = no priority (placed after the prioritized entries, sorted by date among themselves)1, 2, 3, … = ascending display orderWhen assigning an already-taken sort position, all other entries at that position and above are automatically shifted up by one.
The bundle provides two Twig functions for use anywhere in templates, independent of the Sulu block.
get_stored_google_reviews(limit, sort){% set reviews = get_stored_google_reviews(limit, sort) %}
| Parameter | Type | Default | Possible Values |
|---|---|---|---|
limit |
int |
5 |
Any positive integer |
sort |
string |
'date' |
'date', 'rating', 'custom' |
google_review_relative_time(timestamp, locale)Returns a computed, always-current relative time (e.g. "3 months ago") from the timestamp — not Google's stored, ageing string. Localized for ~280 locales via Carbon (diffForHumans).
| Parameter | Type | Description |
|---|---|---|
timestamp |
int |
review.createdAtTimestamp |
locale |
string |
Target locale, e.g. app.request.locale |
Example:
{% for review in get_stored_google_reviews(3, 'rating') %}
<p>{{ review.authorName }}: {{ review.rating }} stars</p>
<p>{{ review.getText(app.request.locale) }}</p>
<p>{{ google_review_relative_time(review.createdAtTimestamp, app.request.locale) }}</p>
{% endfor %}
| Property | Type | Description |
|---|---|---|
authorName |
string |
Reviewer name |
profilePhotoUrl |
string|null |
URL to Google profile photo |
rating |
int |
Star rating (4 or 5) |
getText(locale) |
string |
Review text for the locale, falling back to the original text |
originalText |
string|null |
Original text in its source language (fallback) |
originalLanguage |
string|null |
Language code of the original text |
createdAtTimestamp |
int |
Created date as Unix timestamp |
blocked |
bool |
Block status (when accessing the repository directly) |
sortOrder |
int |
Custom sort position |
Note: the review text is locale-specific — use
review.getText(app.request.locale)(review.textwithout an argument returns the original text as a fallback). The relative time is computed viagoogle_review_relative_time(...)instead of being read from a stored value.
The template uses Bootstrap 5 for grid layout and follows BEM naming conventions:
| BEM Class | Element |
|---|---|
.google-reviews |
Wrapper <section> |
.google-reviews__title |
Optional heading |
.google-reviews__card |
Individual review card |
.google-reviews__author |
Author area |
.google-reviews__avatar |
Profile photo |
.google-reviews__avatar--fallback |
Initial fallback without photo |
.google-reviews__author-name |
Author name |
.google-reviews__rating |
Stars container |
.google-reviews__star |
Single star |
.google-reviews__star--filled |
Filled star |
.google-reviews__text |
Review text |
.google-reviews__time |
Relative time |
.google-reviews__empty |
Fallback message when no reviews |
Template location:
Resources/views/includes/blocks/block--google-reviews.html.twig
Once the bundle is installed via Composer, override the template in your main project:
Create the file:
templates/includes/blocks/block--google-reviews.html.twig
Copy the content from the bundle and customize (CSS classes, HTML structure, etc.)
Twig searches for templates first in the main project, then in bundles — your version will be used automatically.
vendor/depa/sulu-google-reviews-bundle/
├── src/
│ ├── Admin/GoogleReviewsAdmin.php # Sulu navigation & views
│ ├── Command/
│ │ ├── FetchGoogleReviewsCommand.php # Import per webspace locale
│ │ └── TranslateMissingReviewsCommand.php # Backfill missing languages
│ ├── Controller/Admin/GoogleReviewController.php
│ ├── DependencyInjection/
│ │ ├── Compiler/TranslatorIntegrationPass.php # Optionally wire the DeepL adapter
│ │ ├── DepaSuluGoogleReviewsExtension.php # Auto-configuration via prepend
│ │ └── Configuration.php
│ ├── Entity/GoogleReview.php
│ ├── Repository/GoogleReviewRepository.php
│ ├── Translation/
│ │ ├── ReviewTranslatorInterface.php # Optional translator contract
│ │ ├── DeeplReviewTranslator.php # DeepL adapter (duck-typed)
│ │ └── DeeplTranslatorClientInterface.php
│ ├── Twig/GoogleReviewsTwigExtension.php
│ └── DepaSuluGoogleReviewsBundle.php # Registers the compiler pass
└── Resources/
├── config/
│ ├── forms/google_review_details.xml
│ ├── lists/google_reviews.xml
│ ├── routes_admin.yaml
│ └── services.yaml
├── js/
│ ├── index.js # Field type registration
│ └── GoogleReviewDisplay.js # read-only admin field type
└── views/
└── includes/blocks/
└── block--google-reviews.html.twig
The bundle extension automatically registers via PrependExtensionInterface:
So no manual entries in sulu_admin.yaml or twig.yaml are required. Only the admin field type must be wired into the webpack build on the project side (see Installation, step 7).
How can I help you explore Laravel packages today?