Extraer datos de facturas en tiempo real con PHP y AWS Textract
AWS Textract puede extraer campos de facturas y recibos para una aplicación PHP. Empieza con una CLI local acotada y luego añade trabajos asíncronos y un manejo duradero de colas cuando lo necesites. El OCR devuelve valores candidatos, no registros contables verificados: revisa la confianza, los totales, las monedas y los requisitos de revisión humana antes de usar los resultados en procesos posteriores.
¿Por qué automatizar la extracción de datos de facturas?
La automatización puede reducir la transcripción repetitiva y facilitar el procesamiento de las colas de revisión. La mejora depende de la calidad de los documentos y de tu flujo de trabajo; esta guía no establece una reducción porcentual del trabajo manual ni garantiza una latencia en tiempo real.
Configura AWS Textract y el SDK v3 de PHP
Usa una versión mantenida de PHP 8.x con Composer y las extensiones que requiere el AWS SDK, incluidas XML y un transporte HTTPS funcional. Instala el SDK v3 y haz commit del lockfile generado:
composer require aws/aws-sdk-php:^3
Habilita los servicios y permisos de AWS necesarios en una única región elegida. Prefiere los roles de IAM de la carga de trabajo y la cadena de proveedores de credenciales predeterminada del SDK en lugar de claves de larga duración en el código fuente. Las solicitudes reales a Textract pueden generar cargos. Los ejemplos siguientes no aprovisionan cuentas, colas, buckets ni políticas de IAM por ti.
Comprende la API AnalyzeExpense
AnalyzeExpense es síncrono. StartExpenseAnalysis inicia un trabajo asíncrono cuyos resultados
completados se recuperan con GetExpenseAnalysis. No confundas los dos flujos de trabajo de resultados.
Esta CLI acepta deliberadamente solo archivos PNG o JPEG de hasta 5 MiB, un límite conservador de la aplicación para la ruta de subida por bytes. Los archivos PDF/TIFF, las entradas de varias páginas y el procesamiento respaldado por S3 requieren su propia validación frente a las cuotas de documentos vigentes y al contrato de la API. El procesamiento síncrono de PDF/TIFF está limitado a una página. Revisa los límites de la operación seleccionada en lugar de suponer que todas las rutas de entrada aceptan la misma carga útil.
Crea una CLI síncrona de facturas
Guarda esto como invoice.php. Procesa una imagen local e imprime JSON estructurado. No es un
endpoint público de subida: la integración web necesita además autenticación, autorización,
validación de las subidas, protección CSRF cuando corresponda, límites de tasa y respuestas HTTP
saneadas.
<?php
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/invoice_helpers.php';
use Aws\Textract\TextractClient;
if ($argc !== 2) {
fwrite(STDERR, "Usage: php invoice.php <invoice.png|invoice.jpg>\n");
exit(1);
}
try {
$path = $argv[1];
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException('Unreadable input');
}
$bytes = file_get_contents($path, false, null, 0, 5 * 1024 * 1024 + 1);
if ($bytes === false || $bytes === '' || strlen($bytes) > 5 * 1024 * 1024) {
throw new RuntimeException('Input exceeds the application limit');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->buffer($bytes);
if (!in_array($mime, ['image/png', 'image/jpeg'], true)) {
throw new RuntimeException('Use PNG or JPEG');
}
$textract = new TextractClient([
'region' => getenv('AWS_REGION') ?: 'us-east-1',
'version' => '2018-06-27',
'http' => ['connect_timeout' => 5, 'timeout' => 30],
'retries' => 2,
]);
$result = $textract->analyzeExpense(['Document' => ['Bytes' => $bytes]]);
echo json_encode(parseExpense($result->toArray()), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), "\n";
} catch (Throwable $error) {
fwrite(STDERR, "Invoice analysis failed. Check the image, size, credentials, region, and permissions.\n");
exit(1);
}
Usa php invoice.php invoice.png después de añadir la función auxiliar que aparece más abajo. El SDK serializa los
bytes crudos de la imagen; no vuelvas a codificarlos en base64. La salida JSON puede contener datos
sensibles de facturas, así que no la envíes a logs públicos. Los resultados del AWS SDK son objetos:
llama a toArray() antes de pasarlos a una función auxiliar que acepte explícitamente un array.
Implementa el procesamiento asíncrono con SNS + SQS
Los siguientes fragmentos de integración pertenecen a tu servicio de trabajos autenticado. Dan por hecho que ya tienes configurados un cliente de S3, un cliente de Textract, un bucket, una entrada local verificada, un tema de SNS y un rol de publicación. Persiste la propiedad y el estado del trabajo en tu aplicación; estos no son manejadores HTTP independientes.
1 · sube el documento a S3
Usa una clave generada por la aplicación y cierra el stream de origen incluso cuando la subida falle:
$s3Key = 'invoices/' . bin2hex(random_bytes(16)) . '.pdf';
$stream = fopen($localPath, 'rb');
if ($stream === false) {
throw new RuntimeException('Cannot open validated invoice');
}
try {
$s3->putObject(['Bucket' => $bucket, 'Key' => $s3Key, 'Body' => $stream]);
} finally {
fclose($stream);
}
Valida el tamaño, el número de páginas, el cifrado y el tipo del PDF antes de esta etapa. El bucket debe estar en la región de Textract, con permisos de acceso y de cifrado adecuados para tu carga de trabajo.
2 · inicia StartExpenseAnalysis
Crea y persiste un trabajo de la aplicación y su token de idempotencia antes de llamar a Textract. Reutiliza ese token con los mismos parámetros al reintentar una respuesta fallida:
$start = $textract->startExpenseAnalysis([
'DocumentLocation' => ['S3Object' => ['Bucket' => $bucket, 'Name' => $s3Key]],
'NotificationChannel' => ['SNSTopicArn' => $snsTopicArn, 'RoleArn' => $roleArn],
'ClientRequestToken' => $persistedRequestToken,
]);
$jobId = $start->get('JobId');
if (!is_string($jobId) || $jobId === '') {
throw new RuntimeException('Textract returned no job ID');
}
Persiste el ID de trabajo devuelto junto con el propietario, la clave de S3, la región y el estado pendiente. Concilia cualquier reintento con ese registro para que una respuesta perdida no genere trabajo duplicado sin relación.
3 · configura el canal de notificaciones
Crea el tema de SNS y la suscripción de SQS en la región correspondiente. Configura RawMessageDelivery=true
para el decodificador que aparece más abajo. Restringe la política de la cola al tema de SNS y a la
cuenta esperados, y otorga al rol de publicación de Textract los permisos de alcance reducido y la
política de confianza que exige AWS.
Un rol únicamente con sns:Publish no constituye la configuración completa.
4 · ejecuta un worker de long polling
Añade esta función a invoice_helpers.php. Realiza un único long poll. Un supervisor puede invocarla
repetidamente con un Aws\Sqs\SqsClient configurado, la URL de la cola y tu manejador de eventos duradero.
function consumeExpenseMessages(Aws\Sqs\SqsClient $sqs, string $queueUrl, callable $persistEvent): int
{
$response = $sqs->receiveMessage([
'QueueUrl' => $queueUrl, 'MaxNumberOfMessages' => 10, 'WaitTimeSeconds' => 20,
]);
$failures = 0;
foreach ($response['Messages'] ?? [] as $message) {
try {
// Raw SNS delivery contains the Textract event directly, not a Message envelope.
$event = json_decode($message['Body'], true, 512, JSON_THROW_ON_ERROR);
if (!is_array($event) || ($event['API'] ?? null) !== 'StartExpenseAnalysis'
|| !is_string($event['JobId'] ?? null) || $event['JobId'] === ''
|| !in_array($event['Status'] ?? null, ['SUCCEEDED', 'FAILED', 'PARTIAL_SUCCESS'], true)) {
throw new RuntimeException('Invalid Textract event');
}
$persistEvent($event);
$sqs->deleteMessage([
'QueueUrl' => $queueUrl, 'ReceiptHandle' => $message['ReceiptHandle'],
]);
} catch (Throwable $error) {
$failures++;
error_log('Invoice event processing failed; message was not acknowledged');
}
}
return $failures;
}
El callback es un límite obligatorio de la aplicación. Debe buscar el trabajo conocido, verificar la propiedad y la información de origen esperada, recuperar los resultados cuando corresponda y confirmar un cambio de estado duradero e idempotente antes de retornar. Debe lanzar una excepción si la persistencia falla. Un callback vacío confirmaría el mensaje y perdería trabajo. Los trabajos de Textract fallidos o parciales necesitan un estado duradero de revisión/fallo, no un registro de factura correcto.
Configura el tiempo de espera HTTP del SDK para que sea mayor que el sondeo de 20 segundos. Usa un tiempo de espera de visibilidad que cubra el procesamiento y la persistencia, amplíalo para trabajos más largos y configura una cola de mensajes fallidos. Los mensajes duplicados y un fallo de confirmación tras un commit exitoso deben poder reprocesarse de forma segura.
Extrae y estructura los datos de facturas
Guarda esta función auxiliar en invoice_helpers.php y empieza el archivo con <?php. Conserva los tipos
de campo repetidos como entradas separadas y mantiene la confianza en lugar de sobrescribir valores
de forma silenciosa en un mapa indexado por tipo:
function parseExpense(array $result): array
{
return array_map(function (array $document): array {
$fields = static fn (array $field): array => [
'type' => $field['Type']['Text'] ?? null,
'text' => $field['ValueDetection']['Text'] ?? null,
'confidence' => $field['ValueDetection']['Confidence'] ?? null,
];
$groups = [];
foreach ($document['LineItemGroups'] ?? [] as $group) {
$rows = [];
foreach ($group['LineItems'] ?? [] as $row) {
$rows[] = array_map($fields, $row['LineItemExpenseFields'] ?? []);
}
$groups[] = ['index' => $group['LineItemGroupIndex'] ?? null, 'rows' => $rows];
}
return [
'expense_index' => $document['ExpenseIndex'] ?? null,
'summary' => array_map($fields, $document['SummaryFields'] ?? []),
'line_item_groups' => $groups,
];
}, $result['ExpenseDocuments'] ?? []);
}
Fragmento de respuesta de ejemplo
[
{
"expense_index": 1,
"summary": [
{ "type": "VENDOR_NAME", "text": "Example Supplies", "confidence": 98.5 },
{ "type": "TOTAL", "text": "125.00", "confidence": 97.2 }
],
"line_item_groups": []
}
]
Esta es una proyección ilustrativa, no la respuesta completa de AWS. Conserva las respuestas de origen bajo una política de datos adecuada si una revisión posterior requiere la geometría, la moneda, las referencias de página o las etiquetas.
Crea un panel ligero
Muestra los estados pendiente, completado, fallido y con revisión requerida. Escapa el texto del OCR al renderizarlo y aplica protección contra inyección de fórmulas CSV en las exportaciones a hojas de cálculo. Los totales de las facturas son texto no confiable: analiza explícitamente las monedas y las convenciones decimales, concilia las líneas de detalle y exige la aprobación correspondiente antes de una mutación en el ERP o de un pago.
Manejo de errores y estrategias de reintento
| Capa | Comportamiento requerido |
|---|---|
| SDK | Limita los reintentos y los tiempos de espera de las solicitudes; distingue los fallos de configuración de los errores reintentables |
| Cola | Confirma solo después del procesamiento duradero; tolera los duplicados |
| Persistencia | Aplica la identidad del trabajo y escrituras idempotentes |
| Resultados parciales | Conserva las advertencias y dirígelos a revisión en lugar de declarar un éxito completo |
| Logs | Mantén el texto de las facturas, las credenciales, las cargas útiles del proveedor y los stack traces crudos fuera de los logs públicos |
Optimización del rendimiento y consideraciones de costo
Elige entre procesamiento síncrono y asíncrono según los límites de los documentos y tus necesidades de tiempo de respuesta. Monitorea las tasas de solicitudes reales y la latencia de procesamiento en lugar de confiar en las cifras fijas de las cuotas predeterminadas. Configura las políticas de retención y de acceso del almacenamiento para las facturas sensibles. Mantén los reintentos acotados y revisa las métricas de facturación en busca de trabajo duplicado inesperado.
Buenas prácticas de seguridad
Autentica las subidas, autoriza cada trabajo y cada resultado, limita los tamaños y los formatos, y usa almacenamiento privado. Restringe las políticas de SNS/SQS y de las claves de cifrado a los recursos participantes. No expongas el cliente de Textract ni el callback de eventos como un endpoint público sin autenticación. Los errores del servicio deben convertirse en estados de aplicación saneados, no en respuestas crudas del proveedor.
Guía rápida de solución de problemas
| Síntoma | Comprobaciones |
|---|---|
| Acceso denegado | Permisos del llamador, confianza del rol de publicación, políticas de SNS/SQS y acceso a KMS |
| Sin eventos | Suscripción al tema, política de la cola, región y ajuste de entrega sin procesar |
| Campos vacíos o inciertos | Legibilidad del origen, limitaciones del modelo y revisión basada en la confianza |
| Trabajos repetidos | Persistencia del token de idempotencia y manejo de la reentrega en la cola |
Gestiona facturas de varias páginas y el procesamiento por lotes
Recupera todas las páginas de resultados tras el éxito. Este generador omite NextToken en la primera
solicitud y rechaza un token repetido o un estado de trabajo que no sea exitoso. Añádelo al mismo
archivo de funciones auxiliares:
function expensePages(Aws\Textract\TextractClient $textract, string $jobId): Generator
{
$request = ['JobId' => $jobId, 'MaxResults' => 20];
$seen = [];
do {
$result = $textract->getExpenseAnalysis($request);
if (($result['JobStatus'] ?? null) !== 'SUCCEEDED') {
throw new RuntimeException('Expense analysis is not fully successful');
}
yield $result->toArray();
$token = $result['NextToken'] ?? null;
if ($token === null) break;
if (!is_string($token) || $token === '' || isset($seen[$token])) {
throw new RuntimeException('Invalid expense pagination token');
}
$seen[$token] = true;
$request['NextToken'] = $token;
} while (true);
}
Persiste los resultados de cada página con su identidad de trabajo, ExpenseIndex y los índices de los
grupos de líneas de detalle. No asumas que cada respuesta de paginación es una factura distinta ni
marques un trabajo como completo antes de que el generador termine. Si falla una página posterior,
reintenta desde un punto de control duradero o reemplaza los resultados preparados de forma
idempotente; las páginas ya emitidas no son prueba de un trabajo completo.
Referencias
Próximos pasos
Crea un flujo de trabajo de revisión verificada en torno a los campos extraídos antes de automatizar acciones de negocio posteriores. Para preprocesamiento y conversión de documentos gestionados, explora el servicio de procesamiento de documentos de Transloadit.
