Getting Zend Framework 2 and Google's API to play nice

Need to use Google's API from your ZF2 app?  Tired of parsing through StackOverflow and Google's multiple document versions? You're in the right place.  In my particular case, I needed to connect to Google's calendar service.  Examples below.


Setup - ZF2

We're going to make use of the google/apiclient composer package for all of this.  Edit your composer.json and add "google/apiclient": "1.0.*@dev"

You're also going to need an oAuth callback route.  When your app authenticates with Google, it sends a passback token that the Google client uses to generate an auth token.  For now, create a route in a module of your choice, that leads to a controller that contains 'callbackAction'.  I called mine GoogleController, you'll see its code below.


Setup - Google

Most of the confusion that was evidenced, exists at this step.  There's a new UI rolled out, the docs don't match, and the names of the keys referenced in the docs are a function of these visuals.  Head over to https://console.developers.google.com/, and access the Project you would like to connect (create one if required).

Under APIS & AUTH on the left hand side, select APIs.  Here, "turn on" all of the "features" that your program needs.

Next, under Credentials, click on the Create a new Client ID button.  This will show you a modal window, where you have several options.  Select Web Application, and fill out the text boxes at the bottom (with your App's URLs).  Pay special attention to the second, that's the redirect URI whose callback route you configured earlier (callbackAction).  Click Create Client ID, and you're done.

Screenshot of the Create Client ID panel

Screenshot of the Create Client ID panel

Next, under Public API access at the bottom left, click on Create new Key.  This opens another small modal, where you will click on Server key.  A last panel appears which asks you which IP addresses you want to whitelist.  Type them in, and close the modal.  This created a table in the main view with API KEY.  This is commonly called Developer Key in the apps.

Gather all the details...

You can pretty much fill in this worksheet at this stage, we'll reference it in the code that follows:

  • DEVELOPER_KEY: "Key for server applications" > "API KEY"
  • CLIENT_ID: "Client ID for web application" > "CLIENT ID"
  • CLIENT_SECRET: "Client ID for web application" > "CLIENT SECRET"
  • CALLBACK: The callback URI you defined


Into Zend Framework...

The Google API that you installed with composer (you ran your php composer.phar selfupdate and php composer.phar update right?) really is a throwback to old university assignments.  The profs would come up with the most horrendous combinations of words and underscores.  We're going to hide that ugliness away in your main Module.  Create a factory in your getServiceConfig() that looks like:


'GoogleClient' => function( $sm ){
    $client = new \Google_Client();
    $client->setAccessType('online');
    $client->setApplicationName('doesnotmatter');
    $client->setClientId( CLIENT_ID );
    $client->setClientSecret( CLIENT_SECRET );
    $client->setRedirectUri( CALLBACK );
    $client->setDeveloperKey( DEVELOPER_KEY );
    return $client;
},

This is the client we'll use to establish "scope", which is critical to your usage of the API.  In my case, I wanted to schedule a Calendar event via AJAX.  I added a second factory for GoogleCalendar, like so:


'GoogleCalendar' => function( $sm ){
    $client = $sm->get('GoogleClient');
    $client->setScopes( array(
        'https://www.googleapis.com/auth/calendar'
    ));

    if( isset( $_SESSION['google_access_token'] ) )
        $client->setAccessToken( $_SESSION['google_access_token'] );

    if( !$client->getAccessToken() )
        return new \Application\Model\Google\AuthRequest( $client->createAuthUrl() );

    $calendar = new \Google_Service_Calendar( $client );
    return $calendar;
}

The clever you notices an interjected AuthRequest class to signal that the app (at that point in time) had no rights to adjust calendars.  It's a vehicle class, that exists since factories must return objects, it also cleans up the Controller code.  Here's my AuthRequest class, you can do this any way you need:



namespace Application\Model\Google;

class AuthRequest
{

    public $auth_url;


    public function __construct( $url )
    {
        $this->auth_url = $url;
    }

    /**
     * @param mixed $auth_url
     */
    public function setAuthUrl($auth_url)
    {
        $this->auth_url = $auth_url;
    }

    /**
     * @return mixed
     */
    public function getAuthUrl()
    {
        return $this->auth_url;
    }

}

ZF2 Application Flow

  • A modal window appears in my front end, which sends an AJAX request to a controller
  • The controller munges the data received, and tries to schedule the calendar events
  • It's possible that an oAuth dialog needs to be shown, handle it.
  • Conversely, possible that oAuth is already taken care of, schedule the events

Controller Action


function scheduleEvent
{
    // app-specific data processing

    try
    {
    $calendar = $this->getServiceLocator()->get('GoogleCalendar');

     if( $calendar instanceof AuthRequest )
         return new JsonModel( array( 'oauth' => true, 'url' => $calendar->getAuthUrl() ) );

     /** @var \Google_Service_Calendar $calendar */
     $event = new \Google_Service_Calendar_Event();
     $event->setSummary( "This is a test" );
     $dts = new \Google_Service_Calendar_EventDateTime();
     $dts->setDateTime( date( "Y-m-d\\TH:i:s.000-04:00" ) );
     $event->setStart( $dts );

     $dte = new \Google_Service_Calendar_EventDateTime();
     $dte->setDateTime( date( "Y-m-d\\TH:i:s.000-04:00", time() + 1800 ) );
     $event->setEnd( $dte );

     $att = array();
     foreach( $emails as $e )
     {
         $attendee = new \Google_Service_Calendar_EventAttendee();
         $attendee->setEmail( $e );
         $att[] = $attendee;
     }

     $event->setAttendees( $att );
     $x = $calendar->events->insert( "primary", $event, array( 'sendNotifications' => true ) );
            return new JsonModel( array( 'success' => true ) );
    }
    catch( \Exception $x )
    {
        return new JsonModel( array( 'success' => false, 'message' => $x->getMessage() ) );
    }
}


On the front end, the AJAX call looked like this.  In a nutshell, if it sees oauth in the json return, it triggers the Google callback


$.getJSON( '/sales/prospect/case-study', d, function( j ){
    if(j.oauth)
    {
        var popup = window.open(j.url, '_blank', 'width=500px,height=650px' );
        noty({
            layout: 'bottom',
            type: 'information',
            text: "You need to give Central access to your Google Calendar, please authorize Central and try again.",
            timeout: 10000
        });
    }
    else
    {
        if(j.success)
        {
            $("#act_case").modal('hide');
            showSuccess( "Case study successfully scheduled, nice work." );
        }
        else
        {
            showError(j.message);
        }
    }
}).always( function(){
    btn.trigger('ladda-stop');
});

What this does, is open up a Google authorization dialog.  The end user then selects their Google account, authorizes your app, and is driven to your callback URI.  Here's the code to handle the callback.  The official Google PHP docs are missing the parameter to 'authenticate'


public function callbackAction()
{
    $client = $this->getServiceLocator()->get('GoogleClient');
    if( $code = $this->params()->fromQuery('code') )
    {
        $client->authenticate( $code );
        $_SESSION['google_access_token'] = $client->getAccessToken();
    }

    $vm = new ViewModel();
    $vm->setTemplate('application/index/blank_raw' );
    $vm->setVariable( 'output', "" );
    $vm->setTerminal( true );

    return $vm;
}

Hope this helps! Drop me a note if I missed a critical detail that could have saved you time!  Me, I wish I'd found this article yesterday! ;)








Zend Framework 2 - Subqueries (Subselect) and Table Gateway

This one can be a bit of a pain, and I had a few folks ask me how to do this, thought I'd share here and pass a link around.  The question takes many shapes!

How can I perform a subquery in ZF2?

How do I properly escape a subquery in my Table Gateway?

I'm trying to echo my query to debug it and am getting warning messages from ZF2?

It's not terribly difficult, but the docs are a bit piecemeal on the net.  I've tried to find links on StackOverflow to help as well, but haven't found any good ones.  Here goes - hope it helps!

 

The function in your AbstractTableGateway will look like such, pay attention to fetchAllWithVotes:


namespace Application\Model\Table;

use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterAwareInterface;
use Zend\Db\Sql\Expression;
use Zend\Db\Sql\Select;
use Zend\Db\TableGateway\AbstractTableGateway;

class SongsTable extends AbstractTableGateway implements AdapterAwareInterface
{
    protected $table = 'songs';

    public function setDbAdapter( Adapter $adapter)
    {
        $this->adapter = $adapter;
    }

    public function fetchAllWithVotes( $user_id )
    {
        $adapter   = $this->getAdapter();
        $resultset = $this->select( function( Select $select ) use ( $user_id, $adapter ) {

            $sub1   = new Select( 'votes' );
            $sub1->columns( array( 'total' => new Expression('COUNT(*)') ) )
                    ->where( array( 'id' => new Expression( 'song_id' ) ) );

            $sub2   = new Select( 'votes' );
            $sub2->columns( array( 'total' => new Expression('COUNT(*)') ) )
                    ->where( array( 'id' => new Expression( 'song_id' ) ) )
                        ->where( array( 'user_id' => $user_id ) );

            $select->columns(
                array(
                    'id',
                    'title',
                    'ytlink',
                    'total_votes' => new \Zend\Db\Sql\Expression( '?', array( $sub1 ) ),
                    'your_votes' => new \Zend\Db\Sql\Expression( '?', array( $sub2 ) ),
                )
            );

            var_dump( $select->getSqlString( $adapter->getPlatform() ) );

        });

        return $resultset->toArray();
    }
}

Pretty self-explanatory - all the guts are there!  Hope this helps :)

VirtualBox: Mounting Shared Folders on Ubuntu Server 13

Want to mount VirtualBox folders on Ubuntu under an OSX 10.8 host?  Differences between aptitude packages and the VirtualBox Guest Additions ISO, compounded to  "security fixes" in recent versions of VirtualBox that disable symlinks can be a pain.  Read below, it's actually quite simple.  

Starting, I assume that you have VirtualBox 4.3.4+ installed, and have loaded up your Ubuntu Server VM -- but don't spin it up yet.

Configuring Your Shared Folder

Inside VirtualBox, head to your VM's Settings panel and visit the Shared Folders tab.  Click the Add (+) icon on the right hand side and specify the folder on your host (OSX) that'll get shared in your guest (Ubuntu).  Check Auto-mount & Make Permanent, click Ok. [I noticed a strange thing, "Make Permanent" isn't always visible.  Click "OK" to save & close, then double-click your shared folder entry to make it reappear in the modal dialog.] 

Make a mental note your share's name; you'll need it in the steps that follow.  Mine is webroot in the screenshot affixed.

Screenshot of shared folder config panel

Screenshot of shared folder config panel

 

Enabling Symlinks (optional)

If you need Symlinks in your guest, run this command before you start your VM.  This gotcha was a pain.  Switch out "webroot" for your share's name, and VM_NAME for the actual name you give your VM.

VBoxManage setextradata VM_NAME VBoxInternal2/SharedFoldersEnableSymlinksCreate/webroot 1

 

Start & Prep Your VM

Start your VM, log in via SSH, and then:

  1. insert the Guest Additions CD image (Devices > Insert Guest Additions CD image in the menu)
  2. mount the CD with: sudo mount /dev/cdrom /media/cdrom
  3. before we can install what's on the CD, we need to build a few requirements:  sudo apt-get install -y dkms build-essential linux-headers-generic linux-headers-$(uname -r)
  4. now we can use what's on the CD, install with: sudo /media/cdrom/VBoxLinuxAdditions.run

Because you are on Ubuntu Server, it'll crap out at the last step where it tries to install X11 dependencies - but don't worry about it - it'll still work(Could not find the X.Org or XFree86 Window System.)

Reboot your VM at this point, then shell back in.

 

Mount Your Folders, Like Magic

I wanted to mount something that'd get used by nginx; so I mounted them under the www-data uid and gid (33).  I also added www-data to vboxsf.

sudo adduser www-data vboxsf

Mount with this command thereafter (recall your share name noted earlier):

sudo mount -t vboxsf -o uid=33,gid=33 webroot /var/www/html/

You can discover a user's  ID with the id command.

AWS Kinesis, PHP, CURL and Signature V4

Well - I got my hands on a limited preview of AWS Kinesis!  It promises to  be one of the most insane realtime data-grepping vacuums ever built.  Remains to write, I was very excited to tackle it this afternoon (got my welcome email last night).

After I plowed through the morning's chores, I made a coffee, read the docs -- and then Googled around to see if any low-level libraries were available; I knew inside that I was in for the library build given our needs, but I was hopeful to avoid building the signature.  Quickly found a piece I'd met before, the AWS SDK for PHP and its Signature V4 class. This thing is rigged to be configurable, and despite my intuition, I loaded it up and ran a few tests.  It was far too slow.  That old adage about performance vs. configurability always holds doesn't it. The abstractions and options might be great for multipurpose usage, but my Kinesis usage is scheduled to fire several thousand times a second - the overhead on the SDK is simply too great.

Back to the drawing board then, I built a test from scratch that used cURL - with success (after some hacking at it admittedly, the API error messages are a bit cryptic at times).  I thought others could benefit from this, and so I post it here!  

https://gist.github.com/Saeven/7832090

(I put a Gist up for now, sorry to make you jump through hoops)

I'll follow up with our real world experience on the Kinesis