Traitement en arrière-plan avec PHP

De nombreuses applications doivent exécuter un traitement en arrière-plan dans d'autres contextes que celui d'une requête Web. Ce tutoriel indique comment créer une application Web qui permet aux utilisateurs de saisir du texte à traduire, puis qui affiche la liste des traductions précédentes. La traduction est effectuée dans un traitement en arrière-plan pour éviter de bloquer la requête de l'utilisateur.

Le schéma suivant illustre le processus de requête de traduction.

Schéma de l'architecture

Voici, dans l'ordre, comment fonctionne l'application du tutoriel :

  1. L'application accède à la page Web pour consulter la liste des traductions précédentes stockées dans Firestore.
  2. Elle demande la traduction d'un texte en saisissant un formulaire HTML.
  3. La requête de traduction est publiée dans Pub/Sub.
  4. Une application Cloud Run reçoit le message Pub/Sub.
  5. L'application Cloud Run utilise Cloud Translation pour traduire le texte.
  6. L'application Cloud Run stocke le résultat dans Firestore.

Ce tutoriel est destiné à toute personne intéressée par le traitement en arrière-plan avec Google Cloud. Il ne nécessite aucune connaissance particulière de Pub/Sub, Firestore, App Engine ou Cloud Run. Cependant, pour comprendre l'intégralité du code, il peut être utile de se familiariser avec PHP, JavaScript et HTML.

Objectifs

  • Comprendre et déployer les services Cloud Run
  • Comprendre et déployer une application App Engine
  • Tester l'application

Coûts

Ce tutoriel utilise les composants facturables suivants de Google Cloud :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût. Les nouveaux utilisateurs de Google Cloud peuvent bénéficier d'un essai gratuit.

Une fois que vous avez terminé ce tutoriel, vous pouvez éviter de continuer à payer des frais en supprimant les ressources que vous avez créées. Consultez la page Effectuer un nettoyage pour en savoir plus.

Avant de commencer

  1. Connectez-vous à votre compte Google.

    Si vous n'en possédez pas déjà un, vous devez en créer un.

  2. Dans Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Cloud.

    Accéder à la page de sélection du projet

  3. Vérifiez que la facturation est activée pour votre projet Google Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API Firestore, Cloud Run, Pub/Sub, and Cloud Translation.

    Activer les API

  5. Dans Google Cloud Console, ouvrez l'application dans Cloud Shell.

    Accéder à Cloud Shell

    Cloud Shell vous permet d'accéder en ligne de commande à vos ressources cloud, directement depuis votre navigateur. Ouvrez Cloud Shell dans votre navigateur et cliquez sur Proceed (Continuer) pour télécharger l'exemple de code et le modifier dans le répertoire de l'application.

  6. Dans Cloud Shell, configurez l'outil gcloud pour qu'il utilise votre projet Google Cloud :
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID

Comprendre le backend Cloud Run

Vous définissez une seule fonction PHP translateString et configurez votre service Cloud Run pour répondre à un message Pub/Sub en invoquant cette fonction.

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. La fonction doit importer plusieurs dépendances afin de pouvoir se connecter à Firestore et Translation.

    use Google\Cloud\Firestore\FirestoreClient;
    use Google\Cloud\Firestore\Transaction;
    use Google\Cloud\Translate\TranslateClient;
    
  2. Cloud Run commence par initialiser les clients Firestore et Pub/Sub. Ensuite, il analyse les données du message Pub/Sub pour obtenir le texte à traduire et la langue cible souhaitée.

    $firestore = new FirestoreClient();
    $translate = new TranslateClient();
    
    $translation = [
        'original' => $data['text'],
        'lang' => $data['language'],
    ];
  3. L'API Translation permet de traduire la chaîne dans la langue souhaitée.

    $result = $translate->translate($translation['original'], [
        'target' => $translation['lang'],
    ]);
  4. La fonction attribue un nom unique à la requête de traduction pour s'assurer que nous ne stockons pas de traductions en double. Elle procède ensuite à la traduction dans une transaction Firestore afin de s'assurer que les exécutions simultanées n'effectuent pas accidentellement la même traduction deux fois.

    $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'],
            ]);
        }
    );

Créer et déployer le backend Cloud Run

  • Créez l'application Cloud Run dans le répertoire backend :

    gcloud builds submit backend/ \
      --tag gcr.io/PROJECT_ID/background-function
  • Déployez l'application Cloud Run à l'aide du tag d'image de l'étape précédente :

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

    REGION est une région Google Cloud.

  • Une fois le déploiement terminé, une URL de l'application déployée s'affiche dans le résultat de la commande. Exemple :

    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

    Copiez cette URL pour l'étape suivante.

Configurer l'abonnement Pub/Sub

Votre application Cloud Run reçoit des messages de Pub/Sub chaque fois qu'un message est publié sur le sujet translate.

Une fonction de vérification d'authentification intégrée vérifie que le message Pub/Sub contient un jeton d'autorisation valide provenant d'un compte de service autorisé à appeler votre backend Cloud Run.

Les étapes suivantes vous guideront dans la configuration du sujet Pub/Sub, de l'abonnement et du compte de service pour passer des appels authentifiés à votre backend Cloud Run. Consultez la section Authentifier les communications de service à service pour en savoir plus.

  1. Créez le sujet translate pour publier les nouvelles requêtes de traduction :

    gcloud pubsub topics create translate
    
  2. Activez votre projet pour créer des jetons d'authentification 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 correspond au numéro de votre projet Google Cloud, que vous pouvez trouver en exécutant gcloud projects describe PROJECT_ID | grep projectNumber.

  3. Créez ou sélectionnez un compte de service pour représenter l'identité de l'abonnement Pub/Sub.

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

    Remarque : Vous pouvez utiliser cloud-run-pubsub-invoker ou le remplacer par un nom unique dans votre projet Google Cloud.

  4. Autorisez le compte de service demandeur à appeler votre service 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

    La propagation des modifications d'Identity and Access Management peut prendre plusieurs minutes. Pendant ce temps, des erreurs HTTP 403 peuvent s'afficher dans les journaux de service.

  5. Créez un abonnement Pub/Sub avec le compte de service :

    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 correspond à l'URL HTTPS que vous avez copiée après avoir créé et déployé votre backend.

    L'option --push-account-service-account active la fonctionnalité push de Pub/Sub pour l'authentification et l'autorisation.

    Votre domaine de service Cloud Run est automatiquement enregistré pour une utilisation avec les abonnements Pub/Sub.

Comprendre l'application

L'application Web comporte deux composants principaux :

  • Un serveur HTTP PHP pour gérer les requêtes Web. Il comporte les deux points de terminaison suivants :
    • / : répertorie toutes les traductions existantes et affiche un formulaire que les utilisateurs peuvent envoyer pour demander de nouvelles traductions.
    • /request-translation : point de terminaison auquel sont envoyés les formulaires. Il publie la requête sur Pub/Sub pour qu'elle soit traduite de manière asynchrone.
  • Un modèle HTML alimenté par les traductions existantes via le serveur PHP.

Le serveur HTTP

  • Dans le répertoire app, index.php commence par configurer l'application Lumen et enregistrer les gestionnaires HTTP :

    $app = new Laravel\Lumen\Application(__DIR__);
    $app->router->group([
    ], function ($router) {
        require __DIR__ . '/routes/web.php';
    });
    $app->run();
  • Le gestionnaire d'index (/) récupère toutes les traductions existantes dans Firestore et affiche un modèle avec la liste :

    /**
     * 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]);
    });
  • Le gestionnaire des requêtes de traduction, enregistré sur /request-translation, analyse le formulaire HTML, valide la requête, puis publie un message dans 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 '';
    });

Le modèle HTML

Le modèle HTML représente la base de la page HTML affichée à l'utilisateur afin qu'il puisse consulter les traductions précédentes et en demander de nouvelles. Ce modèle est rempli par le serveur HTTP à l'aide de la liste des traductions existantes.

  • L'élément <head> du modèle HTML inclut des métadonnées, des feuilles de style et le code JavaScript de la page :

    Cette page intègre des éléments Material Design Lite (MDL) CSS et JavaScript. MDL vous permet d'apporter un aspect Material Design à vos sites Web.

    JQuery permet d'attendre la fin de chargement du document et de définir un gestionnaire d'envoi de formulaires. Chaque fois que le formulaire de requête de traduction est envoyé, la page effectue une validation minimale du formulaire pour vérifier que la valeur n'est pas vide, puis envoie une requête asynchrone au point de terminaison /request-translation.

    Enfin, un snackbar MDL apparaît pour indiquer si la requête a abouti ou si une erreur s'est produite.

  • Le corps HTML de la page utilise une mise en page MDL et plusieurs composants MDL pour afficher la liste des traductions ainsi qu'un formulaire de requête de traductions supplémentaires :
    <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>
    

Exécuter l'application dans Cloud Shell

Avant d'essayer de déployer l'application Web, installez les dépendances et exécutez-la localement.

  1. Commencez par installer les dépendances avec Composer. L'extension gRPC pour PHP est requise. Elle est pré-installée sur Cloud Shell.

    composer install -d app
    
  2. Ensuite, exécutez le serveur Web PHP intégré pour diffuser votre application :

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

    L'option APP_DEBUG=true affiche toutes les exceptions qui se produisent.

  3. Dans Cloud Shell, cliquez sur Aperçu sur le Web , puis sélectionnez Prévisualiser sur le port 8080. Une nouvelle fenêtre s'affiche avec votre application en cours d'exécution.

Déployer l'application Web

Vous pouvez utiliser l'environnement standard App Engine pour créer et déployer une application qui s'exécute de manière fiable, même lorsqu'elle est soumise à une charge importante et doit gérer de grandes quantités de données.

Dans ce tutoriel, l'interface HTTP est déployée dans l'environnement standard App Engine.

Le fichier app.yaml configure l'application App Engine :

runtime: php73

env_variables:
  APP_DEBUG: true
  LOG_CHANNEL: stderr
  APP_STORAGE: /tmp
  • Déployez l'application dans l'environnement standard App Engine à partir du répertoire où se trouve le fichier app.yaml :
    gcloud app deploy

Tester l'application

Après avoir déployé la fonction Cloud et l'application App Engine, essayez d'envoyer une requête de traduction.

  1. Pour afficher l'application dans votre navigateur, saisissez l'URL suivante :

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

    Remplacez les éléments suivants :

    Il s'agit d'une page affichant une liste de traductions vide ainsi qu'un formulaire de requête de nouvelles traductions.

  2. Dans le champ Texte à traduire, saisissez un texte à traduire. Exemple : Hello, World.
  3. Dans la liste déroulante, sélectionnez la langue dans laquelle vous souhaitez traduire le texte.
  4. Cliquez sur Envoyer.
  5. Pour actualiser la page, cliquez sur Actualiser . Une nouvelle ligne s'affiche dans la liste de traduction. Si vous ne voyez pas de traduction, attendez encore quelques secondes, puis réessayez. Si vous ne voyez toujours pas de traduction, consultez la section suivante sur le débogage de l'application.

Déboguer l'application

Si vous ne parvenez pas à vous connecter à votre application App Engine ou si vous ne voyez pas de nouvelle traduction, vérifiez les éléments suivants :

  1. Vérifiez que les commandes de déploiement gcloud ont bien abouti et n'ont généré aucune erreur. Si vous rencontrez des erreurs (par exemple, message=Build failed), corrigez-les et réessayez de créer et déployer l'application Cloud Run, puis de déployer l'application App Engine.
  2. Dans Google Cloud Console, accédez à la page Visionneuse de journaux.

    Accéder à la page "Visionneuse de journaux"

    1. Dans la liste déroulante Ressources sélectionnées récemment, cliquez sur Application GAE, puis sur Tous les ID de module. La liste des requêtes correspondant à votre accès à l'application s'affiche. Si cette liste n'apparaît pas, vérifiez que vous avez bien sélectionné Tous les ID de module dans la liste déroulante. Si des messages d'erreur s'affichent dans Cloud Console, vérifiez que le code de votre application correspond au code décrit dans la section de présentation de l'application Web.
    2. Dans la liste déroulante Ressources sélectionnées récemment, cliquez sur Révision dans Cloud Run, puis sur Tous les journaux. Une requête POST va être envoyée à l'URL de votre application déployée. Dans le cas contraire, vérifiez que les applications Cloud Run et App Engine utilisent le même sujet Pub/Sub et qu'un abonnement Pub/Sub existe pour la publication vers votre point de terminaison Cloud Run.

Nettoyer

Pour éviter que les ressources utilisées dans ce tutoriel soient facturées sur votre compte Google Cloud Platform :

Supprimer le projet Cloud

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer .
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer les ressources du tutoriel

  1. Supprimez l'application App Engine que vous avez créée dans ce tutoriel :

    1. Dans Cloud Console, accédez à la page Versions pour App Engine.

      Accéder à la page Versions

    2. Cochez la case correspondant à la version de l'application autre que celle par défaut que vous souhaitez supprimer.
    3. Cliquez sur Supprimer  pour supprimer la version de l'application.

  2. Supprimez le service Cloud Run que vous avez déployé dans ce tutoriel :

    gcloud run services delete background-processing-function

    Vous pouvez également supprimer des services Cloud Run à partir de Google Cloud Console.

  3. Supprimez les autres ressources Google Cloud créées dans ce tutoriel :

Étapes suivantes