Contents


Build and deploy a mobile-friendly calorie counter on IBM Bluemix with PHP, MySQL, AngularJS, and the Nutritionix API

Comments

Until a few years ago, figuring out how many calories were in that sandwich you just ate required a combination of guesswork and squinting at package wrappers. Today, that same information is available from any number of online nutrition databases, making it easier to track your food intake.

For developers, the availability of nutrition data via open APIs is exciting because it creates new opportunities to build useful, relevant health management applications.

In this article, I show you how to create an online calorie counter that enables users to:

  • Search for food items by name, with results retrieved through an API to the online nutrition database Nutritionix.
  • Group selected food items together to create meal records, and save these records to a MySQL database, together with their calorie counts, by using a PHP/AngularJS application.
  • Retrieve reports of their total calories that are consumed for the current day, the last seven days, and the last 30 days.
  • Access the app from mobile devices such as tablets and smartphones.

On the client side of things, I use jQuery Mobile to create a mobile-friendly user interface for the application and AngularJS to enable some of the application's interactive features. On the server, I use Slim, a PHP micro-framework, to control interaction with the Nutritionix API and to save and retrieve data from the MySQL server.

In the final part of this article, I show you how to deploy the application to the Bluemix cloud, which provides a scalable and robust infrastructure for application deployment to ensure that users have around-the-clock access.

Sound interesting? Come on in, and let's get started!

Run the appGet the code

What you need for your application

Step 1. Set up the application database

Use the following code listing that includes a MySQL table definition and sample data to set up the application database.

  • If you're developing and deploying only locally, you can use this code to initialize a MySQL database table for the application to connect to.
  • If you're deploying on Bluemix, skip this step for the moment; I'll return to it in Step 8 after you initialize and bind a MySQL service instance on Bluemix.
CREATE TABLE meals (
  id int(11) NOT NULL AUTO_INCREMENT,
  uid varchar(255) NOT NULL,
  calories decimal(10,2) NOT NULL,
  rdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  ip varchar(20) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8


CREATE TABLE users (
  id int(11) NOT NULL AUTO_INCREMENT,
  email varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  code varchar(255) DEFAULT NULL,
  `status` int(11) NOT NULL,
  rdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  ip varchar(20) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Step 2. Install Slim

Download and set up the Slim micro-framework. Why Slim? Slim comes with a sophisticated URL router and support for Flash messages, encrypted cookies, and middleware. It's also easy to understand and use, and it comes with great documentation.

I use Composer, the PHP dependency manager, to download and set up Slim. In addition to Slim, I also add the SendGrid client libraries for PHP. (Learn more about SendGrid and why it's needed in a bit.) The following code listing is the Composer configuration file. Save this file to $APP_ROOT/composer.json (where $APP_ROOT refers to your working directory).

{
    "require": {
        "slim/slim": "2.*",
        "sendgrid/sendgrid": "2.0.5"
    }
}

You can now install Slim using Composer with the command:

shell> php composer.phar install

Note: If you use Windows, include the OpenSSL extension in your php.ini file, or add the line extension=php_openssl.dll to it before you run php composer.phar install.

To make it easier to access the application, you can also define a new virtual host in your development environment and point its document root to $APP_ROOT. This step, although optional, is recommended because it creates a closer replica of the target deployment environment on Bluemix.

To set up a named virtual host for the application in Apache, open the Apache configuration file (httpd.conf or httpd-vhosts.conf) and add these lines to it:

NameVirtualHost 127.0.0.1
<VirtualHost 127.0.0.1>
    DocumentRoot "/var/www/calories"
    ServerName calories.localhost
</VirtualHost>

To set up a named virtual host for the application in nginx, open the nginx configuration file (nginx.conf) and add these lines to it:

server {
    server_name calories.localhost;
     root /var/www/calories;
     try_files $uri /index.php;
     
     location ~ \.php$ {
        try_files $uri =404;            
        include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
 # assumes you are using php-fcgi
        fastcgi_pass 127.0.0.1:90;
    }        
}

These lines define a new virtual host, http://calories.localhost/, for which the document root corresponds to the $APP_ROOT (remember to update it to reflect your own local settings). Restart the web server to activate these new settings. You might need to update your network's local DNS server to indicate the new host.

Step 3. Understand the Nutritionix API

As with many other web APIs, the Nutritionix API works over HTTP and expects an HTTP request to a designated endpoint. On receipt of this request, the API server replies to the query with a JSON feed that contains the requested data. It's then possible to parse this data by using either a server-side programming language (for example, PHP or Perl) or a client-side toolkit (for example, jQuery or AngularJS) and extract content from it for integration into a web page.

After you sign up for a Nutritionix API account and have a valid appId and appKey, you can take the API for a test drive by searching for food items that match the term "chicken." The free developer account entitles you to only 500 searches per day (although you can ask for an increased quota by emailing the API team).

Consider the next image, which shows the response to an authenticated GET request to https://api.nutritionix.com/v1_1/search/chicken?fields=item_name,brand_name,nf_calories&item_type=3&appId=[APP-ID]&appKey=[APP-KEY], the API endpoint for search queries (remember to update the previous URL to reflect your API credentials before you issue the request).

Screen capture of Nutritionix API response
Screen capture of Nutritionix API response

As the image illustrates, the Nutritionix API responds to the request with a JSON document that lists food items that match the search term "chicken." The query string includes the item_type=3 parameter, which limits the search to the USDA database only. For each food item, the response includes the item name, brand name, and calorie count. Other fields are also supported; look in the Nutritionix API documentation for details.

Step 4. Enable the search interface

Develop a simple search interface, which enables the user to search for food items and view a list of results. The result page must include controls for the user to add selected food items to his meal record.

Set up the basic structure of this user interface and save it as $APP_ROOT/templates/main.php.

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" />
  <script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
  <script src="//code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.6/angular.min.js"></script>
</head>
<body>

  <div data-role="page">

    <div data-role="header">
      <h1>Calorie Counter</h1>
    </div>

    <div data-role="content" ng-app="myApp">	
      <div data-role="tabs" ng-controller="myAppController">
      
        <div data-role="navbar">
          <ul>
            <li><a href="#search" data-theme="a" class="ui-btn-active">Search</a></li>
            <li><a href="#record" data-theme="a">Record</a></li>
            <li><a href="#report" data-theme="a">Report</a></li>
          </ul>
        </div>
        
        <div id="search"></div>

        <div id="record"></div>
        
        <div id="report"></div>

      </div>      
    </div>
    
  </div>

</body>
</html>

The previous code listing shows a page that is formatted according to standard jQuery Mobile conventions. The primary page element is a <div> element with a data-role="page" attribute. Within this <div> element are separate <div> elements for the page header and content. The page content consists of a series of tabs. Each tab represents one of the tasks ("search", "record," and "report"). Clicking the name of the tab in the top navigation bar shows its content.

Next, add elements to the search tab, as shown in the following code listing:

<!DOCTYPE html>
...
    <div data-role="content" ng-app="myApp">	
      <div data-role="tabs" ng-controller="myAppController">
       
        <div id="search">
          <h2 class="ui-bar ui-bar-a">Food Item Search</h2>
          <div class="ui-body">
              <input type="search" name="query" ng-model="foodItems.query" />
              <button ng-click="search()">Search</button>
          </div>   
          
          <h2 class="ui-bar ui-bar-a">Search Results</h2>   
          <div class="ui-body">
            <ul data-role="listview" data-split-theme="d">
              <li ng-repeat="r in foodItems.results">
                <a>{{r.fields.item_name}} / {{r.fields.nf_calories + ' calories'}}</a>
<a href="#" data-inline="true" data-role="button" data-icon="plus" 
                  data-theme="a" ng-click="addToMeal(r)">Add</a>
              </li>
            </ul>                    
          </div>
        </div>

      </div>      
    </div>
...

The search tab now consists of two areas: a search input field at the top, and a search results list at the bottom. Both areas are controlled by an AngularJS controller and both use an AngularJS model named foodItems. The following code listing shows the controller code.

  <script>
  var myApp = angular.module('myApp', []);
 
  function myAppController($scope, $http) {
    // related to search functionality
    $scope.mealItems = [];
    $scope.foodItems = {};
    $scope.foodItems.results = [];
    $scope.foodItems.query = '';
    
    $scope.search = function() {
      if ($scope.foodItems.query != '') {
        $http({
            method: 'GET',
            url: '/search/' + $scope.foodItems.query,
          }).
          success(function(data) {
            $scope.foodItems.results = data.hits;
          });
      };
    };
    
    $scope.addToMeal = function(foodItem) {
       $scope.mealItems.push(foodItem);
    };     
  }
  </script>

And here's what the search interface looks like in action.

Screen capture of searching for food items using the Nutritionix API
Screen capture of searching for food items using the Nutritionix API

How does it work? When a user enters a search term and clicks Search, the AngularJS search() function retrieves the input via the foodItems model and generates an Ajax request to the /search application endpoint. This request is not the Nutritionix API endpoint, but rather an intermediary API endpoint that is managed by the application itself (more on this shortly).

The response to the Ajax request is a JSON packet similar to that shown earlier. This response is attached to the foodItems.results property, and the AngularJS data binding takes care of iterating over this collection, parsing it, and displaying it as a list of search results.

Look closely at the search interface. Notice that next to each search result is a button, which is linked to the addToMeal() function. When this button is clicked, the corresponding food item is added to a mealItems array in the scope. Later, this array is used to construct the view for the record tab.

On the server end, you need a handler for the Ajax request to the /search endpoint, which is where Slim comes in. Slim uses the Ajax request data to connect to the Nutritionix API and run a search, similar to the search shown earlier. The following listing has the code to make it work.

<?php
// use Composer autoloader
require 'vendor/autoload.php';
\Slim\Slim::registerAutoloader();

// configure Slim application instance
$app = new \Slim\Slim();
$app->config(array(
  'debug' => true,
  'templates.path' => './templates'
));

// configure credentials
// ... for Nutritionix
$config["nutritionix"]["appId"] = 'APP-ID';
$config["nutritionix"]["appKey"] = 'APP-KEY';

// index page handlers
$app->get('/', function () use ($app) {
  $app->redirect('/index');
});

$app->get('/index', function () use ($app) {
  $app->render('main.php');
});

// search handler
$app->get('/search/:query', function ($query) use ($app, $config) {
  try {
    // execute search on Nutritionix API
    // specify search scope and required response fields
    // replace with your API credentials
$qs = http_build_query(array('appId' => $config["nutritionix"]["appId"], 
 'appKey' => $config["nutritionix"]["appKey"], 'item_type' => '3', 
            'fields' => 'item_name,brand_name,nf_calories'));
$url = 'https://api.nutritionix.com/v1_1/search/' . 
              str_replace(' ', '+', $query) . '?' . $qs;
    $ch = curl_init();    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);  
    curl_exec($ch);
    curl_close($ch);
  } catch (Exception $e) {
    $app->response()->status(400);
    $app->response()->header('X-Status-Reason', $e->getMessage());
  }
});

$app->run();

This script, which you need to save as $APP_ROOT/index.php, begins by loading the Slim libraries and initializing and configuring a new Slim application object. In particular, the Slim application object must be configured with the path to the jQuery Mobile page templates so that it can render them as needed.

Slim works by defining router callbacks for HTTP methods and endpoints. To perform this action, it calls the corresponding method (for example, get() for GET requests or post() for POST requests) and passes the URL route to be matched as the first argument to the method. The second argument to the method is a function, which specifies the actions to be taken when the route is matched to an incoming request. The previous listing sets up two such router callbacks: /index and /search.

  • /index callback: Renders the main application page template. This callback contains the various tabs, jQuery Mobile page elements, and AngularJS controller code.
  • /search callback: Handles Ajax search requests sent by the AngularJS controller. It accepts a search term, then uses a PHP http_build_query() method to construct a request URL to the Nutritionix API. The request is sent to the API via cURL, and the response is returned to the application front end as a JSON document. AngularJS then takes care of parsing the response data and binding it to the scope.

You can also run the Ajax request directly from the application front-end by using AngularJS. However, doing this action exposes your private Nutritionix API application key to users, a practice that is not recommended for a publicly accessible application. Having a server-side script perform the request instead adds some overhead, but offers greater security.

Step 5. Calculate and store meal records

With the search interface complete, the next step is to build the second tab in the user interface. This tab displays a list of the user's selected food items and a running total of the calories in those food items. It includes controls to save the meal record to the database. Use the following code.

<!DOCTYPE html>
…
<head>
  <script>
  var myApp = angular.module('myApp', []);
  
  function myAppController($scope, $http) {

    // related to record functionality
    $scope.removeFromMeal = function(index) {
       $scope.mealItems.splice(index, 1);
    };    
    
    $scope.clearMeal = function() {
      $scope.mealItems.length = 0;
    };
    
    $scope.getTotalCalories = function() {
      var sum = 0;
      for(i=0; i<$scope.mealItems.length; i++) {
        sum += $scope.mealItems[i].fields.nf_calories;
      }
      return sum.toFixed(2);
    };    
    
    $scope.record = function() {
      if ($scope.getTotalCalories() > 0) {
        $http({
            method: 'POST',
            url: '/record',
            data: {'totalCalories': $scope.getTotalCalories()}
          }).
          success(function(data) {
            $scope.clearMeal();
          });
        };   
    };
  }
  </script>
</head>
<body>
...
    <div data-role="content" ng-app="myApp">	
      <div data-role="tabs" ng-controller="myAppController">

        <div data-role="navbar">
          <ul>
            <li><a href="#search" data-theme="a" class="ui-btn-active">Search</a></li>
            <li><a href="#record" data-theme="a">Record <span class="ui-li-count"> {{ getTotalCalories() }} / {{ mealItems.length }}</span></a></li>
            <li><a href="#report" data-theme="a">Report</a></li>
          </ul>
        </div>

        <div id="record">
          <h2 class="ui-bar ui-bar-a">Meal Record</h2>
          <div class="ui-body">
            <ul data-role="listview" data-split-theme="d">
              <li ng-repeat="item in mealItems track by $index">
                <a>{{item.fields.item_name}} / {{item.fields.nf_calories + ' calories'}}</a>
<a href="#" data-inline="true" data-role="button" data-icon="minus" 
                  data-theme="a" ng-click="removeFromMeal($index)">Add</a>
              </li>
            </ul>
          </div>          
          <div class="ui-body">
            <button ng-click="record()">Save</button>
          </div>
        </div>

      </div>
      
    </div>
…
</body>
</html>

The next screen capture shows what the interface looks like in action.

Screen capture fo recording meal contents
Screen capture fo recording meal contents

The list of selected food items is easily generated by iterating over the mealItems array from Step 4. The AngularJS data-binding mechanism ensures that the list is updated instantly, as the user selects new items in the search interface.

After the meal record is complete, the user clicks Save to save the record to the database. The record() function creates an Ajax POST request to the /record endpoint, passing it the total calorie count for the meal record. If the Ajax request is successful, the mealItems array is cleared, ready for the next meal.

Notice a couple of other things from the previous listing:

  • The navigation bar includes two counters: the total calories and the total number of selected food items. This count is automatically updated as the user adds and removes food items from the meal record. This update is again accomplished with data-binding. The two values are dynamically updated from the mealItems array length and the getTotalCalories() controller method.
  • The user can remove selected food items from the meal record by clicking the button next to each meal item. This action invokes the removeFromMeal() controller method, which uses the index of the selected item to remove it from the mealItems array. Data-binding takes care of updating the view and the navigation bar counters.

On the server side, you need to add a Slim callback for the /record endpoint. As you might guess, this callback reads the total calorie count that is sent by the application front end and persist it to the MySQL database created in Step 1. The code for the callback is:

<?php
// use Composer autoloader
require 'vendor/autoload.php';
\Slim\Slim::registerAutoloader();

// configure credentials
// ... for Nutritionix
$config["nutritionix"]["appId"] = 'APP-ID';
$config["nutritionix"]["appKey"] = 'APP-KEY';
// ... for MySQL
$config["db"]["name"] = 'test';
$config["db"]["host"] = 'localhost';
$config["db"]["port"] = '3306';
$config["db"]["user"] = 'root';
$config["db"]["password"] = 'guessme';

// if Bluemix VCAP_SERVICES environment available
// overwrite with credentials from Bluemix
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  $config["db"] = $services_json["mysql-5.5"][0]["credentials"];
}

// configure Slim application instance
$app = new \Slim\Slim();
$app->config(array(
  'debug' => true,
  'templates.path' => './templates'
));

// initialize PDO object
$db = $config["db"]["name"];
$host = $config["db"]["host"];
$port = $config["db"]["port"];
$username = $config["db"]["user"];
$password = $config["db"]["password"];  
$dbh = new PDO("mysql:host=$host;dbname=$db;port=$port;charset=utf8", $username, $password);

// start session
session_start();

// record handler
$app->post('/record', function () use ($app, $dbh) {
  try {
    // get and decode JSON request body
    $request = $app->request();
    $body = $request->getBody();
    $input = json_decode($body);

    // insert meal record
    $stmt = $dbh->prepare('INSERT INTO meals (uid, calories, rdate, ip) VALUES(?, ?, ?, ?)');
$stmt->execute(array($_SESSION['uid'], $input->totalCalories, 
      date('Y-m-d h:i:s', time()), $_SERVER['SERVER_ADDR']));
    $input->id = $dbh->lastInsertId();
    
    // return JSON-encoded response body
    $app->response()->header('Content-Type', 'application/json');
    echo json_encode($input);    
  } catch (Exception $e) {
    $app->response()->status(400);
    $app->response()->header('X-Status-Reason', $e->getMessage());
  }  
});

// snip: other handlers

$app->run();

This process starts by configuring credentials for the local application database. It then checks the PHP environment for the special VCAP_SERVICES environment variable. On Bluemix, this variable holds credentials for bound service instances. If the variable is found, the script assumes it's running on Bluemix and uses the credentials to initialize a PDO connection to the bound MySQL instance. If the variable is not found, the script assumes it's running on a local development instance and uses the credentials for the local database.

Next, the listing defines a POST callback handler for the /record route. This handler receives Ajax POST requests containing total calorie counts and creates SQL INSERT statements to save these counts to the database. In addition to the calorie count, the handler automatically adds the time stamp, the IP address of the client, and the logged-in user's unique identifier to the statement. If the INSERT is successful, the handler returns a JSON packet with the record identifier to the requesting Ajax script.

You're probably wondering where the user identifier comes from. This is covered in detail in Step 7, but briefly, every application user has a unique identifier, generated at registration time. When the user logs in, this identifier is added to the session in the $_SESSION['uid'] variable and is interpolated into the various SQL statements when you save and retrieve user-specific information.

Step 6. Display reports

The third tab of the user interface displays reports for the user's calorie consumption. To keep it simple, the reports are pre-set to display calorie counts for the current day, the last seven days, and the last 30 days. You can easily make this tab more complex, by allowing custom date ranges or more detailed breakdowns.

The following listing displays the code for the reporting interface:

<!DOCTYPE html>
…
<head>
  <script>
  var myApp = angular.module('myApp', []);
  
  function myAppController($scope, $http) {

    // related to report functionality
    $scope.report = function() {
      $http({
          method: 'GET',
          url: '/report'
        }).
        success(function(data) {
          $scope.counts = data;
        });
    };   

  }
  </script>
</head>
<body>
...
    <div data-role="content" ng-app="myApp">	
      <div data-role="tabs" ng-controller="myAppController">

        <div data-role="navbar">
          <ul>
            <li><a href="#search" data-theme="a" class="ui-btn-active">Search</a></li>
<li><a href="#record" data-theme="a">Record 
                <span class="ui-li-count"> {{ getTotalCalories() }} /
                  {{ mealItems.length }}</span></a></li>
            <li><a href="#report" data-theme="a" ng-click="report()">Report</a></li>
          </ul>
        </div>

        <div id="report">
          <h2 class="ui-bar ui-bar-a">Summary</h2>
          <div class="ui-body">
            <ul data-role="listview" data-inset="true" data-split-theme="d">
              <li>Today <span class="ui-li-count">{{ counts.c1 }}</span></li>
              <li>Last 7 days <span class="ui-li-count">{{ counts.c7 }}</span></li>
              <li>Last 30 days <span class="ui-li-count">{{ counts.c30 }}</span></li>
            </ul>          
          </div>          
          <div class="ui-body">
            <button ng-click="report()">Refresh</button>
          </div>
        </div>

      </div>
      
    </div>
…
</body>
</html>

The code listing sets up a list view with three placeholders that are bound to scope variables, which represent the calorie counts for the current day, the last seven days, and the last 30 days. The report() function takes care of setting these scope variables by making an Ajax call to the /report endpoint and parsing the JSON response.

The /report endpoint is the real workhorse of this section. It runs three SQL queries to calculate the user's total calorie consumption over the three time periods. The counts are then massaged into a JSON document returned to the application via Ajax. The code for these tasks is.

<?php
// use Composer autoloader
require 'vendor/autoload.php';
\Slim\Slim::registerAutoloader();

// snip: configure credentials
// snip: initialize PDO object
// snip: start session

// configure Slim application instance
$app = new \Slim\Slim();
$app->config(array(
  'debug' => true,
  'templates.path' => './templates'
));

// report handler
$app->get('/report', function () use ($app, $dbh) {
  $counts = array();
  $counts['c1'] = $counts['c7'] = $counts['c30'] = 0;
  try {
    // get calorie counts    
    // ... for today
$stmt = $dbh->query("SELECT IFNULL(SUM(calories),0) AS sum FROM meals WHERE 
              uid = '" . $_SESSION['uid'] . "' and DATE(rdate) = DATE (NOW())");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $counts['c1'] = $row['sum'];
    
    // ... for the last 7 days
$stmt = $dbh->query("SELECT IFNULL(SUM(calories),0) AS sum FROM meals WHERE 
 uid = '" . $_SESSION['uid'] . "' and DATE(rdate) BETWEEN 
              DATE(DATE_SUB(NOW(), INTERVAL 7 DAY)) AND DATE (NOW())");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $counts['c7'] = $row['sum'];
    
    // ... for the last 30 days
$stmt = $dbh->query("SELECT IFNULL(SUM(calories),0) AS sum FROM meals WHERE 
 uid = '" . $_SESSION['uid'] . "' and DATE(rdate) BETWEEN 
              DATE(DATE_SUB(NOW(), INTERVAL 30 DAY)) AND DATE (NOW())");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $counts['c30'] = $row['sum'];
    
    // return JSON-encoded response body
    $app->response()->header('Content-Type', 'application/json');
    echo json_encode($counts);    
  } catch (Exception $e) {
    $app->response()->status(400);
    $app->response()->header('X-Status-Reason', $e->getMessage());
  }  
});

// snip: other handlers

$app->run();

The following image shows an example of the reports that a user sees.

Screen capture of generating consumption reports
Screen capture of generating consumption reports

Step 7. Add user registration and authentication

Most of the application's key functionality is now complete. All that's left is to add a user registration workflow that enables users to sign up, and a log-in/log-out system for authentication.

The following code for the user registration form can be saved as $APP_ROOT/templates/register.php:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" />
  <script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
  <script src="//code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script>
</head>
<body>

  <div data-role="page">
    <div data-role="header">
      <h1>Calorie Counter</h1>
    </div>
    
    <div data-role="content">	
      <div style="text-align:center"><?php echo $flash['message']; ?></div>
      <form action="/register" method="post" data-ajax="false">
      <div data-role="fieldcontain">
          <label for="email" class="ui-hidden-accessible">Email address:</label>
          <input type="text" name="email" id="email" placeholder="Email address" />
      </div>

      <div data-role="fieldcontain">
          <label for="password" class="ui-hidden-accessible">Password:</label>
          <input type="password" name="password" id="password" placeholder="Password" />
      </div>

      <div data-role="fieldcontain">
          <label for="passwordconfirm" class="ui-hidden-accessible">Password (again):</label>
<input type="password" name="passwordconfirm" id="passwordconfirm" 
             placeholder="Password (again)" />
      </div>
      
      <div>
        <button type="submit" id="submit">Sign Up</button>
      </div>
      </form>
    </div>

  </div>

</body>
</html>

Here's how the user registration form looks:

Screen capture of user registration form
Screen capture of user registration form

This form contains fields for the user's email address and password. When the user submits the form, the user-entered information must be validated and an email message be generated to ask the user to confirm the account. This validation verifies the correctness of the submitted email address and helps reduce automated form submissions. The user's account remains inactive until the confirmation step is completed.

Here's the code to accomplish these tasks:

<?php
// use Composer autoloader
require 'vendor/autoload.php';
\Slim\Slim::registerAutoloader();

// configure credentials
// ... for Nutritionix
$config["nutritionix"]["appId"] = 'APP-ID';
$config["nutritionix"]["appKey"] = 'APP-KEY';
// ... for MySQL
$config["db"]["name"] = 'test';
$config["db"]["host"] = 'localhost';
$config["db"]["port"] = '3306';
$config["db"]["user"] = 'root';
$config["db"]["password"] = 'guessme';
// ... for SendGrid
$config["sg"] = '';

// if Bluemix VCAP_SERVICES environment available
// overwrite with credentials from Bluemix
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  $config["db"] = $services_json["mysql-5.5"][0]["credentials"];
  $config["sg"] = $services_json["sendgrid"][0]["credentials"];
}

// snip: configure Slim application instance
// snip: initialize PDO object
// snip: start session

// registration handlers
$app->get('/register', function () use ($app) {
  $app->render('register.php');
});

// registration processor
$app->post('/register', function () use ($app, $dbh, $config) {
  try {
    $userEmail = $app->request->params('email');
    $userPassword = $app->request->params('password');    
    $userPasswordConfirm = $app->request->params('passwordconfirm');
    
    // validate user input
    if (!filter_var($userEmail, FILTER_VALIDATE_EMAIL)) {
      throw new Exception('Invalid email address');
    }
    if ($userPassword != $userPasswordConfirm) {
      throw new Exception('Passwords do not match');
    }
    $stmt = $dbh->query("SELECT id FROM users WHERE email = '$userEmail'");
    if ($stmt->rowCount() == 1) {
      throw new Exception('Email address already in use');
    }
    
    // generate unique code for confirmation email
    // create account with status inactive
    $userHash = md5(uniqid(rand(), true));
$stmt = $dbh->prepare('INSERT INTO users (email, password, code, status, ip) 
              VALUES(?, PASSWORD(?), ?, ?, ?)');
    $stmt->execute(array($userEmail, $userPassword, $userHash, '0', $_SERVER['SERVER_ADDR']));
    
    // generate confirmation email
    $confirmUrl = 'http://' . $_SERVER['HTTP_HOST'] . "/confirm/$userEmail/$userHash";
    $message = "Please confirm your account: $confirmUrl";
    $subject = 'Calorie counter: account confirmation';
    $from = 'no-reply@' . $_SERVER['HTTP_HOST'];
    
    if (!empty($config["sg"])) {
      $sendgrid = new SendGrid($config["sg"]['username'], $config["sg"]['password']);
      $email = new SendGrid\Email();
      $email->addTo($userEmail)
            ->setFrom($from)
            ->setSubject($subject)
            ->setText($message);
      $sendgrid->send($email);
    } else {
      mail($userEmail, $subject, $message, "From: $from");
    }
    
    $app->flash('message', 'You will shortly receive an email to confirm your account.');
  } catch (Exception $e) {
    $app->flash('message', $e->getMessage());
  }
  $app->redirect('/login');        
});

// account confirmation handler
$app->get('/confirm/:email/:code', function ($email, $code) use ($app, $dbh) {
  try {
    // check for a matching email and code
    // if found, remove code and make account active
    $stmt = $dbh->query("SELECT id FROM users WHERE email = '$email' AND code = '$code'");
    if ($stmt->rowCount() == 1) {
      $row = $stmt->fetch(PDO::FETCH_ASSOC);
      $dbh->exec("UPDATE users SET code = '', status = '1' WHERE id = '" . $row['id'] . "'");
      $app->flash('message', 'Thank you for confirming your account. You can now sign in.');
    }
  } catch (Exception $e) {
    $app->flash('message', $e->getMessage());
  }
  $app->redirect('/login');
});

// snip: other handlers
  
$app->run();

The /register handler in the previous listing renders the user registration form when it receives a GET request. When it detects a POST submission, it performs a few basic checks to verify the email address format, passwords, and email address uniqueness. It then creates a unique code for the account confirmation email and saves the account details, together with the source IP address, time stamp, and email confirmation code, to the application database. The account status is set to inactive (0).

The next step is to send the user a confirmation email that asks him to click a link to verify and activate the account. On Bluemix, this step is easier said than done. If you develop locally, or on a system where you have administrative privileges, it's easy to configure a mail server to handle outgoing email traffic, then use the PHP mail() function to send out the confirmation email. But when you run an application on Bluemix, this step doesn't work the same way. Instead, you need to configure an email service, bind it to your application, and then use that service to send out email.

Bluemix offers the SendGrid service for this purpose and in Step 8, I walk you through the process of binding it to your application instance. For the moment, all you need to know is that SendGrid comes with a PHP client library, which you can use within your application to connect to the bound SendGrid service, to authenticate, and to send email. You already downloaded this library with Composer in Step 2 and now you can use it. Authentication credentials for the service are obtained, as with MySQL, from the Bluemix VCAP_SERVICES environment variable at the top of the listing.

The email message contains a link in the format:

http://[host]/confirm/[email address]/[code]

The /confirm handler in the previous listing handles requests for this URL. It reads in the email address and code, then performs an SQL query against the database to see whether they match. If they do, it then performs an extra query to UPDATE the account status to active (1) and displays a message to invite the user to log in. It also removes the confirmation code from the account record so that it cannot be reused.

The next step is the user log-in form, which is shown in the following code listing. You can save it to $APP_ROOT/templates/login.php.

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.css" />
  <script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
  <script src="//code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.min.js"></script>
</head>
<body>

  <div data-role="page">
    <div data-role="header">
      <h1>Calorie Counter</h1>
    </div>
    
    <div data-role="content">	
      <div style="text-align:center"><?php echo $flash['message']; ?></div>
      <form action="/login" method="post" data-ajax="false">
      <div data-role="fieldcontain">
          <label for="email" class="ui-hidden-accessible">Email address:</label>
          <input type="text" name="email" id="email" placeholder="Email address" />
      </div>

      <div data-role="fieldcontain">
          <label for="password" class="ui-hidden-accessible">Password:</label>
          <input type="password" name="password" id="password" placeholder="Password" />
      </div>

      <div>
        <input type="submit" id="submit" value="Sign In" />
      </div>
      </form>
<!-- using a separate form here to ensure that both buttons 
           look the same once clicked -->
      <form action="/register" method="get" data-ajax="false">
        <input type="submit" id="submit" value="Sign Up" />      
      </form>
    </div>

  </div>

</body>
</html>

The user log-in form looks like:

Screen capture of the user login form
Screen capture of the user login form

The user log-in form includes fields for the user's email address and password. On submission, the /login callback validates the user's credentials against the application database and permit or denies access.

<?php
// use Composer autoloader
require 'vendor/autoload.php';
\Slim\Slim::registerAutoloader();

// snip: configure credentials
// snip: configure Slim application instance
// snip: initialize PDO object

// start session
session_start();

// login handlers
$app->get('/login', function () use ($app) {
  $app->render('login.php');
});

// login processor
$app->post('/login', function () use ($app, $dbh) {
  try {
    // check for valid login
    // if found, set user id in session
    $userEmail = $app->request->params('email');
    $userPassword = $app->request->params('password');  
$stmt = $dbh->query("SELECT id FROM users WHERE email = '$userEmail' 
              AND password = PASSWORD('$userPassword') AND status = '1'");
    if ($stmt->rowCount() == 1) {
      $row = $stmt->fetch(PDO::FETCH_ASSOC);
      $_SESSION['uid'] = $row['id'];
    } else {
      throw new Exception('Login failed');
    }
  } catch (Exception $e) {
    $app->flash('message', $e->getMessage());
    $app->redirect('/login');      
  }
  $app->redirect('/index');
});

// logout handler
$app->get('/logout', function () use ($app) {
  session_destroy();
  $app->redirect('/login');
});

// snip: other handlers

$app->run();

// middleware to restrict access to authenticated users only
function authenticate () {
  $app = \Slim\Slim::getInstance();
  if (!isset($_SESSION['uid'])) {
    $app->redirect('/login');
  }
}

The key elements in the code listing include the following functions:

  • /login handler: Verifies the user's credentials against the application database. If they match, it creates a session variable with the user's unique identifier as $_SESSION['uid']. This session variable is then used by the /record and /report handlers to link calorie counts to users in the database.
  • /logout handler: Destroys the session and all its variables.
  • authenticate() function: Checks for the presence of the user identifier in the session. If not present, the function redirects the user to the /login URL, forcing a re-login. This function can be used as Slim middleware to be run before a request is processed. By adding this middleware to specific route handlers, it becomes possible to protect access to application functions that are only available to authenticated users. In the source code of the application, you can see this middleware in use on the /search, /record, and /report handlers.

Notice my usage of Slim's flash() method in the previous listings. This method is a convenient way to display a message to the user because the message is stored in the session and remains extant until the next request. You can see that both registration and log-in forms include placeholders for this message.

Step 8. Deploy to IBM Bluemix

Now that the application is coded, the final step is to deploy it. If you deploy locally, you're done. You can use the application normally. However, if you deploy on Bluemix, you need a Bluemix account and you also need to download and install the Cloud Foundry command line client. Complete the deployment process with the following steps.

1. Create your application manifest

The application manifest file tells Bluemix how to deploy your application. In particular, it specifies the PHP runtime environment (build pack) to use. Create this file at $APP_ROOT/manifest.yml, and fill it in with the following information.

---
applications:
- name: calorie-counter-[random-number]
memory: 256M
instances: 1
host: calorie-counter-[random-number]
buildpack: https://github.com/dmikusa-pivotal/cf-php-build-pack.git

Remember to update the host and application name to make it unique, either by changing it or by appending a random number to it. I use the CloudFoundry PHP build pack, although other alternatives are also available.

2. Set up URL routing for BulletPHP

By default, the Cloud Foundry PHP build pack uses Apache as its web server. Nginx is a lighter alternative. To use it, you need to override the build pack's default settings to use nginx instead as the web server. Before you start, you can obtain all the files in this section from the project's JazzHub source code repository.

First, create a $APP_ROOT/.bp-config directory and then create $APP_ROOT/.bp-config/options.json with the following content.

{
    "WEB_SERVER": "nginx"
}

It's also necessary to set URL rewriting rules for nginx so that it correctly passes API routes to BulletPHP's URL router. First, create $APP_ROOT/.bp-config/nginx/server-defaults.conf with the following content.

        listen @{VCAP_APP_PORT};
        server_name _;

        fastcgi_temp_path @{TMPDIR}/nginx_fastcgi 1 2;
        client_body_temp_path @{TMPDIR}/nginx_client_body 1 2;
        proxy_temp_path @{TMPDIR}/nginx_proxy 1 2;

        real_ip_header x-forwarded-for;
        set_real_ip_from 10.0.0.0/8;
        real_ip_recursive on;

        try_files $uri /index.php;

Then, create $APP_ROOT/.bp-config/nginx/server-locations.conf with the following content.

        # Some basic cache-control for static files to be sent to the browser
        location ~* \.(?:ico|css|js|gif|jpeg|jpg|png)$ {
            expires max;
            add_header Pragma public;
            add_header Cache-Control "public, must-revalidate, proxy-revalidate";
        }

        # Deny hidden files (.htaccess, .htpasswd, .DS_Store).
        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }

        # pass .php files to fastcgi
        location ~ .*\.php$ {
            try_files $uri =404;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass php_fpm;
        }

3. Connect to Bluemix and deploy the application

Use the cf command-line tool to log in to Bluemix with your IBM user name and password.

shell> cf api https://api.mybluemix.net
shell> cf login

Change to the $APP_ROOT directory and push the application to Bluemix.

shell> cf push

Here's a sample of what you see during this process.

Screen capture of output of 'cf push' command
Screen capture of output of 'cf push' command

Your application should be deployed to Bluemix. But you're not done yet!

4. Bind MySQL and SendGrid services to the application

Your application is now deployed, but you still need to connect it with a MySQL database instance so that your API has data to work with. Go to the Bluemix administration dashboard and log in with your IBM user name and password. You should see your application listed in the Apps menu bar.

Bluemix dashboard - application listing

Select your application and on the resulting page, use the Add new service option to add the mysql service to your application.

Screen capture of Bluemix dashboard - adding a service
Screen capture of Bluemix dashboard - adding a service

You should now see a MySQL service instance that is bound to your application in the Bluemix administration dashboard.

Screen capture of Bluemix dashboard - listing bound services
Screen capture of Bluemix dashboard - listing bound services

Next, do the same thing with the SendGrid service.

Screen capture of Bluemix dashboard - listing bound services
Screen capture of Bluemix dashboard - listing bound services

Your application details should include the MySQL and SendGrid access credentials in the VCAP_SERVICES environment variable.

Bluemix dashboard - environment variables
Bluemix dashboard - environment variables

5. Install the example schema

You can initialize the application database by using the example schema that is shown in Step 1. Although there's no way to directly run the SQL commands in Step 1 in the Bluemix environment, the application includes a special route named /install-schema which you can request through your browser to set up the database table and sample data. Although this route is not documented in this article, you can find it in the source code repository for the application.

6. Start using your application

After your application is deployed, start to use it by browsing to the host you specified in your application manifest, for example, http://calorie-counter-[random-number].mybluemix.net.

Conclusion

As this article illustrated, Bluemix provides a solid foundation for creating and deploying mobile-friendly applications on a cloud-based platform. Add in Slim, jQuery Mobile, and AngularJS, and you have all the tools that you need to quickly prototype and deploy scalable, sophisticated, and interactive applications.

You can download all the code that is implemented in this article from its JazzHub repository, together with the configuration files for the PHP build pack that is used in this article. I recommend that you get the code, start playing with it, and try adding new features to it. I guarantee you won't break anything, and it will definitely add to your learning.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Mobile development, Cloud computing
ArticleID=975166
ArticleTitle=Build and deploy a mobile-friendly calorie counter on IBM Bluemix with PHP, MySQL, AngularJS, and the Nutritionix API
publish-date=04082015