PHP を使用したバックグラウンド処理

多くのアプリでは、ウェブ リクエストとは関連のないバックグラウンド処理を行う必要があります。このチュートリアルでは、ユーザーが翻訳するテキストを入力した後、以前の翻訳の一覧を表示するウェブアプリを作成します。翻訳は、ユーザーのリクエストをブロックしないようにバックグラウンド プロセスで行われます。

次の図は、翻訳リクエストのプロセスを示しています。

アーキテクチャの図

チュートリアル アプリが動作する際のイベントの順序は次のとおりです。

  1. ウェブページにアクセスすると、Firestore に保存されている以前の翻訳の一覧が表示されます。
  2. HTML フォームに入力してテキストの翻訳をリクエストします。
  3. 翻訳リクエストは Pub/Sub にパブリッシュされます。
  4. Cloud Run アプリが Pub/Sub メッセージを受信します。
  5. Cloud Run アプリが Cloud Translation を使用してテキストを翻訳します。
  6. Cloud Run アプリが結果を Firestore に保存します。

このチュートリアルは、Google Cloud でのバックグラウンド処理の詳細に関心をお持ちの方を対象としています。Pub/Sub、Firestore、App Engine、Cloud Run についての経験は必須要件ではありません。ただし、PHP、JavaScript、HTML の経験をお持ちであれば、すべてのコードを理解するうえで役立ちます。

目標

  • Cloud Run サービスを理解し、デプロイします。
  • App Engine アプリを理解し、デプロイします。
  • アプリを試してみます。

料金

このチュートリアルでは、課金対象である次の Google Cloud コンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。新しい Google Cloud ユーザーは無料トライアルをご利用いただけます。

このチュートリアルを終了した後、作成したリソースを削除すると、それ以上の請求は発生しません。詳しくは、クリーンアップをご覧ください。

始める前に

  1. Google アカウントにログインします。

    Google アカウントをまだお持ちでない場合は、新しいアカウントを登録します。

  2. Cloud Console のプロジェクト セレクタページで、Cloud プロジェクトを選択または作成します。

    プロジェクト セレクタのページに移動

  3. Google Cloud プロジェクトに対して課金が有効になっていることを確認します。プロジェクトに対して課金が有効になっていることを確認する方法を学習する

  4. Firestore, Cloud Run, Pub/Sub, and Cloud Translation API を有効にします。

    API を有効にする

  5. Google Cloud Console から、Cloud Shell でアプリを開きます。

    Cloud Shell に移動

    Cloud Shell を使用すると、ブラウザからコマンドラインで直接クラウド リソースにアクセスできます。ブラウザで Cloud Shell を開き、[続行] をクリックしてサンプルコードをダウンロードし、アプリ ディレクトリに移動します。

  6. Cloud Shell で、gcloud ツールを構成して Google Cloud プロジェクトを使用します。
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Cloud Run バックエンドについて

1 つの PHP 関数 translateString を定義し、この関数を呼び出して Pub/Sub メッセージに応答するように Cloud Run サービスを構成します。

use Google\Cloud\Firestore\FirestoreClient;
use Google\Cloud\Firestore\Transaction;
use Google\Cloud\Translate\TranslateClient;

/**
 * @param array $data {
 *     The PubSub message data containing text and target language.
 *
 *     @type string $text
 *           The full text to translate.
 *     @type string $language
 *           The target language for the translation.
 * }
 */
function translateString(array $data)
{
    if (empty($data['language']) || empty($data['text'])) {
        throw new Exception('Error parsing translation data');
    }

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();

    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);

    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }

            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

    echo "Done.";
}
  1. Firestore と Translation に接続するには、関数で複数の依存関係をインポートする必要があります。

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. Cloud Run は、最初に Firestore クライアントと Pub/Sub クライアントを初期化します。次に、Pub/Sub メッセージ データを解析して、翻訳するテキストとターゲット言語を取得します。

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. Translation API を使用して文字列を目的の言語に翻訳します。

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. 重複した訳文が保存されないようにするために、翻訳リクエストに一意の名前を付けます。その後、同時実行によってアプリが同じ翻訳を誤って 2 回実行しないように、Firestore トランザクションで翻訳を行います。

    $docId = sprintf('%s:%s', $data['language'], base64_encode($data['text']));
    $docRef = $firestore->collection('translations')->document($docId);
    
    $firestore->runTransaction(
        function (Transaction $transaction) use ($translate, $translation, $docRef) {
            $snapshot = $transaction->snapshot($docRef);
            if ($snapshot->exists()) {
                return; // Do nothing if the document already exists
            }
    
            $result = $translate->translate($translation['original'], [
                'target' => $translation['lang'],
            ]);
            $transaction->set($docRef, $translation + [
                'translated' => $result['text'],
                'originalLang' => $result['source'],
            ]);
        }
    );

Cloud Run バックエンドのビルドとデプロイ

  • backend ディレクトリで Cloud Run アプリをビルドします。

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • image タグを使用して、前のステップの Cloud Run アプリをデプロイします。

    gcloud run deploy background-processing-function --platform managed \
      --image gcr.io/PROJECT_ID/background-function --region REGION

    REGIONGoogle Cloud のリージョンです。

  • デプロイが完了すると、コマンド出力にデプロイされたアプリの URL が表示されます。例:

    Service [background-processing-function] revision [default-00002-vav] has been deployed and is serving 100 percent of traffic at https://default-c457u4v2ma-uc.a.run.app

    次の手順のためにこの URL をコピーします。

Pub/Sub サブスクリプションの設定

メッセージがトピック translate にパブリッシュされるたびに、Cloud Run アプリは Pub/Sub からメッセージを受信します。

組み込みの認証チェックによって、Cloud Run バックエンドを呼び出す権限を持つサービス アカウントの有効な認証トークンが Pub/Sub メッセージに含まれているかどうかが確認されます。

次の手順では、Cloud Run バックエンドへの認証済み呼び出しを行うための Pub/Sub トピック、サブスクリプション、サービス アカウントの設定について説明します。この統合の詳細については、サービス間の認証をご覧ください。

  1. 新しい翻訳リクエストをパブリッシュするためのトピック translate を作成します。

    gcloud pubsub topics create translate
    
  2. プロジェクトで Pub/Sub 認証トークンを作成できるようにします。

    gcloud projects add-iam-policy-binding PROJECT_ID \
         --member=serviceAccount:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
         --role=roles/iam.serviceAccountTokenCreator

    PROJECT_NUMBER は Google Cloud のプロジェクト番号です。この番号を確認するには、gcloud projects describe PROJECT_ID | grep projectNumber を実行します。

  3. Pub/Sub サブスクリプションの ID を表すサービス アカウントを作成または選択します。

    gcloud iam service-accounts create cloud-run-pubsub-invoker \
         --display-name "Cloud Run Pub/Sub Invoker"

    メモ: cloud-run-pubsub-invoker を使用できます。または、Google Cloud プロジェクト内で一意の名前に置き換えます。

  4. 呼び出し元のサービス アカウントに、background-processing-function サービスを呼び出すための権限を付与します。

    gcloud run services add-iam-policy-binding background-processing-function \
       --member=serviceAccount:cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com \
       --role=roles/run.invoker  --platform managed --region REGION

    Identity and Access Management の変更が反映されるまでに数分かかることがあります。その間に、サービスログに HTTP 403 エラーが報告されることがあります。

  5. このサービス アカウントを使用して Pub/Sub サブスクリプションを作成します。

    gcloud pubsub subscriptions create run-translate-string --topic translate \
       --push-endpoint=CLOUD_RUN_URL \
       --push-auth-service-account=cloud-run-pubsub-invoker@PROJECT_ID.iam.gserviceaccount.com

    CLOUD_RUN_URL は、バックエンドのビルドとデプロイを行った後でコピーした HTTPS URL です。

    --push-account-service-account フラグは、認証と認可のために Pub/Sub の push 機能を有効にします。

    Pub/Sub サブスクリプションで使用するための Cloud Run サービス ドメインが自動的に登録されます。

アプリについて

ウェブアプリには主に次の 2 つのコンポーネントがあります。

  • ウェブ リクエストを処理するための PHP HTTP サーバー。サーバーには、次の 2 つのエンドポイントがあります。
    • /: 既存のすべての翻訳を一覧表示し、ユーザーが新しい翻訳をリクエストする際に送信できるフォームを表示します。
    • /request-translation: フォーム送信はこのエンドポイントに送信され、Pub/Sub へのリクエストがパブリッシュされ、翻訳は非同期で処理されます。
  • PHP サーバーによって既存の翻訳が入力された HTML テンプレート。

HTTP サーバー

  • app ディレクトリ内の index.php が起動され、Lumen アプリの設定と HTTP ハンドラの登録が行われます。

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • インデックス ハンドラ(/)は、Firestore から既存の翻訳をすべて取得し、そのリストを使用してテンプレートをレンダリングします。

    /**
     * Homepage listing all requested translations and their results.
     */
    $router->get('/', function (Request $request) use ($projectId) {
        $firestore = new FirestoreClient([
            'projectId' => $projectId,
        ]);
        $translations = $firestore->collection('translations')->documents();
        return view('home', ['translations' => $translations]);
    });
  • /request-translation に登録されているリクエスト翻訳ハンドラは、HTML フォーム送信を解析してリクエストを検証し、メッセージを Pub/Sub にパブリッシュします。

    /**
     * Endpoint which publishes a PubSub request for a new translation.
     */
    $router->post('/request-translation', function (Request $request) use ($projectId) {
        $acceptableLanguages = ['de', 'en', 'es', 'fr', 'ja', 'sw'];
        if (!in_array($lang = $request->get('lang'), $acceptableLanguages)) {
            throw new Exception('Unsupported Language: ' . $lang);
        }
        if (!$text = $request->get('v')) {
            throw new Exception('No text to translate');
        }
        $pubsub = new PubSubClient([
            'projectId' => $projectId,
        ]);
        $topic = $pubsub->topic('translate');
        $topic->publish(['data' => json_encode([
            'language' => $lang,
            'text' => $text,
        ])]);
    
        return '';
    });

HTML テンプレート

HTML テンプレートはユーザーに表示される HTML ページの基礎となり、以前の翻訳を表示して新しい翻訳をリクエストできます。HTTP サーバーは、既存の翻訳のリストを使用してテンプレートへの入力を行います。

  • HTML テンプレートの <head> 要素には、ページのメタデータ、スタイルシート、JavaScript が含まれます。

    このページでは、Material Design Lite(MDL)の CSS アセットと JavaScript アセットを取得します。MDL によって、ウェブサイトに Material Design の外観を追加できます。

    ページは JQuery を使用してドキュメントの読み込みが完了するのを待ち、フォーム送信ハンドラを設定します。翻訳のリクエスト フォームが送信されるたびに、ページは最小限のフォームの検証を行い値が空でないことを確認してから、非同期リクエストを /request-translation エンドポイントに送信します。

    最後に、リクエストが成功したか、またはエラーが発生したかを示す MDL スナックバーが表示されます。

  • ページの HTML 本文は、MDL レイアウトといくつかの MDL コンポーネントを使用して、翻訳のリストと追加の翻訳をリクエストするフォームを表示します。
    <body>
      <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
        <header class="mdl-layout__header">
          <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title">Translate with Background Processing</span>
          </div>
        </header>
        <main class="mdl-layout__content">
          <div class="page-content">
            <div class="mdl-grid">
              <div class="mdl-cell mdl-cell--1-col"></div>
              <div class="mdl-cell mdl-cell--3-col">
                <form id="translate-form" class="translate-form">
                  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="text" id="v" name="v">
                    <label class="mdl-textfield__label" for="v">Text to translate...</label>
                  </div>
                  <select class="mdl-textfield__input lang" name="lang">
                    <option value="de">de</option>
                    <option value="en">en</option>
                    <option value="es">es</option>
                    <option value="fr">fr</option>
                    <option value="ja">ja</option>
                    <option value="sw">sw</option>
                  </select>
                  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent" type="submit"
                      name="submit">Submit</button>
                </form>
              </div>
              <div class="mdl-cell mdl-cell--8-col">
                <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
                  <thead>
                    <tr>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Original</strong></th>
                      <th class="mdl-data-table__cell--non-numeric"><strong>Translation</strong></th>
                    </tr>
                  </thead>
                  <tbody>
                  <?php foreach ($translations as $translation): ?>
                    <tr>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--primary">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['originalLang'] ?></span>
                        </span>
                      <?= $translation['original'] ?>
                      </td>
                      <td class="mdl-data-table__cell--non-numeric">
                        <span class="mdl-chip mdl-color--accent">
                          <span class="mdl-chip__text mdl-color-text--white"><?= $translation['lang'] ?></span>
                        </span>
                        <?= $translation['translated'] ?>
                      </td>
                    </tr>
                  <?php endforeach ?>
                  </tbody>
                </table>
                <br/>
                <button class="mdl-button mdl-js-button mdl-button--raised" type="button" onClick="window.location.reload();">Refresh</button>
              </div>
            </div>
          </div>
          <div aria-live="assertive" aria-atomic="true" aria-relevant="text" class="mdl-snackbar mdl-js-snackbar" id="snackbar">
            <div class="mdl-snackbar__text mdl-color-text--black"></div>
            <button type="button" class="mdl-snackbar__action"></button>
          </div>
        </main>
      </div>
    </body>
    </html>
    

Cloud Shell でアプリを実行する

ウェブアプリをデプロイする前に、依存関係をインストールしてローカルで実行します。

  1. まず、Composer を使用して依存関係をインストールします。 PHP 用 gRPC 拡張機能は必須ですが、Cloud Shell にはプリインストールされています。

    composer install -d app
    
  2. 次に、PHP 組み込みのウェブサーバーを実行してアプリのサービスを提供します。

    APP_DEBUG=true php -S localhost:8080 -t app
    

    APP_DEBUG=true フラグを指定すると、発生した例外がすべて表示されます。

  3. Cloud Shell で、[ウェブでプレビュー] をクリックし、[ポート 8080 でプレビュー] を選択します。新しいウィンドウが開き、実行中のアプリが表示されます。

ウェブアプリのデプロイ

App Engine スタンダード環境を使用すると、高負荷で大量のデータが存在する状況でも確実に動作するアプリをビルドしてデプロイできます。

このチュートリアルでは、App Engine スタンダード環境を使用して HTTP フロントエンドをデプロイします。

app.yaml は App Engine アプリを構成します。

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • app.yaml ファイルと同じディレクトリから、App Engine スタンダード環境にアプリをデプロイします。
    gcloud app deploy

アプリをテストする

Cloud 関数と App Engine アプリをデプロイしたら、翻訳のリクエストを試行します。

  1. ブラウザでアプリを表示するには、次の URL を入力します。

    https://PROJECT_ID.REGION_ID.r.appspot.com

    以下を置き換えます。

    翻訳に関する空のリストと新しい翻訳をリクエストするためのフォームを掲載したページがあります。

  2. [翻訳するテキスト] フィールドに、翻訳するテキスト(Hello, World など)を入力します。
  3. テキストを翻訳する対象言語をプルダウン リストから選択します。
  4. [送信] をクリックします。
  5. ページを更新するには、[Refresh](更新)をクリックします。翻訳リストに新しい行が追加されます。翻訳が表示されない場合は、数秒待ってからもう一度お試しください。それでも翻訳が表示されない場合は、アプリのデバッグに関する次のセクションをご覧ください。

アプリのデバッグ

App Engine アプリに接続できない場合や、新しい翻訳が表示されない場合は、次の点をご確認ください。

  1. gcloud デプロイ コマンドが正常に終了して、エラーを出力しなかったことを確認します。エラー(message=Build failed など)が発生した場合は、それらを修正してから、もう一度 Cloud Run アプリのビルドとデプロイApp Engine アプリのデプロイを行います。
  2. Google Cloud Console で、ログビューア ページに移動します。

    [ログビューア] ページに移動

    1. [最近選択したリソース] プルダウン リストで [GAE アプリケーション] をクリックし、[All module_id] をクリックします。アプリにアクセスした以降のリクエストのリストが表示されます。リクエストのリストが表示されない場合は、プルダウン リストで [All module_id ] が選択されていることを確認します。エラー メッセージが Cloud Console に出力された場合は、アプリのコードがウェブアプリの理解に関するセクション内のコードと一致することを確認します。
    2. [最近選択したリソース] プルダウン リストで [Cloud Run のリビジョン] をクリックし、[すべてのログ] をクリックします。デプロイされたアプリの URL に送信された POST リクエストが表示されます。リクエストが表示されない場合は、Cloud Run と App Engine アプリが同じ Pub/Sub トピックを使用していること、Pub/Sub サブスクリプションが存在し、Cloud Run エンドポイントに push されていることを確認してください。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud Platform アカウントに課金されないようにする手順は次のとおりです。

Cloud プロジェクトの削除

  1. Cloud Console で [リソースの管理] ページに移動します。

    [リソースの管理] ページに移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

チュートリアルのリソースの削除

  1. このチュートリアルで作成した App Engine アプリを削除します。

    1. Cloud Console で、App Engine の [バージョン] ページに移動します。

      [バージョン] ページに移動

    2. デフォルト以外で削除するアプリのバージョンのチェックボックスをオンにします。
    3. [削除] をクリックして、アプリのバージョンを削除します。

  2. このチュートリアルでデプロイした Cloud Run サービスを削除します。

    gcloud run services delete background-processing-function

    Cloud Run サービスを Google Cloud Console から削除することもできます。

  3. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ