depa/sulu-google-reviews-bundle

Fetches and displays Google Places reviews in Sulu CMS with admin moderation

Maintainers

Package info

github.com/depa-berlin/sulu-google-reviews-bundle

Type:symfony-bundle

pkg:composer/depa/sulu-google-reviews-bundle

Transparency log

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0-rc4 2026-07-08 15:09 UTC

This package is auto-updated.

Last update: 2026-07-09 14:47:47 UTC


README

php workflow symfony workflow License: MIT GitHub Tag Supports Sulu 3.0 or later

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

Requirements

  • PHP 8.2+
  • Sulu CMS 3.0+
  • Symfony 7.0+
  • MySQL 8.0+

Installation

1. Install via Composer

composer require depa/sulu-google-reviews-bundle

2. Register 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],

3. Import admin routes

Add to config/routes/routes_admin.yaml:

DepaSuluGoogleReviewsBundle:
    resource: "@DepaSuluGoogleReviewsBundle/Resources/config/routes_admin.yaml"
    prefix: /admin/api

4. Set environment variables

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)%'

5. Update the database schema

bin/adminconsole doctrine:schema:update --force

Creates the depa_googlereviews_reviews table.

6. Clear cache

bin/adminconsole cache:clear

7. Wire the admin field type (frontend build)

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:

  1. Reference the package in assets/admin/package.json:

    "dependencies": {
        "sulu-google-reviews-bundle": "file:../../vendor/depa/sulu-google-reviews-bundle/Resources/js"
    }
  2. Import it in the admin entry file (assets/admin/app.js):

    import 'sulu-google-reviews-bundle';
  3. 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.

Integrating the Sulu Block

Register block type in section template

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.

Block fields in editor

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)

Sort modes

Value Behavior
By Date Newest reviews first
By Rating Highest star rating first
Custom Order Sorted by the "Sort Order" field of individual reviews

Fetching reviews

Manual fetch

bin/adminconsole sulu:google-reviews:fetch

Automated fetch via cronjob

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.

Fetching the newest reviews instead of "most relevant" (optional)

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:

  • the legacy "Places API" enabled for the Google Cloud project (separate from "Places API (New)"; newly created projects can no longer enable it), and
  • an API key allowed to use both APIs (New + Legacy).

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.

Multilingual support

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.

  • In addition, the original text and its language are stored; the frontend uses them as a fallback when no translation exists for the current locale.
  • When a language is added to the webspace later, it is filled in automatically on the next fetch run — without a database migration (translations are stored in a JSON column). This only applies to reviews Google currently returns (max. 5); older reviews outside that window are covered by the backfill command (see below).

Backfilling missing translations (optional)

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
  • Only missing language versions are added; existing ones (e.g. imported from Google) are left untouched.
  • The translation service is an optional dependency. If 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.
  • A custom translation service can be plugged in by binding Depa\SuluGoogleReviewsBundle\Translation\ReviewTranslatorInterface to your own implementation in the project; it then takes precedence over the DeepL auto-wiring.

Google API notes

  • Places API (New): the bundle uses the current Places API (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.
  • Maximum 5 reviews per request: the Places API returns at most 5 reviews without pagination and without a sort option. If more than 5 new reviews appear between two cron runs, gaps can occur.
  • API cost: one API call is made per webspace locale (Place Details with reviews is the billable Enterprise SKU). With e.g. three languages the number of requests per fetch run triples.

Moderation in Sulu Admin

After installation, the menu item "Google Reviews" appears in the Sulu backend (icon: comment).

List view

Shows all imported reviews with author, star rating, date, block status, and sort order.

Detail view

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).

Block review

Blocked reviews won't appear on the frontend and are excluded from get_stored_google_reviews() results.

Custom sort order

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 order

When assigning an already-taken sort position, all other entries at that position and above are automatically shifted up by one.

Twig functions (direct usage)

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 %}

Available review properties

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.text without an argument returns the original text as a fallback). The relative time is computed via google_review_relative_time(...) instead of being read from a stored value.

Frontend styling

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

Customizing the template

Once the bundle is installed via Composer, override the template in your main project:

  1. Create the file:

    templates/includes/blocks/block--google-reviews.html.twig
    
  2. 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.

Architecture overview

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:

  • Sulu Admin: list and form directories, resource routes
  • Doctrine ORM: entity mapping
  • Twig: views directory

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).