Skip to content

Instantly share code, notes, and snippets.

@iambrianreich
Last active November 12, 2021 21:02
Show Gist options
  • Save iambrianreich/8e0a7e1f65908c76f8d13aea82c0ccf8 to your computer and use it in GitHub Desktop.
Save iambrianreich/8e0a7e1f65908c76f8d13aea82c0ccf8 to your computer and use it in GitHub Desktop.
Use the Google Places API to render a Google My Business reviews badge on a WordPress site using shortcodes.
<?php /** @noinspection SpellCheckingInspection */
/**
* This file contains the WordPress code for rendering a Google Reviews badge.
*
* Usage:
*
* 1. Create a Google API Console account if you have not already done so.
* 2. Register for the Google Places API.
* 3. Create an API key.
* 4. Ensure that the API key can be used from wherever this script will execute
* by configuring Key Restrictions.
* 5. Enter your new Google API key into the last line of this file where it says
* 'YOUR API KEY HERE'
* 6. Include this file in your theme or plugin by calling include('google-reviews.php');
* 7. Call the shortcode using either a query attribute (which will lookup the
* Place by name), or by specifying the placeId:
*
* [google-reviews-badge query="Reich Web Consulting"]
* [google-reviews-badge placeId="ChIJwaKJP7oZz4kROfVyv4AGTw8"]
*
* Note: lookups using a place id are more specific and reduce the risk of
* showing the wrong result. You can find your place id by using the following
* tool from Google:
*
* https://developers.google.com/maps/documentation/javascript/examples/places-placeid-finder
*
* @author Brian Reich <breich@reich-consulting.net>
* @copyright Copyright (C) 2018 Reich Web Consulting https://www.reich-consulting.net/
*
* Copyright (C) 2017 Reich Web Consulting
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* A wrapper for the Google Places API.
*
* In order to use the Google Places API wrapper, you need to provide an
* API key. You can get an API key from the Google Developer Console. The
* API key can be passed manually via the "apiKey" configuration option.
*
* @see https://console.developers.google.com/
* @author Brian Reich <breich@reich-consulting.net>
* @copyright Copyright (C) 2017 Reich Web Consulting
*/
class RWC_Google_Places
{
/**
* The API key.
*
* @var string
* @access private
*/
private $apiKey;
/**
* The base URL for querying the places API.
*
* @var string
* @access private
*/
private $url = 'https://maps.googleapis.com/maps/api/place';
/**
* Create a new Places instance.
*
* The options array is completely optional. However, the Places
* instance needs to be able to find a Google API key somewhere. You
* can set the API key directly by passing the "apiKey" option in the
* configuration array.
*
* @param array $options An array of configuration options.
*
* @constructor
*/
public function __construct(array $options = array())
{
$this->setOptions($options);
}
/**
* Sets the configuration options.
*
* @param array $options An array of configuration options.
*
* @return void
*/
public function setOptions(array $options = array())
{
// Mix-in defaults.
$options = array_merge([
'apiKey' => null
], $options);
$this->setApiKey($options[ 'apiKey' ]);
}
/**
* Sets the API key passed to setOptions().
*
* @param string $apiKey The Google API Key to use for queries.
*
* @return void
*/
private function setApiKey($apiKey)
{
$this->apiKey = $apiKey;
}
/**
* Returns the API key used to query the Google Places API.
*
* If an API was configured, it will be used. If an API key cannot be
* found, an Exception will be thrown.
*
* @return string Returns the Google API key.
* @throws Exception if no API is configured.
*/
private function getApiKey()
{
// If a key was configured, use it.
if ($this->apiKey !== null) {
return $this->apiKey;
}
throw new Exception(
'No Google API key has been configured.'
);
}
/**
* Finds a Place given a search string.
*
* Finds a Place given a search string. Can be the name of a location,
* an address, etc. If the query succeeds a PHP stdClass will be
* returned by converting the Places JSON response to an object. If an
* error occurs while making the request, an Exception will be thrown.
*
* @param string $place The place search string.
*
* @return stdClass Returns a PHP object containing matching places.
* @throws Exception if an error occurs while making the request.
*/
public function findPlaces($place)
{
// Make the request.
/** @noinspection SpellCheckingInspection */
$response = wp_remote_get($this->url . sprintf(
'/textsearch/json?key=%s&query=%s',
$this->getApiKey(),
urlencode($place)
));
if (is_wp_error($response)) {
throw new Exception('An error occurred while querying Google ' .
'Places for location data: ' .
$response->get_error_message());
}
return json_decode($response[ 'body' ]);
}
/**
* Returns details about a particular Google Place, by placeId.
*
* The getDetails() method will return details about a specific location
* in Google Places, by it's unique placeId. The placeId can be found
* by first searching for the location by name or by address using
* findPlaces.
*
* @param string $placeId The unique id of the place in Google Places.
*
* @return stdClass Returns a stdClass with the query results.
* @throws Exception if an error occurs while making the request.
*/
public function getDetails($placeId)
{
$response = wp_remote_get($this->url . sprintf(
'/details/json?key=%s&placeid=%s&',
$this->getApiKey(),
urlencode($placeId)
));
if (is_wp_error($response)) {
throw new Exception('An error occurred while querying Google ' .
'Places for location details: ' .
$response->get_error_message());
}
return json_decode($response[ 'body' ]);
}
}
/**
* A class that provides shortcode handlers for generating Google review badges.
*
* @see https://console.developers.google.com/
* @author Brian Reich <breich@reich-consulting.net>
* @copyright Copyright (C) 2017 Reich Web Consulting
*/
class RWC_Google_Reviews
{
/**
* Stores the Google API key used to access the API.
*
* @var string
* @access private
*/
private $apiKey = null;
/**
* Sets the API key used to access the Google API.
*
* @param string $apiKey The Google API key.
*
* @return void
*/
public function setApiKey($apiKey)
{
$this->apiKey = $apiKey;
}
/**
* Returns the Google API key.
*
* @return string Returns the Google API key.
*/
public function getApiKey()
{
return $this->apiKey;
}
/**
* Creates a new RWC_Google_Reviews instance.
*
* @param string $apiKey The Google API key.
*
* @constructor
*/
public function __construct($apiKey)
{
$this->setApiKey($apiKey);
// Register the shortcode
add_shortcode(
'google-reviews-badge',
array( $this, 'googleReviewBadge' )
);
}
/**
* Returns HTML which will provide Rich Data about a business listed
* on Google Places.
*
* The shortcode accepts the following options.
*
* The "apiKey" option can be used to manually specify an API key. If the
* API Key is omitted, the shortcode handler will check to see if one was
* specified when creating the RWC_Google_Reviews instance.
*
* The "query" option can be used to render a badge based on a Google Places
* query, based on a place name or an address. If this option is used, a
* badge will be rendered for all matching locations.
*
* The "placeId" option can be used to manually specify the id of the place
* to be rendered.
*
* If the API key was valid and a business is found using the query or
* placeId specified in the shortcode, the method will return HTML in
* schema.org LocalBusiness format specifying the required business
* information and review aggregation data.
*
* Example Shortcodes:
*
* [google_reviews_badge query="Reich Web Consulting"]
* [google_reviews_badge placeId="XXXXXXXXX"]
*
* @param array $options The shortcode options.
* @return string Returns HTML with LocalBusiness syntax with review counts.
*/
public function googleReviewBadge($options)
{
try {
// Merge in shortcode attributes with default options
$options = array_merge([
'apiKey' => $this->getApiKey(),
'query' => null,
'placeid' => null,
'placeId' => null
], $options);
// Apparently shortcode attributes are forced to lowercase
$options['placeId'] = $options['placeid'];
// Create an instance of the RWC_Google_Places
$places = new RWC_Google_Places($options);
// A query was specified, which may return multiple locations.
if ($options[ 'query' ] !== null) {
// Do the query.
$place = $places->findPlaces($options[ 'query' ]);
// If the query failed, return a message stating why.
if (isset($place->error_message)) {
return sprintf(
"Query failed: %s",
$place->error_message
);
}
// If no results, show message stating it.
if (count($place->results) == 0) {
return sprintf(
"No Google Places found matching %s",
$options[ 'query' ]
);
}
// Show badge for each.
$html = '';
foreach ($place->results as $result) {
$html .= $this->getPlaceBadgeHtml(
$result->place_id,
$places
);
}
return $html;
}
// A single placeId was specified. Generate and return a badge for
// only that place.
if ($options[ 'placeId' ] !== null) {
return $this->getPlaceBadgeHtml(
$options[ 'placeId' ],
$places
);
}
// Neither option was specified. Not an error, but nothing to show.
return 'No query or placeId attributes specified on shortcode.';
} catch (\Exception $e) {
// An error occurred while querying the Google API.
return $e->getMessage();
}
}
/**
* Renders the badge HTML for specific Google Place.
*
* If you explore this method you'll notice that we're taking advantage of
* the WordPress Transients API to cache HTML. We do this so we
* don't hammer Google with too many queries.
*
* @param string $place The unique id of the place in Google Places.
* @param \RWC\Google\Places $places The Places API wrapper.
*
* @return string Returns an HTML string for the badge.
*/
private function getPlaceBadgeHtml($placeId, $places)
{
// Store the HTML in a transient so we don't need to do this often.
$transient = 'places_badge_html_' . $placeId;
$html = get_transient($transient);
// If transient has value, use it.
if ($html !== false) {
return $html;
}
// Get details about this Place from the API
$places = $places->getDetails($placeId);
// Make sure a Place was found.
if (count($places->result) != 1) {
return sprintf(
"No Google Places found matching placeId %s",
$placeId
);
}
// Set the basic details.
$name = $places->result->name;
$url = $places->result->website;
$rating = $places->result->rating;
$reviewCount = count($places->result->reviews);
// Use a reduction function to find and set city.
$city = array_reduce($places->result->address_components, function ($c, $i) {
if (in_array('locality', $i->types)) {
$c = $i->long_name;
}
return $c;
});
// Use a reduction function to find and set state
$state = array_reduce($places->result->address_components, function ($c, $i) {
if (in_array('administrative_area_level_1', $i->types)) {
$c = $i->short_name;
}
return $c;
});
// Use a reduction function to find and set zipcode
$zipcode = array_reduce($places->result->address_components, function ($c, $i) {
if (in_array('postal_code', $i->types)) {
$c = $i->short_name;
}
return $c;
});
// Use a reduction function to find and set country
$country = array_reduce($places->result->address_components, function ($c, $i) {
if (in_array('country', $i->types)) {
$c = $i->short_name;
}
return $c;
});
// Start output buffer and generate HTML.
ob_start(); ?>
<div itemscope itemtype="http://schema.org/LocalBusiness">
<a itemprop="url" href="<?php echo esc_html($url); ?>">
<div itemprop="name"><?php echo esc_html($name); ?></div>
</a>
<div itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
<?php if ($pobox !== null) : ?>
P.O. Box: <span itemprop="postOfficeBoxNumber"><?php echo esc_html($pobox); ?></span>,
<?php endif; ?>
<span itemprop="addressLocality"><?php echo esc_html($city); ?></span>
<span itemprop="addressRegion"><?php echo esc_html($state); ?></span>
<span itemprop="postalCode"><?php echo esc_html($zipcode); ?></span>
<span itemprop="addressCountry"><?php echo esc_html($country); ?></span>
</div>
<?php if ($tel !== null) : ?>
<span class="tel">Tel :
<span itemprop="telephone">
<a href="tel:<?php echo esc_attr($tel); ?>"><?php echo esc_html($tel); ?></a>
</span>
</span>
<?php endif; ?>
<?php if ($fax !== null) : ?>
<span class="fax">Fax :
<span itemprop="faxNumber">555-555-5555</span>
</span>
<?php endif; ?>
<?php if ($email !== null) : ?>
<span class="email">Email :
<span itemprop="email">
<a href="mailto:breich@reich-consulting.net">breich@reich-consulting.net</a>
</span>
</span>
<?php endif; ?>
<span itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating">
Rated <span itemprop="ratingValue">
<?php echo esc_html($rating); ?></span>/<span itemprop="bestRating">
<?php echo esc_html($maxRating); ?></span>
based on <span itemprop="reviewCount"><?php echo esc_html($reviewCount); ?></span>
customer reviews.
</span>
</div>
<?php
// Grab buffered content
$html = ob_get_contents();
// Stop buffering
ob_end_clean();
// Save HTML so we don't have to do that mess for a while.
set_transient($transient, $html, 60 * 60); // One Hour
// Return it.
return $html;
}
}
$reviews = new RWC_Google_Reviews('YOUR API KEY HERE');
@nounours77
Copy link

@GetYooWoo

could you solve the problem in the mean time? Having exactly the same problem!!!

@ThomasRafael
Copy link

ThomasRafael commented Apr 14, 2018

@GetYooWoo @nounours77
I was able to figure it out. Line 80, put your API key here like this:
// Mix-in defaults. $options = array_merge( $options, [ 'apiKey' => 'Your_API_Key_Here' ] );
*Note: Still add your API Key to the line 446 one too

What I can't figure out is how to make the placeId option work. The 'query' option works just fine in the shortcode, but for some reason the 'placeId' option won't run. It's like the placeId value is not getting passed at all?

No matter how I spin up the code, I keep hitting a wall at line 298:
// A single placeId was specified. Generate and return a badge for // only that place. if( $options[ 'placeId' ] !== null ) { return $this->get_place_badge_html( $options[ 'placeId' ], $places ); } // Neither option was specified. Not an error, but nothing to show. return 'No query or placeId attributes specified on shortcode.';

It continually returns the 'No query or placeId attributes specified on shortcode.' response even when I have a correct placeId in the shortcode. Any idea @reichwebconsulting or an example of a working page? Great code by the way, thank you very much for your hard work on this!

@iambrianreich
Copy link
Author

Hi folks, it's up and running on my own company's website but honestly, it's not an idea or code I've thought much about since posted it here. If I get some time freed up in the next week or so I'll try and troubleshoot and see where the problem lies. If you figure something out in the meantime please post a comment and let me know.

@QQStars2000
Copy link

QQStars2000 commented Apr 17, 2018

@reichwebconsulting - took a look over your website and couldn't find a working example of this. If you can point to where you're using the placeId option to return results, maybe we could figure out what is not working. As noted by @ThomasRafael , the query option does work with some adjustments, but using a direct placeId is not returning any results. You could directly enter the placeId into the code, but that sort of defeats the purpose of this since it's most useful to place the shortcode with a placeId and have it work. Would be great if you could review for us.

@Casiopia
Copy link

Anyone figure out how to get this to work yet? Would really like to use it but can't get place ID working either. Thanks in advance! @reichwebconsulting @GetYooWoo @ThomasRafael @QQStars2000

@truwebs
Copy link

truwebs commented Apr 23, 2018

I spent some time playing around with this but just can't figure out how to make the placeId search work either. I can make it work if I put it directly into the code, but if it's just in the shortcode it doesn't get passed. Maybe it would be easier @reichwebconsulting if you simplified and split this into 1 version for the search shortcode and 1 version for the placeId shortcode? I really need to get this working too, have a big project that can really use this. Thank you for this code and help @reichwebconsulting.

@CodeAwards
Copy link

If this code could get fixed, I'd like to highlight it in a few articles I am writing @reichwebconsulting.

@iambrianreich
Copy link
Author

Good news, everyone! I got an opportunity to test this code tonight and made some bug fixes. There were a handful of what amounted to "typos" preventing the code as entered in this Gist from working. They have been fixed.

In addition, I added some better instructions to the file DocBlock which explain EXACTLY what you need to do in order to make this code work on your own site. Hopefully, that proves beneficial.

Finally, I'd add: if there is interest in this being an affordable premium plugin for WordPress, let me know.

@iambrianreich
Copy link
Author

And @CodeAwards, let me know if you'd like to discuss. Feel free to write that article, provided you're giving credit where it's due I'm happy to help in any way I can.

@draney
Copy link

draney commented May 7, 2018

I just tied your updated code and it still doesn't work for me. Found a typo on line 272 where the example shortcode contains underscores, while on line 18 it contains dashes. Dashes work, but if I use the place ID I get the message

Query failed: API keys with referer restrictions cannot be used with this API.

And if I use the place name I get the message

No Google Places found matching placeId ChIJ3WgTmGcJ9YgRV8SWTStZuDU

Thanks again for your help @reichwebconsulting.

@iambrianreich
Copy link
Author

That indicates that your API key is not set up to allow API queries from whatever domain you're making the request from. My recommendation would be to login to Google API Console edit your API key restrictions, and set it to "No Restrictions" and re-evaluate. Then once you know the code works with your key, restrict access as appropriate.

@draney
Copy link

draney commented May 12, 2018

Got it working with the place ID. Thanks! But now I am wondering where the results come from? It reports Rated 4.9/ based on 5 customer reviews. When I Google the same place, Google says 4.9 based on 64 Google reviews. So why does this code report only 5 reviews?

Thanks!!! @reichwebconsulting.

@draney
Copy link

draney commented May 12, 2018

FYI: @reichwebconsulting
I just noticed this on your website above the footer:
No Google Places found matching Reich Web Consulting

@APISnail
Copy link

@draney - the Google API is limited to 5 reviews, so @reichwebconsulting's code works as intended. Unfortunately with the Google API, it has been this way for years. There is a long running issue tracker on Google for an improvement someday if you want to add your vote and track any progress. https://issuetracker.google.com/issues/35825957

@nobutsta
Copy link

Fatal error: Call to undefined function add_shortcode() for me :(

@albusaidys
Copy link

Hello,
I'm coming at the tail end of this thread!
I'm currently using a paid version which is slowing my website tremendously... 7 seconds, which is a lot.
So I'm looking for a solution, basically much lighter code, I hope this code will help.

So I'm sure I can edit code and change what I need once I get some error!

But right now, my question is how do we add code to the website?
Do we add the complete google-reviews.php file as a separate code? If so, which directory do we put it in?
If we are to add it in another existing code, which code do I add to? and where do I locate the code?
Thank you so much for your help.
Samir

@Nono8D
Copy link

Nono8D commented Sep 9, 2019

Hello,
your code is wonderfull, it works very well.
But since the PHP 7.2 version, I have this error message when I open my page:
"Warning: count(): Parameter must be an array or an object that implements Countable in.."

Someone on the web says the problem is the new PHP 7.2.
I tried to find a solution, but I'm only a designer and it's too hard for me to understand what I have to modify in your code.

Is it possible for you or another developper to adjust the code for the last versions of PHP (with countables parameters)?
Thank you for your help.

@Nono8D
Copy link

Nono8D commented Sep 12, 2019

I've just add this [ ] on line 363:
if (count([$places->result]) != 1) {
It seems to work fine now, I haven't the PHP warning message.
Anyone could confirm the good code writing?

@FilipSmolik
Copy link

Got it working with the place ID. Thanks! But now I am wondering where the results come from? It reports Rated 4.9/ based on 5 customer reviews. When I Google the same place, Google says 4.9 based on 64 Google reviews. So why does this code report only 5 reviews?

Thanks!!! @reichwebconsulting.

REPLACE THIS: $reviewCount = count($places->result->reviews);
WITH THIS: $reviewCount = $places->result->user_ratings_total;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment