Intégration PHP asynchrone pour utiliser Transloadit efficacement
Les utilisateurs doivent attendre la fin d’un téléversement, mais ils n’ont pas besoin de garder un navigateur ouvert pendant le traitement des images. Les Assembly Notifications permettent à Transloadit d’informer votre backend PHP de la fin du traitement. Votre backend peut enregistrer ce résultat indépendamment du navigateur.

Ce guide met à jour le tutoriel de Joseph de juillet 2021 avec le Dashboard et le plugin Transloadit d’Uppy, qui remplacent l’outil de téléversement Robodog, désormais retiré. Nous allons créer une petite application PHP à opérateur unique, avec une table MySQL de reçus, des téléversements signés côté serveur et un callback authentifié. Elle utilise PHP 8.2 ou une version plus récente avec PDO MySQL, et conserve tous les secrets du compte sur le serveur.
Configuration du site web
Créez un projet contenant common.php et un répertoire public/. Seul public/ sert de racine
des documents. L’opérateur de l’application se connecte via l’authentification HTTP Basic sur
HTTPS ; les notifications s’authentifient quant à elles avec la signature de Transloadit. Ainsi, la
signature et la consultation des reçus restent privées sans nécessiter de système complet de
comptes utilisateurs pour ce tutoriel.
Fournissez ces variables d’environnement à PHP via le gestionnaire de secrets de votre déploiement :
TRANSLOADIT_KEY,TRANSLOADIT_SECRETetTRANSLOADIT_TEMPLATE_ID, issus de votre compte.APP_USERet unAPP_PASSWORDlong et aléatoire pour l’opérateur de ce tutoriel.APP_ORIGIN: l’origine HTTPS exacte de l’application, sans barre oblique finale.TRANSLOADIT_NOTIFY_URL: cette origine suivie de/notify.php.DB_DSN: par exemple,mysql:host=127.0.0.1;dbname=transloadit;charset=utf8mb4.DB_USERetDB_PASSWORD: un compte de base de données limité àSELECTetINSERTsur la table des reçus.
Conservez les fichiers secrets en dehors de public/ et du contrôle de version. Dans une application
multi-utilisateur, remplacez l’authentification Basic par votre autorisation de session existante,
limitez chaque reçu à son propriétaire et appliquez des quotas de téléversement et des limites de
débit par utilisateur au niveau du point de terminaison de signature.
Configuration de la base de données
À l’aide d’une connexion MySQL d’administration, créez la base de données et la table :
CREATE DATABASE transloadit CHARACTER SET utf8mb4;
USE transloadit;
CREATE TABLE assemblies (
id CHAR(32) CHARACTER SET ascii COLLATE ascii_bin PRIMARY KEY,
status VARCHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
http_code SMALLINT UNSIGNED NOT NULL,
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
L’identifiant d’Assembly est la clé unique du reçu. Les nouvelles tentatives de notification ne doivent ni créer de lignes supplémentaires ni répéter une action métier. Cet exemple ne stocke que le statut final ; il ne publie pas de fichiers de résultat et n’exécute pas de tâches en aval.
Création de notre Template
Créez un Template dans votre compte avec ces Instructions et copiez son identifiant dans la configuration du serveur. La destination du callback sera fournie par le point de terminaison de signature, et non par un champ du navigateur ou par une expression de Template fondée sur une saisie utilisateur.
{
"steps": {
":original": { "robot": "/upload/handle" },
"resized": {
"robot": "/image/resize",
"use": ":original",
"width": 500,
"format": "jpeg",
"resize_strategy": "fit",
"imagemagick_stack": "v3.0.1"
}
}
}
Activez la Signature Authentication pour votre Auth Key afin que les Instructions
modifiées ou non signées soient rejetées. Le serveur signe la chaîne params sérialisée exacte
avec HMAC-SHA384, selon le format utilisé par
@transloadit/utils.
Exposition des notifications locales pendant le développement
Pour ce tutoriel, utilisez un Cloudflare Quick Tunnel.
Après avoir installé cloudflared, démarrez le tunnel :
cloudflared tunnel --url http://127.0.0.1:8000
Définissez APP_ORIGIN sur l’origine HTTPS attribuée et TRANSLOADIT_NOTIFY_URL sur son URL /notify.php.
Ensuite, après avoir configuré les autres variables d’environnement, démarrez PHP depuis le
répertoire du projet :
php -d display_errors=0 -d log_errors=1 -d post_max_size=512K \
-d upload_max_filesize=256K -d max_input_vars=10 -d file_uploads=0 \
-S 127.0.0.1:8000 -t public
Ouvrez l’URL HTTPS du tunnel dans votre navigateur. Uppy continue d’utiliser
https://api2.transloadit.com ; les téléversements contournent PHP. Arrêtez les deux processus avec Ctrl+C après vos
tests. Mettez à jour les deux paramètres d’URL et redémarrez PHP chaque fois que le nom d’hôte
temporaire du tunnel change.
Le relais de notifications officiel est une autre option de développement : il doit recevoir le trafic de création d’Assemblies via son proxy local, interroger régulièrement le statut des Assemblies et signer les notifications transférées à l’aide de votre Auth Secret. Le simple fait de le démarrer pendant qu’Uppy utilise le point de terminaison normal de l’API ne relaiera pas les notifications de cette application. La configuration de tunnel ci-dessus ne nécessite ni le relais ni un callback localhost dans le Template.
Connexion à notre base de données
Enregistrez ce fichier sous common.php, en dehors de la racine publique des documents. Il fournit la
configuration, l’authentification, l’accès à la base de données et une frontière d’erreur unique et
assainie pour les quatre fichiers PHP.
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $error): void {
error_log('Notification application request failed.');
respond(500, 'Request could not be completed.');
});
function respond(int $status, string $message): never {
http_response_code($status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
echo $message;
exit;
}
function setting(string $name): string {
$value = getenv($name);
if ($value === false || $value === '') {
throw new RuntimeException('Missing server configuration.');
}
return $value;
}
function requireOperator(): void {
if (!hash_equals(setting('APP_USER'), $_SERVER['PHP_AUTH_USER'] ?? '') ||
!hash_equals(setting('APP_PASSWORD'), $_SERVER['PHP_AUTH_PW'] ?? '')) {
header('WWW-Authenticate: Basic realm="Upload demo", charset="UTF-8"');
respond(401, 'Sign in to continue.');
}
header('Cache-Control: no-store');
}
function database(): PDO {
return new PDO(setting('DB_DSN'), setting('DB_USER'), setting('DB_PASSWORD'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
Enregistrez le point de terminaison de signature sous public/sign.php. Il n’accepte pas d’Instructions
fournies par l’appelant. Les vérifications de l’en-tête Origin et de l’en-tête personnalisé
protègent cette requête POST avec identifiants contre les soumissions de formulaires intersites ;
n’ajoutez pas d’en-têtes CORS permissifs. La courte durée d’expiration et la limite signée de
taille de téléversement s’appliquent même si quelqu’un contourne les restrictions côté client
d’Uppy.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/common.php';
requireOperator();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
respond(405, 'Use POST.');
}
$origin = setting('APP_ORIGIN');
if (!preg_match('~\Ahttps://[a-zA-Z0-9.-]+(?::[0-9]+)?\z~D', $origin) ||
setting('TRANSLOADIT_NOTIFY_URL') !== $origin . '/notify.php') {
throw new RuntimeException('Invalid server URL configuration.');
}
if (($_SERVER['HTTP_ORIGIN'] ?? '') !== $origin ||
($_SERVER['HTTP_X_UPLOAD_REQUEST'] ?? '') !== '1') {
respond(403, 'Request not allowed.');
}
if (!preg_match('/\A[a-f0-9]{32}\z/D', setting('TRANSLOADIT_TEMPLATE_ID'))) {
throw new RuntimeException('Invalid Template configuration.');
}
$params = json_encode([
'auth' => [
'key' => setting('TRANSLOADIT_KEY'),
'expires' => gmdate('Y-m-d\TH:i:s\Z', time() + 300),
'max_size' => 10 * 1024 * 1024,
],
'template_id' => setting('TRANSLOADIT_TEMPLATE_ID'),
'notify_url' => setting('TRANSLOADIT_NOTIFY_URL'),
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$signature = 'sha384:' . hash_hmac('sha384', $params, setting('TRANSLOADIT_SECRET'));
header('Content-Type: application/json');
echo json_encode(['params' => $params, 'signature' => $signature], JSON_THROW_ON_ERROR);
Ajout de données dans la base de données
Enregistrez ce fichier sous public/notify.php. Vérifiez les octets d’origine du champ de formulaire transloadit
avant de décoder le JSON. Réencoder d’abord le JSON modifierait le message signé. La liste des
algorithmes autorisés inclut le format SHA-1 hérité du SDK pour la compatibilité des
notifications ; les nouvelles Instructions de téléversement ci-dessus utilisent SHA384. Les
algorithmes inconnus, les champs mal formés et les signatures invalides entraînent un échec.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/common.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
respond(405, 'Use POST.');
}
$raw = $_POST['transloadit'] ?? null;
$signature = $_POST['signature'] ?? null;
if (!is_string($raw) || strlen($raw) > 262144 || !is_string($signature) ||
strlen($signature) > 140 || count($_POST) !== 2 || count($_FILES) !== 0) {
respond(400, 'Invalid notification.');
}
$parts = explode(':', $signature, 2);
[$algorithm, $digest] = count($parts) === 2 ? $parts : ['sha1', $signature];
$lengths = ['sha1' => 40, 'sha256' => 64, 'sha384' => 96, 'sha512' => 128];
if (!isset($lengths[$algorithm]) || strlen($digest) !== $lengths[$algorithm] ||
!ctype_xdigit($digest) ||
!hash_equals(hash_hmac($algorithm, $raw, setting('TRANSLOADIT_SECRET')), $digest)) {
respond(403, 'Invalid notification signature.');
}
try {
$assembly = json_decode($raw, true, 32, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
respond(400, 'Invalid notification JSON.');
}
if (!is_array($assembly) || array_is_list($assembly)) {
respond(400, 'Invalid notification.');
}
$id = $assembly['assembly_id'] ?? null;
$status = $assembly['error'] ?? $assembly['ok'] ?? null;
$httpCode = $assembly['http_code'] ?? null;
if (!is_string($id) || !preg_match('/\A[a-f0-9]{32}\z/D', $id) ||
!is_string($status) || !preg_match('/\A[A-Z][A-Z0-9_]{0,63}\z/D', $status) ||
!is_int($httpCode) || $httpCode < 100 || $httpCode > 599 ||
(!isset($assembly['error']) && !in_array($status, ['ASSEMBLY_COMPLETED', 'ASSEMBLY_CANCELED'], true))) {
respond(400, 'Invalid terminal Assembly status.');
}
$db = database();
try {
$insert = $db->prepare('INSERT INTO assemblies (id, status, http_code) VALUES (?, ?, ?)');
$insert->execute([$id, $status, $httpCode]);
} catch (PDOException $error) {
// MySQL error 1062 is the unique receipt key, not a successful new delivery.
if (($error->errorInfo[1] ?? null) !== 1062) {
throw $error;
}
$existing = $db->prepare('SELECT status, http_code FROM assemblies WHERE id = ?');
$existing->execute([$id]);
$row = $existing->fetch(PDO::FETCH_ASSOC);
if (!$row || $row['status'] !== $status || (int) $row['http_code'] !== $httpCode) {
respond(409, 'Conflicting notification receipt.');
}
}
respond(200, 'Notification recorded.');
Une nouvelle tentative identique renvoie 200 après confirmation du résultat enregistré. Un résultat contradictoire pour le même identifiant d’Assembly renvoie 409 afin de pouvoir être examiné ; les défaillances de la base de données renvoient une erreur 500 générique pour que l’expéditeur puisse réessayer. La clé primaire protège également en cas de livraisons simultanées. Si vous ajoutez des tâches en aval, insérez une tâche dans une outbox au cours de la même transaction de base de données que le reçu, puis traitez-la avec un worker idempotent. N’envoyez pas d’e-mails et ne publiez pas de fichiers avant d’avoir enregistré le reçu.
Récupération des données de la base de données
Enregistrez cette page de téléversement complète sous public/index.php. Elle affiche à l’opérateur
authentifié les cinq résultats d’Assembly reçus le plus récemment et échappe chaque valeur de la
base de données avant l’affichage. Le plugin Transloadit d’Uppy demande des options
signées à notre backend.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/common.php';
requireOperator();
$rows = database()->query(
'SELECT id, status, received_at FROM assemblies ORDER BY received_at DESC, id DESC LIMIT 5'
)->fetchAll(PDO::FETCH_ASSOC);
function escape(string $value): string {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Asynchronous image uploads</title>
<link rel="stylesheet" href="https://releases.transloadit.com/uppy/v5.2.1/uppy.min.css">
</head>
<body>
<h1>Upload an image</h1>
<p>Wait until the upload finishes. Processing continues after you close this page.</p>
<div id="dashboard"></div>
<h2>Recent processing outcomes</h2>
<p>Refresh this page to see newly received notifications.</p>
<ul>
<?php foreach ($rows as $row): ?>
<li><?= escape($row['id']) ?> — <?= escape($row['status']) ?> — <?= escape($row['received_at']) ?></li>
<?php endforeach; ?>
</ul>
<?php if (count($rows) === 0): ?><p>No notifications received yet.</p><?php endif; ?>
<script type="module">
import { Uppy, Dashboard, Transloadit } from 'https://releases.transloadit.com/uppy/v5.2.1/uppy.min.mjs'
new Uppy({ restrictions: { maxNumberOfFiles: 1, maxFileSize: 10 * 1024 * 1024, allowedFileTypes: ['image/*'] } })
.use(Dashboard, { target: '#dashboard', inline: true })
.use(Transloadit, {
waitForEncoding: false,
async assemblyOptions() {
const response = await fetch('/sign.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-Upload-Request': '1' },
})
if (!response.ok) throw new Error('Could not authorize this upload.')
return response.json()
},
})
</script>
</body>
</html>
Tests
Vérifiez d’abord que les requêtes non authentifiées vers la page et le point de terminaison de
signature reçoivent une réponse 401, et qu’un POST non signé vers /notify.php reçoit une réponse 403
sans insérer de ligne. Une charge utile manquante ou mal formée reçoit une réponse 400. L’envoi d’un
identifiant d’Assembly contenant de la syntaxe SQL doit également échouer à la validation.
Avec votre propre compte, une vérification de bout en bout facultative consiste à téléverser une petite image de test via la page HTTPS, à attendre la fin du téléversement, puis à actualiser la page après le traitement. Un reçu au statut terminé devrait apparaître même si l’onglet de téléversement a été fermé. Un échec de traitement doit s’afficher comme un statut d’échec, et non comme une image terminée. Renvoyer une notification depuis la page de l’Assembly ne devrait laisser qu’un seul reçu.
Testez également en local des notifications synthétiques signées : les nouvelles tentatives inchangées devraient renvoyer 200, un corps modifié avec l’ancienne signature devrait renvoyer 403, et un résultat final contradictoire signé devrait renvoyer 409. Ne désactivez jamais les vérifications de signature pour faire réussir un test via le tunnel. Les limites de taille du corps définies ci-dessus pour le développement conviennent à cet exemple avec une seule image ; définissez des limites explicites pour le proxy inverse et pour PHP, adaptées aux charges utiles de vos notifications en production, et surveillez les livraisons rejetées.
Pour conclure
Le navigateur gère désormais le téléversement, tandis que PHP enregistre les résultats de traitement authentifiés. Conservez les reçus suffisamment longtemps pour couvrir les nouvelles tentatives de notification et vos propres renvois, et surveillez les notifications en échec dans votre compte. L’authentification par signature établit l’identité de l’expéditeur ; l’autorisation applicative, la propriété des reçus et le travail idempotent en aval restent du ressort de votre backend.
