# Tutorial de API de películas en PHP

> Crea una función PHP reutilizable y una página de película mínima. La función llama a amodb con cURL, lee Retry-After ante un 429 y guarda cada respuesta en un archivo durante 6 horas. La página muestra póster, reparto y nota de IMDb. Requiere PHP 8.2 o superior con la extensión curl y lleva unos 20 minutos.

Vas a crear dos archivos pequeños. `amodb.php` contiene una función, `amodb_get()`, que llama a la API con cURL y guarda las respuestas en archivos. `movie.php` es una página que muestra una película por id con póster, reparto y nota de IMDb. Necesitas PHP 8.2 o superior con la extensión `curl` y unos 20 minutos.

## Paso 1: obtén una clave de API

Compra un periodo prepago en [precios](https://amodb.dev/es/pricing/). No hay nivel gratuito, y todos los planes devuelven el conjunto completo de datos con 20, 40 o 120 solicitudes por minuto. Crea una clave en el panel de tu cuenta. Tiene 32 caracteres hexadecimales en minúscula. Expórtala antes de iniciar PHP:

```bash
export AMODB_KEY="YOUR_API_KEY"
```

El código la lee con `getenv('AMODB_KEY')`. Con PHP-FPM, los workers no ven las variables de tu terminal, así que define `env[AMODB_KEY]` en la configuración del pool.

## Paso 2: prepara el proyecto

Crea una carpeta y comprueba que cURL está disponible:

```bash
mkdir movie-page && cd movie-page
php -v
php -m | grep -i curl
```

El primer comando debe informar 8.2 o superior. El segundo debe imprimir `curl`.

## Paso 3: envía la primera solicitud

Una llamada básica de cURL necesita cuatro cosas: la URL, un tiempo de espera, el encabezado `Authorization` y una comprobación de estado. En PHP 8 el handle se libera solo, así que no hay `curl_close()`.

```php
<?php
$ch = curl_init('https://api.amodb.dev/3/movie/550?language=en-US');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL;
```

## Paso 4: envuélvelo en una función

Llamar a cURL desde cada página repite la misma configuración. `amodb_get($path, $query, $ttl)` construye la URL con `http_build_query`, envía la llamada y devuelve un array decodificado. La página de película pasa `append_to_response=credits,ratings`, así que el reparto y las puntuaciones llegan en la misma solicitud. Los detalles de los campos están en [películas](https://amodb.dev/es/movies/), y los tamaños de póster en [imágenes](https://amodb.dev/es/images-api/).

## Paso 5: gestiona errores y límites de solicitudes

La función lanza `AmodbException` ante cualquier fallo, y la página lo captura en un solo lugar.

| Estado        | Qué hace la función                                                                                       |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| 401           | Lanza la excepción de inmediato. Reintentar no arregla una clave incorrecta o suspendida.                 |
| 404           | Lanza la excepción con el `status_message`, para que la página pueda decir que el id no existe.           |
| 429           | Lee el encabezado `Retry-After`, espera esa cantidad de segundos (de 1 a 30) y reintenta hasta dos veces. |
| 5xx           | Lanza la excepción con el código de estado y el mensaje.                                                  |
| Error de cURL | Lanza `Network error` con el texto de `curl_error()`.                                                     |

El encabezado llega mediante `CURLOPT_HEADERFUNCTION`, una función de retorno que cURL ejecuta por cada línea de encabezado de la respuesta. Consulta [errores](https://amodb.dev/es/errors/) para ver la lista completa de códigos.

## Paso 6: guarda las respuestas en archivos

El caché perimetral mantiene los detalles durante 6 horas. Un archivo local ahorra la solicitud en sí, y un acierto de caché no gasta nada de tu límite. Las antigüedades se suman, así que un archivo de 6 horas puede contener datos que ya tenían varias horas en el perímetro. Reduce el TTL si eso importa. La función calcula el hash `sha1` de la URL final, busca un archivo con ese nombre y lo devuelve cuando su fecha de modificación es más reciente que el TTL. Si no, obtiene la respuesta, escribe el cuerpo con `LOCK_EX` y devuelve los datos.

## Paso 7: el programa completo

Guarda la función como `amodb.php`.

```php
<?php
declare(strict_types=1);

const AMODB_BASE = 'https://api.amodb.dev/3';

final class AmodbException extends RuntimeException
{
}

function amodb_get(string $path, array $query = [], int $ttl = 21600): array
{
    $key = getenv('AMODB_KEY');
    if ($key === false || $key === '') {
        throw new AmodbException('Set the AMODB_KEY environment variable.');
    }

    $url = AMODB_BASE . $path . ($query ? '?' . http_build_query($query) : '');
    $dir = sys_get_temp_dir() . '/amodb-cache';
    $file = $dir . '/' . sha1($url) . '.json';
    if (is_file($file) && time() - filemtime($file) < $ttl) {
        return json_decode((string) file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
    }

    for ($attempt = 1; $attempt <= 3; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Accept: application/json'],
            CURLOPT_HEADERFUNCTION => static function ($handle, string $line) use (&$retryAfter): int {
                if (stripos($line, 'retry-after:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);

        if ($body === false) {
            throw new AmodbException('Network error: ' . $error);
        }
        if ($status !== 429 || $attempt === 3) {
            break;
        }
        sleep(max(1, min($retryAfter, 30)));
    }

    if ($status === 401) {
        throw new AmodbException('401: the API key is invalid or suspended.');
    }
    if ($status !== 200) {
        $detail = json_decode((string) $body, true)['status_message'] ?? 'no message';
        throw new AmodbException("amodb returned $status: $detail");
    }

    if (!is_dir($dir)) {
        mkdir($dir, 0775, true);
    }
    file_put_contents($file, $body, LOCK_EX);

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}
```

Después guarda la página como `movie.php`. Cada valor pasa por `htmlspecialchars` antes de llegar al HTML.

```php
<?php
declare(strict_types=1);

require __DIR__ . '/amodb.php';

function h(?string $text): string
{
    return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8');
}

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 550;
$movie = null;
$error = null;
try {
    $movie = amodb_get("/movie/$id", ['language' => 'en-US', 'append_to_response' => 'credits,ratings']);
} catch (AmodbException $e) {
    http_response_code(502);
    $error = $e->getMessage();
}
$imdb = $movie['ratings']['imdb'] ?? null;
$cast = $movie ? array_slice(array_column($movie['credits']['cast'], 'name'), 0, 3) : [];
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title><?= h($movie['title'] ?? 'Movie lookup') ?></title>
</head>
<body>
<?php if ($error !== null): ?>
  <p>Could not load the movie: <?= h($error) ?></p>
<?php else: ?>
  <h1><?= h($movie['title']) ?> (<?= h(substr((string) ($movie['release_date'] ?? ''), 0, 4)) ?>)</h1>
  <?php if (!empty($movie['poster_path'])): ?>
    <img src="https://img.amodb.dev/t/p/w342<?= h($movie['poster_path']) ?>" alt="Poster of <?= h($movie['title']) ?>" width="342">
  <?php endif; ?>
  <p><?= h($movie['overview'] ?? '') ?></p>
  <p>Cast: <?= h(implode(', ', $cast)) ?></p>
  <p>IMDb: <?= $imdb ? h((string) $imdb['value']) : 'not known' ?></p>
<?php endif; ?>
</body>
</html>
```

Inicia el servidor integrado con `php -S localhost:8000` y abre `http://localhost:8000/movie.php?id=603`. La URL del póster apunta al host de imágenes, que no necesita clave.

## Próximos pasos

- Mueve el caché de la carpeta temporal a un directorio que controles y borra los archivos de más de un día con una tarea de cron.
- Añade `language=ru-RU` u otro de los [14 idiomas](https://amodb.dev/es/languages/) y guarda cada idioma por separado en caché, algo que el hash de la URL ya hace.
- Añade `moods` a `append_to_response` e imprime la entrada de `$movie['moods']['results']` con el `avg` más alto. Consulta [moods](https://amodb.dev/es/moods/) (ambiente emocional).
- Lee sobre los [límites de solicitudes](https://amodb.dev/es/rate-limits/) o compáralo con el [tutorial de Go](https://amodb.dev/es/go-movie-api/).

## Preguntas y respuestas

### ¿Qué extensiones de PHP necesita el código?

Solo cURL. El soporte de JSON viene incluido en PHP 8. Ejecuta php -m para comprobar que curl aparece en la lista. El código funciona en PHP 8.2 o superior y no tiene dependencias de Composer, así que puedes copiar los dos archivos a cualquier proyecto.

### ¿Dónde guarda sus datos el caché en archivos?

En una carpeta llamada amodb-cache dentro del directorio temporal del sistema. Cada solicitud tiene un archivo JSON con el nombre de un hash de su URL. Un archivo se reutiliza mientras su antigüedad sea menor que el TTL, que por defecto es de 6 horas.

### ¿Cómo paso la clave con PHP-FPM?

Añade env[AMODB_KEY] a la configuración del pool, porque los workers no heredan las variables de tu terminal. Con el servidor integrado basta la variable que exportaste antes de ejecutar php -S. Nunca escribas la clave en un archivo bajo la raíz web.

### ¿Qué debe hacer la página cuando amodb devuelve un error?

Mostrar un mensaje breve y enviar un 502 para que tu propia monitorización lo detecte. Registra el status_message en el servidor y nunca imprimas la clave. Un 401 significa que todas las llamadas posteriores también fallarán, así que corrige la clave antes de reintentar.

### ¿Es seguro usar sleep ante un 429 dentro de una solicitud web?

Mantén la espera corta. Una solicitud que espera 30 segundos bloquea un worker, así que limita la espera en las páginas o sirve una copia antigua del caché. Con el caché en archivos, los ids repetidos rara vez llegan a un 429.

## Páginas relacionadas

- [API de detalles de película](https://amodb.dev/es/movies/): API de detalles de película con rutas y JSON de TMDB v3. Obtén una película por id y añade créditos, puntuaciones y moods con append_to_response, en 14 idiomas.
- [API de imágenes y host de imágenes](https://amodb.dev/es/images-api/): API de imágenes de películas con rutas TMDB v3: lista pósters, fondos, logos y fotogramas por título y cárgalos desde img.amodb.dev en 10 tamaños sin clave.
- [Autenticación](https://amodb.dev/es/authentication/): Cómo enviar tu clave de amodb: api_key en la consulta, un encabezado Bearer o apikey en llamadas OMDb. Formato, almacenamiento, rotación y errores 401.
- [Errores](https://amodb.dev/es/errors/): Cada error de amodb con su estado HTTP, su status_code de TMDB y su mensaje, más la forma de error de OMDb. Qué significa cada uno y qué debe hacer tu cliente.
- [Tutorial de API de películas en Python](https://amodb.dev/es/python-movie-api/): Crea una herramienta de línea de comandos en Python que busca una película y muestra año, reparto, nota de IMDb y póster, con reintento ante 429 y caché.
- [Tutorial de API de películas en JavaScript](https://amodb.dev/es/javascript-movie-api/): Crea un script de Node.js que carga detalles, créditos y puntuaciones con una solicitud por película, caché TTL, reintento ante 429 y sin dependencias.

Actualizado: 2026-10-18. Source: https://amodb.dev/es/php-movie-api/
