Contents


Build a photo storage service in the cloud with PHP and IBM Bluemix, Part 1

Build a web app to upload, view, and delete photos

Comments

This tutorial was written using a previous version of the IBM Bluemix® interface. Given the rapid evolution of technology, some steps and illustrations may have changed.

More and more applications are taking advantage of cheap, reliable object storage in the cloud. With the IBM® Bluemix™ Object Storage service, you too can add cloud-based storage to your application. This tutorial series shows you how to build a photo storage service in the cloud with IBM Bluemix and PHP. It uses the Silex PHP micro-framework to build the application, together with the HybridAuth authentication library for multi-user support and the Bootstrap templating kit for a responsive, mobile-friendly user interface.

Part 1 shows you how to create the application stub, enable the user interface, process photo uploads, and start working with photos. Part 2 shows you how to deploy the app to IBM Bluemix and how to add support for multiple containers and multiple users.

What you will need

The example application described in this tutorial lets users upload photos through their desktop or mobile browser. These photos are then stored in the Bluemix cloud and become accessible via a unique URI. Once stored, users can also delete or view their stored photos.

Since the application runs in the cloud, it should ideally support multiple users. The Bluemix Object Storage service makes this easy because it has built-in support for provisioning independent object stores and it creates an individual sub-account for each object store. User authentication is performed against an external service (in this case, Google+) using OAuth 2.0 and the Google+ API.

On the client side, I use Bootstrap to create a user interface that works on both mobile devices and desktop browsers. On the server, I use the Silex PHP micro-framework to manage the application flow, the Twig template engine to render page templates, the IBM jStart PHP client library for the Bluemix Object Storage service, and the HybridAuth open source authentication library for interaction with the Google+ API.

There are a lot of technologies in use here, so here's what you'll need:

Run the appGet the code

Overview of the Object Storage service and API

The IBM Bluemix Object Storage service is based on OpenStack Swift and follows Swift's three-tier hierarchy for organizing data: accounts, containers, and objects. Here's how it works:

  • The primary unit in the hierarchy is an account. Broadly, 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 sub-directories in a traditional file system.
  • Each container can host multiple objects. Objects can include files or data, together with associated metadata, and each object has a unique URL. You can store an unlimited number of objects.

Apart from the standard hierarchy above, Bluemix Object Storage offers one additional feature: the ability to create child accounts or sub-accounts under each parent account. This makes it possible to support multiple users under a single parent account without overlap between their containers and objects.

When you instantiate a new Bluemix Object Storage service instance, Bluemix automatically creates an account and makes its credentials available through the special Bluemix VCAP_SERVICES environment variable. Here's an example of what these credentials look like:

An example of Bluemix credentials
An example of Bluemix credentials

You can use these credentials to access the Object Storage API and save or retrieve objects from it. Typically, you begin by sending a GET request to the authentication URL for the service, appending the sub-account username to the URL and including an Authorization: header containing the Base64-encoded parent account username and password. If authentication is successful, the server returns a 200 OK response code and two headers that can be used for subsequent requests.

For example, if the authentication URL for the service is https://swift.ng.bluemix.net/auth/xyz, send a GET request with the appropriate Authorization: header as described above to https://swift.ng.bluemix.net/auth/xyz/me, where me is the sub-account you wish to access or create. If authenticated, the server returns a response containing X-Auth-Token: and X-Storage-Url: headers.

Here's a request and response example:

An example request and response
An example request and response

The X-Auth-Token: header contains an authentication token that must accompany all subsequent requests to the Object Storage service, while the X-Storage-Url: header specifies the storage URL for that sub-account, for all container and object manipulation actions. You can now use these two pieces of information to transact with the Object Storage service using the OpenStack Swift API.

For example, to add a container to the sub-user account, send a PUT request to the X-Storage-Url endpoint, adding the new container name to the end of the URL and remembering to include the X-Auth-Token: header in the request. So, if the X-Storage-URL for the sub-account is https://xyz.objectstorage.softlayer,net/AUTH_123, send a PUT request to https://xyz.objectstorage.softlayer,net/AUTH_123/container_1 to create a new container named container_1.

Here's a request and response example:

An example request and response
An example request and response

Similarly, to list all containers in the sub-user account, send a GET request to the corresponding X-Storage-Url endpoint without any parameters as shown in this example using https://xyz.objectstorage.softlayer,net/AUTH_123:

A request and response example
A request and response example

For the application you'll be building in this tutorial, you won't need to work with raw requests and responses as described above. The IBM jStart PHP client library for the Bluemix Object Storage service takes care of all those tasks in a convenient wrapper, so you'll simply need to call the appropriate method — for example, createContainer() or listContainers()— and the client library will take care of formulating the request and decoding the response for you. You still need to know what's going on in the background, though, both to debug errors and in case you want to perform an operation that's not currently supported in the client.

Step 1: Create the application stub

The first step is to create a bare application stub containing the Silex PHP micro-framework and other key components. These can be easily downloaded and installed using Composer, the PHP dependency manager.

Change to the web server's document root directory (typically /usr/local/apache/htdocs on Linux or C:\Program Files\Apache\htdocs on Windows) and create a new directory for the application:

shell> cd /usr/local/apache/htdocs
shell> mkdir photos

This directory will be referenced throughout this tutorial as $APP_ROOT. With this arrangement, your application should be directly accessible at http://localhost/photos. Remember that once you deploy the application to Bluemix, its URL will change.

Next, create the Composer configuration file, which should be saved to $APP_ROOT/composer.json:

{
    "require": {
        "silex/silex": "*",
        "twig/twig": "*",        
        "ibmjstart/zendservice-openstack": "dev-master"
    },
    "minimum-stability" : "dev",
    "prefer-stable": true
}

Install Silex, Twig, and the Object Storage PHP client library using Composer:

shell> cd /usr/local/apache/htdocs/photos
shell> php composer.phar install

Step 2: Enable the user interface

The next step is to build a simple user interface that lists available photos and includes controls to upload new images or delete existing ones. Here's the basic structure of this user interface, which should be saved as $APP_ROOT/views/main.tpl.php:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <!-- 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="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>

    <div class="panel panel-default">
      <div class="panel-heading clearfix">
        <h4 class="pull-left">Photos</h4>
      </div>
    </div>
   <div class="container">
      <a href="{{ app.request.basepath }}/add" role="button" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-plus"></span> Add</a>
    </div>
  
    
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>   
  </body>
</html>

This stub page sets up Bootstrap and jQuery from their content delivery networks (CDNs) and defines a generic Bootstrap page template. Currently, the only content is a header and a single button to add new photos; however, as the application evolves, you will see this expand to include a photo list and additional control buttons.

On the server, Silex will take care of rendering this template when it encounters requests for URL routes like / or /index. Here's the code, which should be saved to $APP_ROOT/index.php:

<?php
// use Composer autoloader
require 'vendor/autoload.php';

// load classes
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Silex\Application;
use ZendService\OpenStack\ObjectStorage;

// initialize Silex application
$app = new Application();

// register Twig template provider
$app->register(new Silex\Provider\TwigServiceProvider(), array(
  'twig.path' => __DIR__.'/views',
));

// register URL generator
$app->register(new Silex\Provider\UrlGeneratorServiceProvider());

// 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');
})->bind('index');

$app->run();

The script above is the main control script for the application. It begins by loading all the required classes and initializing a new Silex application object. As part of the initialization process, the Twig template engine, template directory location, and URL generator service are also registered with the application.

Once the application object is configured, the script sets up a router callback for the /index route and defines a function to be invoked when this route is matched to an incoming request. In the script above, the /index callback simply renders the main application page template shown above. The script also sets up a router callback for the / route, which simply redirects to the /index route. Notice the usage of the Silex URL generator, which automatically generates the correct URLs for named routes after taking into account the current application path on the server.

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 and fill it with the following content:

<IfModule mod_rewrite.c>
    Options -MultiViews
    
    RewriteEngine On
    #RewriteBase /path/to/app
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [QSA,L]
</IfModule>

And now, when you access your application at http://localhost/photos/index, you should see the Bootstrap page template you set up earlier:

Bootstrap page template

Step 3: Process photo uploads

The page template you set up earlier already includes an Add button, hyperlinked to the /add URL route. The next logical step is to flesh out that functionality by adding a form and a form processor for new photos. To do this, create the following simple web form and save it as $APP_ROOT/views/add.twig:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
  
    <!-- 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="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>
    <div class="panel panel-default">
      <div class="panel-heading clearfix">
        <h4 class="pull-left">Add Photo</h4>
      </div>
    </div>
    <form enctype="multipart/form-data" action="{{ app.request.basepath }}/add" method="post">
      <div class="form-group">
        <div class="input-group">
          <span class="input-group-addon">Select photo</span>
          <input type="file" name="file" class="form-control" style="height: auto" />
        </div>
      </div>      
      <div class="form-group">
        <button type="submit" name="submit" class="btn btn-default">Upload</button>
      </div>   
    </form>
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>   
  </body>
</html>

This form contains a file selector:

form for file selection
form for file selection

When submitted, the selected file needs to be checked to make sure that it's an image and then added to the object store, as this update to $APP_ROOT/index.php demonstrates:

<?php
// use Composer autoloader – snipped!
// load classes – snipped!

// define configuration array
// ... for object storage service
$config["storage"] = array(
  'service' => array(
  ),
  'adapter' => array(
    'adapter' => 'Zend\Http\Client\Adapter\Curl',
    'curloptions' => array(CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 6000),  
  )
);

// use BlueMix VCAP_SERVICES environment
// create 'me' subaccount in storage service
if ($services = getenv("VCAP_SERVICES")) {
  $services_json = json_decode($services, true);
  $config["storage"]["service"]["url"] = $services_json["objectstorage"][0]["credentials"]["auth_uri"] . '/me';
  $config["storage"]["service"]["user"] = $services_json["objectstorage"][0]["credentials"]["username"];
  $config["storage"]["service"]["key"] = $services_json["objectstorage"][0]["credentials"]["password"];
} else {
  throw new Exception('Not in Bluemix environment');
}

// initialize Silex application
$app = new Application();

// register Twig template provider
$app->register(new Silex\Provider\TwigServiceProvider(), array(
  'twig.path' => __DIR__.'/views',
));

// register URL generator
$app->register(new Silex\Provider\UrlGeneratorServiceProvider());

// initialize ObjectStorage client
// attach to Silex application
$app['os'] = new ObjectStorage(
  $config["storage"]["service"],
  new Zend\Http\Client('', $config["storage"]["adapter"])
);


// index page handlers – snipped!

// file upload form
$app->get('/add', function () use ($app) {
  return $app['twig']->render('add.twig');
});

// file upload processor
// get and check uploaded file
// if valid, create container and add file
$app->post('/add', function (Request $request) use ($app) {
  $file = $request->files->get('file');
  if ($file && $file->isValid()) {
    if (in_array($file->getClientMimeType(), array('image/gif', 'image/jpeg', 'image/png'))) {
      $app['os']->createContainer('default');
      $app['os']->setObject('default', $file->getClientOriginalName(), file_get_contents($file->getRealPath()));
    } else {
      throw new Exception('Invalid image format');
    }
  } else {
    throw new Exception('Invalid upload');
  }
  return $app->redirect($app["url_generator"]->generate('index'));    
});

$app->error(function (\Exception $e, $code) use ($app) {
  return $app['twig']->render('error.twig', array('error' => $e->getMessage()));
});

$app->run();

There's a lot going on here, so let's look at it in detail:

  • The script begins by initializing a new ObjectStorage client object and configuring it to use the connected Object Storage service on Bluemix. The URL to the associated Object Service service is obtained from the special Bluemix VCAP_SERVICES environment variable. Since we haven't yet added multi-user support to the application, a temporary sub-account named "me" will be used for storage; this sub-account name is appended to the end of the service URL.
  • The script adds a GET route handler for the /add endpoint. As you can imagine, this is fairly trivial, simply rendering the form template shown previously.
  • The script also adds a POST route handler for the same endpoint, which is responsible for processing the file that’s uploaded through the form. This handler retrieves the uploaded file from the Request object as a File object, and then uses the File object's getClientMimeType() method to verify that the file is an image file.
  • Assuming the upload is a valid image, the handler then uses the configured ObjectStorage client to create a container named "default" using the client's createContainer() method. (Remember that every object must be associated with a container.) The final step is to use the client object's setObject() method to save the uploaded file to the container, using the original file name and binary content.

There are a few important simplifications in this script – namely, the pre-defined sub-account name and container name. In subsequent steps, you'll modify the code to remove these constraints, but this example provides a good working model to build upon.

If the file upload is unsuccessful, or if the uploaded file is not detected as a valid image, the POST handler throws an exception. This exception is handled by the application's error() method, which simply renders an error template containing the message and a Try Again button. The error template is omitted here for brevity, but you can find it in the source code repository for the application. Here's what it looks like in action:

error template
error template

Step 4: List and display photos

The main page template doesn't (yet) include any code to list uploaded objects, so you'll still see an empty page after uploading a new photo. Let's fix that next by updating the /index handler in $APP_ROOT/index.php to list the photos in the "default" container:

<?php
// application initialization and configuration - snipped!

// index page handler
$app->get('/index', function () use ($app) {
  $containers = array( array('name' => 'default') );
  foreach ($containers as &$c) {
    $objects = json_decode($app['os']->listObjects($c['name']));
    foreach ($objects as &$o) {
      $o = (array) $o;
      $o['url'] = $app['os']->getObjectUrl($c['name'], $o['name']);
    }
    $c['objects'] = $objects;
  }
  return $app['twig']->render('index.twig', array(
    'containers' => $containers
    ));
})->bind('index');

// other handlers – snipped!

$app->run();

Here, the client object's listObjects() method returns all the stored objects in the "default" container with their unique URLs. This information is then passed to the index page template.

The index page template also needs a refresh now to display the list of photos. Here's the updated template, which should be saved to $APP_ROOT/views/index.twig:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <!-- 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="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>

    <div class="panel panel-default">
      <div class="panel-heading clearfix">
        <h4 class="pull-left">Photos</h4>
      </div>
    </div>
    
    {% if containers|length > 0 %}
    {% for c in containers %}
    <div class="container">
      <p class="clearfix">
          <span class="btn btn-success"> {{ c.name }} <span class="badge">{{ c.objects|length }}</span></span>
      </p>
      <div class="clearfix">
          <a href="{{ app.request.basepath }}/add" role="button" class="btn btn-primary btn-sm"><span class="glyphicon glyphicon-plus"></span> Add Photo</a>
      </div>
      <hr/>
      {% for o in c.objects %}        
      <ul class="list-group row clearfix">
        <li class="list-group-item col-xs-3 clearfix" style="border:none"><img src="{{ o.url }}" class="img-responsive" /></li>
        <li class="list-group-item col-xs-5 clearfix" style="border:none">
          <p> {{ o.name }} </p>
          <h6> {{ o.bytes }} bytes </h6>
        </li>
        <li class="list-group-item col-xs-4 clearfix" style="border:none">
          <p>
            <a href="{{ o.url }}" role="button" class="btn btn-primary btn-sm">View</a> <br/>
          </p>
          <p>
            <a href="{{ app.request.basepath }}/delete/{{ c.name|url_encode }}/{{ o.name }}" role="button" class="btn btn-primary btn-sm">Delete</a>
          </p>          
        </li>
      </ul>
      <hr/>
      {% endfor %}
    </div>
    {% endfor %}
    {% else %}
    <div class="container">
      <h4>No photos found.</h4>
      <a href="{{ app.request.basepath }}/add" role="button" class="btn btn-default btn-sm"><span class="glyphicon glyphicon-plus"></span> Add</a>
    </div>
    {% endif %}
  
    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>   
  </body>
</html>

Here, a foreach() loop iterates over the array of information retrieved from the Object Storage service and prints each photo's name and size. Additionally, two buttons are attached to each record for display and deletion, hyperlinked to the unique object URL and /delete URL endpoints, respectively. Notice that the /delete hyperlink includes both the container name and object name as route parameters — for example, /delete/default/file.png.

Step 5: Delete existing photos

As described previously, each photo has a Delete button next to it, hyperlinked to the /delete URL endpoint, and contains the container name and object name. It's easy enough to add a handler for this route that reads these parameters and calls the ObjectStorage client object's deleteObject() method to delete the corresponding object from the object store. Here's the code to be added to $APP_ROOT/index.php:

<?php
// application initialization and configuration - snipped!

// delete handler
$app->get('/delete/{container}/{object}', function ($container, $object) use ($app) {
  $container = urldecode($container);
  $object = urldecode($object);
  $app['os']->deleteObject($container, $object);  
  return $app->redirect($app["url_generator"]->generate('index'));
});

// other handlers – snipped!

$app->run();

At this point, you have an application that supports adding, listing, and deleting photos using Bluemix Object Storage. Of course, you haven't been able to take it for a spin yet, because it needs Bluemix and a connected Object Storage service instance to function correctly. I'll show you how to do that in Part 2, where you'll also learn how to add support for multiple containers and multiple users.


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=Cloud computing
ArticleID=1008886
ArticleTitle=Build a photo storage service in the cloud with PHP and IBM Bluemix, Part 1
publish-date=06232015