Integração assíncrona com PHP para uso eficiente da Transloadit
Os usuários precisam esperar o upload terminar, mas não precisam manter o navegador aberto durante o processamento das imagens. As Assembly Notifications permitem que a Transloadit avise o seu backend PHP quando o processamento termina. O seu backend pode registrar esse resultado independentemente do navegador.

Esta é uma atualização do tutorial de julho de 2021 do Joseph, agora com o Dashboard e o plugin Transloadit do Uppy, substituindo o uploader Robodog, que foi descontinuado. Vamos criar uma pequena aplicação PHP de operador único, com uma tabela de recibos no MySQL, uploads assinados pelo servidor e um callback autenticado. Ela usa PHP 8.2 ou mais recente com PDO MySQL e mantém todos os segredos da conta no servidor.
Configurando o site
Crie um projeto contendo common.php e um diretório public/. Somente public/ é a raiz de
documentos. O operador da aplicação faz login usando autenticação HTTP Basic via HTTPS; já as
notificações se autenticam com a assinatura da Transloadit. Assim, tanto a assinatura quanto a
visualização dos recibos ficam privadas, sem exigir um sistema completo de contas de usuário para o
tutorial.
Forneça estas variáveis de ambiente ao PHP pelo gerenciador de segredos do seu deploy:
TRANSLOADIT_KEY,TRANSLOADIT_SECRETeTRANSLOADIT_TEMPLATE_IDda sua conta.APP_USERe um valor longo e aleatório paraAPP_PASSWORDpara o operador deste tutorial.APP_ORIGIN: a origem HTTPS exata da aplicação, sem barra no final.TRANSLOADIT_NOTIFY_URL: essa origem seguida de/notify.php.DB_DSN: por exemplo,mysql:host=127.0.0.1;dbname=transloadit;charset=utf8mb4.DB_USEReDB_PASSWORD: uma conta de banco de dados limitada aSELECTeINSERTna tabela de recibos.
Mantenha os arquivos secretos fora de public/ e do controle de versão. Em uma aplicação multiusuário,
substitua a autenticação Basic pela autorização de sessão que você já tem, restrinja os recibos ao
respectivo dono e aplique cotas de upload e limites de taxa por usuário no endpoint de assinatura.
Configurando o banco de dados
Usando uma conexão administrativa com o MySQL, crie o banco de dados e a tabela:
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
);
O Assembly ID é a chave única do recibo. Novas tentativas de notificação não devem criar linhas extras nem repetir uma ação de negócio. Este exemplo armazena apenas o status terminal; ele não publica arquivos de resultado nem executa tarefas subsequentes.
Criando nosso Template
Crie um Template na sua conta com estas Instructions e copie o ID dele para a configuração do servidor. O destino do callback será fornecido pelo endpoint de assinatura, e não por um campo do navegador ou por uma expressão do Template baseada em entrada do usuário.
{
"steps": {
":original": { "robot": "/upload/handle" },
"resized": {
"robot": "/image/resize",
"use": ":original",
"width": 500,
"format": "jpeg",
"resize_strategy": "fit",
"imagemagick_stack": "v3.0.1"
}
}
}
Ative a Signature Authentication para a sua Auth Key, para que
Instructions modificadas ou não assinadas sejam rejeitadas. O servidor assina a string params
exatamente como foi serializada, com HMAC-SHA384, no mesmo formato usado por
@transloadit/utils.
Expondo notificações locais durante o desenvolvimento
Para este tutorial, use um Cloudflare Quick Tunnel.
Depois de instalar o cloudflared, inicie o túnel:
cloudflared tunnel --url http://127.0.0.1:8000
Defina APP_ORIGIN com a origem HTTPS atribuída e TRANSLOADIT_NOTIFY_URL com a URL /notify.php
correspondente. Em seguida, depois de configurar as demais variáveis de ambiente, inicie o PHP a
partir do diretório do projeto:
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
Abra a URL HTTPS do túnel no navegador. O Uppy continua usando
https://api2.transloadit.com; os uploads não passam pelo PHP. Depois de testar, encerre os dois processos com Ctrl+C.
Atualize as duas configurações de URL e reinicie o PHP sempre que o hostname temporário do túnel
mudar.
O relay de notificações oficial é uma opção de desenvolvimento diferente: ele precisa receber o tráfego de criação de Assemblies pelo proxy local dele, consultar periodicamente o status da Assembly e assinar as notificações encaminhadas usando o seu Auth Secret. Apenas iniciá-lo enquanto o Uppy usa o endpoint normal da API não vai encaminhar as notificações desta aplicação. A configuração de túnel acima não exige o relay nem um callback em localhost no Template.
Conectando ao nosso banco de dados
Salve isto como common.php, fora da raiz pública de documentos. Ele fornece configuração, login,
acesso ao banco de dados e uma única camada sanitizada de tratamento de erros para os quatro
arquivos 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,
]);
}
Salve o endpoint de assinatura como public/sign.php. Ele não aceita Instructions fornecidas por quem faz
a chamada. As verificações de Origin e de cabeçalho personalizado protegem esse POST com credenciais
contra envios de formulário cross-site; não adicione cabeçalhos CORS permissivos. A expiração curta
e o limite assinado de tamanho de upload continuam valendo mesmo que alguém contorne as restrições
do lado do cliente do 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);
Adicionando dados ao banco de dados
Salve isto como public/notify.php. Verifique os bytes originais do campo de formulário transloadit antes
de decodificar o JSON. Recodificar o JSON antes alteraria a mensagem assinada. A lista de algoritmos
permitidos inclui o formato SHA-1 legado do SDK para compatibilidade com notificações; as novas
Instructions de upload acima usam SHA384. Algoritmos desconhecidos, campos malformados e assinaturas
inválidas resultam em falha.
<?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.');
Uma nova tentativa idêntica retorna 200 depois de confirmar o resultado salvo. Um resultado conflitante para o mesmo Assembly ID retorna 409 para investigação; falhas no banco de dados retornam um 500 genérico para que o remetente possa tentar novamente. A chave primária também protege contra entregas simultâneas. Se você adicionar tarefas subsequentes, insira uma tarefa em uma outbox na mesma transação de banco de dados do recibo e depois processe-a com um worker idempotente. Não envie e-mails nem publique arquivos antes de registrar o recibo.
Recuperando dados do banco de dados
Salve esta página de upload completa como public/index.php. Ela mostra ao operador autenticado os cinco
resultados de Assembly recebidos mais recentemente e escapa todos os valores do banco de dados antes
de renderizar. O plugin Transloadit do Uppy pede opções assinadas ao nosso 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>
Testes
Primeiro, verifique se requisições não autenticadas à página e ao endpoint de assinatura recebem 401
e se um POST não assinado para /notify.php recebe 403 sem inserir nenhuma linha. Um payload ausente
ou malformado recebe 400. Enviar um Assembly ID contendo sintaxe SQL também deve falhar na validação.
Com a sua própria conta, uma verificação opcional de ponta a ponta é fazer upload de uma pequena imagem de teste pela página HTTPS, aguardar a conclusão do upload e atualizar a página depois do processamento. Um recibo de conclusão deve aparecer mesmo que a aba do upload tenha sido fechada. Uma falha de processamento deve ser exibida como status de falha, não como uma imagem concluída. Reenviar uma notificação pela página da Assembly deve manter um único recibo.
Teste também localmente notificações sintéticas assinadas: novas tentativas sem alterações devem retornar 200, um corpo alterado com a assinatura antiga deve retornar 403 e um resultado terminal conflitante assinado deve retornar 409. Nunca desative as verificações de assinatura para fazer um teste com túnel passar. Os limites de corpo de desenvolvimento acima servem para este exemplo de uma imagem; defina limites explícitos no proxy reverso e no PHP adequados aos payloads de notificação de produção e monitore as entregas rejeitadas.
Finalizando
Agora o navegador cuida do upload, enquanto o PHP registra resultados de processamento autenticados. Mantenha os recibos retidos por tempo suficiente para cobrir as novas tentativas de notificação e os seus próprios reenvios, e monitore as notificações com falha na sua conta. A autenticação por assinatura estabelece o remetente; a autorização da aplicação, a propriedade dos recibos e o trabalho subsequente idempotente continuam sendo responsabilidade do seu backend.
