Using the Calendar API in PHP


In the same spirit as my Python experiment, I decided to give PHP a try and see how far I could get with the Office 365 APIs. The same disclaimers apply: I am a total PHP neophyte. I'm certain that my code is probably non-optimal, but hey, it's a learning experience!

The idea

I had this idea for a simple app that could make use of the Calendar API. The app is a list of upcoming shows for a local community theater's Shakespearean festival. Visitors to their site can connect their Office 365 account and then add events directly to their calendar for the shows that they are attending.

The setup

There are a lot of options for PHP development, on multiple platforms. I expect this sample to run on any of them (please let me know if it doesn't!). In my case, I decided to install IIS 8 on my laptop and install PHP via the Web Platform Installer. If you're developing on a Windows machine, this is super easy. In just a few short minutes I was up and running. I didn't have to install anything else, the installer included all of the libraries I needed.

The execution

I started by creating a home page (home.php) that would show a table of upcoming shows. To make it a little dynamic, I created a class to generate the list based on the current date, and to randomize the location and whether or not a voucher was required (more on this later). To keep things constant throughout a session, I save that list in the $_SESSION global so I can reuse it. I ended up with a page that looks like this:

Now to make the "Connect My Calendar" button do something.

OAuth

I decided to create a static class to implement all of the Office 365-related functionality. I created Office365Service.php and created the Office365Service class. The first thing we need it to do is "connect" the user's calendar. Under the covers what that really means is having the user logon and provide consent to the app, then retrieving an access token. If you're familiar with OAuth2, then this is pretty straightforward. Essentially, we need to implement the authorization code grant flow against Azure AD.

To start that process, the "Connect My Calendar" button will send the user right to the authorization code request link. I added the getLoginUrl function to build this link based on my client ID:

 private static $authority = "https://login.windows.net";
private static $authorizeUrl = '/common/oauth2/authorize?client_id=%1$s&redirect_uri=%2$s&response_type=code';

// Builds a login URL based on the client ID and redirect URI
public static function getLoginUrl($redirectUri) {
  $loginUrl = self::$authority.sprintf(self::$authorizeUrl, ClientReg::$clientId,
    urlencode($redirectUri));
  error_log("Generated login URL: ".$loginUrl);
  return $loginUrl;
}

I created authorize.php to serve as the redirect page, which is where Azure sends the authorization code response. All this file needs to do is extract the code parameter from the request, and use that to issue a token request.

 $session_state = $_GET['session_state'];

...

// Use the code supplied by Azure to request an access token.
$tokens = Office365Service::getTokenFromAuthCode($code, $redirectUri);

So that's the next function I added to Office365Service:

 // Sends a request to the token endpoint to exchange an auth code
// for an access token.
public static function getTokenFromAuthCode($authCode, $redirectUri) {
  // Build the form data to post to the OAuth2 token endpoint
  $token_request_data = array(
    "grant_type" => "authorization_code",
    "code" => $authCode,
    "redirect_uri" => $redirectUri,
    "resource" => "https://outlook.office365.com/",
    "client_id" => ClientReg::$clientId,
    "client_secret" => ClientReg::$clientSecret
  );

  // Calling http_build_query is important to get the data
  // formatted as Azure expects.
  $token_request_body = http_build_query($token_request_data);

  $curl = curl_init(self::$authority.self::$tokenUrl);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curl, CURLOPT_POST, true);
  curl_setopt($curl, CURLOPT_POSTFIELDS, $token_request_body);

  if (self::$enableFiddler) {
    // ENABLE FIDDLER TRACE
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
    // SET PROXY TO FIDDLER PROXY
    curl_setopt($curl, CURLOPT_PROXY, "127.0.0.1:8888");
  }

  $response = curl_exec($curl);

  $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  if (self::isFailure($httpCode)) {
    return array('errorNumber' => $httpCode,
                 'error' => 'Token request returned HTTP error '.$httpCode);
  }

  // Check error
  $curl_errno = curl_errno($curl);
  $curl_err = curl_error($curl);
  if ($curl_errno) {
    $msg = $curl_errno.": ".$curl_err;
    return array('errorNumber' => $curl_errno,
                 'error' => $msg);
  }

  curl_close($curl);

  // The response is a JSON payload, so decode it into
  // an array.
  $json_vals = json_decode($response, true);
  return $json_vals;
}

As you can see, I used curl for issuing requests. I found it very well suited for the job. I could easily build the request body as a standard array, encode it with http_build_query, and send it. Handling the response was easy too, with the built-in JSON function json_decode. That puts the response into an easy to manage array.

Now, back in authorize.php, I can save the tokens into the $_SESSION global and redirect back to the home page:

 // Save the access token and refresh token to the session.
$_SESSION['accessToken'] = $tokens['access_token'];
$_SESSION['refreshToken'] = $tokens['refresh_token'];
// Parse the id token returned in the response to get the user name.
$_SESSION['userName'] = Office365Service::getUserName($tokens['id_token']);

// Redirect back to the homepage.
$homePage = "http".(($_SERVER["HTTPS"] == "on") ? "s://" : "://")
                  .$_SERVER["HTTP_HOST"]."/php-calendar/home.php";
header("Location: ".$homePage);

Notice that I also get the user's name from the ID token. This is just so I can display the logged on user's name in my app. Check the getUserName function in Office365Service.php if you're interested to see how that's done.

Calendar API

Now that the user can login, my homepage looks a little different:

Notice that the buttons now say "Add to Calendar", and the user's name appears in the top right along with a "logout" link. The logout link is very simple, it just goes to:

 https://login.windows.net/common/oauth2/logout?post_logout_redirect_uri=<URL to post-logout page>

The value you set in the post_logout_redirect_uri is where Azure will send the browser after logging the user out. In my case, I created logout.php, which removes the data I stored in the $_SESSION global and then redirects to the home page.

CalendarView

Now on to what we came to see, the Calendar API. Clicking on the "Add to Calendar" takes us to the addToCalendar.php page:

So the first use of the Calendar API is the table of events on the right-hand side. To get this data, I created the getEventsForData function in Office365Service.php:

 // Uses the Calendar API's CalendarView to get all events
// on a specific day. CalendarView handles expansion of recurring items.
public static function getEventsForDate($access_token, $date) {
  // Set the start of our view window to midnight of the specified day.
  $windowStart = $date->setTime(0,0,0);
  $windowStartUrl = self::encodeDateTime($windowStart);

  // Add one day to the window start time to get the window end.
  $windowEnd = $windowStart->add(new DateInterval("P1D"));
  $windowEndUrl = self::encodeDateTime($windowEnd);

  // Build the API request URL
  $calendarViewUrl = self::$outlookApiUrl."/Me/CalendarView?"
                    ."startDateTime=".$windowStartUrl
                    ."&endDateTime=".$windowEndUrl
                    ."&\$select=Subject,Start,End" // Limit the data returned
                    ."&\$orderby=Start"; // Sort the results by the start time.

  return self::makeApiCall($access_token, "GET", $calendarViewUrl);
}

This function uses the CalendarView API to get the list of events on a specific day. The advantage of using CalendarView is that when responding to a CalendarView request, the server handles expanding recurring meetings to figure out if a recurring meeting has an instance that falls in the specified time window. The instance will be included in the results like a normal appointment!

You may have noticed that the function ends by calling another function, makeApiCall. Even though it's a detour from the Calendar API, let's take a quick look at that function, because it shows some things that apply to all of the Office 365 REST APIs.

 // Make an API call.
public static function makeApiCall($access_token, $method, $url, $payload = NULL) {
  // Generate the list of headers to always send.
  $headers = array(
    "User-Agent: php-calendar/1.0", // Sending a User-Agent header is a best practice.
    "Authorization: Bearer ".$access_token, // Always need our auth token!
    "Accept: application/json", // Always accept JSON response.
    "client-request-id: ".self::makeGuid(), // Stamp each request with a new GUID.
    "return-client-request-id: true"// The server will send request-id in response.
  );

  $curl = curl_init($url);

  if (self::$enableFiddler) {
    // ENABLE FIDDLER TRACE
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
    // SET PROXY TO FIDDLER PROXY
    curl_setopt($curl, CURLOPT_PROXY, "127.0.0.1:8888");
  }

  switch(strtoupper($method)) {
    case "GET":
      // Nothing to do, GET is the default and needs no
      // extra headers.
      break;
    case "POST":
      // Add a Content-Type header (IMPORTANT!)
      $headers[] = "Content-Type: application/json";
      curl_setopt($curl, CURLOPT_POST, true);
      curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
      break;
    case "PATCH":
      // Add a Content-Type header (IMPORTANT!)
      $headers[] = "Content-Type: application/json";
      curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PATCH");
      curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
      break;
    case "DELETE":
      curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
      break;
    default:
      error_log("INVALID METHOD: ".$method);
      exit;
  }

  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  $response = curl_exec($curl);

  $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

  if (self::isFailure($httpCode)) {
    return array('errorNumber' => $httpCode,
                 'error' => 'Request returned HTTP error '.$httpCode);
  }

  $curl_errno = curl_errno($curl);
  $curl_err = curl_error($curl);

  if ($curl_errno) {
    $msg = $curl_errno.": ".$curl_err;
    curl_close($curl);
    return array('errorNumber' => $curl_errno,
                 'error' => $msg);
  }
  else {
    curl_close($curl);
    return json_decode($response, true);
  }
}

The first thing I want to bring attention to is the set of headers sent with every request. Matthias and I have both talked about this before, but it bears repeating. While Authorization and Accept are self-explanatory, the others are related to what we refer to as client instrumentation. Doing this should be considered a "must-have" when using the REST APIs. It can be invaluable if you run into errors.

The second thing is subtle but important. For POST and PATCH requests, you MUST set the Content-Type header to application/json. If you don't, you'll get an ErrorInvalidRequest error with the message "Cannot read the request body."

Creating Events

Ok, detour over, back to the Calendar API! The second use of the API is to create the event when the user clicks on the "Add to my calendar" button on the addToCalendar.php page. Clicking that button takes the user to doAdd.php, which does the actual adding. For this, I added the addEventToCalendar function to Office365Service.php:

 // Use the Calendar API to add an event to the default calendar.
public static function addEventToCalendar($access_token, $subject, $location,
    $startTime, $endTime, $attendeeString) {
  // Create a static body.
  $htmlBody = "<html><body>Added by php-calendar app.</body></html>";

  // Generate the JSON payload
  $event = array(
    "Subject" => $subject,
    "Location" => array("DisplayName" => $location),
    "Start" => self::encodeDateTime($startTime),
    "End" => self::encodeDateTime($endTime),
    "Body" => array("ContentType" => "HTML", "Content" => $htmlBody)
  );

  if (!is_null($attendeeString) && strlen($attendeeString) > 0) {
    $attendeeAddresses = array_filter(explode(';', $attendeeString));

    $attendees = array();
    foreach($attendeeAddresses as $address) {
      error_log("Adding ".$address);

      $attendee = array(
        "EmailAddress" => array ("Address" => $address),
        "Type" => "Required"
      );

      $attendees[] = $attendee;
    }

    $event["Attendees"] = $attendees;
  }

  $eventPayload = json_encode($event);

  $createEventUrl = self::$outlookApiUrl."/Me/Events";

  $response = self::makeApiCall($access_token, "POST", $createEventUrl, $eventPayload);

  // If the call succeeded, the response should be a JSON representation of the
  // new event. Try getting the Id property and return it.
  if ($response['Id']) {
    return $response['Id'];
  }

  else {
    return $response;
  }
}

Notice how easy PHP makes it to build the JSON payloads. All I need do is create an array and use json_encode to generate the payload. Very nice! Again this uses makeApiCall to send the request. We also don't need to worry about sending meeting invites. By adding attendees, the server takes care of that for us!

Adding an attachment

Remember before I said we'd get to the "voucher required" thing later? The voucher was really an excuse to add an attachment. If you add an event that requires a voucher to your calendar, the app will add the voucher as an attachment on the event. To do this, I added the addAttachmentToEvent function:

 // Use the Calendar API to add an attachment to an event.
public static function addAttachmentToEvent($access_token, $eventId, $attachmentData) {
  // Generate the JSON payload
  $attachment = array(
    "@odata.type" => "#Microsoft.OutlookServices.FileAttachment",
    "Name" => "voucher.txt",
    "ContentBytes" => base64_encode($attachmentData)
  );

  $attachmentPayload = json_encode($attachment);
  error_log("ATTACHMENT PAYLOAD: ".$attachmentPayload);

  $createAttachmentUrl = self::$outlookApiUrl."/Me/Events/".$eventId."/Attachments";

  return self::makeApiCall($access_token, "POST", $createAttachmentUrl,
    $attachmentPayload);
}

The value of $attachmentData is the binary contents of the file to attach. In this case, it's a simple text file.

At this point you might be wondering: "Why not just include the attachment as part of the event when you created it? Wouldn't that be more efficient?" Well yes, it would, but it doesn't work! In order to add an attachment, you have to create the event first then POST to the event's Attachments collection.

The end result

If I stick with the premise of this being an experiment, then I have to conclude that PHP is a great language for calling the Office 365 REST APIs. Using just the built in libraries I was able to do everything I needed to do in a straightforward way. If PHP is your language of choice, you should have no trouble integrating with Office 365.

The sample app is available on GitHub. As always, I'd love to hear your feedback in the comments or on Twitter (@JasonJohMSFT).

Update: There's a great PHP tutorial for the Outlook API's available on https://dev.outlook.com.

Comments (16)

  1. Azure says:

    Hi, thanks for this tutorail. Can I do the same with this code but without using Azure. I see that is a paid service.

    Thanks

  2. As of today, you must register your application in Azure using an Office 365 account. You do not have to pay for Azure, but the Office 365 account is a paid service (though there are free trials, and you can get a free year of Office 365 Developer Subscription by signing up for the Office Developer Program (which is free!) at dev.office.com/devprogram. If you want to avoid signing up for Azure management portal access (which is still free, but you do have to provide a credit card), you can use the Outlook Dev Portal app registration tool for free: dev.outlook.com/appregistration.

  3. Adam says:

    Thanks for your great and only good tutorial in php

    i am trying your code to implement microsoft 365 calendar api to create/update event in 365 calendar

    i can not go ahead due to some authentical error.

    i have read your comment related azure

    i have created developer account now i can access my calendar on following website

    https://portal.office.com

    then after i have added/registered app on following website

    https://apps.dev.microsoft.com

    in above website,i have added return url like http://localhost/php-calendar/o365/authorize.php also i have copied Application Id and Application Secrets in your code

    i am trying to connect with calendar by following url

    http://localhost/php-calendar/home.php

    when i press connect to my calendar then i can redirect on http://localhost/php-calendar/o365/authorize.php but its give me following url after redirect http://localhost/php-calendar/error.php?errorMsg=There+was+no+%27code%27+parameter+in+the+query+string.

    so i can not get code from microsoft thats why i am receiving There was no 'code' parameter in the query string. error,so whats wrong i did

    I hope you will give answer of my question,thanks

  4. @Adam: This sample uses the v1 Azure OAuth flow, which isn't compatible with the https://apps.dev.microsoft.com app registrations. You would need to register the app using the Outlook Dev Portal tool: dev.outlook.com/appregistration.

    Alternatively, if you want to take advantage of the v2 OAuth flow (which I recommend!), you can take a look at this PHP tutorial which uses it: dev.outlook.com/…/php

    1. Jan Hajek says:

      Alternatively, you can use https://github.com/TheNetworg/oauth2-azure to interact with the APIs – no matter whether it is v2.0 or v1 endpoints.

  5. Misael says:

    Hi, Im following this tutorial, and also https://dev.outlook.com/restapi/tutorial/php this one (actually It looks like these are the only).. however, in both examples, I’m getting an error of maximum execution time exceded on oauth.php file in line 62 which is the ‘curl_exec’ line…. Would you please give an idea about the problem, do I need to change the execution time, (even if its 120 secs),.. Does the curl_exec needs more time?? Thank you very much.

    1. It should not take that long to do the OAuth sign-in. You may want to capture the request response with Fiddler or some other web debugging tool to look for clues.

  6. Ryan Lawrence says:

    This was a very helpful tutorial, thank you! I’d like to do a similar calendar API implementation that uses key pair authentication rather than requiring login via OAuth. Has anyone had luck setting this up?

  7. Promith says:

    Hi,
    I am working with localhost. But not working.
    I have got this error “Array ( [errorNumber] => 403 [error] => Request returned HTTP error 403 )”.

  8. promith says:

    i am trying to connect with calendar by following url

    http://localhost/php-calendar/home.php

    when I hit connect to my calendar then i can redirect on http://localhost/php-calendar/o365/authorize.php but its give me following url after redirect http://localhost//php-calendar/error.php?errorMsg=Error+adding+event+to+calendar%3A+Request+returned+HTTP+error+403

    1. Please post this on Stack Overflow, using the “outlook-restapi” tag.

  9. Hi, I am getting 404 while trying to get the event from calendar and curl_errorno is 0, curl_error is NULL. I really don’t have clue why am i getting 404. I cannot move further.

    1. Please post this with details on Stack Overflow. Use the “outlook-restapi” tag.

  10. Jordi says:

    Wanted to try this, because it is exactely what I’m looking for, but after following all the steps, I only get this error:
    In the header
    “Notice: Undefined index: userName in [LocalInstallationpath]\php-calendar\common\header.php on line 17”

    In the main body:
    Notice: Undefined index: userName in [LocalInstallationpath]\php-calendar\home.php on line 12
    Notice: Undefined index: HTTPS in [LocalInstallationpath]\php-calendar\home.php on line 17
    Notice: Undefined index: events in [LocalInstallationpath]\php-calendar\home.php on line 38
    Fatal error: Class ‘DateTimeImmutable’ not found in [LocalInstallationpath]\php-calendar\site-event-list.php on line 27

    1. Sorry to hear that. It’s been a while since I wrote this post, and the API has evolved since then. There’s a more recent walkthrough here: https://docs.microsoft.com/en-us/outlook/rest/php-tutorial

Skip to main content