Fetching Google My Business reviews using the Google API and OAuth2 – a WordPress boilerplate class
Published on 22/08/2022, last modified on 26/08/2022
A business website I am currently building has multiple physical locations, each with their own Google My Business listing. To improve trust and highlight their service quality, we wanted to display the Google My Business reviews on the website. Google doesn’t provide an easy public feed for that though. You need to use the Google My Business API and authenticate using OAuth2 to get a location’s reviews.
There’s a few threads on StackOverflow about getting reviews using the My Business API, but many are a few years old and the tools used are outdated. Apart from the documentation, there’s not too much up-to-date info about the whole process, so there was quite some digging and testing involved. I ended up with a nice little helper class though.
Prerequisites
This whole article builds upon a few prerequisites. You will need to have the following parameters set up in order to fetch business reviews, as outlined in the Google My Business API guidelines:
- You have requested and been granted access to the Google My Business API
- You have activated the required APIs in the Google API Console project
- You have created OAuth2 client credentials
- You have set up the correct authorized domains and authorized redirect URIs
- The app expects the Google API Client library to be present (see notes later on)
Using the Google API Client PHP library to authenticate via OAuth2
Google recommends using the Google API Client PHP library to authenticate and query the Google My Business API. Setting up the authentication process using the library is easy, and I therefore stuck with that. Using the library to query the various APIs is – as far as I could understand – not quite as easy. I barely found any documentation for the library (the APIs have plenty though), so I couldn’t quite get this to work as needed.
In the end, I took a split approach: the whole authentication process is handled using the library, but any querying is a simple REST endpoint call using PHP’s curl
. At this point, I find this to be the simpler and easier to understand and maintain setup.
Making the Google My Business review data accessible in the WordPress admin
I built a small dashboard for being able to see the current authentication status, access token data and what review data is currently present. Here’s what it looks like (some data blurred for security and privacy reasons):
If the app is not authenticated, the status dashboard will just show a button to authenticate. The authentication process will redirect you to Google where you need to sign in with the Google account that can access your My Business locations. The authentication process will then submit an auth token back to the app, which it uses from there on to call the Google My Business API in your account’s name. That’s basically the OAuth2 process.
Once authenticated, the dashboard shows you the access token (truncated), when it was created, when it expires and what that means in remaining time-to-live. The access token is valid for one hour. The class will refresh the access token automatically once it’s valid for under 600 seconds / 10 minutes. The refresh token that was obtained during authentication will be used for that.
Below, you will be able to fetch locations that are accessible by your Google account and after that, fetch the reviews for that location. Each of those data sets are saved in separate database options. That way, accessing, deleting or changing data is easy. For example, it’s possible to refresh a single location’s review data without touching the other data points.
GMB_Reviews: a boilerplate class for handling Google My Business review data in WordPress
I’ve set up all the necessary functions for authentication, querying the necessary API endpoints and showing the apps status in a class. Here’s the full code:
<?php
defined( 'ABSPATH' ) || die();
class GMB_Reviews {
private $option_prefix;
private $status_page_slug;
private $status_page_base_url;
private $api_client_id = 'DEFINE_HERE';
private $api_client_secret = 'DEFINE_HERE';
private $api_client;
private $token_data;
private $refresh_token;
private $access_token;
public function __construct() {
$this->setup_variables();
$this->setup_api_client();
$this->prepare_token_data();
$this->maybe_refresh_access_token();
add_action( 'admin_menu', array( $this, 'register_status_page' ) );
add_action( 'admin_init', array( $this, 'handle_status_page_actions' ) );
}
public function setup_variables() {
$this->option_prefix = 'gmb_reviews_';
$this->status_page_slug = 'gmb-reviews-status';
$this->status_page_base_url = admin_url( 'options-general.php?page=' . $this->status_page_slug );
}
public function setup_api_client() {
require_once get_template_directory() . '/inc/google-api-client/vendor/autoload.php';
$this->client = new Google\Client();
$this->client->setClientId( $this->api_client_id );
$this->client->setClientSecret( $this->api_client_secret );
$this->client->addScope( 'https://www.googleapis.com/auth/business.manage' );
$this->client->setRedirectUri( $this->get_action_url( 'auth' ) );
$this->client->setAccessType( 'offline' );
$this->client->setApprovalPrompt( 'force' );
$this->client->setIncludeGrantedScopes( true );
}
public function auth() {
if( ! isset( $_GET['code'] ) ) {
header( 'Location: ' . filter_var( $this->client->createAuthUrl(), FILTER_SANITIZE_URL ) );
} else {
$this->client->authenticate( $_GET['code'] );
$token_data = (array) $this->client->getAccessToken();
update_option( $this->option_prefix . 'token_data', $token_data );
$this->prepare_token_data();
$this->fetch_account_name();
$this->redirect_to_base();
}
}
public function unset_auth() {
delete_option( $this->option_prefix . 'token_data' );
}
public function is_app_authenticated() {
return empty( get_option( $this->option_prefix . 'token_data' ) ) ? false : true;;
}
public function prepare_token_data() {
$token_data = get_option( $this->option_prefix . 'token_data' );
if( empty( $token_data ) )
return false;
$this->token_data = $token_data;
$this->refresh_token = $token_data['refresh_token'];
$this->access_token = $token_data['access_token'];
$this->client->setAccessToken( $this->access_token );
}
public function get_refresh_token() {
return $this->refresh_token;
}
public function get_access_token() {
return $this->access_token;
}
public function get_access_token_ttl() {
return ( $this->token_data['created'] + $this->token_data['expires_in'] - time() );
}
public function maybe_refresh_access_token() {
if( $this->is_app_authenticated() && $this->get_access_token_ttl() < 600 ) {
$this->refresh_access_token();
$this->prepare_token_data();
return true;
}
return false;
}
public function refresh_access_token() {
$this->client->fetchAccessTokenWithRefreshToken( $this->get_refresh_token() );
$token_data = (array) $this->client->getAccessToken();
update_option( $this->option_prefix . 'token_data', $token_data );
}
public function get_action_url( $action ) {
return add_query_arg( 'action', $action, $this->status_page_base_url );
}
public function redirect_to_auth() {
header( 'Location: ' . filter_var( get_action_url( 'auth' ), FILTER_SANITIZE_URL ) );
}
public function redirect_to_base() {
header( 'Location: ' . filter_var( $this->status_page_base_url, FILTER_SANITIZE_URL ) );
}
public function curl( $url ) {
$curler = curl_init();
curl_setopt( $curler, CURLOPT_URL, $url );
curl_setopt( $curler, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json', 'Authorization: Bearer ' . $this->get_access_token() ) );
curl_setopt( $curler, CURLOPT_RETURNTRANSFER, 1 );
$curl_response = curl_exec( $curler );
curl_close( $curler );
return json_decode( $curl_response, true );
}
public function fetch_account_name() {
$url = 'https://mybusinessaccountmanagement.googleapis.com/v1/accounts';
$response = $this->curl( $url );
$account_name = $response['accounts'][0]['name'];
update_option( $this->option_prefix . 'account_name', $account_name );
}
public function get_account_name() {
return get_option( $this->option_prefix . 'account_name' );
}
public function unset_account_name() {
delete_option( $this->option_prefix . 'account_name' );
}
public function fetch_locations() {
$account_name = $this->get_account_name();
$url = "https://mybusinessbusinessinformation.googleapis.com/v1/$account_name/locations?readMask=title,name";
$response = $this->curl( $url );
$locations = $response['locations'];
update_option( $this->option_prefix . 'locations', $locations );
}
public function get_locations() {
return get_option( $this->option_prefix . 'locations' );
}
public function unset_locations() {
delete_option( $this->option_prefix . 'locations' );
}
public function fetch_reviews_for_all_locations() {
$locations = $this->get_locations();
if( empty( $locations ) )
return false;
foreach( $locations as $location ) {
$this->fetch_reviews_for_location( $location['name'] );
}
}
public function fetch_reviews_for_location( $location_name ) {
$account_name = $this->get_account_name();
$url = "https://mybusiness.googleapis.com/v4/$account_name/$location_name/reviews";
$response = $this->curl( $url );
$db_reviews = get_option( $this->option_prefix . 'reviews' );
$db_reviews[$location_name] = $response['reviews'];
update_option( $this->option_prefix . 'reviews', $db_reviews );
}
public function get_reviews() {
return get_option( $this->option_prefix . 'reviews' );
}
public function unset_reviews() {
delete_option( $this->option_prefix . 'reviews' );
}
public function get_review_count( $reviews ) {
return count( $reviews );
}
public function convert_rating( $star_rating ) {
$map = array( 'ONE' => 1, 'TWO' => 2, 'THREE' => 3, 'FOUR' => 4, 'FIVE' => 5 );
return $map[ $star_rating ];
}
public function get_average_rating( $reviews ) {
$count = $this->get_review_count( $reviews );
$rating_sum = 0;
foreach( $reviews as $review ) {
$rating_sum = $rating_sum + $this->convert_rating( $review['starRating'] );
}
return number_format( (float) ( $rating_sum / $count ), 1, ',', '' );
}
public function register_status_page() {
add_options_page( 'GMB Reviews Status', 'GMB Reviews', 'manage_options', 'gmb-reviews-status', array( $this, 'render_status_page' ) );
}
public function handle_status_page_actions() {
if( ! is_admin() )
return false;
if( ! current_user_can( 'manage_options' ) )
return false;
if( ! ( isset( $_GET['page'] ) && $this->status_page_slug == $_GET['page'] ) )
return false;
// We should be on our status page at this point
if( ! $this->is_app_authenticated() && isset( $_GET['action'] ) && 'auth' == $_GET['action'] ) {
$this->auth();
}
// unset_auth action
if( $this->is_app_authenticated() && isset( $_GET['action'] ) && 'unset_auth' == $_GET['action'] ) {
$this->unset_reviews();
$this->unset_locations();
$this->unset_account_name();
$this->unset_auth();
$this->redirect_to_base();
}
// refresh_access_token action
if( $this->is_app_authenticated() && isset( $_GET['action'] ) && 'refresh_access_token' == $_GET['action'] ) {
$this->refresh_access_token();
$this->redirect_to_base();
}
// fetch_locations action
if( $this->is_app_authenticated() && isset( $_GET['action'] ) && 'fetch_locations' == $_GET['action'] ) {
$this->fetch_locations();
$this->redirect_to_base();
}
// unset_locations action
if( isset( $_GET['action'] ) && 'unset_locations' == $_GET['action'] ) {
$this->unset_reviews();
$this->unset_locations();
$this->redirect_to_base();
}
// fetch_reviews action
if( $this->is_app_authenticated() && isset( $_GET['action'] ) && 'fetch_reviews' == $_GET['action'] ) {
$this->fetch_reviews_for_all_locations();
$this->redirect_to_base();
}
// unset_reviews action
if( isset( $_GET['action'] ) && 'unset_reviews' == $_GET['action'] ) {
$this->unset_reviews();
$this->redirect_to_base();
}
}
public function render_status_page() {
?>
<h2>GMB Reviews Status</h2>
<?php if( ! $this->is_app_authenticated() ) { ?>
<p>Authenticate the app with your Google account.</p>
<p><a href="<?php echo $this->get_action_url( 'auth' ); ?>" class="button button-primary">Authenticate API</a></p>
<?php } else { ?>
<style>
.form-table {
margin-bottom: 1em;
}
.form-table tr th,
.form-table tr td {
padding: 5px 10px 5px 0;
}
.form-table tr th:first-of-type {
width: 30%;
}
</style>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th>Access token</th>
<td><?php echo substr( $this->get_access_token(), 0, 50 ); ?>...</td>
</tr>
<tr>
<th>Access token created</th>
<?php
$token_created_dt = new DateTime();
$token_created_dt->setTimezone( wp_timezone() );
$token_created_dt->setTimestamp( $this->token_data['created'] );
?>
<td><?php echo $token_created_dt->format( 'Y-m-d H:i:s' ); ?></td>
</tr>
<tr>
<th>Access token expires</th>
<?php
$token_expires_dt = new DateTime();
$token_expires_dt->setTimezone( wp_timezone() );
$token_expires_dt->setTimestamp( $this->token_data['created'] + $this->token_data['expires_in'] );
?>
<td><?php echo $token_expires_dt->format( 'Y-m-d H:i:s' ); ?></td>
</tr>
<tr>
<th>Access token TTL</th>
<td><?php echo date( 'H:i:s', $this->get_access_token_ttl() ); echo ' / ' . $this->get_access_token_ttl() . 's'; ?></td>
</tr>
</tbody>
</table>
<p>
<a href="<?php echo $this->get_action_url( 'refresh_access_token' ); ?>" class="button button-primary">Refresh access token</a>
<a href="<?php echo $this->get_action_url( 'unset_auth' ); ?>" class="button button-secondary">Unset authentication</a>
</p>
<h2>Locations</h2>
<?php if( ! empty( $locations = $this->get_locations() ) ) { ?>
<table class="form-table" role="presentation">
<thead>
<tr>
<th>location_name</th>
<th>location_title</th>
</tr>
</thead>
<tbody>
<?php foreach( $locations as $location ) { ?>
<tr>
<td><?php echo $location['name']; ?></td>
<td><?php echo $location['title']; ?></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<p>
<a href="<?php echo $this->get_action_url( 'fetch_locations' ); ?>" class="button button-primary">Fetch locations</a>
<a href="<?php echo $this->get_action_url( 'unset_locations' ); ?>" class="button button-secondary">Unset locations</a>
</p>
<?php if( ! empty( $locations = $this->get_locations() ) ) { ?>
<h2>Reviews</h2>
<?php if( ! empty( $reviews = $this->get_reviews() ) ) { ?>
<table class="form-table" role="presentation">
<thead>
<tr>
<th>location_name</th>
<th>reviews</th>
</tr>
</thead>
<tbody>
<?php foreach( $reviews as $location_name => $location_reviews ) { ?>
<tr>
<td><?php echo $location_name; ?></td>
<td><?php echo $this->get_review_count( $location_reviews ) . ' reviews / ⌀ ' . $this->get_average_rating( $location_reviews ); ?></td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
<p>
<a href="<?php echo $this->get_action_url( 'fetch_reviews' ); ?>" class="button button-primary">Fetch reviews</a>
<a href="<?php echo $this->get_action_url( 'unset_reviews' ); ?>" class="button button-secondary">Unset reviews</a>
</p>
<?php } ?>
<?php } ?>
<?php
}
}
$gmb_reviews = new GMB_Reviews();
There’s a few things to note here:
- Obviously, you will need to define your own
$api_client_id
and$api_client_secret
variables. - If you intend to use this class as-is, you will need to add the Google API PHP library code and make sure it’s loaded from your defined location in the
setup_api_client()
function. - The functions are named with
fetch_
andget_
prefixes.fetch_
will query new data from the API,get_
retrieves data from the local database. - The dashboard actions are not yet protected with nonces, which might be a good idea when using this in production.
- The class uses the first
account_name
that the corresponding function retrieves. I’m not sure in which cases there would be more than one account, so I hardcoded it to just use the first. - The class is a boilerplate for fetching Google My Business review data, but it does not yet display it on the frontend in any way. I will be adding custom blocks for that in my project.
- The class does not yet automatically fetch reviews, e.g. every 6 hours. I will be adding cron jobs for that in my project.
- The review data is stored as received from the REST endpoint. All the properties are available, including comment, rating, reviewer name and image thumbnail, time created, and even the businesses’ response if available.
var_dump
the$this->option_prefix . 'reviews'
database option to see all properties.
Wrapping it up
I personally learned a lot while setting this up, which is always a good thing. I got to know the OAuth2 process, which is a solid standard at this point. I imagine other services like Facebook, Github et al use a similar approach, so implementing a “Sign in with” process in future projects doesn’t feel like an impossible task anymore.
Building general purpose classes is always a pleasure, because it feels like you can really reuse it in the future – and others can reuse it as well. If you do, let me know in the comments. I’d love to hear if the things I learned are any helpful to others.