Conteúdo


Desenvolver e implementar um contador de calorias fácil e simples para dispositivos móveis no IBM Cloud com PHP, MySQL, AngularJS e a API Nutritionix

Comments

Até alguns anos atrás, descobrir quantas calorias havia em um sanduíche que você havia acabado de comer exigia uma combinação de adivinhação e consulta aos papéis da embalagem. Hoje, essas mesmas informações estão disponíveis em vários bancos de dados de nutrição online, facilitando o acompanhamento da ingestão de calorias.

Para desenvolvedores, a disponibilidade de dados de nutrição por meio de APIs abertas é empolgante porque cria novas oportunidades para desenvolver aplicativos de gerenciamento de saúde úteis e relevantes.

Neste artigo, mostro como criar um contador de calorias online que permite aos usuários:

  • Pesquisar itens alimentícios pelo nome, com os resultados recuperados por meio de uma API para o banco de dados de nutrição online Nutritionix.
  • Agrupar itens alimentícios selecionados para criar registros de refeição e salvar esses registros em um banco de dados MySQL, junto com suas contagens de calorias, usando um aplicativo PHP/AngularJS.
  • Recupere relatórios do total de calorias consumidas no dia atual, nos últimos sete dias e nos últimos 30 dias.
  • Acesse o aplicativo usando dispositivos móveis, como tablets e smartphones.

No lado do cliente, eu uso jQuery Mobile para criar uma interface com o usuário mais fácil e simples para o aplicativo e AngularJS para ativar alguns dos recursos interativos do aplicativo. No servidor, uso Slim, uma microestrutura PHP, para controlar a interação com a API Nutritionix e salvar e recuperar dados do servidor MySQL.

Na parte final deste artigo, mostro como implementar o aplicativo na nuvem IBM Cloud, o que fornece uma infraestrutura escalável e robusta para a implementação do aplicativo para garantir que os usuários tenham acesso ininterrupto.

Parece interessante? Vamos lá, vamos começar!

Execute o aplicativoObtenha o código

O que é necessário para o aplicativo

Etapa 1. Configurar o banco de dados do aplicativo

Use a seguinte listagem de código, que inclui uma definição de tabela MySQL e dados de amostra para configurar o banco de dados do aplicativo.

  • Se você estiver desenvolvendo e implementando apenas localmente, pode usar esse código para inicializar uma tabela de banco de dados MySQL para o aplicativo ao qual se conectar.
  • Se estiver implementando em IBM Cloud, ignora esta etapa por enquanto. Voltaremos a ela na Etapa 8 depois de inicializar e ligar uma instância de serviço MySQL no IBM Cloud.
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;

Etapa 2. Instalar Slim

Faça o download e configure a microestrutura Slim. Por que Slim? Slim vem com um roteador de URL sofisticado e oferece suporte para mensagens Flash, cookies criptografados e middleware. Também é fácil de entender e utilizar e vem com ótima documentação.

Eu uso o Composer, o gerenciador de dependência PHP, para fazer o download e configurar o Slim. Além do Slim, também adiciono as bibliotecas do cliente SendGrid para PHP. (Saiba mais sobre SendGrid e por que ele é necessário em seguida.) A listagem de código a seguir é o arquivo de configuração do Composer. Salve esse arquivo em $APP_ROOT/composer.json (em que $APP_ROOT se refere ao seu diretório de trabalho).

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

Agora é possível instalar o Slim usando o Composer com o comando:

shell> php composer.phar install

Para facilitar o acesso ao aplicativo, é possível também definir um novo host virtual no ambiente de desenvolvimento e apontar sua raiz de documento para $APP_ROOT. Esta etapa, embora opcional, é recomendada porque cria uma réplica mais fiel do ambiente de implementação de destino no IBM Cloud.

Para configurar um host virtual nomeado para o aplicativo em Apache, abra o arquivo de configuração Apache (httpd.conf ou httpd-vhosts.conf) e inclua estas linhas a ele:

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

Para configurar um hot virtual nomeado para o aplicativo no nginx, abra o arquivo de configuração do nginx (nginx.conf) e inclua estas linhas a ele:

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;
    }        
}

Estas linhas definem um novo host virtual, http://calories.localhost/, para o qual a raiz de documento corresponde a $APP_ROOT (lembre-se de atualizá-la para refletir suas próprias configurações locais). Reinicie o servidor da web para ativar as novas configurações. Pode ser necessário atualizar o servidor DNS local da rede para indicar o novo host.

Etapa 3. Entender a API Nutritionix

Como ocorre com outras APIs da web, a API Nutritionix funciona sobre HTTP e espera uma solicitação HTTP para um terminal designado. Ao recebimento dessa solicitação, o servidor da API responde à consulta com um feed JSON que contém os dados solicitados. É possível analisar esses dados usando uma linguagem de programação no lado do servidor (por exemplo, PHP ou Perl) ou um kit de ferramentas no lado do cliente (por exemplo, jQuery ou AngularJS) e extrair o conteúdo dele para integração em uma página da web.

Depois de inscrever-se para uma conta da API Nutritionix e ter um appId e appKey válidos, é possível fazer um test drive da API procurando itens alimentícios que correspondam ao termo "chicken". A conta de desenvolvedor gratuita dá o direito a apenas 500 procuras por dia (embora seja possível solicitar um aumento de cota enviando um email à equipe da API).

Considere a próxima imagem, que mostra a resposta a uma solicitação GET autenticada para https://api.nutritionix.com/v1_1/search/chicken?fields=item_name,brand_name,nf_calories&item_type=3&appId=[APP-ID]&appKey=[APP-KEY], o terminal da API para consultas de procura (lembre-se de atualizar a URL anterior para refletir as credenciais da sua API antes de emitir a solicitação).

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

Como a imagem ilustra, a API Nutritionix responde à solicitação com um documento JSON que lista itens alimentícios que correspondem ao termo de procura "chicken". A cadeia de caractere de procura inclui o parâmetro item_type=3 , que limita a pesquisa apenas ao banco de dados da USDA. Para cada item alimentício, a resposta inclui o nome do item, o nome da marca e a contagem de calorias. Também há suporte a outros campos. Consulte a documentação da API Nutritionix para obter detalhes.

Etapa 4. Ativar a interface de procura

Desenvolva uma interface de procura simples que permita ao usuário procurar itens alimentícios e visualizar uma lista de resultados. A página de resultados deve incluir controles para o usuário incluir itens alimentícios selecionados ao seu registro de refeições.

Configure a estrutura básica dessa interface com o usuário e salve-a como $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>

A listagem de código anterior mostra uma página formatada de acordo com as convenções padrão de jQuery Mobile. O elemento da página primário é um elemento <div> com um atributo data-role="page" . Dentro desse elemento <div> , há elementos <div> separados para o cabeçalho e o conteúdo da página. O conteúdo da página consiste em uma série de guias. Cada guia representa uma das tarefas ("search", "record" e "report"). Clicar no nome da guia na barra de navegação superior mostra seu conteúdo.

A seguir, inclua elementos à guia de pesquisa, como mostra a listagem de código a seguir:

<!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>
...

A guia de procura agora consiste em duas áreas: o campo de entrada de procura na parte superior e uma lista de resultados da procura na parte inferior. Ambas as áreas são controladas por um controlador AngularJS e ambas usam um modelo AngularJS chamado foodItems. A seguinte listagem de código mostra o código do controlador.

  <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>

E é assim que a interface de procura fica em ação.

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

Como funciona? Quando um usuário insere um termo de procura e clica em Search, a função search() do AngularJS recupera a entrada por meio do modelo foodItems e gera uma solicitação Ajax para o terminal do aplicativo /search . Essa solicitação não é o terminal da API Nutritionix, mas, em vez disso, um terminal de API intermediário gerenciado pelo aplicativo em si (mais sobre isso em breve).

A resposta à solicitação do Ajax é um pacote JSON similar ao mostrado anteriormente. Essa resposta é anexada à propriedade foodItems.results , e a ligação de dados AngularJS cuida da iteração sobre essa coleção, analisando-a e exibindo-a como uma lista de resultados da procura.

Olhe de perto a interface de procura. Observe que, ao lado de cada resultado de procura há um botão, que está vinculado à função addToMeal() . Quando o usuário clica nesse botão, o item alimentício correspondente é incluído ao array mealItems no escopo. Mais tarde, esse array é usado para construir a visualização para a guia de registro.

No lado do servidor, é necessário ter um manipulador para a solicitação do Ajax para o terminal /search , que é onde o Slim entra. O Slim usa os dados da solicitação do Ajax para se conectar à API Nutritionix e executar uma procura, similar à procura mostrada anteriormente. A listagem a seguir tem o código para fazer isso funcionar.

<?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();

Esse script, que deve ser salvo como $APP_ROOT/index.php, começa carregando as bibliotecas Slim e configurando um novo objeto de aplicativo Slim. Em particular, o objeto de aplicativo Slim deve ser configurado com o caminho para os modelos de página jQuery Mobile de modo que possa renderizá-los conforme o necessário.

O Slim opera definindo retornos de chamada de roteador para métodos de HTTP e terminais. Para realizar essa ação, ele chama o método correspondente (por exemplo, solicitações get() forGET ou solicitações post() forPOST ) e envia a rota de URL a ser combinada com o primeiro argumento para o método. O segundo argumento do método é uma função que especifica as ações a serem realizadas quando se faz a correspondência entre a rota e uma solicitação recebida. A listagem anterior configura dois desses retornos de chamada do roteador: /index e/search.

  • Retorno de chamada /index : renderiza o principal modelo de página do aplicativo. Esse retorno de chamada contém as várias guias, elementos de página jQuery Mobile e código do controlador AngularJS.
  • Retorno de chamada /search : manipula solicitações de procura Ajax enviadas pelo controlador AngularJS. Aceita um termo de procura, então usa o método PHP http_build_query() para construir uma URL de solicitação ara a API Nutritionix. A solicitação é enviada para a API via cURL, e a resposta é retornada ao frontend do aplicativo como um documento JSON. O AngularJS então cuida da análise dos dados da resposta e da sua vinculação ao escopo.

Também é possível executar a solicitação Ajax diretamente do frontend do aplicativo usando AngularJS. Porém, fazer isso expõe sua chave de aplicativo privada da API Nutritionix a usuários, uma prática que não é recomendada para aplicativos acessíveis ao público. Ter um script no lado do servidor para realizar a solicitação, em vez disso, adiciona alguma sobrecarga, mas aumenta a segurança.

Etapa 5. Calcular e armazenar registros de refeição

Com a interface de pesquisa concluída, a próxima etapa é desenvolver a segunda guia na interface com o usuário. Essa guia exibe uma lista dos itens alimentícios selecionados pelo usuário e um total em execução de calorias em tais itens. Inclui controles para salvar registros de alimentos no banco de dados. Use o código a seguir.

<!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>

A próxima captura de tela mostra como é a aparência da interface em ação.

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

A lista de itens alimentícios selecionados é gerada facilmente iterando sobre o array mealItems da Etapa 4. O mecanismo de ligação de dados AngularJS garante que a lista seja atualizada de maneira instantânea, conforme o usuário seleciona novos itens na interface de procura.

Depois da conclusão do registro de refeição, o usuário clica em Save para salvar o registro no banco de dados. A função record() cria uma solicitação Ajax POST para o terminal do /record , enviando a ele a contagem total de calorias para o registro de refeição. Se a solicitação do Ajax for bem-sucedida, o array mealItems é limpo, ficando pronto para a próxima refeição.

Observe alguns outros itens da listagem anterior:

  • a barra de navegação inclui dois contadores: o total de calorias e o número total de itens alimentícios selecionados. Essa contagem é atualizada automaticamente conforme o usuário inclui e remove itens alimentícios do registro de refeição. Essa atualização é realizada novamente com ligação de dados. Os dois valores são atualizados dinamicamente a partir do comprimento do array mealItems e do método de controlador getTotalCalories() .
  • O usuário pode remover itens alimentícios selecionados do registro de refeição clicando no botão ao lado de cada item de refeição. Essa ação chama o método de controlador removeFromMeal() , que usa o índice do item selecionado para removê-lo do array mealItems . A ligação de dados cuida de atualizar a visualização e os contadores de barra de navegação.

No lado do servidor, é necessário incluir um retorno de chamada Slim para o terminal /record . Como você pode adivinhar, esse retorno de chamada lê a contagem do total de calorias enviada pelo frontend do aplicativo e persiste-a no banco de dados MySQL criado na Etapa 1. O código para o retorno de chamada é:

<?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();

Esse processo começa configurando as credenciais para o banco de dados de aplicativos lógico. Então verifica o ambiente PHP quanto à variável de ambiente VCAP_SERVICES especial. No IBM Cloud, essa variável contém credenciais para instâncias de serviço limitadas. Se a variável for encontrada, o script presume que ela está em execução em IBM Cloud e usa as credenciais para inicializar a conexão de PDO para a instância MySQL limitada. Se a variável não for encontrada, o script presume que ela está em execução em uma instância de desenvolvimento local e usa as credenciais para o banco de dados local.

A seguir, a listagem define um manipulador de retorno de chamada POST para a rota /record . Esse manipulador recebe solicitações Ajax POST contendo contagens de total de calorias e cria instruções SQL INSERT para salvar essas contagens no banco de dados. Além da contagem de calorias, o manipulador inclui automaticamente o registro de data e hora, o endereço IP do cliente e o identificador exclusivo do usuário conectado à instrução. Se INSERT for bem-sucedida, o manipulador retorna um pacote JSON com o identificador de registro para o script Ajax responsável pela solicitação.

Você provavelmente está imaginando de onde o identificador de usuário vem. Isso é abordado em detalhes na Etapa 7, mas, em resumo, todo aplicativo do usuário possui um identificador exclusivo, gerado no momento do registro. Quando o usuário efetua login, esse identificador é incluído à sessão na variável $_SESSION['uid'] e é interpolado nas várias instruções SQL quando você salva e recupera informações específicas do usuário.

Etapa 6. Exibir relatórios

A terceira guia da interface com o usuário exibe relatórios para o consumo de calorias do usuário. Para manter a simplicidade, os relatórios são predefinidos para exibir as contagens de caloria para o dia atual, os últimos sete dias e os últimos 30 dias. É possível facilmente tornar essa guia mais complexa permitindo interfaces de dados personalizados ou detalhamentos mais minuciosos.

A listagem a seguir exibe o código para a interface de relatório:

<!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>

A listagem de código configura uma visualização de lista com três itens temporários ligados à variável de escopo, que representa contagens de calorias para o dia atual, os últimos sete dias e os últimos 30 dias. A função report() cuida da configuração dessas variáveis de escopo fazendo uma chamada do Ajax para o terminal /report e analisando a resposta JSON.

O terminal /report é o verdadeiro carro-chefe desta seção. Ele executa três consultas SQL para calcular o consumo total de calorias do usuário ao longo de três períodos de tempo. As contagens então são enviadas para um documento JSON retornado ao aplicativo via Ajax. O código para essas tarefas é:

<?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();

A imagem a seguir mostra um exemplo dos relatórios que um usuário vê.

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

Etapa 7. Incluir registro e autenticação do usuário

A maioria da funcionalidade principal do aplicativo agora está concluída. Tudo o que resta é incluir um fluxo de trabalho de registro do usuário que permita aos usuários se inscrever e efetuar login/logout no sistema para autenticação.

O código a seguir para o formulário de registro do usuário pode ser salvo como $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>

Esta é a aparência do formulário de registro do usuário:

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

Esse formulário contém campos para o endereço de email e senha do usuário. Quando o usuário envia o formulário, as informações inseridas pelo usuário devem ser validadas e uma mensagem de email deve ser gerada para pedir para o usuário confirmar a conta. Essa validação verifica a correção do endereço de email enviado e ajuda a reduzir os envios de formulário automatizados. A conta do usuário permanece inativa até a etapa de confirmação ser concluída.

Aqui está o código para realizar essas tarefas:

<?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();

O manipulador /register na listagem anterior renderiza o formulário de registro do usuário quando recebe uma solicitação GET . Quando ele detecta um envio POST , ele realiza algumas verificações básicas para verificar o formato de endereço de email, as senhas e a singularidade do endereço de email. Ele cria um código exclusivo para o email de confirmação da conta e salva os detalhes da conta, junto com o endereço IP de origem, o registro de data e hora e o código de confirmação de email, no banco de dados do aplicativo. O status da conta é definido para inativo (0).

A próxima etapa é enviar ao usuário um email de confirmação pedindo para ele clicar em um link para verificar e ativar a conta. No IBM Cloud, é mais falar do que fazer essa etapa. Se você desenvolver localmente, ou em um sistema para o qual tenha privilégios administrativos, é fácil configurar um servidor de correio para manipular o tráfego de email de saída e então usar a função PHP mail() para enviar o email de confirmação. Mas quando você executa um aplicativo no IBM Cloud, essa etapa não funciona da mesma maneira. Em vez disso, é necessário configurar um serviço de email, vinculá-lo ao aplicativo e usá-lo para enviar email.

O IBM Cloud oferece o serviço SendGrid para esse fim e, na Etapa 8, apresentei o processo de ligá-lo à sua instância de aplicativo. Por enquanto, é necessário saber apenas que o SendGrid vem com uma biblioteca do cliente PHP, que pode ser usada dentro do seu aplicativo para conexão ao serviço SendGrid ligado, para autenticação e para envio de email. Você já fez o download dessa biblioteca com um Composer na Etapa 2 e agora pode utilizá-lo. As credenciais de autenticação para o serviço foram obtidas, como com o MySQL, da variável de ambiente IBM Cloud VCAP_SERVICES na parte superior da listagem.

A mensagem de email contém um link no formato:

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

O manipulador /confirm na listagem anterior manipula solicitações para essa URL. Ele lê o endereço de email e o código e depois realiza uma consulta SQL com relação ao banco de dados para ver se correspondem. Se corresponderem, ele realiza uma consulta extra para realizar o UPDATE do status da conta para active (1) e exibe uma mensagem para convidar o usuário a efetuar login. O manipulador também remove o código de confirmação do registro da conta de modo que não possa ser reutilizado.

A próxima etapa é o formulário de login do usuário, mostrado na listagem de código a seguir. É possível salvá-lo em $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>

O formulário de login do usuário tem esta aparência:

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

O formulário de login do usuário inclui campos para o endereço de email e senha do usuário. Ao envio, o retorno de chamada /login valida as credenciais do usuário com relação ao banco de dados do aplicativo e permite ou nega o acesso.

<?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');
  }
}

Os principais elementos na listagem de código incluem as seguintes funções:

  • Manipulador /login : verifica as credenciais do usuário com relação ao banco de dados do aplicativo. Se corresponderem, ele cria uma variável de sessão com o identificar exclusivo do usuário como $_SESSION['uid']. Essa variável de sessão então é usada pelos manipuladores /record e /report para vincular as contagens de calorias aos usuários no banco de dados.
  • Manipulador /logout : destrói a sessão e todas as suas variáveis.
  • Função authenticate() : Verifica se há presença do identificador de usuário na sessão. Se não estiver presente, a função redireciona o usuário para a URL /login , forçando um novo login. Essa função pode ser usada como o middleware Slim a ser executado antes de processar uma solicitação. Incluindo esse middleware a manipuladores de rota específicos, torna-se possível proteger o acesso a funções do aplicativo disponíveis apenas a usuários autenticados. No código-fonte do aplicativo, é possível ver esse middleware em uso nos manipuladores /search, /record e /report .

Observe meu uso do método flash() do Slim em listagens anteriores. Esse método é uma maneira conveniente de exibir uma mensagem ao usuário porque a mensagem é armazenada na sessão e continua existente até a próxima solicitação. Veja que os formulários de registro e de login incluem itens temporários para essa mensagem.

Etapa 8. Implementar para o IBM Cloud

Agora que o aplicativo está codificado, a etapa final é implementá-lo. Se você implementar localmente, está pronto. É possível usar o aplicativo normalmente. Porém, se implementar em IBM Cloud, é necessário ter uma conta IBM Cloud e fazer o download e instalar o cliente de linha de comando Cloud Foundry. Conclua o processo de implementação com as etapas a seguir.

1. Criar o manifest do aplicativo

O arquivo manifest do aplicativo diz ao IBM Cloud como implementar o seu aplicativo. Em particular, especifica o ambiente de tempo de execução do PHP (pacote de desenvolvimento) a usar. Crie esse arquivo em $APP_ROOT/manifest.yml e preencha-o com as informações a seguir.

---
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

Lembre-se de atualizar o nome de host e do aplicativo para torná-lo exclusivo, seja alterando-o ou anexando um número aleatório a ele. Eu uso o pacote de desenvolvimento PHP CloudFoundry, embora também haja outras alternativas.

2. Configurar roteamento de URL para BulletPHP

Por padrão, o pacote de desenvolvimento PHP Cloud Foundry usa Apache com servidor da web. O Nginx é uma alternativa mais leve. Para usá-lo, é necessário substituir as configurações padrão do pacote de desenvolvimento para usar nginx, em vez do servidor da web. Antes de iniciar, é possível obter todos os arquivos nessa seção do repositório de código-fonte JazzHub do projeto.

Primeiro, crie um diretório $APP_ROOT/.bp-config e depois crie $APP_ROOT/.bp-config/options.json com o seguinte conteúdo.

{
    "WEB_SERVER": "nginx"
}

Também é necessário definir as regras de regravação de URL para nginx de modo que passem corretamente as rotas de API para o roteador URL do BulletPHP. Primeiro, crie $APP_ROOT/.bp-config/nginx/server-defaults.conf com o seguinte conteúdo.

        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;

Então, crie $APP_ROOT/.bp-config/nginx/server-locations.conf com o seguinte conteúdo.

        # 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. Conectar-se ao IBM Cloud e implementar o aplicativo

Use a ferramenta de linha de comando cf para efetuar login no IBM Cloud com seu nome de usuário e senha IBM.

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

Mude para o diretório $APP_ROOT e realize o push do aplicativo para o IBM Cloud.

shell> cf push

Aqui está uma amostra do que você vê durante esse processo.

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

Seu aplicativo deve ser implementado no IBM Cloud. Mas você ainda não terminou!

4. Ligar os serviços MySQL e SendGrid ao aplicativo

Seu aplicativo agora está implementado, mas você ainda precisa conectá-lo a uma instância de banco de dados MySQL de modo que sua API tenha dados com os quais trabalhar. Acesse o painel de administração IBM Cloud e efetue login com seu nome de usuário e senha IBM. Deve ser possível ver o aplicativo listado na barra de menu Apps .

IBM Cloud dashboard - application listing

Selecione seu aplicativo e, na página resultante, use a opção Add new service para incluir o serviço mysql ao aplicativo.

Screen capture of IBM Cloud dashboard - adding a service
Screen capture of IBM Cloud dashboard - adding a service

Agora deve ser possível ver uma instância de serviço MySQL ligada ao seu aplicativo no painel de administração do IBM Cloud.

Screen capture of IBM Cloud dashboard - listing bound services
Screen capture of IBM Cloud dashboard - listing bound services

A seguir, faça o mesmo com o serviço SendGrid .

Screen capture of IBM Cloud dashboard - listing bound services
Screen capture of IBM Cloud dashboard - listing bound services

Os detalhes do seu aplicativo devem incluir as credenciais de acesso MySQL e SendGrid na variável de ambiente VCAP_SERVICES .

IBM Cloud dashboard - environment variables
IBM Cloud dashboard - environment variables

5. Instalar o esquema de exemplo

É possível inicializar o banco de dados do aplicativo usando o esquema de exemplo mostrado na Etapa 1. Embora não seja possível executar diretamente os comandos SQL na Etapa 1 no ambiente do IBM Cloud, o aplicativo inclui uma rota especial chamada /install-schema que é possível solicitar por meio do seu navegador para configurar a tabela de banco de dados e os dados de amostra. Embora essa rota não seja documentada neste artigo, é possível encontrá-la no repositório de código-fonte para o aplicativo.

6. Começar a usar o aplicativo

Depois da implementação do aplicativo, comece a usá-lo navegando para o host especificado no manifest do aplicativo, por exemplo, http://calorie-counter-[random-number].mybluemix.net. Ou use o link Execute o aplicativo perto do início deste artigo para experimentar uma demo em tempo real do aplicativo.

Conclusão

Como este artigo ilustrou, o IBM Cloud fornece uma base sólida para criar e implementar aplicativos fáceis e simples para dispositivos móveis em uma plataforma baseada em nuvem. Inclua em Slim, jQuery Mobile e AngularJS, e todas as ferramentas necessárias para rapidamente criar um protótipo e implementar aplicativos escaláveis, sofisticados e interativos estarão disponíveis.

É possível fazer o download de todo o código implementado neste artigo a partir do repositório JazzHub, junto com os arquivos de configuração para o pacote de desenvolvimento PHP usado neste artigo. Recomendo obter o código, começar a experimentá-lo e tentar incluir recursos novos a ele. Garanto que isso danificará nada, e isso definitivamente contribuirá para seu aprendizado.


Recursos para download


Temas relacionados


Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Desenvolvimento móvel, Cloud computing
ArticleID=987343
ArticleTitle=Desenvolver e implementar um contador de calorias fácil e simples para dispositivos móveis no IBM Cloud com PHP, MySQL, AngularJS e a API Nutritionix
publish-date=03052018