Build a mobile web app for assisting stray dogs, Part 1

Use Bluemix, PHP, and developerWorks Premium benefits to help people report homeless animals in distress


Content series:

This content is part # of 2 in the series: Real-world applications using developerWorks Premium, Part 1

Stay tuned for additional content in this series.

This content is part of the series:Real-world applications using developerWorks Premium, Part 1

Stay tuned for additional content in this series.

Stray dogs in busy cities are at high risk for injury or illness. Organizations dedicated to helping strays depend in part on alert citizens to notify them about dogs in distress. But the information they receive is often sketchy, hampering their efforts to find and assist the animals.

Recently, I started thinking about how technology could help address this problem. I imagined a mobile application with which citizens could easily report injured or ill strays to a central database, including photos and GPS locations. But I knew that configuring, orchestrating, and maintaining the various components would require time and effort, and that I'd also need to figure out where and how to host the app.

At around the same time, I heard about developerWorks Premium. This paid program includes a 12-month subscription to IBM Bluemix (a platform I was already familiar with from many previous tutorials) and additional monthly credit toward development servers and hosted services, including object- and data-storage services. I realized this program would give me enough credits to prototype my application. And the hosted nature of the services would significantly reduce the effort I'd otherwise need to invest in dealing with security and configuration issues.

The Cloudant instance exposes its contents to a REST interface, which makes it easy to create, delete, and search for stored documents via standard HTTP requests.

In this two-part tutorial, I walk you through building my prototype. The final result is code that leverages modern smartphone capabilities like built-in GPS and cameras, works well on all mobile devices, and can be easily extended to cover other usage scenarios.

Collecting data from mobile users is only half of the solution. The rescue agencies also need a way to view submitted reports and photos, and identify specified locations on a map. That's why the example application also includes a powerful search engine with map integration, fully optimized for mobile usage.

Behind the scenes, the application orchestrates various Bluemix and third-party services. The Bluemix services are Object Storage, which provides a secure storage area for uploaded photos; and Cloudant NoSQL DB, which provides a NoSQL data layer for submitted reports. The app also uses the Google Static Maps API, which produces a map with a specific location pinpointed on it. On the client side, you'll use Bootstrap to create a mobile-friendly UI for the app. On the server, you'll use the Silex PHP microframework to manage the application flow. You'll host the application on Bluemix.

You'll build the code as you work through the tutorial. For reference, you can view (or obtain) my complete project code by clicking the Get the code button below. If you want to try out the application, click the Run the app button below. Remember that this is a public demo, so be careful not to upload confidential or sensitive information to it. (You can also use the application's handy System Reset button to erase all uploaded data.)

Run the appGet the code

What you'll need


Create a stub application

The first step is to initialize a basic application with the Silex PHP microframework and other dependencies. You can use Composer to download and install these components easily.

  1. Save the following contents in a Composer configuration file named $APP_ROOT/composer.json, where $APP_ROOT is the project directory in your development environment:
        "require": {
            "silex/silex": "*",
            "twig/twig": "*",
            "symfony/validator": "*",
            "guzzlehttp/guzzle": "*",
            "php-opencloud/openstack": "*"
        "minimum-stability": "dev",
        "prefer-stable": true
  2. From your OS command line or shell, run the following command to download and install the components:
    php composer.phar install
  3. Under $APP_ROOT, create a directory named public (for all web-accessible files), a directory named views (for all views), and a config.php file (for configuration information).
  4. Silex depends on URL rewriting for "friendly URLs," so this is also a good time to configure your web server's URL-rewriting rules. Since the application will eventually be deployed to an Apache web server, create a $APP_ROOT/.htaccess file with the following contents:
    <IfModule mod_rewrite.c>
        Options -MultiViews
        RewriteEngine On
        #RewriteBase /path/to/app
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteRule ^ index.php [QSA,L]
  5. 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/public. This step, although optional, is recommended because it creates a closer replica of the target deployment environment on Bluemix. See the Apache Virtual Host documentation for more information on how to use virtual hosts.
    • If you define a new virtual host named stray-assist.localhost (for example), you'll be able to access your application via a URL such as http://stray-assist.localhost/index. (You might need to update your network's local DNS server to tell it about the new host.)
    • If you decide not to use a virtual host and instead store your files in a stray-assist/ directory under the web server document root (for example), you'll be able to access your app via a URL such as http://localhost/stray-assist/public/index.
  6. Create a controller script at $APP_ROOT/public/index.php, with the following contents:
    // use Composer autoloader
    require '../vendor/autoload.php';
    // load configuration
    require '../config.php';
    // load classes
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Validator\Constraints as Assert;
    use GuzzleHttp\Psr7\Stream;
    use GuzzleHttp\Psr7;
    use Silex\Application;
    // initialize Silex application
    $app = new Application();
    // turn on application debugging
    // set to false for production environments
    $app['debug'] = true;
    // load configuration from file
    $app->config = $config;
    // register Twig template provider
    $app->register(new Silex\Provider\TwigServiceProvider(), array(
      'twig.path' => __DIR__.'/../views',
    // register validator service provider
    $app->register(new Silex\Provider\ValidatorServiceProvider());
    // register session service provider
    $app->register(new Silex\Provider\SessionServiceProvider());

    So far, the script code initializes the Silex framework and the necessary component classes. The script also reads in configuration data from the application configuration file, initializes the Twig template renderer, and registers it with Silex.
  7. Continue the script by adding router callbacks for various routes and defining the function —get() for GET requests, post() for POST requests, and so on — to be called when each route is matched to an incoming request:
    // index page handlers
    $app->get('/', function () use ($app) {
      return $app->redirect($app["url_generator"]->generate('index'));
    $app->get('/index', function () use ($app) {
      return $app['twig']->render('index.twig', array());
    // report submission form
    $app->get('/report', function (Request $request) use ($app) {
      // todo
    // search form
    $app->get('/search', function (Request $request) use ($app) {
      // todo
    // legal page handler
    $app->get('/legal', function (Request $request) use ($app) {
     // todo
    // reset handler
    $app->get('/reset-system', function (Request $request) use ($app) {
      // todo  

    The URL route to be matched is passed as the first argument to each HTTP method; the second argument is a function that specifies the actions to be taken when the route is matched to an incoming request. For example, in your script, the /index callback renders the main application page template, named index.twig.

    The script also sets up a router callback for the / route, which simply redirects to the /index route. Similarly, other callbacks are defined for routes such as /search and /report. You'll fill in these callbacks as you progress through the tutorial.

  8. As the final bit of preparation, create a simple Bootstrap-based UI with header, footer, and content areas. Here's an example, which you'll use for all the application views shown in subsequent code listings:
    <!DOCTYPE html>
    <html lang="en">
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Stray Assist</title>
        <link rel="stylesheet" href="">
        <link rel="stylesheet" href="">
        <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
        <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
        <!--[if lt IE 9]>
          <script src=""></script>
          <script src=""></script>
        <div class="container">
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Stray Assist</h4>
          {% for message in app.session.flashbag.get('success') %}
          <div class="alert alert-success">
            <strong>Success!</strong> {{ message }}
          {% endfor %}      
          {% for message in app.session.flashbag.get('error') %}
          <div class="alert alert-danger">
            <strong>Error!</strong> {{ message }}
          {% endfor %}
            <!-- content body -->      
        <div class="container">
          <p class="text-center">
            <a href="{{ app.url_generator.generate('legal') }}" role="button" class="btn btn-default btn-sm">Legal</a>
            <a href="{{ app.url_generator.generate('legal') }}" role="button" class="btn btn-danger btn-sm">System Reset</a>

With all the prep work out of the way, you can now start building the application.


Create the application welcome page

In Step 1, you populated the callbacks for the / and /index routes with the necessary code to render the index template. Now, create the corresponding view script at $APP_ROOT/views/index.twig, using the Bootstrap template from Step 1, and fill the content area with a basic menu:

      <div class="panel panel-default">
        <div class="panel-heading">Report a stray dog near you that needs help.</div>
        <div class="btn-group btn-group-justified">
          <a role="button" class="btn btn-primary" href="{{ app.url_generator.generate('report') }}">Report</a>
      <div class="panel panel-default">
        <div class="panel-heading">Search for previous reports and track them on a map.</div>
        <div class="btn-group btn-group-justified">
          <a role="button" class="btn btn-primary" href="{{ app.url_generator.generate('search') }}">Search</a>

Notice that Silex's URL generator is used to dynamically create the URLs for the /search and /report routes. This approach ensures that the application routes remain valid whether the application is hosted at the domain root or in a subdirectory of the domain root.

Fire up your browser and point it to http://stray-assist.localhost/index (or the equivalent URL in your development environment) to see the welcome page. The page includes a button for accessing the reporting function and a button for accessing the search function:

Screenshot of the Stray Assist welcome page
Screenshot of the Stray Assist welcome page

Create the reporting interface

Now that all the basic pieces are working in harmony, it's time to jump into something bigger. Users need a way to submit reports of injured strays they come across, and it's important that those reports be structured and fully searchable using multiple criteria.

  1. Create a view script, $APP_ROOT/views/report.twig, and start it with the following code:
      <form method="post" action="{{ app.url_generator.generate('report') }}">
        <input type="hidden" name="latitude" value="{{ latitude }}" />
        <input type="hidden" name="longitude" value="{{ longitude }}" />
  2. Add the following three main sections:
    • An Identification section in which users enter details about the stray, such as its color, sex, age, and any identifying marks:
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Identification</h4>
            <div class="panel-body">
              <div class="form-group">
                <label for="color">Color</label>
                <input type="text" class="form-control" id="color" name="color" required="true"></input>
              <div class="form-group">
                <label for="gender">Sex</label>
                <select name="gender" id="gender" class="form-control" required="true">
                  <option value="male">Male</option>
                  <option value="female">Female</option>
                  <option value="unknown">Unknown</option>
              <div class="form-group">
                <label for="gender">Age</label>
                <select name="age" id="age" class="form-control" required="true">
                  <option value="pup">Pup</option>
                  <option value="adult">Adult</option>
                  <option value="unknown">Unknown</option>
              <div class="form-group">
                <label for="identifiers">Identifying marks</label>
                <textarea name="identifiers" id="identifiers" class="form-control" rows="3"></textarea>
    • A Details section that supports a free-form description of the problem or injury:
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Details</h4>
            <div class="panel-body">
              <div class="form-group">
                <label for="description">Description of injury</label>
                <textarea name="description" id="identifiers" class="form-control" rows="3" required="true"></textarea>
    • A Reporter section that asks for the user's name, phone number, and email address:
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Reporter</h4>
            <div class="panel-body">
              <div class="form-group">
                <label for="name">Name</label>
                <input type="text" class="form-control" id="name" name="name" required="true"></input>
              <div class="form-group">
                <label for="phone">Phone number</label>
                <input type="text" class="form-control" id="phone" name="phone" required="true"></input>
              <div class="form-group">
                <label for="email">Email address</label>
                <input type="email" class="form-control" id="email" name="email" required="true"></input>
              <div class="form-group">
                <button type="submit" name="submit" class="btn btn-primary">Submit</button>
  3. To geotag each report with the user's current location, use the HTML5 geolocation API that most modern browsers include. Update the view script by adding this code near the top:
    {% if not latitude and not longitude %}    
    $(document).ready(function() {
      if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(handle_geo_query, handle_error);
      function handle_geo_query(location) {
        window.location = '?latitude=' + location.coords.latitude + '&longitude=' + location.coords.longitude;
      function handle_error(e) {
        alert('An error occurred during geolocation.');
    {% endif %}

    Typically, when the geotagging code runs in a browser, a dialog box pops up to ask the user for permission to disclose the current location: Screenshot of a dialog box asking for permission to disclose current location
    Screenshot of a dialog box asking for permission to disclose current location

    The user must explicitly allow such disclosure for the application to receive the current location, which is usually identified via the nearest cellular tower or the GPS system on the device. If the user agrees, the script calls the handle_geo_query function, which reads in the latitude and longitude and reloads the page URL, this time appending these values to the URL as GET parameters.

    If an error occurs — for example, if the user denies permission to share the location or the location can't be identified — the handle_error function is called to display an alert box. (You can fine-tune the error message based on the error type.) In this situation, the report is geotagged with latitude 0 and longitude 0.

  4. Update the controller script at $APP_ROOT/public/index.php by adding the following code to the existing stub callback function for the /report route:
    // Silex application initialization  snipped
    // report submission form
    // get lat/long from browser and set as hidden form fields
    $app->get('/report', function (Request $request) use ($app) {
      $latitude = $request->get('latitude');
      $longitude = $request->get('longitude');
      return $app['twig']->render('report.twig', array('latitude' => $latitude, 'longitude' => $longitude));
    // other callbacks  snipped

    The callback checks the URL for the latitude and longitude obtained through geolocation, transfers those values to the view script as template variables, and renders the form (which you can see at http://stray-assist.localhost/report or the equivalent URL in your development environment).

Initialize a Cloudant database instance

Users now have a way to interact with the application and submit reports. The Cloudant NoSQL DB service in Bluemix makes it easy to store the reports in a database. The service provisions an empty Cloudant database instance that can be bound to your application. This database instance exposes its contents to a REST interface, which makes it easy to create, delete, and search for stored documents via standard HTTP requests. The default service only offers a limited quota of free storage, but with your developerWorks Premium subscription, you'll usually have enough credits to cover most reasonable data storage needs.

  1. Log in to your Bluemix account. In the Console, select Data & Analytics from the available services.
  2. Click the + button and then select the Cloudant NoSQL DB service.
  3. Ensure that the Connect to field is set to Leave unbound and that you're using the Shared plan. (Because this service instance will initially run in an unbound state, you can develop the application on a separate host, with the database service instance hosted on Bluemix.) Click Create.
  4. Click LAUNCH to initialize the Cloudant database service instance.
  5. On the service information page, click the Service Credentials tab to view the username and password for the service instance: Screenshot of the contents of the Service Credentials tab, containing the service instance credentials in JSON format
    Screenshot of the contents of the Service Credentials tab, containing the service instance credentials in JSON format
  6. Copy and paste the relevant details from the JSON credentials block into your application's configuration file at $APP_ROOT/config.php. Then, on the same Cloudant page, select the Manage tab and click LAUNCH to launch the Cloudant dashboard.
  7. Click the Create Database button in the top menu bar, enter stray_assist as the name of the new database, and click Create.

Your Cloudant database instance is now active, and your application is configured to use it. In the next step, you use this database to store user reports.


Validate and save reports

With the persistent data store in place, your task now is to add a form processor that accepts and validates user submissions and saves them to the database. Add all of this step's code to $APP_ROOT/public/index.php. The code defines a callback to handle form submissions via POST.

The form code begins by collecting the various input parameters — color, sex, age, description, and contact information — and validating each by using Symfony's various validators:


// Silex application initialization  snipped

// initialize HTTP client
$guzzle = new GuzzleHttp\Client([
  'base_uri' => $app->config['settings']['db']['uri'] . '/',

// report submission handler
$app->post('/report', function (Request $request) use ($app, $guzzle) {
  // collect input parameters
  $params = array(
    'color' => strip_tags(trim(strtolower($request->get('color')))),
    'gender' => strip_tags(trim($request->get('gender'))),
    'age' => strip_tags(trim($request->get('age'))),
    'identifiers' => strip_tags(trim($request->get('identifiers'))),
    'description' => strip_tags(trim($request->get('description'))),
    'email' => strip_tags(trim($request->get('email'))),
    'name' => strip_tags(trim($request->get('name'))),
    'phone' => (int)strip_tags(trim($request->get('phone'))),
    'latitude' => (float)strip_tags(trim($request->get('latitude'))),
    'longitude' => (float)strip_tags(trim($request->get('longitude')))
  // define validation constraints
  $constraints = new Assert\Collection(array(
    'color' => new Assert\NotBlank(array('groups' => 'report')),
    'gender' => new Assert\Choice(array('choices' => array('male', 'female', 'unknown'
), 'groups' => 'report')),
    'age' => new Assert\Choice(array('choices' => array('pup', 'adult', 'unknown'
), 'groups' => 'report')),
    'description' => new Assert\NotBlank(array('groups' => 'report')),
    'email' =>  new Assert\Email(array('groups' => 'report')),
    'name' => new Assert\NotBlank(array('groups' => 'report')),
    'phone' => new Assert\Type(array('type' => 'numeric', 'groups' => 'report')),
    'latitude' => new Assert\Type(array('type' => 'float', 'groups' => 'report')),
    'longitude' => new Assert\Type(array('type' => 'float', 'groups' => 'report')),
    'identifiers' => new Assert\Type(array('type' => 'string', 'groups' => 'report'))
  // validate input and set errors if any as flash messages
  // if errors, redirect to input form
  $errors = $app['validator']->validate($params, $constraints, array('report'));
  if (count($errors) > 0) {
    foreach ($errors as $error) {
      $app['session']->getFlashBag()->add('error', 'Invalid input in field ' . $error->getPropertyPath());
    return $app->redirect($app["url_generator"]->generate('report'));

If the input is valid, the values are formatted into a JSON document suitable for insertion into Cloudant:

// if input passes validation
  // produce JSON document with input values
  $doc = [
    'type' => 'report',
    'latitude' => $params['latitude'],
    'longitude' => $params['longitude'],
    'color' => $params['color'],
    'gender' => $params['gender'],
    'age' => $params['age'],
    'identifiers' => $params['identifiers'],
    'description' => $params['description'],
    'name' => $params['name'],
    'phone' => $params['phone'],
    'email' => $params['email']
    'datetime' => time()

The Guzzle HTTP client is then used to formulate and send a POST request containing the JSON document to the Cloudant REST API; this creates and saves a new document in the Cloudant database:

// save document to database
  // retrieve unique document identifier
  $response = $guzzle->post($app->config['settings']['db']['name'], [ 'json' => $doc ]);
  $result = json_decode($response->getBody());
  $id = $result->id;

Finally, the client browser is redirected back to the application's index page with a success notification:

$app['session']->getFlashBag()->add('success', 'Report added.');
  return $app->redirect($app["url_generator"]->generate('index'));

// other callbacks  snipped


To see the form processor in action, try adding a report through the application. Then browse back to the Bluemix console, launch the Cloudant dashboard, and view All Documents in your database to see a new record containing the details for your newly added report:

Screenshot of the new record's details
Screenshot of the new record's details

Initialize an Object Storage instance

The next feature to add to the reporting system is the ability to attach photos. In this step, you enable photo attachments with the help of the Bluemix Object Storage service. Object Storage gives you easy storage of unstructured data in the cloud. The service supports the OpenStack Swift API and follows Swift's three-tier hierarchy for organizing data:

  • The primary unit in the hierarchy is an account. Accounts correspond to users; to access an account, a user must provide authentication credentials.
  • An account can host multiple containers, which are broadly equivalent to folders or subdirectories in a traditional file system.
  • Each container can store multiple objects, which can be files or data. Objects can have additional user-defined metadata. Usually, you can store an unlimited number of objects.

This description is only the tip of the iceberg. To get a thorough grounding in Swift (and by extension, Object Storage) concepts, take a look at the book OpenStack Swift by Joe Arnold (O'Reilly, ISBN 978-1-4919-0082-6). This book, along with hundreds of other programming titles, is available through Safari Books Online as part of the developerWorks Premium program.

To initialize a new Object Storage service instance on Bluemix:

  1. Log in to your Bluemix account. From the Console, select Storage from the list of services.
  2. Click the + button and then select the Object Storage service.
  3. Ensure that the Connect to field is set to Leave unbound, and choose either the Free plan or the Standard plan. (As with the Cloudant service instance in Step 4, running the service in an unbound state makes it possible to develop the application on a separate host with the object store hosted on Bluemix.) Click Create.
  4. After the service instance initializes, click the Service Credentials tab to view the URL, region, username, password, and other credentials for the service instance: Screenshot of the contents of the Service Credentials tab, containing the service instance credentials in JSON format
    Screenshot of the contents of the Service Credentials tab, containing the service instance credentials in JSON format

    Copy and paste the relevant details from the JSON credentials block into your application's configuration file at $APP_ROOT/config.php.

Support photo attachments in reports

You're now ready to add photo-upload support to the application:

  1. Edit the report form at $APP_ROOT/views/report.twig by making the following two additions. (Refer to the report.twig file in my project to see where in the file to add this new code.)
    • Update the form definition to support file uploads:
      <form method="post" enctype="multipart/form-data" action="{{ app.url_generator.generate('report') }}">
          <input type="hidden" name="MAX_FILE_SIZE" value="300000000" />
    • Add a new file upload field:
      <div class="form-group">
        <label for="upload">Photo</label>
        <span class="btn btn-default btn-file">
          <input type="file" name="upload" />
  2. To interact with the Object Storage API, the easiest solution is to use php-opencloud, a PHP SDK for OpenStack-based deployments. This SDK provides a convenient PHP wrapper around the Swift API methods, so you simply call the appropriate method — for example, createContainer() or listContainers()— and the client library takes care of formulating the request and decoding the response for you. To use php-opencloud, add the following code — which uses the Silex dependency-injection container to initialize a new OpenStack client — to $APP_ROOT/public/index.php:
    // Silex application initialization  snipped
    // initialize OpenStack client
    $openstack = new OpenStack\OpenStack(array(
      'authUrl' => $app->config['settings']['object-storage']['url'],
      'region'  => $app->config['settings']['object-storage']['region'],
      'user'    => array(
        'id'       => $app->config['settings']['object-storage']['user'],
        'password' => $app->config['settings']['object-storage']['pass']
    $objectstore = $openstack->objectStoreV1();
    // other callbacks  snipped
  3. Make the following three additions to index.php to update the callback for the /reportPOST handler. (Refer to the index.php file in my project to see where to make these additions.)
    • 'upload' => $request->files->get('upload')
    • 'upload' => new Assert\Image(array('groups' => 'report'))
      'file' => !is_null($params['upload']) ? trim($params['upload']->getClientOriginalName()) : '',
    • // if report includes photo
      // create container in object storage service
      // with name = document identifier
      // and upload photo to it
      if (!is_null($params['upload'])) {
        $container = $objectstore->createContainer(array(
          'name' => $id
        $stream = new Stream(fopen($params['upload']->getRealPath(), 'r'));
        $options = array(
          'name'   => trim($params['upload']->getClientOriginalName()),
          'stream' => $stream,

    With these updates, the script checks for a valid file upload. If the file is present, the script uses the OpenStack client to transfer the file to the Object Storage service instance.

    Now — once the report is saved in the Cloudant database as a new document — the OpenStack client's createContainer() method creates a new container matching the document identifier, and the uploaded file is saved within that container via the client's createObject() method. The image file name is stored within the Cloudant document as an additional property.
  4. To see photo attachment support in action, try adding a report through the application and attach a photo to it using the new file-upload field. Browse back to the Bluemix console and launch the Object Storage service instance's dashboard. You can see a new folder containing your uploaded photo: Screenshot of the Object Storage instance dashboard containing code for uploaded photo
    Screenshot of the Object Storage instance dashboard containing code for uploaded photo

Conclusion to Part 1

At this point, you have a working prototype of a system that makes it possible for users to report injured strays — complete with geotagging and photos.

In the second and concluding part of this tutorial, I walk you through building a search interface for the application and mapping reports to actual street addresses using Google Maps. These features are designed to make reports more accessible to rescue agencies and help them to quickly locate records by search criteria or keywords. Finally, I show you how to deploy your application to a secure, robust, and scalable environment in the Bluemix cloud.

Downloadable resources

Related topics


Sign in or register to add and subscribe to comments.

Zone=Mobile development, Web development, Cloud computing
ArticleTitle=Real-world applications using developerWorks Premium, Part 1: Build a mobile web app for assisting stray dogs, Part 1