目次


IBM Bluemix と PHP を使用してインボイスを作成し、オンラインで配信する

インボイス処理の面倒を省くツールをビルドおよびデプロイする

Comments

多くのプロフェッショナルたちと同じく、私も業務の途中や終了時にインボイスを顧客に送っています。そして多くのプロフェッショナルたちと同じく、このタスクは必要ではあるものの、かなり面倒なものだと思っています。インボイスを作成するための主なツールとして、私は長年、ワード・プロセッサーと既製のテンプレートを使用していました。ワード・プロセッサーを使用してテンプレートに重要な情報を入力してからインボイスを印刷または送信するという方法です。経理とバックアップに使用できるよう、インボイスごとにデジタル・コピーも保管していましたが、そのうちディレクトリーがインボイス・ファイルでいっぱいになり、結局は手作業で、特定の年に取引したすべての顧客や完了した全プロジェクトを確認してその概要を把握しなければなりませんでした。

このことから、これよりも有効な方法があるはずだという結論に至りました。そこで考え始めたのが、フォーム入力からインボイスを生成し、それらのインボイスをデータベースに保存した上で、顧客に e-メールで送信するオンライン・ツールを作成することです。

それにはある程度の取り組みが必要になりますが、そのようなツールを完成できたとしたら、インボイスを簡単に作成できるだけでなく、顧客、プロジェクト、あるいは収益の概要をより効率的に把握できるようになります。

このツールが役立ちそうだと思ったら、この先を読み進めてください!以降に説明する手順で、このツールを作成して IBM Bluemix 上にデプロイするプロセスを説明します。その過程で、ClearDB MySQL Database および Object Storage という 2 つの主要な Bluemix サービスについて紹介します。

このアプリケーションでいろいろと実験したいと思ったら、まずはライブ・デモを実行してください。ただし、このデモは公開デモなので、機密情報をアップロードしないようにしてください。

ライブ・デモを実行するGitHub からコードを入手する

必要になるもの

このチュートリアルで説明するアプリケーションでは、ユーザーが Web フォームにインボイス情報を入力して、その情報を PDF 形式のインボイスに変換することができます。これらのインボイスはオンライン・ストレージ・エリアに保存され、ユーザーは Web ベースのダッシュボードを使用して以前に生成したインボイスの一覧を表示し、特定のインボイスをダウンロードしたり、顧客に e-メールで送信したりできます。アプリケーション全体がモバイル用に最適化されているので、ユーザーは移動中でもインボイスを送信または表示することができます (勤務するオフィスや職場が一定していないユーザーにはまさに理想的です)。

舞台裏では、このアプリケーションは各種のサービスをオーケストレーションすることによって機能します。これらのサービスには、Bluemix から直接提供されているものも、サード・パーティー・サービスとして提供されているものもあります。利用するサービスについて、以下に簡単にまとめます。

このアプリケーションではまた、Bootstrap を使用してモバイル用に最適化されたインターフェースを作成し、Silex PHP マイクロフレームワークで mPDF ライブラリーを使用して PDF 形式のインボイス生成に対応します。

多数のテクノロジーを利用するこのアプリケーションには、以下のものが必要になります。

注: SendGrid サービスを利用するアプリケーションは、このリンク先のページに記載されている SendGrid サービスの利用規約に、例外なく従う必要があります。同様に、ClearDB および Object Storage サービスを利用するアプリケーションも、このリンク先の ClearDB Terms of Service および Object Storage サービス・カタログ・ページに記載されている、それぞれのサービスの利用条件に従わなければなりません。プロジェクトを開始する前に、利用条件に書かれている要件を読んで、アプリケーションがそれらの要求事項を満たしていることを確認してください。

ステップ 1. 必要最低限のアプリケーションを作成する

  1. 最初のステップは、Slim PHP マイクロフレームワークと Twig テンプレート・エンジンを使用して、基本的なアプリケーションを初期化することです。インボイスの生成、e-メール配信、オブジェクト・ストレージへのアクセスには、追加のパッケージが必要になりますが、これらの依存関係はすべて、Composer を使用することで簡単にダウンロードしてインストールできます。以下の Composer 構成ファイルを使用してください。このファイルは $APP_ROOT/composer.json に保存する必要があります ($APP_ROOT は、皆さんのプロジェクト・ディレクトリーです)。

    {
        "require": {
            "silex/silex": "*",
            "twig/twig": "*",
            "symfony/validator": "*",        
            "mpdf/mpdf": "*",
            "php-opencloud/openstack": "*",
            "sendgrid/sendgrid": "*"
        },
        "minimum-stability": "dev",
        "prefer-stable": true
    }
  2. 以下のコマンドを発行し、Composer を使用して依存関係をインストールします。

    shell> php composer.phar install
  3. 次に、アプリケーションのメイン制御スクリプトをセットアップします。このスクリプトは、Silex フレームワークをロードして、Silex アプリケーションを初期化します。また、このスクリプトにはアプリケーションのルートのそれぞれに対するコールバックが含まれており、それぞれのコールバックでは、送られてきたリクエストとルートが一致したときに実行されるコードを定義しています。このスクリプトを $APP_ROOT/public/index.php に作成します。スクリプトの内容は以下のとおりです。

    <?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. このアプリケーションはインボイスの一覧、追加、削除、ダウンロード、および e-メール配信をサポートするため、スクリプトでは /index/create/delete/download、および /send の各ルートに対応する URL ルートとプレースホルダーを定義しています。これらのルート用のコールバックは、チュートリアルを進めていく中で作り込んでいきます。このスクリプトはまた、アプリケーション構成ファイルから構成データを読み取り、Twig テンプレート・レンダラーを初期化して Silex に登録します。準備を完了するための最後の作業として、ヘッダー、フッター、コンテンツ・エリアの 3 つからなる Bootstrap ベースの単純なユーザー・インターフェース (UI) を作成します。以下に一例を示します。以降のコード・リスティングに示す、すべてのアプリケーション・ビューには、このサンプル UI を使用します。

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

ステップ 2. インボイス生成用フォームを作成する

  1. 準備作業は終わったので、ここからは、アプリケーションそのものの開発を進めていきます。アプリケーションを開発する最初のステップとして、顧客とインボイスの情報を保持するフォームを定義します。このフォームを $APP_DIR/views/create.twig として定義し、以下の内容を含めます。

    <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. このフォームは主に 2 つのセクションで構成されています。1 つは顧客の名前、住所、e-メール・アドレスを入力するセクション、もう 1 つはインボイスの詳細を入力するセクションです。後者のセクションには 3 つのフィールドがあり、それぞれがインボイスの各品目を表します。具体的には、インボイス対象の品目の説明フィールド、料金フィールド、数量フィールドです。インボイスに複数の品目が含まれるのはよくあることなので、「Add Invoice Line (インボイス品目の追加)」ボタンを使ってこのセクションを複製し、新しい品目をインボイスに追加できるようになっています。このボタンをクリックすると、以下の JavaScript を使用して動的に新しいインボイス品目セクションがフォームに追加されます。

    <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. インボイス品目のデータは、複数レベルの配列として構成されていることに注目してください。lines[0] は 最初の品目を表し、lines[1] は 2 番目の品目を表すといった具合です。最初の品目に含まれる lines[0][item] にはその品目の説明が格納され、対応する料金と数量がそれぞれ ines[0][rate]lines[0][qty] に格納されます。

    以下に一例として、このフォームのスクリーンショットを示します。

    図 1. インボイス生成フォーム
    インボイス生成フォーム
    インボイス生成フォーム

    このフォームを送信すると、フォームに入力されたデータが POST リクエストとして /create コールバック・ハンドラーに送信されます。このコールバック・ハンドラーが、アプリケーション内での手間のかかる処理の大部分を引き受けます。具体的には、以下の処理を行います。

    • フォームに記入して送信されたデータを検証する
    • 各インボイス品目の小計を計算する
    • インボイスの最終的な合計金額を計算する
    • 定義済みテンプレートから PDF 形式のインボイスを生成する
    • インボイスをストレージ・エリアに保存する
    • 主要なインボイス詳細を含むレコードをデータベースに追加する

上記の処理内容から明らかなように、コールバック・ハンドラーを実装する前にデータベースとストレージ・エリアを初期化すること、そしてインボイスの生成時に使用するテンプレートを定義することが必要になります。以降のセクションで、これらのタスクについて説明します。

ステップ 3. Bluemix データベースおよびストレージ・サービスを初期化する

Bluemix ではサービスとしてのデータベースを多数の選択肢の中から選べるようになっています。その 1 つが、ClearDB MySQL Database サービスです。名前から想像できるように、このサービスは、アプリケーションにバインドできる、空の MySQL データベース・インスタンスをプロビジョニングします。デフォルトのプランでは、限られた量の無料ストレージだけが割り当てられます。

  1. このサービスの動作を確認するために、Bluemix 上で ClearDB MySQL Database サービスのインスタンスを初期化します。それにはまず、自分の Bluemixアカウントにログインして、ダッシュボードから「Console (コンソール)」ボタンをクリックします。表示されるサービスのリストから、「Data and Analytics (データおよびアナリティクス)」を選択します。「Add (追加)」ボタンをクリックし、表示されるサービスのリストから「ClearDB MySQL Database」を選択します。料金プランは無料プランを選択してください。また、「Connect to (接続先)」フィールドが「Leave unbound (アンバインドしたままにする)」に設定されていることを確認します。このように設定すると、アプリケーションの開発は別個のホスト上で進めて、データベース・サービス・インスタンスだけが Bluemix 上でホストされた状態にすることができます。

    図 2. サービスの作成
    サービスを作成する ClearDB MySQL Database 画面のスクリーンショット
    サービスを作成する ClearDB MySQL Database 画面のスクリーンショット
  2. ClearDB サービス・インスタンスが初期化されたので、サービス詳細ページから ClearDB ダッシュボードを開きます。「Endpoint Information (エンドポイント情報)」をクリックしてインスタンスの資格情報を表示し、そこに表示されているクラスター名、ホスト名、ユーザー名、パスワードの値を、アプリケーションの $APP_DIR/config.php ファイルに追加します。

    図 3. サービス資格情報
    サービス資格情報を表示する ClearDB MySQL Database 画面のスクリーンショット
    サービス資格情報を表示する ClearDB MySQL Database 画面のスクリーンショット
  3. 入手した資格情報を使用して MySQL データベースに接続し (MySQL CLI または phpMyAdmin などのツールを使用してください)、以下の SQL コードを使用して、インボイス情報を格納するための新規テーブルを作成します。

    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. Bluemix ダッシュボードの「Storage (ストレージ)」セクションから、ClearDB サービス・インスタンスを初期化したときと同様の手順に従って新規 Object Storage インスタンスを初期化します。前と同じく、「Connect to (接続先)」フィールドが「Leave unbound (アンバインドしたままにする)」に設定されていることを確認してください。

    図 4. サービス作成
    サービスを作成する Object Storage 画面のスクリーンショット
    サービスを作成する Object Storage 画面のスクリーンショット
  5. サービス詳細ページから「Service Credentials (サービス資格情報)」タブにアクセスして、インスタンスの資格情報を表示し、そこに表示されている値をアプリケーションの $APP_DIR/config.php ファイルに追加します。

    図 5. サービス資格情報
    サービス資格情報を表示する Object Storage 画面のスクリーンショット
    サービス資格情報を表示する Object Storage 画面のスクリーンショット
  6. また、「Manage (管理)」ページから、生成されるインボイスを格納するための「invoices」という名前の新規ストレージ・コンテナーを作成します。

    図 6. ストレージ・コンテナーの作成
    ストレージ・コンテナーを作成する Object Storage 画面のスクリーンショット
    ストレージ・コンテナーを作成する Object Storage 画面のスクリーンショット

ステップ 4. PDF 形式のインボイスを生成する

  1. データベースおよびオブジェクト・ストレージ・システムの初期化に続き、次は出力インボイスを生成するためのテンプレートを作成します。mPDF ライブラリーには、HTML を PDF に変換する強力な機能が含まれています。したがって、最初に HTML を使用してテンプレートを作成し、そのテンプレートに実際のデータを入力してから PDF に変換するという手順が最も簡単な方法になります。

    サンプル・インボイス・テンプレートは以下のとおりです。このコードを、$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>

    上記のコードに意外な部分はありません。このテンプレートは Bootstrap を使用して、顧客情報のセクションを作成した後、インボイス詳細用の複数行のテーブルを作成し、続いて支払い条件のセクションを作成しているだけです。当然、このテンプレートは、ヘッダーに (例えば) 会社のログや発注書番号が表示されるように簡単にカスタマイズすることができます。

  2. 最後のステップは、/create コールバック・ハンドラーを更新して、送信されたデータの検証、品目の小計とインボイスの合計金額の計算、および上記のテンプレートを使用した実際のインボイスの作成に対処できるようにすることです。そのためのコードは以下のとおりです。

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

    上記のコード内ではさまざまな処理が行われているため、セクションごとに順を追って見ていきましょう。

    1. コールバック・ハンドラーを初期化する前に、このコードは PDF を生成するための mPDF オブジェクトを初期化します。さらに、データベース操作用の MySQL クライアント・オブジェクトを (PHP の mysqli 拡張機能を使用して) 初期化し、オブジェクト・ストレージ API とやり取りするための OpenStack クライアント・オブジェクトを初期化します。初期化されたこれらのオブジェクトが /create コールバック・ハンドラーに渡されます。
    2. /create コールバック・ハンドラーはまず、各種の入力パラメーター (顧客情報、品目の説明、数量、料金) を収集し、Symfony を使用して各パラメーターを検証します。具体的には、インボイス品目の検証にはカスタム Lines バリデーター (アプリケーション・ソース・コードのリポジトリーから入手可能) を使用します。このバリデーターによって、各インボイス品目をチェックし、パラメーターが完全に入力されていること、数量と料金が数値として指定されていることを確認します。
    3. 入力が有効であれば、品目ごとの小計を計算します。さらにそれらの小計を合計して、インボイスの合計金額を算出します。
    4. MySQL クライアント・オブジェクトを使用して、新しいインボイスを表す新しいレコードを生成し、そのレコードをアプリケーション・データベースに挿入します。このデータベース・レコードには、顧客の名前と住所、インボイスの合計金額、作成日が含まれます。それぞれのレコードには固有の自動生成された ID が割り当てられます。
    5. Twig テンプレート・エンジンの render() メソッドを使用して、各種の入力変数をインボイス・テンプレートに挿入し、HTML 形式のインボイスを生成します。生成された HTML インボイスを PDF 形式のインボイスに変換するためには、mPDF オブジェクトの WriteHTML() および Output() メソッドを使用します。これらのメソッドは最終的に、PDF ファイルを表すバイトのストリングを返します。
    6. OpenStack クライアント・オブジェクトの getContainer() メソッドを使用して、前に作成した「invoices」コンテナーへの参照を取得し、同じくこのオブジェクトの createObject() メソッドを使用して PDF 形式のインボイスをコンテナーに保存します。PDF 形式のインボイスのファイル名は、MySQL データベース内の対応するレコードに対して自動生成された ID と一致するように設定されることに注意してください。

以上のステップのすべてが正常に完了すると、クライアント・ブラウザーは、成功メッセージを表示するアプリケーションの index ページにリダイレクトされます。

一例として、PDF 形式のインボイスは以下のようになります。

図 7. PDF 形式のインボイスの例
PDF 形式のインボイスの例を示す図
PDF 形式のインボイスの例を示す図

ステップ 5. インボイスのダウンロードおよび配信を可能にする

インボイスを生成するための面倒な処理は完了しました。残された作業は、アプリケーションにダッシュボード機能を追加することだけです。それには、ユーザーがインボイスを一覧表示、ダウンロード、削除、e-メール配信できるようにするためのコールバック・ハンドラーを追加する必要があります。

  1. 以下の /index コールバック・ハンドラーと /delete コールバック・ハンドラーは、それぞれ MySQL クライアント・オブジェクトと OpenStack クライアント・オブジェクトを使用してインボイスを一覧表示、削除します。インボイスを削除する場合は、データベースとオブジェクト・ストレージ・エリアの両方から削除する必要があることに注意してください。

    <?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. ダッシュボードでは、/download ルートを介して、ユーザーが生成済みのインボイスをデスクトップにダウンロードできるようにする必要もあります。このタスクには、OpenStack クライアントの getObject() および download() メソッドによって簡単に対処できます。取得したファイルは、該当する応答ヘッダーを使用して、添付ファイルとしてクライアント・ブラウザーに送信されます。

    <?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. インボイスを e-メールで送信するには、SendGrid e-メール・サーバーの力を少々借りる必要があります。SendGrid アカウントを取得済みという前提で説明を続けると、アカウントにログインして、「Settings (設定)」 -> 「API Keys (API キー)」メニューの順に選択して API キーを生成します。

    図 8. SendGrid API キーの生成
    SendGrid API キーを生成する SendGrid 画面のスクリーンショット
    SendGrid API キーを生成する SendGrid 画面のスクリーンショット

    生成された API キーを $APP_DIR/config.php にあるアプリケーション構成ファイルに追加した後、$APP_DIR/public/index.php ファイルに以下のコードを追加します。

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

    上記の /email ハンドラーが使用するのは (スクリプトの先頭で初期化される) PHP SendGrid クライアント・オブジェクトです。このハンドラーはます、データベースに接続して、指定されたインボイス番号に対応する顧客の e-メール・アドレスを取得してから、OpenStack クライアントの download() メソッドを使用してオブジェクト・ストレージ・エリアから実際のインボイスを取得します。続いて SendGrid クライアント・オブジェクトを使用して新しい Email オブジェクトと Attachment オブジェクトを作成し、SendGrid サービスを利用して、これらの新規オブジェクトを指定された顧客の e-メール・アドレスに送信します。

ステップ 6. IBM Bluemix にデプロイする

  1. この時点で、アプリケーションは完成して Bluemix にデプロイできる状態になっています。アプリケーションを Bluemix にデプロイするには、まず、アプリケーション・マニフェスト・ファイルを作成します。その際、ホスト名とアプリケーション名の末尾にランダムなストリング (自分のイニシャルなど) を追加して、使用する名前が一意になるようにしてください。

    ---
    applications:
    - name: invoice-generator-[initials]
    memory: 256M
    instances: 1
    host: invoice-generator-[initials]
    buildpack: https://github.com/cloudfoundry/php-buildpack.git
    stack: cflinuxfs2
  2. Cloud Foundry PHP ビルドパックには、デフォルトでは PHP MySQLi 拡張機能も (OpenStack クライアントが使用する) curl 拡張機能も含まれていないので、デプロイ時にこれらの拡張機能を使用可能にするようにビルドパックを構成する必要があります。同様に、このビルドバックは、アプリケーションの public ディレクトリーを Web サーバー・ディレクトリーとして使用するように構成されていなければなりません。$APP_ROOT/.bp-config/options.json ファイルを新規に作成して、以下の内容を含めます。

    {
        "WEB_SERVER": "httpd",
        "PHP_EXTENSIONS": ["bz2", "zlib", "mysqli", "curl"],
        "COMPOSER_VENDOR_DIR": "vendor",
        "WEBDIR": "public",
        "PHP_VERSION": "{PHP_56_LATEST}"
    }
  3. また、ClearDB サービスと Object Storage サービスの資格情報を Bluemix から自動的に取り込むようにするために、以下のように Bluemix の VCAP_SERVICES 変数を使用して $APP_ROOT/public/index.php スクリプトを更新します。

    <?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. これで、アプリケーションを Bluemix にプッシュして、前に初期化した ClearDB および Object Storage サービスをアプリケーションにバインドできるようになりました。サービスが正常にアプリケーションにバインドされるよう、必ず各サービス・インスタンスの正しい ID を使用してください。

    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. アプリケーションを使い始めるには、アプリケーション・マニフェストに指定したホスト (例えば、http://invoice-generator-[イニシャル].mybluemix.net) をブラウズします。空白のページやその他のエラーが表示されたら、このリンク先のページ「Debugging PHP Errors on IBM Bluemix」を参照して、PHP コードをデバッグして問題の原因となっている場所を突き止めてください。

まとめ

このチュートリアルでは、オンライン・インボイス生成用アプリケーションを迅速に作成するために、Bluemix と PHP を使用することに重点を置きました。私が目的としたのは、よく使われている PHP フレームワークとオープンソース・ライブラリーに、高可用性を備えたスケーラブルな Bluemix ストレージおよびデータ・サービスを組み合わせることで、開発者がクラウド内でプロトタイプを作成して有用なビジネスを立ち上げるのが、いかに簡単なことであるかを明らかにすることです。

このアプリケーションでいろいろと実験したいと思ったら、まずはライブ・デモを実行してください。ただし、このデモは公開デモなので、機密情報をアップロードしないようにしてください (このアプリケーションの「System Reset (システム・リセット)」ボタンを使用すると、アップロードしたすべてのデータを簡単に消去できます)。デモを試した後は、ソース・コードを GitHub リポジトリーからダウンロードして、このアプリケーションの仕組みを詳しく見て行くことができます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing
ArticleID=1040796
ArticleTitle=IBM Bluemix と PHP を使用してインボイスを作成し、オンラインで配信する
publish-date=12152016