Sunday, May 23, 2010

Zend Gdata Calendar Tutorial DataMapper

I first started using the Zend library when I found out that the Google Apps API for PHP was distributed in the Zend_Gdata library. It's been a while since then, and I've become more and more fond of the Zend Framework since. But this post isn't about my swoon for Zend, it's actually meant to explain how to use Zend_Gdata_Calendar to list events and to change, read and update extended properties to the events.

First, I'll explain my basic setup.
The whole app is based on a schedule for a radio station with some logging features. I have an object Application_Model_Show which gets its data from a google calendar. The actual object which is responsible for populating the show object with the right information is Application_Model_Mapper_ShowMapper (located in the appropriate directory vis-a-vis the Zend system). I'll explain how to set up this in Zend in a bit, for now... let's stay a little simpler.

The Zend_Gdata_Calendar object is the first one that we'll be concerned with. I started off playing with this library in a non-Zend environment to start. This just gave me ability to see the nuts and bolts of this library. Let's start off by just creating a connection to the service.

Because we don't have the luxury of the Zend Autoload feature, we're going to have to load our classes   manually.
<?php
require_once('Zend/Loader.php');
$classes = array('Zend_Gdata','Zend_Gdata_Query','Zend_Gdata_ClientLogin','Zend_Gdata_Calendar');
foreach($classes as $class) {
    Zend_Loader::loadClass($class);
}
Great, now we can set our credentials and create the authorized connection to the google. There are three connection methods which google implements. AuthSub is used so that your clients are sent to a google page first, authenticated, and then returned back to your page with an authentication slip. MagicCookie is a read only method, and the method we'll be using is ClientLogin. This one is best suited for this situation, because there is only one calendar that I'm going to need to access. Following is the setup for clientLogin, if you're following along, concatenate it to the above code.
$user = 'someemail@gmail.com';
$pass = 'supersecret';
$service = Zend_Gdata_Calendar::AUTH_SERVICE_NAME;
$client = Zend_Gdata_ClientLogin::getHttpClient($user,$pass,$service);
$service = $Zend_Gdata_Calendar($client);
Cool, now we have a connection that we can process our requests through. If we were going to be making a lot of calls to the google server, it would be wise to maintain this one connection rather than recreating it every time. We'll keep this in mind for when we discuss the Application_Model_Mapper_ShowMapper object in the MVC.

So, what good is this gateway to google without some need for information. The first thing your'e going to want to do is get a list of the Calendars accessible to this user. Let's throw this request into a try block just in case anything goes wrong
try{
    $listFeed = $service->getCalendarListFeed();
}
catch (Zend_Gdata_App_Exception $e){
    echo "Error: " . $e->getMessage();
}
// echo it back so you can see the id
echo "<ul>
foreach($listFeed as $listEntry) {     echo "
<li> . $listEntry->title . "(Event Feed: " . $listEntry->id . ")"</li>
} echo "</ul>
";
This will give us all of the calendars that you normally see in the google calendar user interface. Go ahead, create a new one and see how it shows up. Isn't that cool? Note that your default calendar has an id that looks like
someemail@gmail.com (Event Feed: http://www.google.com/calendar/feeds/default/someemail%40gmail.com)

But any of the non-default calendars look more like
OtherCalendarName (Event Feed: http://www.google.com/calendar/feeds/default/[SOME26CHARACHTERSTRING]%40group.calendar.google.com)
This is important to note if you want to access the non-default calendar.

Now, Let's do something useful.... like finding the events that are scheduled that day on either calendar.

To get this information, at the Zend level, we create a query object and send this query object to a get method. A eventFeed object is returned, and we iterate through the eventFeed to see the eventEntries.

Sounds simple right? Well, if you're comfortable with OO, I'm sure it'll be simple... but if you're not, then it's comforting to know, that the only thing that's happening here is that we're making an authenticated http request for an XML response, in turn, the Zend Library turns the XML response into an object which plays nicely with us.

we start by building a query.
$timezone = "00:00:00-08:00"; //My application is in PST (Vancouver, BC)
$today = date('Y-m-d') . $timezone;
$tonight = date('Y-m-d',strtotime($today + 24*60*60)) . $timezone;

$queryDefault = service->getEventQuery();
              ->setUser('default'); // for this query we'll look at the default calendar
              ->setVisibility('public');  // since we aren't planning on editing the calendar at this point, we can just set this to public; otherwise, it'd have to be private.
              ->setOrderBy('starttime');
              ->setStartMin($today);
              ->setStartMax($today);

$queryOtherCalendar = $queryDefault->setUser('[THAT26CHARACHTERSTRING]%40group.calendar.google.com');

As I said above, we're only building a URL here that we're going to request an XML response from. So, the $queryDefault url is actually http://www.google.com/calendar/feeds/default/full/public. The response back from google is an XML file which is turned into a nice object

Let's process these requests.
try{
   $eventFeedDefault = $service->getCalendarEventFeed($queryDefault);
   $eventFeedOtherCalendar = $service->getCalendarEventFeed($queryOtherCalendar);
}
catch (Zend_Gdata_App_Exception $e){
    echo "Error :" . $e->getMessage();
}
and display the results
echo "<h1>On the Default Calendar, the events of the day are</h1>";
echo "<ul>"; 
foreach ($eventFeedDefault as $event) {     
    echo "<li>" . $event->title . "(Event Id: " . $event->id . ")</li>
"; } 
echo "</ul>";

echo "<h1>On the other calendar, the events of the day are</h1>";
echo "<ul>"; 
foreach ($eventFeedOtherCalendar as $event) {     
    echo "<li>" . $event->title . "(Event Id: " . $event->id . ")</li>
"; } 
echo "</ul>";
Well, there we have it! We've now listed all of the events of the day on both calendars. This is a pretty big step for now. Let's start including this into the Application_Model_Mapper_ShowMapper class (located in application/models/Mapper/ShowMapper.php). Here's some more information.

The ShowMapper has the following skeleton
<?php 
class Application_Model_Mapper_ShowsMapper extends Application_Model_Mapper_AbstractMapper
{
    private static $_instance = null;
    private $_service = '';

    public function __construct() {
        // create the connection in here and return it to $this->_service
    }

    private function getExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name){
        // more on this later
    }

    private function addExtendedProperty(Zend_Gdata_Calendar_EventEntry $name, $value){
        // more on this later
    }

    private function getEventById($id){
        // returns a Zend_Gdata_Calendar_EventEntry
    }

    public function getShows(){
        // returns an array of Application_Model_Show
    }

    public function getShowById(){
        // returns a single Application_Model_Show
    }

    public function save(Application_Model_Show $show){
        // calls addExtendedProperty, returns boolean
    }

    public static function getInstance() {
        //return the singleton in $this->_instance
    }
}
So, for public methods, we have getShows(), getShowById(), save(), and getInstance(). Just a note, show is an instance of Application_Model_Show, shows refers to an array of show, and Event is an instance of Zend_Gdata_Calendar_EventEntry.

we call the creation of the object through
$showMapper = Application_Model_ShowMapper::getInstance();
so that only one showMapper object is being created no matter how many requests are made in the session. To make sure that only one connection is created no matter how many times it's called, we populate $_service in the construct with our code from above (with some exception handling built in).
public function __construct() {
    $user = 'someemail@gmail.com';
    $pass = 'superSecret';
    $service = Zend_Gdata_Calendar::AUTH_SERVICE_NAME;
    try {
        $client = Zend_Gdata_clientLogin::getHttpClient($user,$pass, $service);
    } catch (Zend_Gdata_App_CaptchaRequiredException $e) {
        echo 'URL of Captcha image: ' . $e->getCaptchaUrl() . "\n";
        echo 'Token ID: ' . $e->getCaptchaToken() . "\n";
    } catch (Zend_Gdata_App_AuthException $e) {
        echo 'Problem Authenticating: ' . $e->exception() . "\n";
    }
    $this->_service = new Zend_Gdata_Calendar($client);
}
The constructor creates the service variable which each request on all instances of ShowMapper will marshal through. We look for a couple exceptions here to make sure that we're connecting properly. If you catch CaptchaRequiredException, you should goto the google calendar UI and follow their captcha instructions on their site to confirm you're note a hackbot.

Google Calendar extended properties are only available through the API, so far, there is no way to view them through the google UI. They're quite useful for storing different attributes for your event. For instance, in this application, shows are either spoken word shows or not. This comes into play for calculating the amount of spoken word we have on the station (a CRTC requirement). Additionally, Show has a boolean of 'syndicated' as well. Here's an example of how to set and get extended properties.
private function getExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name)
{
    $extProps = $event->extendedProperty;
    foreach($extProps as $extProp){
        if ($name == $extProp->name){
            return $extProp->value;
        }
    }
}

private function addExtendedProperty(Zend_Gdata_Calendar_EventEntry $event, $name, $value)
{
    $extProp = $this->_service->newExtendedProperty($name,$value);
    $extProps = array_merge($event->extendedProperty, array($extProp));
    $event->extendedProperty = $extProps;
    $eventNew = $event->save();
    return $eventNew;
}
These are fairly self-explanatory; I'll move on.
private function getEventById($id)
{
    // this method is giving me some issues right now, I'll report back on this one when I have it working
    // currently, an event is loading, but without $event->when being populated. :(
    $query = $this->_service->newEventQuery()
           ->setUser('default')
           ->setVisibility('private')
           ->setProjection('full')
           ->setEvent($id);
    try {
        $event = $this->_service->getCalendarEventEntry($query);
    }
    catch (Zend_Gdata_App_Exception $e) {
        "Error $id: " . $e->getMessage();
    }

    return $event;
}
Currently, I'm tempted to just request the whole 2 week span once a day and to store that as a site-cached variable while allowing the admin user to refresh the cache when they visit the site. This could get around the $event->when not populating properly. I can't find any reason why this wouldn't come back. The next step is to dive into the XML and figure out what the hell is going on.

Anyways </rant>.

the getShows() method is very similar to some code way up there - just that we populate the Application_Model_Show object with our returned data.

public function getShows()
{
    $timezone = "T00:00:00-08:00";  // this works for PST
    $startTime = date("Y-m-d", time() - 60*60*24*14) . $timezone;
    $stopTime = date("Y-m-d", time() + 60*60*24*14) . $timezone;
    // this looks for all the events within 2 weeks either size of today
    $query = $this->_service->newEventQuery()
           -> setUser('default')
           -> setVisibility('private')
           -> setStartMin($startTime)
           -> setStartMax($stopTime)
           -> setMaxResults(336) // the default is 25
    try {
        $eventFeed = $this->_service->getCalendarEventFeed($query);
    }
    catch (Zend_Gdata_App_Exception $e) {
        echo "Error: " . $e->getMessage();
    }
    $shows = array();
    foreach ($eventFeed as $event)
    {
        $show = new Application_Model_Show(array(
            'id'          => substr($event->id,strrpos($showData->id,'/')+1,26), //(the id is 26 charachters long, sometimes followed by a time variable)
            'name'        => $event->title,
            'description' => $event->content,
            'when'        => $event->when(),
            'spokenWord' => $this->getExtendedProperty($event, 'spokenWord'),
            'syndicated' => $this->getExtendedProperty($event, 'syndicated')
        ));
        $shows[] = $show;
    }
    return $shows;
}

You can populate the data into your show variable however you want, i use the construct to shove the array entries in.

Two more and we're done.

public function getShowById($id){
    $event = $this->getEventById($id);
    $show = new Application_Model_Show(array(
            'id'          => substr($event->id,strrpos($showData->id,'/')+1,26), //(the id is 26 charachters long, sometimes followed by a time variable)
            'name'        => $event->title,
            'description' => $event->content,
            'when'        => $event->when(),
            'spokenWord' => $this->getExtendedProperty($event, 'spokenWord'),
            'syndicated' => $this->getExtendedProperty($event, 'syndicated')
        ));
    return $show;
}
and finally a method we can call to save our extended properties in one call
public function save(Application_Model_Show $show)
{
    $event = $this->getEventById($show->getId());
    $this->addExtendedProperty($event, 'spokenWord', $show->getSpokenWord());
    $this->addExtendedProperty($event, 'syndicated', $show->getSyndicated());
}


Well, there we have it!

7 comments:

  1. Thank you my friend, you just made my day,
    this tutorial was very usefull to me for one reason:

    This line --> $queryDefault->setUser('[THAT26CHARACHTERSTRING]%40group.calendar.google.com');

    It seriously helped me selecting another calendar!!! I've searcher so long for this!

    ~Dino

    ReplyDelete
  2. I solved the when issue by looping them and assigning the start and end separately:

    foreach ($event->when as $when) {
    $start = $when->startTime;
    $end = $when->endTime;
    }

    ReplyDelete
  3. i'm getting an "unexpected '<'"

    in your 3rd code-listing, shouldn't there be a closing quote, after the echo ul?

    ReplyDelete
  4. i'm getting "expected string function name" on "$service = $Zend_Gdata_Calendar($client);". i got rid of the error message by changing your parens to square-brackets.

    now i'm getting "Call to a member function getCalendarListFeed() on a non-object" on "$listFeed = $service->getCalendarListFeed();".

    does that mean the previous line failed? what should i do?

    thanks!

    ReplyDelete
  5. Hello,

    Thanks for making a tutorial like this! I've been trying to compile one myself as I go through trying to learn how to do this.

    That said, I haven't gotten very far, as I'm having trouble with pulling information from framework still!

    Right now, I have gotten up to this part:

    ------------------------
    try{
    $listFeed = $service->getCalendarListFeed();
    }
    catch (Zend_Gdata_App_Exception $e){
    echo "Error: " . $e->getMessage();
    }
    // echo it back so you can see the id
    .
    .
    .
    ------------------------

    But I get an error saying:

    Fatal error: Function name must be a string

    which refers to this line:

    $service = $Zend_Gdata_Calendar($client);"
    I have tried that with and without the $ in front of the Zend_Gdata_Calendar, and when I do it without it yells at me and says:

    Fatal error: Call to undefined function Zend_Gdata_Calendar()

    What am I missing? I feel like it should be pretty obvious.

    ReplyDelete
  6. Oh, nevermind!

    I figured it out--the line should read

    $service = new Zend_Gdata_Calendar($client);

    :) Hopefully this helps anyone else who reads this!

    ReplyDelete
  7. phew - thank you M Plant for solution on how to get the !@#%$# startTime out of the when - twas driving me crazy.

    ReplyDelete