We are getting to the end of work on integrating Google Calendar via the Google API into a client site (Laravel). Our developer, Tom Metcalfe thought he would share our solution to hopefully help the next person who needs to do this...

The Need

The client wanted to connect venues that offer free or low cost spaces with youth organisations that needed space for their activities.

The venues have profiles on the site with a calendar to show when they have availability. The requirement was for venues to be able to add bookings to this calendar and it automatically add them to the linked Google Calendar and vice versa.

Overview

In order to pull any data from the venue’s linked Google Calendar the venue must give consent to the site to access it.

As documented in the diagram to the right, the venue granting consent provides the site with an authorisation code that the site can then swap for an access token with the Google API. This access token can only be used to interact with the service the venue has consented access to. In this case it is Google Calendar.

Creating a project

The very first step was to get the client to setup a project using their Google account. This quick start guide from Google gives a good walkthrough: https://developers.google.com/google-apps/calendar/quickstart/php

It also goes onto getting a demo up and running, but I’m going to skip that.

Once the project was setup the client needed to setup credentials for accessing the Google Calendar API through this project.

Adding credentials

There is a wizard for creating credentials. Its not amazingly clear so here are screenshots of what we setup. Note: I’ve used dummy content instead of the client data.

The first step asks which API will be used and how it will be accessed.

Google API credentials screen

 

The second step whitelists URLs and setup oAuth callback paths.

 

The third step sets up settings for the consent form the venues will be presented with.

 

The fourth step gives you the client ID and credentials.

Clicking ‘download’ on that last screen gives you the client_id.json file which is the sites key to the API via the clients project. This should be stored on the server in a private location.

{  
   "web":{  
      "client_id":"[hash-string].apps.googleusercontent.com",
      "project_id":"calendar-integration-[project-id]",
      "auth_uri":"https://accounts.google.com/o/oauth2/auth",
      "token_uri":"https://accounts.google.com/o/oauth2/token",
      "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
      "client_secret":"[hash-string]",
      "redirect_uris":[  
         "https://www.example.com/oauth2callback"
      ],
      "javascript_origins":[  
         "https://www.example.com"
      ]
   }
}

Require the Google API Client

As this is a Laravel site we already have Composer setup so first up we require the Google API client:

composer require google/apiclient:^2.0

This gives us a PHP Library to communicate with the Google APIs plus a load of helper functions for each API and OAuth2.

More information can be found here: https://github.com/google/google-api-php-client

Authorisation

Requesting consent

First step for the site is to provide a means for venues to grant consent for the site to access their Google Calendar.

For that we need to create a link that will send venues to Google which will show the consent screen.

For this we will initialise the Google Client provided by google/apiclient and set our application specific settings.

<?php
namespace App\Helpers;

// Initialise the client.
$client = new Google_Client();
// Set the application name, this is included in the User-Agent HTTP header.
$client->setApplicationName('Calendar integration');
// Set the authentication credentials we downloaded from Google.
$client->setAuthConfig('[private-path]/client_id.json');
// Setting offline here means we can pull data from the venue's calendar when they are not actively using the site.
$client->setAccessType("offline");
// This will include any other scopes (Google APIs) previously granted by the venue
$client->setIncludeGrantedScopes(true);
// Set this to force to consent form to display.
$client->setApprovalPrompt('force');
// Add the Google Calendar scope to the request.
$client->addScope(Google_Service_Calendar::CALENDAR);
// Set the redirect URL back to the site to handle the OAuth2 response. This handles both the success and failure journeys.
$client->setRedirectUri(URL::to('/') . '/oauth2callback');

When venue clicks the link they are redirected to Google and asked to grant consent for the site to access their Google Calendar.

Handling the response

Google will redirect the venue back to the site at the URL specified here  $client->setRedirectUri(URL::to('/') . '/oauth2callback');.

This route is used both if the venue has consented to access to their Google Calendar and if they have refused.

<?php
/**
 * Google OAuth2 route
 */
Route::get('oauth2callback', [
    'as' => 'oauth',
    'uses' => 'OAuthController@index'
]);

This route says when a GET request is made to /oauth2callback then use the index method in the OAuthController controller.

More information about Laravels routing can be found here: https://laravel.com/docs/5.5/routing

This is what that method looks like:

<?php

public function index(Request $request)
    {
        // Get all the request parameters
        $input = $request->all();

        // Attempt to load the venue from the state we set in $client->setState($venue->id);
        $venue = Venue::findOrFail($input['state']);

        // If the user cancels the process then they should be send back to
        // the venue with a message.
        if (isset($input['error']) &&  $input['error'] == 'access_denied') {
            \Session::flash('global-error', 'Authentication was cancelled. Your calendar has not been integrated.');
            return redirect()->route('venues.show', ['slug' => $venue->slug]);

        } elseif (isset($input['code'])) {
            // Else we have an auth code we can use to generate an access token

            // This is the helper we added to setup the Google Client with our             
            // application settings
            $gcHelper = new GoogleCalendarHelper($venue);

            // This helper method calls fetchAccessTokenWithAuthCode() provided by 
            // the Google Client and returns the access and refresh tokens or 
            // throws an exception
            $accessToken = $gcHelper->getAccessTokenFromAuthCode($input['code']);

            // We store the access and refresh tokens against the venue and set the 
            // integration to active.
            $venue->update([
                'gcalendar_credentials' => json_encode($accessToken),
                'gcalendar_integration_active' => true,
            ]);

            \Session::flash('global-success', 'Google Calendar integration enabled.');
            return redirect()->route('venues.show', ['slug' => $venue->slug]);
        }
    }

This now allows us to access the venue’s Google Calendar using the access token returned from fetchAccessTokenWithAuthCode(). This method doesn’t just return the access token it returns a few other bits too:

{  
   "access_token":"[hash-string]",
   "token_type":"Bearer",
   "expires_in":3600,
   "created":1510917377,
   "refresh_token":"[hash-string]"
}

This return has been json encoded for storage in the database.

This token only lives for an hour as denoted by the expires_in above. When the access token expires we will need to use the refresh_token to request a new access token.

Refreshing the access token

The Google Client provides methods for checking if the current access token is expired. We pass in the value of $venue->gcalendar_credentials which is the JSON above to setAccessToken() and then refresh the token if expired.

<?php
// Refresh the token if it's expired.
$client->setAccessToken($venue->gcalendar_credentials);
if ($client->isAccessTokenExpired()) {
    $accessToken = $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
    $venue->update([
        'gcalendar_credentials' => json_encode($accessToken),
    ]);
}

We save that in the database against the venue again. This was added to the helper class we setup that initialises the Google Client.

Pulling bookings from Google Calendar

Now we have a valid access token we can start polling the venue’s Google Calendar for events.

<?php

// Load up the helper to initialise the Google Client
$gcHelper = new GoogleCalendarHelper($venue);

// Use the Google Client calendar service. This gives us methods for interacting 
// with the Google Calendar API
$service = $gcHelper->getCalendarService();

// Set over what timeframe we want to grab calendar events
// Dates must be an RFC3339 timestamp with mandatory time zone offset, e.g.,
// 2011-06-03T10:00:00-07:00
$optParams = array(
    'orderBy' => 'startTime',
    'singleEvents' => true,
    'timeMin' => '2011-06-03T10:00:00-07:00',
    'timeMax' => '2011-06-03T10:00:00-23:00',
);

// Poll this venue's Google Calendar
$googleBookings = $service->events->listEvents($calendarId, $optParams);

// Check if we have any events returned
if (count($googleBookings->getItems()) > 0) {

Once we have a list of events from Google Calendar we save those to the database to display as bookings on the site.

Pushing bookings to Google Calendar

When a venue adds a booking to the site, one is automatically created in their Google Calendar.

<?php

// Set the start time and date for pushing to Google.
$tz = new DateTimeZone('Europe/London');
$startTime = Carbon::create(
    2017,
    11,
    25,
    10,
    0,
    0,
    $tz
);

// Use the Google date handling provided by the Google Client
$googleStartTime = new Google_Service_Calendar_EventDateTime();
$googleStartTime->setTimeZone($tz);
$googleStartTime->setDateTime($endTime->format('c'));

// Create the calendar event and give a default title.
$event = new Google_Service_Calendar_Event();
$event->setStart($googleStartTime);
// Same process to create end time as we use for the start time above. Code 
// omitted for brevity.
$event->setEnd($googleEndTime);
$event->setSummary('Booking automatically created from Calendar Example');

// Use the Google Client calendar service to push 
$service = $gcHelper->getCalendarService();
$createdEvent = $service->events->insert($calendarId, $event);

// Save the newly created event id against the booking.
if (!empty($createdEvent->id)) {

If the $createdEvent has an id then we have been successful in pushing this booking to Google Calendar as a new event.

This id can be used to delete the event from Google Calendar like so $service->events->delete($calendarId,$eventId);.

Gotchas

Refresh token

From our experience $client->setApprovalPrompt('force'); is required to get a refresh token back along with the auth token. Having looked at a load of SO articles it looks like this should also force the user to grant consent to their Google Calendar each time the auth token expires, but this hasn’t been the case for us.

Timezones

You’ll notice when we created the event in the venue’s Google Calendar we set the timezone to Europe/London. By default the API uses one of the American timezones. This wasn’t suitable for us in the UK.

Created events

When an event is created via the API the venue still needed to pull them into their calendar. This works the same way as when someone invites you to an event and you have to accept before it goes into your calendar.

Summary

In summary using the Google API was pretty straight forward with the google/apiclient. The OAuth stuff was the trickiest part and that was mostly down to a lot of conflicting information on SO and other sites about how auth/access/refresh tokens work.

The Google documentation itself is good for the best case scenario, but information on when things go wrong is non-existent.

Reason Digital News