Wednesday, April 10, 2013

Outputting iCal with PHP

I'm a big Google Calendar user. If I don't have it on my calendar, then it's probably *not* going to happen. If I'm trying to schedule something into my week, then I'm always consulting my calendar to see how it fits in with everything else, or if its making my week too busy. And, hey, I'm pretty sure I'm not the only GCal addict out there. (Oh, and before GCal, I was totally a Yahoo! Calendar user. Retro!)

So, when I first joined Coursera, I brought with me a list of ways I wanted to improve the student experience, and one of those was "Create a Google calendar of deadlines."

I was hoping this would be an easy thing, something I'd do in my first month. Of course, I didn't realize then that our legacy codebase was a tangle of PHP, that it was split across 5 git repositories, and that it was largely untested. So I repressed my dreams and worked on improving our architecture so that features like that *would* be an easy thing.

Well, as I just announced on the Coursera blog, I finally got to a place where I could write and test the feature, and we've started surfacing it on our classes.

I still had to write it in our legacy PHP codebase, but I don't actually mind PHP when it's written relatively cleanly and testable. I found the hardest part was figuring out exactly how to format my ICS files, and I spent a while going back and forth between this handy iCal Validator and the rather boring iCalendar specification.

I started by writing 2 general classes - CalendarEvent for generating VEVENTs, and Calendar for generating VCALs. Here's the most important function of the CalendarEvent class, the one that generates the string based on the event data:

public function generateString() {
  $created = new DateTime();
  $content = '';

  $content = "BEGIN:VEVENT\r\n"
           . "UID:{$this->uid}\r\n"
           . "DTSTART:{$this->formatDate($this->start)}\r\n"
           . "DTEND:{$this->formatDate($this->end)}\r\n"
           . "DTSTAMP:{$this->formatDate($this->start)}\r\n"
           . "CREATED:{$this->formatDate($created)}\r\n"
           . "DESCRIPTION:{$this->formatValue($this->description)}\r\n"
           . "LAST-MODIFIED:{$this->formatDate($this->start)}\r\n"
           . "LOCATION:{$this->location}\r\n"
           . "SUMMARY:{$this->formatValue($this->summary)}\r\n"
           . "SEQUENCE:0\r\n"
           . "STATUS:CONFIRMED\r\n"
           . "TRANSP:OPAQUE\r\n"
           . "END:VEVENT\r\n";
  return $content;
}
And the function for the Calendar Class that generates the string of events:
public function generateString() {
  $content = "BEGIN:VCALENDAR\r\n"
             . "VERSION:2.0\r\n"
             . "PRODID:-//" . $this->author . "//NONSGML//EN\r\n"
             . "X-WR-CALNAME:" . $this->title . "\r\n"
             . "CALSCALE:GREGORIAN\r\n";

  foreach($this->events as $event) {
    $content .= $event->generateString();
  }
  $content .= "END:VCALENDAR";
  return $content;
}

Here's an example of using those classes to create a calendar with one event:

$event_parameters = array(
            'uid' =>  '123',
            'summary' => 'Introduction Quiz Deadline',
            'description' => 'Make sure you check the website for the latest information',
            'start' => new DateTime('@'.($time - (60*60))),
            'end' => new DateTime('@'.$time),
            'location' => 'http://class.coursera.org/ml/quiz/index?id=2'
        );
$event = new CalendarEvent($event_parameters);

$calendar = new Calendar();
$calendar->events = array($event);
$calendar->title  = 'Machine Learning Deadlines';
$calendar->author = 'Coursera Calendars';
$calendar->generateDownload();

In our own code, I wrote two more classes to help with generating those events for our own data, CourseItem and CourseCalendar (a subclass of Calendar).

You can check out the Calendar classes in this gist. If you've worked with iCalendar files in the past and know anything that we should be tweaking about what we're outputting, let me know more in the comments.

No comments: