Menu

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:

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:

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.

Leave a comment