Build Your App in the Cloud with Heroku and the Facebook SDK

When I first came across Heroku it was a Ruby-only cloud service. I wasn’t a Ruby developer so I quickly forgot about it. But then they partnered with Facebook and you could create a Facebook app hosted on Heroku with the Facebook PHP-SDK in just a couple of clicks.

Now the question: is it possible to create a PHP application with Heroku that works both outside and inside of Facebook? The answer is yes, and I’ll show you how.

In this article you’ll create a simple link-sharing application. Its user can share a URL with a title and an optional description. Each user is identified by a name and email address (which is kept private). If the user runs the app inside Facebook, the name and email fields are taken from his or her profile.

Create Your Heroku Account

All you need to get started with Heroku is to create a free account; it only takes 5 minutes and an email address.

Basic usage of Heroku is free. As stated in the platform documentation, “each app you create has free access to 750 dyno hours per month and a 5MB database.” A dyno is a single web process responding to HTTP requests and running your code.

If you want to take advantage of HTTPS, you need to verify your Heroku account using a credit card. This is required if you want to serve your Facebook app over HTTPS, but you won’t be billed if you use the basic SSL addon.

Install the Tools of the Trade

The Heroku development process is based on Ruby and Git – the heroku command-line utility is a Ruby gem, and every time you push your code with Git the app will be updated in real time. Thus, you need to:

  1. Install Ruby (in OSX version 1.8.x is already installed, which is fine).
  2. Install Git.
  3. Install the Heroku gem (as simple as sudo gem install heroku).
  4. Configure your environment with your Heroku credentials and your SSH keys.

The best way to do all of these things is to follow the Getting Started with Heroku quickstart guide.

Also, for this project you want to install the Taps gem to help transferring databases.

Vitos-Laptop:~ vito$ sudo gems install taps

Create Your App

To create the app, you’ll write your code locally, initialize the Git repository, and then push the code to Heroku.

Create Your App Locally

Create an empty directory for your application and setup your local web server.

Vitos-Laptop:~ vito$ cd /Users/Shared/WebServer/Sites
Vitos-Laptop:Sites vito$ mkdir HeroLinks

Configure your local web server with a new site that points to that directory with an URL like http://herolinks.local.

Init Your Git Repository

Initialize your repository inside the app directory:

Vitos-Laptop:Sites vito$ cd HeroLinks
Vitos-Laptop:HeroLinks vito$ git init
Initialized empty Git repository in .git/
Vitos-Laptop:HeroLinks vito$ cat
Vitos-Laptop:HeroLinks vito$ git add .
Vitos-Laptop:HeroLinks vito$ git commit -m "HeroLinks app created."

Create the App on Heroku

By default, Heroku uses a Ruby environment for its hosted apps. To create a PHP application you need to customize the command’s arguments to use the stack named cedar. The command to create an app looks like:

Vitos-Laptop:HeroLinks vito$ heroku create HeroLinks --stack cedar
Creating herolinks... done, stack is cedar
http://herolinks.herokuapp.com/ | git@heroku.com:herolinks.git
Git remote heroku added

With this command, Heroku creates the application and the remote repository and then returns the URL and app details.

You publish your application with the following:

Vitos-Laptop:HeroLinks vito$ git push heroku master

You must create and commit at least one file or the command will fail. I created an index.php file with a welcome message.

The output to the push should look similar to:

Counting objects: 3, done.
        Writing objects: 100% (3/3), 253 bytes, done.
        Total 3 (delta 0), reused 0 (delta 0)

        -----> Heroku receiving push
        -----> PHP app detected
        -----> Bundling Apache v2.2.19
        -----> Bundling PHP v5.3.6
        -----> Discovering process types
               Procfile declares types -> (none)
               Default types for PHP   -> web
        -----> Compiled slug size is 21.5MB
        -----> Launching... done, v4
               http://herolinks.heroku.com deployed to Heroku

        To git@heroku.com:herolinks.git
         * [new branch]      master -> master

Start Coding

I don’t want to detract from the main focus of this article, Heroku and Facebook, by coding every detail from scratch. Instead, I’m using some pre-made stuff:

  • Bootstrap – a CSS toolkit from Twitter (for interface styling)
  • Slim – a lightweight but powerful PHP framework (for the main controller)
  • a couple libraries extracted from the CakePHP framework (for data validation and sanitization)
  • a simple PDO wrapper database class (self-made)

As you can see from the following directory structure, the application is very simple.

Directory structure of Heroku application

An .htaccess file redirects all traffic to the index.php page which is the main application file and contains all the business logic.

The lib directory contains the PHP libraries and the folder named templates holds the interface files. I have three pages: a home page that displays a list of all the links shared (home.php), a page with the form needed to submit a new link (new.php), and a search results page (search.php).

Database Access

The Heroku platform provides each Ruby application with a shared 5MB PosgreSQL database. For PHP applications, the shared database is not created by default and must be added with the command:

Vitos-Laptop:HeroLinks vito$ heroku addons:add shared-database

Each application has access to the database using a special environment variable DATABASE_URL which contains the connection string. It is accessible from your PHP code with $_ENV["DATABASE_URL"].

If you purchase a dedicated database add-on such as Heroku Postgres, you can use the psql console application to manage your database. For shared databases you have to manage it manually, with the exception of the db:push and db:pull import/export utilities.

I don’t have PosgreSQL installed on my local machine, so my code use SQLite locally and the data directory contains my database. Here’s how I switch between PostgreSQL and SQLite in code:

<?php
// if $_ENV["DATABASE_URL"] is empty then the app is not running on Heroku
if (empty($_ENV["DATABASE_URL"])) {
    $config["db"]["driver"] = "sqlite";
    $config["db"]["url"] = "sqlite://" . realpath("data/my.db");
}
else {
    // translate the database URL to a PDO-friendly DSN
    $url = parse_url($_ENV["DATABASE_URL"]);
    $config["db"]["driver"] = $url["scheme"];
    $config["db"]["url"] = sprintf(
        "pgsql:user=%s;password=%s;host=%s;dbname=%s",
         $url["user"], $url["pass"], $url["host"],
         trim($url["path"], "/"));
}

First I set the default SQLite database if $_ENV["DATABASE_URL"] is empty as it means the app is not running on Heroku. If the app is running on Heroku then I parse the database URL and convert it into a PDO-friendly connection string. The URL looks like this:

postgres://username:password@host/database

and looks like this after it is converted:

pgsql:user=username;password=password;host=host;dbname=database

The Main Application Controller

The index.php file is the main application controller. After the database setup, there is the Facebook configuration, the loading of other PHP libraries, and then a new instance of a Slim application object is created.

<?php
// Load the core Slim framework...
require_once "lib/Slim/Slim.php";

// ...add other accessory libraries
require_once "lib/db/db.class.php";
require_once "lib/cake/sanitize.php";
require_once "lib/cake/validation.php";

// and then the Facebook SDK
require_once "lib/facebook/facebook.php";

// Create a new Slim application
$app = new Slim();

Here I create the Slim application using its default settings, I could also pass an associative array of settings, but the defaults are enough for now.

I use Slim to map the application’s URLs to PHP functions. The application has a total of four URLs: / (the root), /new, /search and /install.

With Slim you can map a URL to different functions that handle different HTTP methods or you can map the same function to one or more HTTP methods. I want my root URL to be accessible by the GET and POST methods because when it’s running inside Facebook’s iframe element it receives some POST data from the hosting environment called a signed request. Here’s how to do it:

<?php
$app->map("/", function () use ($app, $config) {
        // do something
})->via("GET", "POST");

The code maps the root URL to an anonymous function allowing only the GET and POST HTTP methods. The function is called whenever the user requests the application root URL using GET or POST.

The Application Installer

I would normally build a cross-environment installer, but there are some differences between SQLite and PosgresSQL that make doing so beyond the scope of this article. Instead, I enable the installer only in the local version of the app and then use Heroku’s database utilities to copy the data to the remote database.

The function associated with the /install URI first performs a check against the global $config variable to decide which environment is running in. If you are on your local system then the table-creation query is executed using standard PDO statements.

<?php
$app->get("/install", function() use ($app) {
    global $config;
    // check driver and perform install only for SQLite/local
    if ($config["db"]["driver"] == "sqlite") {
        if ($db = Db::getConnection()) {
            $query = "CREATE TABLE IF NOT EXISTS links (
                id INTEGER PRIMARY KEY,
                url VARCHAR(255), 
                title VARCHAR (100), 
                description VARCHAR(512), 
                username VARCHAR(50), 
                useremail VARCHAR(100), 
                created DATE DEFAULT (datetime('now','localtime'))
            )";
            try {
                $stmt = $db->prepare($query);
                $stmt->execute();
                $app->flash("info", "Application installed successfully!");
                $app->redirect("/");
            }
            catch (PDOException $e) {
                $app->flashNow("error", "Unable to install application: " .
                    $e->getMessage());
            }
        }
        else {
            $app->flashNow("error", "Unable to open DB");
        }
    }
    else {
        $app->flashNow("info", "Install command is for local/SQLite only, try to run <code>heroku db:push sqlite://data/my.db</code> instead!");
    }
    $app->render("default.php", array("action" => "install"));
});

The $db variable is a PDO object, the static method Db::getConnection() takes care of managing the connection and returns NULL on error.

The flash() method stores a message in the current session, giving it a key label of “info”. The message is then available to the next request (i.e., the next page).

I also have a flashNow() method; the message stored with this method is available in the current request inside the $flash variable.

Using the appropriate keys (i.e., error, info, warning, etc.) I can style the message accordingly using Bootstrap’s pre-made CSS styles, for example:

<?php
if (!empty($flash["error"])) {
?>
<div class="alert-message error">
 <?php echo $flash["error"] ?>
</div>
<?php
}

The render() method takes two parameters:

  • the PHP (or HTML) view file to render
  • an associative array of variables which are passed to the view file

In this case I pass a variable named “action” containing the value “install”; the default.php file will have a local $action variable to use. The view files are placed in the templates directory by default, but this path can be customized using Slim’s configuration API (see Slim’s manual for more info). Once your database is functioning locally, you can use Heroku’s tools to import the schema (and data if found) into the remote database.

Vitos-Laptop:HeroLinks vito$ heroku db:push sqlite://data/my.db

The output should look similar to:

Loaded Taps v0.3.23
        Warning: Data in the app 'herolinks' will be overwritten and will not be recoverable.

         !    WARNING: Potentially Destructive Action
         !    This command will affect the app: herolinks
         !    To proceed, type "herolinks" or re-run this command with --confirm herolinks

Type the name of your app to confirm the import and your database will be imported into Heroku.

Application Flow

The rest of the application is straightforward and follows an approach similar to the Model-View-Controller pattern, except there isn’t a model.

The homepage fetches the last 10 links from the database using a common SQL query. The results are stored in the $links array and passed to the view file (home.php) by the render() method.

<?php
$app->map("/", function () use ($app) {
    $pageTitle = "Latest Links";
    $action = "home";
    $links = array();
    if ($db = Db::getConnection()) {
        $query = "SELECT * FROM links ORDER BY created DESC LIMIT 10";
        try {
            foreach ($db->query($query) as $link) {
                $links[] = $link;
            }
        }
        catch (PDOException $e) {
            $app->flashNow("error", $e->getMessage());
        }
    }
    else {
        $app->flashNow("error", "Unable to open DB");
    }
    $app->render("home.php", array(
        "pageTitle" => $pageTitle,
        "action" => $action,
        "links" => $links));
})->via("GET", "POST");

<?php
if (!empty($links)) {
?>
<h2>Latest links</h2>
<table class="linklist zebra-striped" summary="Latest submitted links">
 <tr>
  <th>Site</th>
  <th>Description</th>
  <th>User</th>
  <th>Date</th>
 </tr>
<?php
    foreach($links as $link){
?>
 <tr>
  <td><a href="<?php echo $link["url"] ?>" rel="external"><?php echo $link["title"] ?></a></td>
  <td><?php echo $link["description"] ?></td>
  <td><?php echo $link["username"] ?></td>
  <td><?php echo date("d F Y H:i", strtotime($link["created"])) ?></td>
 </tr>
<?php
    }
?>
</table>
<?php
}
else {
?>
<div class="alert-message block-message info">
 <p>Sorry, the link database is empty!</p>
 <div class="alert-actions"><a class="btn primary" href="/new" >Add a link now</a></div>
</div>
<?php
}

The Add Link page retrieves the form’s data using Slim’s request()->isPost() and request()->post() methods. The data is cleaned and validated with the Sanitize and Validation classes from the CakePHP framework.

<?php
$app->map("/new", function () use ($app) {
    $pageTitle = "Add new link";
    $action = "new";
    $data = array();
    $errors = array();
    if ($app->request()->isPost()) {
        $data = $app->request()->post();
        $data = Sanitize::clean($data, array("escape" => false));

        $valid = Validation::getInstance();
        if (!$valid->email($data["useremail"])) {
            $errors["useremail"] = "Invalid email address";
        }
        if (!$valid->notEmpty($data["username"])) {
            $errors["username"] = "Please insert your name";
        }
        if (!$valid->notEmpty($data["title"])) {
            $errors["title"] = "Please insert a title";
        }
        if (!$valid->url($data["url"])) {
            $errors["url"] = "Invalid or empty URL";
        }

        if (empty($errors)) {
            if ($db = Db::getConnection()) {
                $query = "INSERT INTO links (url, title, description, username, useremail) VALUES(:url, :title, :description, :username, :useremail)";
                try {
                    $stmt = $db->prepare($query);
                    $stmt->bindParam(":url", $data["url"]);
                    $stmt->bindParam(":title", $data["title"]);
                    $stmt->bindParam(":description", $data["description"]);
                    $stmt->bindParam(":username", $data["username"]);
                    $stmt->bindParam(":useremail", $data["useremail"]);
                    $stmt->execute();
                    $app->flash("info", "Link added successfully!");
                    $app->redirect("/");                    
                }
                catch (PDOException $e) {
                    $app->flashNow("error", "Unable to save your URL: " . $e->getMessage());
                }
            }
            else {
                $app->flashNow("error", "Unable to open DB");
            }
        }
    }
 
    $app->render("new.php", array(
        "pageTitle" => $pageTitle,
        "action" => $action,
        "data" => $data,
        "errors" => $errors));

})->via("GET", "POST");

Errors are stored in the $errors array which is used by both the controller and the view. The controller checks for errors to determine if the data should be inserted in the database and the view to display the error message near to each form field.

<form action="" method="post" accept-charset="utf-8">
 <fieldset>
  <div class="clearfix<?php if (!empty($errors["url"])) echo " error" ?>">
   <label for="url">Site URL</label>
   <div class="input">
    <input type="text" size="30" name="url" id="url" class="xlarge" value="<?php echo (!empty($data["url"])) ? $data["url"] : ""; ?>">
<?php
$field = "url";
if (!empty($errors[$field])) {
?>
    <span class="help-inline"><?php echo $errors[$field] ?></span>
<?php
}
?>
   </div>
  </div>
  <!-- Other fields here -->
 </fieldset>
</form>

PDO prepared statements are used to insert data into the database, wrapped in a try-catch block. The flashNow() method displays any PDO error messages (though obviously in a production environment this should be avoided).

The search page is similar to the homepage but with the difference of processing the search term before executing the SQL query.

<?php
$app->get("/search(/:key) ", function($key = null) use ($app) {  
    $pageTitle = "Link Search";
    $action = "search";
    $links = array();
    if ($app->request()->isGet()) {
        if (empty($key)) {
            $key = $app->request()->get("key");
        }
        $key = Sanitize::clean($key, array("escape" => false));
    }

    if ($db = Db::getConnection()) {
        $query = "SELECT * FROM links WHERE (title LIKE :key OR url LIKE :key) ORDER BY created DESC";
        try {
            $stmt = $db->prepare($query);
            $needle = "%" . $key . "%";
            $stmt->bindParam(":key", $needle, PDO::PARAM_STR);
            $stmt->execute();
            while ($link = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $links[] = $link;
            }
        }
        catch (PDOException $e) {
            $app->flashNow("error", "Unable to execut search: " . $e->getMessage());
        }
    }
    else {
        $app->flashNow("error", "Unable to open DB");
    }

    $app->render("search.php", array(
        "pageTitle" => $pageTitle,
        "action" => $action,
        "key" => $key,
        "links" => $links));
});

The syntax $app->get(“/search(/:key) “, function($key = null) means that any string after the /search action name is treated as a search term and stored inside the $key parameter for the function. In any case the $key variable is sanitized before use.

Adding Facebook Functionality

The application works well in stand-alone mode, but in order to use it inside Facebook and take advantage of the user’s data you need to:

  • download the Facebook PHP SDK from GitHub
  • register your application within Facebook starting at the Developer’s page

I copied the src directory of the SDK into my app’s lib directory and renamed it to facebook.

To register a Facebook app you must provide some details such as the URL of the application (e.g., http://myapp.heroku.com) and the name of the canvas (e,g., myapp) which is the identifier for the application inside Facebook (e.g., https://apps.facebook.com/myapp/).

Note: It’s possible to omit the HTTPS url. By doing this your application can be used only with the secure-mode option disabled (check your Facebook account’s settings for more info).

Once your application is registered with Facebook you can provide the configuration details in the index.php controller:

<?php
$config["facebook"]["appId"] = "YOUR_APP_ID";
$config["facebook"]["secret"] = "YOUR_APP_SECRET";
$config["facebook"]["canvas"] = "your-app-canvas";
$config["facebook"]["canvas_url"] = "https://apps.facebook.com/your-app-canvas/";

You also need to provide another configuration setting, the home URL which is passed to the authorization API:

<?php
$config["app"]["home"] = "https://your-app.heroku.com";

The integration with Facebook happens in two code sections. The first creates a new Facebook object as a property of the $app object. The advantage of this approach is that you have access to this connection anywhere you have access to the application object. Set the facebookCanvas property to tell the application it’s running inside an iframe provided by Facebook.

<?php
$app = new Slim();
$app->Facebook = new Facebook($config["facebook"]);
$app->facebookUserProfile = null;
$app->facebookCanvas = isRunningInsideFacebook();

The second integration point is the function isRunningInsideFacebook() which tells the app if it’s running on Facebook (essentially it checks for the signed_request variable sent via the HTTP POST method).

<?php
function isRunningInsideFacebook() { 
    return !empty($_REQUEST["signed_request"]);
}

The function facebookInit() takes care of all the other stuff. For example, it checks to see if it has the right authorization. The signed request is parsed using the Facebook SDK searching for the user_id field. If it doesn’t have that field then the application is not authorized by the user and it redirects him to the authorization page.

<?php
function facebookInit() {
    global $app;
    global $config;

    if ($fbData = $app->Facebook->getSignedRequest()) {
        // redirect to Facebook Authorization if the User ID is not in the request
        if (empty($fbData["user_id"])) {
            $auth_url = "https://www.facebook.com/dialog/oauth?client_id=" 
                      . $config["facebook"]["appId"] . "&redirect_uri=" . urlencode($config["app"]["home"])
                      . "&scope=email";
            echo '<script>top.location.href = "' . $auth_url . '";</script>';
            exit;
        } 
    } 

    if ($fbAccessToken = $app->Facebook->getAccessToken()) {
        // if code is present it was sent by an auth request, so redirect back to Facebook App Page
        if ($code = $app->request()->get("code")) {
            $app->redirect($config['facebook']["canvas_url"]);
        }
        $user = $app->Facebook->getUser();
        if ($user) {
            try {
                // proceed knowing you have a logged-in user who is authenticated
                $app->facebookUserProfile = $app->Facebook->api("/me");
                $app->facebookUserProfile["logout"] = $app->Facebook->getLogoutUrl();
            }
            catch (FacebookApiException $e) {
                $app->flashNow("error", $e->getMessage());
                $user = null;
            }
        }
    }
}

If the app is authorized to see the user’s data, it checks for the code parameter passed by Facebook’s OAuth service after a successful authorization. The service redirects the user to the application’s URL outside of the iframe so I chose to redirect back to the framed version. Different parameters are passed in the query string if the authorization is denied for some reason, but I ignore them for this article.

The code then requests the user’s profile using the Facebook API which is saved into the $app->facebookUserProfile property.

But exactly when is all of this code executed? I’ve slightly edited the routing code of the main URLs to accommodate this:

<?php
$app->map("/", "facebookInit", function () use ($app, $config) {
    // other code here
})->via("GET", "POST");


In Slim’s terminology this is called a Middleware. The facebookInit() function is called before the anonymous function that processes the route. By using this feature, each page can access and use the Facebook data.

Summary

I’ve shown you in this article an example of a simple app built taking advantage of Heroku, the Facebook SDK, and other free components. I hope it has stimulated your curiosity and your desire to build your next application in Heroku’s cloud. You will find the full source code of the application on CloudSpring’s GitHub account if you’d like to clone it and explore. Happy coding!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Jon Mountjoy

    Note you can install the Heroku command line using the toolbelt instead, saving you a few steps. See http://devcenter.heroku.com/articles/heroku-command

  • James

    Where do you recommend storing static content like images, html files, js files, etc? Does Heroku allow (and then serve) static content?