Contents


Create and deliver invoices online with IBM Bluemix and PHP

Build and deploy a tool that takes the tedium out of invoicing

Comments

Like many professionals, I send invoices to my customers both during and at the conclusion of my assignments. And like many professionals, I find this task, however necessary, to be tedious. For a long time, my primary tools for creating these invoices were a word processor and a pre-built invoice template, which I would fill up with key information before printing or emailing. For bookkeeping and backup purposes, I'd also keep a digital copy of each invoice. In practice, I ended up with a directory full of invoice files, which I had to manually review in order to get an overview of all my customers or completed projects in any given year.

Eventually, I realized that there had to be a better way. I started thinking about building an online tool that would generate invoices from form input, save them to a database, and deliver them to customers via email.

Doing this would require some effort. However, if it worked out, I knew that it would make it easier to create invoices, and would be a more efficient way to retrieve an overview of customers, projects, or revenue for any given time period.

If this sounds like a useful tool to you, then read on! In the steps that follow, I'll walk you through the process of building this tool and deploying it on IBM Bluemix®. Along the way, I'll introduce you to two key Bluemix services: the ClearDB MySQL Database service and the Object Storage service.

If you'd like to experiment with this application, start with the live demo. Just remember that the demo is public, so be sure not to upload confidential or sensitive information.

Run the live demoGet the code on GitHub

What you will need

The application described here allows a user to enter invoicing information into a web form and turn this information into a PDF invoice. The invoice is saved to an online storage area, and a web-based dashboard allows the user to view a list of previously generated invoices and download or email them to customers. The entire application is mobile optimized, enabling users to send or view invoices even when on the move (perfect for users who don't have a fixed office or place of work).

Behind the scenes, the application works by orchestrating various services, some available directly through Bluemix and others available as third-party services. Here's a quick list:

This application also uses Bootstrap to create a mobile-optimized interface, and the Silex PHP micro-framework with the mPDF library for PDF invoice generation.

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

Note: Any application that uses the SendGrid service must comply with the SendGrid Terms of Service. Similarly, any application that uses the ClearDB and Object Storage services must comply with each service's terms of use, as described in the ClearDB Terms of Service and on the Object Storage catalog page, respectively. Before beginning your project, spend a few minutes reading these requirements and ensuring that your application complies with them.

Step 1. Create the bare application

  1. The first step is to initialize a basic application with the Silex PHP micro-framework and Twig templating engine. Additional packages are needed for invoice generation, email delivery, and object storage access. All these dependencies can be easily downloaded and installed using Composer. Use the following Composer configuration file, which should be saved to $APP_ROOT/composer.json (where $APP_ROOT is your project directory).

    {
        "require": {
            "silex/silex": "*",
            "twig/twig": "*",
            "symfony/validator": "*",        
            "mpdf/mpdf": "*",
            "php-opencloud/openstack": "*",
            "sendgrid/sendgrid": "*"
        },
        "minimum-stability": "dev",
        "prefer-stable": true
    }
  2. Install using Composer with this command:

    shell> php composer.phar install
  3. Next, set up the main control script for the application. This script loads the Silex framework and initializes the Silex application. It also contains callbacks for each of the application's routes, with each callback defining the code to be executed when the route is matched to an incoming request. Create this script at $APP_ROOT/public/index.php with the following content:

    <?php
    // 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 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());
    
    // index page handlers
    $app->get('/', function () use ($app) {
      return $app->redirect($app["url_generator"]->generate('index'));
    });
    
    // invoice list display handler
    $app->get('/index', function () use ($app, $db) {
      return $app['twig']->render('index.twig', array('data' => $data));
    })->bind('index');
    
    // invoice form display handler
    $app->get('/create', function () use ($app) {
      // TODO
    })->bind('create');
    
    // invoice generator
    $app->post('/create', function (Request $request) 
      use ($app, $db, $mpdf, $objectstore) {
      // TODO
    });
    
    // invoice deletion request handler
    $app->get('/delete/{id}', function ($id) use ($app, $db, $objectstore) {
      // TODO
    })->bind('delete');
    
    // invoice download request handler
    $app->get('/download/{id}', function ($id) use ($app, $objectstore) {
      // TODO
    })->bind('download');
    
    // invoice delivery request handler
    $app->get('/send/{id}', function ($id) use ($app, $objectstore, $db, $sg) {
      // TODO
    })->bind('send');
    
    // run application
    $app->run();
  4. Because the application supports listing, adding, deleting, downloading, and emailing invoices, the script defines URL routes and placeholders for the /index, /create, /delete, /download, and /send routes. These will be filled in as we progress through the tutorial. The script also reads in configuration data from the application configuration file, initializes the Twig template renderer, and registers it with Silex. The final bit of preparation is to create a simple Bootstrap-based user interface with header, footer, and content areas. Here's an example, which will be used for all the application views shown in subsequent code listings:

    <!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">
        <title>Invoice Generator</title>
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
        <script 
          src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
        </script>
        <!-- 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="container">
          <div class="panel panel-default">
            <div class="panel-heading clearfix">
              <h4 class="pull-left">Invoice Generator</h4>
              <a href="{{ app.url_generator.generate('index') }}" 
                class="pull-right btn btn-primary btn">Home</a>
            </div>
          </div> 
    
          {% for message in app.session.flashbag.get('success') %}
          <div class="alert alert-success">
            <strong>Success!</strong> {{ message }}
          </div>
          {% endfor %} 
          
          {% for message in app.session.flashbag.get('error') %}
          <div class="alert alert-danger">
            <strong>Error!</strong> {{ message }}
          </div>
          {% endfor %} 
    
          <!-- --> 
          <!-- individual page content here -->
          <!-- --> 
    
        
      </body>
    </html>

Step 2. Create an invoice generation form

  1. With the preliminaries out of the way, we can now move on to the application development proper. The first step is to define a form that will hold customer and invoicing information. Define this form as $APP_DIR/views/create.twig using the content below:

    <form method="post" action="{{ app.url_generator.generate('create') }}">
      <div class="panel panel-default">
        <div class="panel-heading clearfix">
          <h4 class="pull-left">Customer Information</h4>
        </div>
        <div class="panel-body">
          <div class="form-group">
            <label for="color">Name</label>
            <input type="text" class="form-control" id="name" 
              name="name" required="true"></input>
          </div>
          <div class="form-group">
            <label for="address1">Address (line 1)</label>
            <input type="text" class="form-control" id="address1" 
              name="address1" required="true"></input>
          </div>
          <div class="form-group">
            <label for="address2">Address (line 2)</label>
            <input type="text" class="form-control" id="address2" 
              name="address2"></input>
          </div>
          <div class="form-group">
            <label for="city">City</label>
            <input type="text" class="form-control" id="city" 
              name="city" required="true"></input>
          </div>
          <div class="form-group">
            <label for="state">State</label>
            <input type="text" class="form-control" id="state" 
              name="state" required="true"></input>
          </div>
          <div class="form-group">
            <label for="postcode">Postal code</label>
            <input type="text" class="form-control" id="postcode" 
              name="postcode" required="true"></input>
          </div>
          <div class="form-group">
            <label for="email">Email address</label>
            <input type="email" class="form-control" id="email" 
              name="email" required="true"></input>
          </div>
        </div>
      </div>
      
      <div class="panel panel-default">
        <div class="panel-heading clearfix">
          <h4 class="pull-left">Invoice Information</h4>
        </div>
        <div class="panel-body" id="lines">
          <div id="line-template" class="line">
            <div class="form-group">
              <label>Item description</label>
              <input type="text" class="form-control" 
                name="lines[0][item]"></input>
            </div>
            <div class="form-group">
              <label>Quantity</label>
              <input type="number" min="0" step="any" class="form-control" 
                name="lines[0][qty]"></input>
            </div>
            <div class="form-group">
              <label>Rate</label>
              <input type="number" min="0" step="any" class="form-control" 
                name="lines[0][rate]"></input>
            </div>
          </div>
        </div>
      </div>
      
      <div class="form-group">
        <a id="add" href="#" class="btn btn-primary">
          <span class="glyphicon glyphicon-plus"></span>
          Add Invoice Line</a>
        <button type="submit" name="submit" class="btn btn-primary">
          Submit</button>
      </div>             
    
    </form>
  2. This form broadly consists of two sections: one for the customer's name, address, and email address and the other for the invoice details. The latter section consists of three fields, each representing one line of the invoice: an item description field, a rate field, and a quantity field. Because it's quite common for an invoice to have multiple lines, this section can be cloned to add new lines to the invoice via the Add Invoice Line button, which, when clicked, uses the JavaScript below to dynamically add a new invoice line section to the form.

    <script>
    $(document).ready(function() {  
      var cloneIndex = 0;
      
      $("#add").click(function(e) {  
          e.preventDefault();
          $("#line-template").clone()
              .appendTo("#lines")
              .prepend('<hr />')
              .attr('id', null)
              .find("input").each(function() {
                  var name = this.name;
                  this.name = name.replace("lines[0]", "lines[" + (cloneIndex+1) + "]");
                  this.value = null;
          });
          cloneIndex++;    
      });
    });
    </script>
  3. Notice also that the invoice line data is structured as a multi-level array: lines[0] represents the first line, lines[1] the second line, and so on. Within the first line, lines[0][item] holds the item description; lines[0][rate], the corresponding rate; and lines[0][qty], the quantity.

    Here's a screenshot of what the form looks like:

    Figure 1. Invoice generation form
    Invoice generation form
    Invoice generation form

    On submission, the data entered into the form is submitted as a POST request to the /create callback handler. This callback handler does most of the heavy lifting in the application. More specifically, it will:

    • Validate the data submitted in the form
    • Calculate sub-totals for each invoice line
    • Calculate a final total for the invoices
    • Produce an invoice in PDF format from a defined template
    • Save the invoice to the storage area
    • Add a record to the database with key invoice details

From the above, it should be clear that before the callback handler is implemented, the database and the storage area must be initialized, and a template for use during invoice generation must be defined. The following sections discuss those tasks.

Step 3. Initialize Bluemix database and storage services

Bluemix offers a number of database-as-a-service options. One such option is the ClearDB MySQL Database. As you might guess, this service provisions an empty MySQL database instance that can be bound to your application. The default plan offers only a limited quota of free storage.

  1. To see how this works, initialize a new ClearDB MySQL Database service instance on Bluemix by first logging in to your Bluemix account. Then, from the dashboard, click the Console button. From the resulting list of services, select Data and Analytics. Click the Add button and in the list of services displayed, select the ClearDB MySQL Database service and the free plan. Ensure that the Connect to field is set to Leave unbound, so that you can continue developing the application on a separate host and have only the database service instance hosted on Bluemix.

    Figure 2. Service creation
    Service creation
    Service creation
  2. The ClearDB database service instance is now initialized. From the service details page, open the ClearDB dashboard, click on the Endpoint Information tab to view the credentials for the instance, and add the values of the Cluster name, Hostname, Username, and Password to the application's $APP_DIR/config.php file.

    Figure 3. Service credentials
    Service credentials
    Service credentials
  3. Use those credentials to connect to the MySQL database (using a tool such as the MySQL CLI or phpMyAdmin) and create a new table to hold invoice information with the following SQL code:

    CREATE TABLE invoices ( 
      id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, 
      ts TIMESTAMP NOT NULL, 
      name TEXT NOT NULL, 
      email VARCHAR(255) NOT NULL, 
      amount FLOAT NOT NULL 
    );
  4. Similarly, initialize a new Object Storage instance from the Storage section of the Bluemix dashboard. As before, ensure that the Connect to field is set to Leave unbound.

    Figure 4. Service creation
    Service creation
    Service creation
  5. From the service details page, visit the Service Credentials tab to view the credentials for the instance, and add the values there to the application's $APP_DIR/config.php file.

    Figure 5. Service credentials
    Service credentials
    Service credentials
  6. Also, from the Manage page, create a new storage container named "invoices" to hold generated invoices.

    Figure 6. Storage container creation
    Storage container creation
    Storage container creation

Step 4. Generate PDF invoices

  1. Once the database and object storage system are initialized, the next step is to create a template for the output invoices to be generated. Because the mPDF library includes powerful functionality to convert from HTML to PDF, the easiest approach is to first create the template using HTML, and then populate it with actual data and convert it to PDF.

    Here's an example invoice template, which should be saved to $APP_DIR/views/invoice.twig:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <link rel="stylesheet" 
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
        <style>
        .panel-heading {
          background-color: #d3d3d3;
        }
        .col-md-4 {
          width: 25%;
        }
        </style>    
      </head>
      <body>
    
        <div class="page-header">
          <h1 class="text-center">SAMPLE INVOICE</h1>
        </div>
    
        <div class="container">
          <div class="col-md-4">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">CUSTOMER</h3>
              </div>
              <div class="panel-body">
                {{ data.name }} <br />
                {{ data.address1 }} <br />
                {% if data.address2 %}
                  {{ data.address2 }} <br />
                {% endif %}
                {{ data.city }} <br />
                {{ data.state }} <br />
                {{ data.postcode }} <br />
              </div>
            </div>  
          </div>
        </div>
        
        <div class="container">
          <div class="col-md-12">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">SUMMARY</h3>
              </div>
              <div class="panel-body">
                <table class="table">
                    <tr>
                      <th>Description</th>
                      <th>Quantity</th>
                      <th>Rate</th>
                      <th>Amount</th>                  
                    </tr>
                    {% for line in data.lines %}
                    <tr>
                      <td> {{ line['item'] }}</td>
                      <td> {{ line['qty'] }}</td>
                      <td> {{ line['rate'] }}</td>
                      <td> {{ line['subtotal'] }}</td>
                    </tr>
                    {% endfor %}
                    <tr>
                      <td></td>
                      <td></td>
                      <td></td>
                      <td style="border-top: solid 1px black">
                        <strong>{{ total }}</strong>
                      </td>
                    </tr>
                </table>
              </div>
            </div>  
          </div>
        </div>    
        
        <div class="container">
          <div class="col-md-6">
            <div class="panel panel-default">
              <div class="panel-heading">
                <h3 class="panel-title">PAYMENT TERMS</h3>
              </div>
              <div class="panel-body">
                <ul>
                  <li>Payment within 15 days of invoice date</li>
                  <li>Late payment penalty: 1% per month</li>
                  <li>Sample invoice, not for production use or billing</li>
                </ul>
              </div>
            </div>  
          </div>
        </div>
      
      </body>
    </html>

    No surprises here: The template uses Bootstrap to create a customer information section, followed by a multi-row table for the invoice details, followed by a payment terms section. Obviously, this can be easily customized to include (for example) a company logo in the header or a purchase order number.

  2. The final step is to update the /create callback handler to validate the submitted data, calculate the line item sub-totals and invoice total, and build an actual invoice using the template above. Here's the code:

    <?php
    // load classes and configuration
    require '../vendor/autoload.php';
    require '../config.php';
    
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Validator\Constraints as Assert;
    use Silex\Application;
    
    // initialize Silex application
    $app = new Application();
    
    // register service providers here
    
    // initialize PDF engine
    $mpdf = new mPDF();
    
    // initialize database connection
    $db = new mysqli(
      $app->config['settings']['db']['hostname'], 
      $app->config['settings']['db']['username'], 
      $app->config['settings']['db']['password'], 
      $app->config['settings']['db']['name']
    );
    
    if ($db->connect_errno) {
      throw new Exception('Failed to connect to MySQL: ' . $db->connect_error);
    }
    
    // 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();
    
    // invoice form display handler
    $app->get('/create', function () use ($app) {
      return $app['twig']->render('create.twig');
    })->bind('create');
    
    // invoice generator
    $app->post('/create', function (Request $request) 
      use ($app, $db, $mpdf, $objectstore) {
      // collect input parameters
      $params = array(
        'name' => strip_tags(trim(strtolower($request->get('name')))),
        'address1' => strip_tags(trim($request->get('address1'))),
        'address2' => strip_tags(trim($request->get('address2'))),
        'city' => strip_tags(trim($request->get('city'))),
        'state' => strip_tags(trim($request->get('state'))),
        'postcode' => strip_tags(trim($request->get('postcode'))),
        'email' => strip_tags(trim($request->get('email'))),
        'lines' => $request->get('lines'),
      );
      
      // define validation constraints
      $constraints = new Assert\Collection(array(
        'name' => new Assert\NotBlank(array('groups' => 'invoice')),
        'address1' => new Assert\NotBlank(array('groups' => 'invoice')),
        'address2' => new Assert\Type(
          array('type' => 'string', 'groups' => 'invoice')
        ),
        'city' => new Assert\NotBlank(array('groups' => 'invoice')),
        'state' => new Assert\NotBlank(array('groups' => 'invoice')),
        'postcode' => new Assert\NotBlank(array('groups' => 'invoice')),
        'email' =>  new Assert\Email(array('groups' => 'invoice')),
        'lines' =>  new App\Validator\Constraints\Lines(
          array('groups' => 'invoice')
        ),
      ));
      
      // validate input and set errors if any as flash messages
      // if errors, redirect to input form
      $errors = $app['validator']->validate(
        $params, $constraints, array('invoice')
      );
      if (count($errors) > 0) {
        foreach ($errors as $error) {
          $app['session']->getFlashBag()->add('error', 
            'Invalid input in field ' . $error->getPropertyPath() . ': ' . 
            $error->getMessage()
          );
        }
        return $app->redirect($app["url_generator"]->generate('create'));
      }  
      
      // if input passes validation
      // calculate subtotals and total
      $total = 0;
      foreach ($params['lines'] as $lineNum => &$lineData) {
        $lineData['subtotal'] = $lineData['qty'] * $lineData['rate'];
        $total += $lineData['subtotal'];
      }
      
      // save record to MySQL
      // get record id
      if (!$db->query("INSERT INTO invoices (name, email, amount, ts) 
        VALUES ('" . $params['name'] . "', '" . $params['email'] . "', '" . $total . "', NOW())")) {
        $app['session']->getFlashBag()->add(
          'Failed to save invoice to database: ' . $db->error
        );
        return $app->redirect($app["url_generator"]->generate('index'));
      }
      $id = $db->insert_id;
      
      // generate PDF invoice from template
      $html = $app['twig']->render('invoice.twig', 
        array('data' => $params, 'total' => $total)
      );
      $mpdf->WriteHTML($html);
      $pdf = $mpdf->Output('', 'S'); 
    
      // save PDF to container with id as name
      $container = $objectstore->getContainer('invoices');
      $options = array(
        'name'   => "$id.pdf",
        'content' => $pdf,
      );
      $container->createObject($options);
      
      // display success message
      $app['session']->getFlashBag()->add('success', 
        "Invoice #$id created.");
      return $app->redirect($app["url_generator"]->generate('index'));
    });
    
    // register other handlers here
    
    // run application
    $app->run();

    Several things are happening here, so let's step through this code section by section:

    1. Prior to initializing the callback handlers, the code initializes an mPDF object for PDF generation, a MySQL client object for database operations (via PHP's mysqli extension), and an OpenStack client object for interaction with the object storage API. These three objects are then passed to the /create callback handler.
    2. The handler begins by collecting the various input parameters—customer information, line item descriptions, quantities, and rates—and validating each using Symfony. In particular, invoice lines are validated using a custom Lines validator (available in the application source code repository), which checks each invoice line to make sure it's complete and that the quantity and rate are specified as numeric values.
    3. If the input is valid, a sub-total is calculated for each line item. The various sub-totals are also added together to obtain an invoice total.
    4. The MySQL client object is used to generate and insert a new record into the application database, representing the new invoice. The database record includes the customer name and email address, invoice total, and creation date. Each such record will have a unique, auto-generated ID.
    5. The various input variables are interpolated into the invoice template in order to produce an HTML invoice via the Twig template engine's render() method. The HTML invoice is then converted into a PDF invoice using the mPDF object's WriteHTML() and Output() methods, which eventually return a string of bytes representing the PDF file.
    6. The OpenStack client object's getContainer() method is used to retrieve a reference to the "invoices" container created previously, and its createObject() method is used to save the PDF invoice to the container. Note that the file name of the PDF invoice is set to match the auto-generated ID for the corresponding record in the MySQL database.

Assuming all the steps above are performed successfully, the client browser is redirected back to the application's index page with a success notification.

Here's what a sample PDF invoice looks like:

Figure 7. Sample PDF invoice
Sample PDF invoice
Sample PDF invoice

Step 5. Enable invoice download and delivery

With the hard work of invoice generation out of the way, all that's left is to add some dashboard functionality to the application. This involves adding callback handlers to enable users to list, download, delete, and email invoices.

  1. The following /index and /delete handlers use the MySQL and OpenStack client objects to list and delete invoices. Note that when deleting invoices, the invoices must be deleted from both the database and the object storage area.

    <?php
    // ...
    
    // invoice list display handler
    $app->get('/index', function () use ($app, $db) {
      $result = $db->query("SELECT * FROM invoices ORDER BY ts DESC");
      $data = $result->fetch_all(MYSQLI_ASSOC);
      return $app['twig']->render('index.twig', array('data' => $data));
    })->bind('index');
    
    // invoice deletion request handler
    $app->get('/delete/{id}', function ($id) use ($app, $db, $objectstore) {
      // delete invoice from database
      if (!$db->query("DELETE FROM invoices WHERE id = '$id'")) {
        $app['session']->getFlashBag()->add(
          'Failed to delete invoice from database: ' . $db->error
        );
        return $app->redirect($app["url_generator"]->generate('index'));
      }
      // delete invoice from object storage
      $container = $objectstore->getContainer('invoices');
      $object = $container->getObject("$id.pdf");
      $object->delete();  
      $app['session']->getFlashBag()->add('success', "Invoice #$id deleted.");
      return $app->redirect($app["url_generator"]->generate('index'));  
    })->bind('delete');
  2. The dashboard should also allow users to download previously generated invoices to their desktop via a /download route—a task easily accomplished with the OpenStack client's getObject() and download() methods. The retrieved file is then sent to the client browser as an attachment with the appropriate response headers.

    <?php
    // ...
    
    // invoice download request handler
    $app->get('/download/{id}', function ($id) use ($app, $objectstore) {
      // retrieve invoice file
      $file = $objectstore->getContainer('invoices')
                          ->getObject("$id.pdf")
                          ->download();
      // set response headers and body
      // send file to client
      $response = new Response();
      $response->headers->set('Content-Type', 'application/pdf');
      $response->headers->set('Content-Disposition', 'attachment; 
        filename="' . $id .'.pdf"'
      );
      $response->headers->set('Content-Length', $file->getSize());
      $response->headers->set('Expires', '@0');
      $response->headers->set('Cache-Control', 'must-revalidate');
      $response->headers->set('Pragma', 'public');
      $response->setContent($file);
      return $response;
    })->bind('download');
  3. Emailing an invoice requires a little additional help from the SendGrid email service. Assuming you already have a SendGrid account, log in and create an API key from the Settings -> API Keys menu.

    Figure 8. SendGrid API key generation
    SendGrid API key generation
    SendGrid API key generation

    Add this API key to the application configuration file at $APP_DIR/config.php, and then add the code below to the $APP_DIR/public/index.php file:

    <?php
    // ...
    
    // register service providers
    // initialize SendGrid client
    $sg = new \SendGrid($app->config['settings']['sendgrid']['key']);
    
    //...
    
    // invoice delivery request handler
    $app->get('/send/{id}', function ($id) use ($app, $objectstore, $db, $sg) {
      // retrieve invoice file
      $file = $objectstore->getContainer('invoices')
                          ->getObject("$id.pdf")
                          ->download();
      // change this to any valid email address                    
      $from = new SendGrid\Email(null, "no-reply@example.com");  
      $subject = "Invoice #$id";
      $result = $db->query("SELECT email FROM invoices WHERE id = '$id'");
      $row = $result->fetch_assoc();
      $to = new SendGrid\Email(null, $row['email']);
      $content = new SendGrid\Content(
        "text/plain", 
        "Please note that the attached sample invoice is generated 
        for demonstration purposes only and no payment is required."
      );
      $mail = new SendGrid\Mail($from, $subject, $to, $content);
      $attachment = new SendGrid\Attachment();
      $attachment->setContent(base64_encode($file));
      $attachment->setType("application/pdf");
      $attachment->setFilename("invoice_$id.pdf");
      $attachment->setDisposition("attachment");
      $attachment->setContentId("invoice_$id");
      $mail->addAttachment($attachment);
      $response = $sg->client->mail()->send()->post($mail);
      if ($response->statusCode() == 200 || $response->statusCode() == 202) {
        $app['session']->getFlashBag()->add('success', "Invoice #$id sent.");  
      } else {
        $app['session']->getFlashBag()->add('error', "Failed to send invoice.");    
      }
      return $app->redirect($app["url_generator"]->generate('index'));  
    })->bind('send');

    The /email handler uses the PHP SendGrid client object (initialized at the top of the script). It connects to the database to retrieve the customer email address for the specified invoice number, and then retrieves the actual invoice from the object storage area using the OpenStack client's download() method. Next, it uses the SendGrid client object to create new Email and Attachment objects and deliver them to the specified customer email address using the SendGrid service.

Step 6. Deploy to IBM Bluemix

  1. At this point, the application is complete and can be deployed to Bluemix. To do this, first create the application manifest file at $APP_ROOT/manifest.yml, remembering to use a unique host and application name by appending a random string to it (such as your initials).

    ---
    applications:
    - name: invoice-generator-[initials]
    memory: 256M
    instances: 1
    host: invoice-generator-[initials]
    buildpack: https://github.com/cloudfoundry/php-buildpack.git
    stack: cflinuxfs2
  2. The Cloud Foundry PHP build pack doesn't include the PHP MySQLi extension or the curl extension (used by the OpenStack client) by default, so you must configure the build pack to enable these extensions during deployment. Similarly, you must configure the build pack to use the public directory of the application as the web server directory. Create a $APP_ROOT/.bp-config/options.json file with the following content:

    {
        "WEB_SERVER": "httpd",
        "PHP_EXTENSIONS": ["bz2", "zlib", "mysqli", "curl"],
        "COMPOSER_VENDOR_DIR": "vendor",
        "WEBDIR": "public",
        "PHP_VERSION": "{PHP_56_LATEST}"
    }
  3. Also, to have the service credentials for the ClearDB and Object Storage services automatically sourced from Bluemix, update the $APP_ROOT/public/index.php script to use Bluemix's VCAP_SERVICES variable, as shown below:

    <?php                
    // include autoloader and configuration
    require '../vendor/autoload.php';
    require '../config.php';
                    
    // if BlueMix VCAP_SERVICES environment available
    // overwrite local credentials with BlueMix credentials
    if ($services = getenv("VCAP_SERVICES")) {
      $services_json = json_decode($services, true);
      $app->config['settings']['db']['hostname'] = 
        $services_json['cleardb'][0]['credentials']['hostname'];
      $app->config['settings']['db']['username'] = 
        $services_json['cleardb'][0]['credentials']['username'];
      $app->config['settings']['db']['password'] = 
        $services_json['cleardb'][0]['credentials']['password'];
      $app->config['settings']['db']['name'] = 
        $services_json['cleardb'][0]['credentials']['name'];
      $app->config['settings']['object-storage']['url'] = 
        $services_json["Object-Storage"][0]["credentials"]["auth_url"] . '/v3';
      $app->config['settings']['object-storage']['region'] = 
        $services_json["Object-Storage"][0]["credentials"]["region"];
      $app->config['settings']['object-storage']['user'] = 
        $services_json["Object-Storage"][0]["credentials"]["userId"];
      $app->config['settings']['object-storage']['pass'] = 
        $services_json["Object-Storage"][0]["credentials"]["password"];  
    } 
    
    // ...
    
    // initialize Silex application
    $app = new Application();
    
    // ...
  4. You can now push the application to Bluemix, and then bind the ClearDB and Object Storage services that you initialized earlier to it. Remember to use the correct ID for each service instance to ensure they are correctly bound to the application.

    shell> cf api https://api.ng.bluemix.net
    shell> cf login
    shell> cf push
    shell> cf bind-service invoice-generator-[initials] "ClearDB MySQL Database-[id]"
    shell> cf bind-service invoice-generator-[initials] "Object Storage-[id]"
    shell> cf restage invoice-generator-[initials]
  5. You can start using the application by browsing to the host specified in the application manifest—for example, http://invoice-generator-[initials].mybluemix.net. If you see a blank page or other errors, read "Debugging PHP errors on IBM Bluemix" to find out how to debug your PHP code and determine where things are going wrong.

Conclusion

This tutorial has focused on using Bluemix and PHP to quickly build an application for online invoice generation. By combining common PHP frameworks and open source libraries with highly available and scalable Bluemix storage and data services, I have attempted to demonstrate how simple it is for developers to prototype and launch useful business solutions in the cloud.

If you'd like to experiment with this application, start with the live demo. Just remember that the demo is public, so be sure not to upload confidential or sensitive information. (You can use the handy Reset System button to erase all uploaded data.) Then, download the source code from Github and take a closer look to see how it works.


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=1038122
ArticleTitle=Create and deliver invoices online with IBM Bluemix and PHP
publish-date=10042016