# Tutorial de API de filmes em PHP

> Crie uma função PHP reutilizável e uma página de filme simples. A função chama o amodb com cURL, lê o Retry-After em um 429 e guarda cada resposta em um arquivo por 6 horas. A página mostra pôster, elenco e nota do IMDb. Exige PHP 8.2 ou mais novo com a extensão curl e leva cerca de 20 minutos.

Você vai criar dois arquivos pequenos. O `amodb.php` tem uma função, `amodb_get()`, que chama a API com cURL e guarda as respostas em arquivos. O `movie.php` é uma página que mostra um filme por id com pôster, elenco e nota do IMDb. Você precisa do PHP 8.2 ou mais novo com a extensão `curl` e de cerca de 20 minutos.

## Passo 1: obtenha uma chave de API

Compre um período pré-pago em [preços](https://amodb.dev/pt/pricing/). Não há plano gratuito, e todo plano retorna o conjunto completo de dados a 20, 40 ou 120 requisições por minuto. Crie uma chave no painel da sua conta. Ela tem 32 caracteres hexadecimais em minúsculas. Exporte-a antes de iniciar o PHP:

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

O código a lê com `getenv('AMODB_KEY')`. Sob o PHP-FPM, os workers não enxergam o seu shell, então defina `env[AMODB_KEY]` na configuração do pool.

## Passo 2: prepare o projeto

Crie uma pasta e confira se o cURL está disponível:

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

O primeiro comando deve informar 8.2 ou superior. O segundo deve imprimir `curl`.

## Passo 3: envie a primeira requisição

Uma chamada cURL simples precisa de quatro coisas: a URL, um timeout, o cabeçalho `Authorization` e uma checagem de status. No PHP 8 o handle se libera sozinho, então não há `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;
```

## Passo 4: encapsule em uma função

Chamar o cURL em cada página repete a mesma configuração. `amodb_get($path, $query, $ttl)` monta a URL com `http_build_query`, envia a chamada e retorna um array decodificado. A página do filme passa `append_to_response=credits,ratings`, então elenco e notas chegam na mesma requisição. Os detalhes dos campos estão em [filmes](https://amodb.dev/pt/movies/), e os tamanhos de pôster estão em [imagens](https://amodb.dev/pt/images-api/).

## Passo 5: trate erros e limites de requisições

A função lança `AmodbException` em toda falha, e a página a captura em um só lugar.

| Status       | O que a função faz                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| 401          | Lança a exceção na hora. Uma chave inválida ou suspensa não se resolve tentando de novo.                     |
| 404          | Lança a exceção com o `status_message`, para que a página diga que o id é desconhecido.                      |
| 429          | Lê o cabeçalho `Retry-After`, espera essa quantidade de segundos (de 1 a 30) e tenta de novo até duas vezes. |
| 5xx          | Lança a exceção com o código de status e a mensagem.                                                         |
| Erro do cURL | Lança `Network error` com o texto de `curl_error()`.                                                         |

O cabeçalho chega por `CURLOPT_HEADERFUNCTION`, um callback que o cURL executa para cada linha de cabeçalho da resposta. Veja [erros](https://amodb.dev/pt/errors/) para a lista completa de códigos.

## Passo 6: guarde as respostas em cache de arquivos

A borda guarda os detalhes por 6 horas. Um arquivo local economiza a própria requisição, e um acerto no cache não gasta nada do seu limite. As idades se somam, então um arquivo com 6 horas pode conter dados que já tinham horas na borda. Reduza o TTL se isso for importante. A função calcula o hash da URL final com `sha1`, procura um arquivo com esse nome e o retorna quando a data de modificação é mais recente que o TTL. Caso contrário, ela busca, grava o corpo com `LOCK_EX` e retorna os dados.

## Passo 7: o programa completo

Salve a função 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);
}
```

Depois salve a página como `movie.php`. Todo valor passa por `htmlspecialchars` antes de chegar ao 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>
```

Inicie o servidor embutido com `php -S localhost:8000` e abra `http://localhost:8000/movie.php?id=603`. A URL do pôster aponta para o host de imagens, que não precisa de chave.

## Próximos passos

- Mova o cache da pasta temporária para um diretório que você controle e apague arquivos com mais de um dia com um job do cron.
- Adicione `language=ru-RU` ou outro dos [14 idiomas](https://amodb.dev/pt/languages/) e faça o cache de cada idioma separadamente, o que o hash da URL já faz.
- Adicione `moods` a `append_to_response` e imprima a entrada de `$movie['moods']['results']` com o maior `avg`. Veja [moods](https://amodb.dev/pt/moods/) (clima emocional).
- Leia sobre [limites de requisições](https://amodb.dev/pt/rate-limits/) ou compare com o [tutorial de Go](https://amodb.dev/pt/go-movie-api/).

## Perguntas e respostas

### Quais extensões do PHP o código exige?

Só o cURL. O suporte a JSON já vem no PHP 8. Rode php -m para conferir se curl aparece na lista. O código roda no PHP 8.2 e mais novo e não tem dependências do Composer, então você pode colocar os dois arquivos em qualquer projeto.

### Onde o cache em arquivo guarda os dados?

Em uma pasta chamada amodb-cache dentro do diretório temporário do sistema. Cada requisição gera um arquivo JSON com o nome de um hash da URL. O arquivo é reutilizado enquanto sua idade for menor que o TTL, que por padrão é de 6 horas.

### Como passo a chave no PHP-FPM?

Adicione env[AMODB_KEY] à configuração do pool, porque os workers não herdam o seu shell. Com o servidor embutido, basta a variável que você exportou antes de rodar php -S. Nunca grave a chave em um arquivo dentro da raiz web.

### O que a página deve fazer quando o amodb retorna um erro?

Mostrar uma mensagem curta e enviar um 502 para que o seu monitoramento perceba. Registre o status_message no servidor e nunca imprima a chave. Um 401 significa que todas as chamadas seguintes também falham, então corrija a chave antes de tentar de novo.

### É seguro dormir em um 429 dentro de uma requisição web?

Mantenha a espera curta. Uma requisição que dorme 30 segundos prende um worker, então use um limite baixo nas páginas ou sirva uma cópia antiga do cache. Com o cache em arquivo, ids repetidos raramente chegam a um 429.

## Páginas relacionadas

- [API de detalhes de filmes](https://amodb.dev/pt/movies/): API de detalhes de filmes com caminhos e JSON do TMDB v3. Busque um filme por id e junte créditos, avaliações e moods com append_to_response, em 14 idiomas.
- [API de imagens e host de imagens](https://amodb.dev/pt/images-api/): API de imagens de filmes com caminhos do TMDB v3: liste pôsteres, backdrops, logos e stills por título e carregue-os de img.amodb.dev em 10 tamanhos, sem chave.
- [Autenticação](https://amodb.dev/pt/authentication/): Como enviar sua chave do amodb: api_key na consulta, um cabeçalho Authorization Bearer ou apikey nas chamadas OMDb. Formato, armazenamento, rotação e erros 401.
- [Erros](https://amodb.dev/pt/errors/): Todos os erros do amodb com status HTTP, status_code e mensagem do TMDB, mais o formato de erro do OMDb. O que cada um significa e o que seu cliente deve fazer.
- [Tutorial de API de filmes em Python](https://amodb.dev/pt/python-movie-api/): Crie uma ferramenta de linha de comando em Python que busca um filme e imprime ano, elenco, nota do IMDb e pôster, com nova tentativa no 429 e cache em disco.
- [Tutorial de API de filmes em JavaScript](https://amodb.dev/pt/javascript-movie-api/): Escreva um script Node.js que carrega detalhes, créditos e avaliações de ids de filmes em uma requisição cada, com cache TTL, retry em 429 e sem dependências.

Atualizado: 2026-10-18. Source: https://amodb.dev/pt/php-movie-api/
