# Руководство по PHP movie API

> Создайте одну повторно используемую функцию PHP и крошечную страницу фильма. Функция вызывает amodb через cURL, читает Retry-After при ответе 429 и кэширует каждый ответ в файле на 6 часов. Страница показывает постер, актеров и рейтинг IMDb. Нужен PHP 8.2 или новее с расширением curl, занимает около 20 минут.

Вы создадите два небольших файла. В `amodb.php` находится одна функция `amodb_get()`, которая вызывает API через cURL и кэширует ответы в файлах. `movie.php` является страницей, которая показывает фильм по id с постером, актерами и рейтингом IMDb. Вам нужен PHP 8.2 или новее с расширением `curl` и около 20 минут.

## Шаг 1: получите ключ API

Оплатите период вперед на странице [цены](https://amodb.dev/ru/pricing/). Бесплатного тарифа нет, и каждый тариф отдает полный набор данных со скоростью 20, 40 или 120 запросов в минуту. Создайте ключ в личном кабинете. Он состоит из 32 шестнадцатеричных символов в нижнем регистре. Экспортируйте его до запуска PHP:

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

Код читает его через `getenv('AMODB_KEY')`. Под PHP-FPM воркеры не видят вашу оболочку, поэтому вместо этого задайте `env[AMODB_KEY]` в конфигурации пула.

## Шаг 2: подготовьте проект

Создайте папку и проверьте, что cURL доступен:

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

Первая команда должна показать версию 8.2 или выше. Вторая должна вывести `curl`.

## Шаг 3: отправьте первый запрос

Минимальному вызову cURL нужны четыре вещи: URL, таймаут, заголовок `Authorization` и проверка статуса. В PHP 8 дескриптор освобождается сам, поэтому `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;
```

## Шаг 4: оберните вызов в функцию

Вызов cURL на каждой странице повторяет одну и ту же настройку. `amodb_get($path, $query, $ttl)` собирает URL через `http_build_query`, отправляет вызов и возвращает декодированный массив. Страница фильма передает `append_to_response=credits,ratings`, поэтому актерский состав и оценки приходят в том же запросе. Подробности о полях есть на странице [фильмы](https://amodb.dev/ru/movies/), а размеры постеров на странице [API изображений](https://amodb.dev/ru/images-api/).

## Шаг 5: обработайте ошибки и лимиты запросов

Функция выбрасывает `AmodbException` при любом сбое, а страница перехватывает его в одном месте.

| Статус      | Что делает функция                                                                             |
| ----------- | ---------------------------------------------------------------------------------------------- |
| 401         | Сразу выбрасывает исключение. Неверный или приостановленный ключ повтором не исправить.        |
| 404         | Выбрасывает исключение с `status_message`, чтобы страница могла сообщить, что id неизвестен.   |
| 429         | Читает заголовок `Retry-After`, ждет столько секунд (от 1 до 30), затем повторяет до двух раз. |
| 5xx         | Выбрасывает исключение с кодом статуса и сообщением.                                           |
| Ошибка cURL | Выбрасывает `Network error` с текстом из `curl_error()`.                                       |

Заголовок приходит через `CURLOPT_HEADERFUNCTION`, колбэк, который cURL вызывает для каждой строки заголовков ответа. Полный список кодов см. на странице [ошибки](https://amodb.dev/ru/errors/).

## Шаг 6: кэшируйте ответы в файлах

Edge-кэш хранит детали 6 часов. Локальный файл экономит сам запрос, а попадание в кэш ничего не списывает с вашего лимита. Возраст складывается, поэтому файл возрастом 6 часов может содержать данные, которые на edge уже были несвежими на несколько часов. Если это важно, уменьшите TTL. Функция хеширует итоговый URL через `sha1`, ищет файл с таким именем и возвращает его, когда время изменения новее TTL. Иначе она делает запрос, записывает тело с `LOCK_EX` и возвращает данные.

## Шаг 7: полная программа

Сохраните функцию как `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);
}
```

Затем сохраните страницу как `movie.php`. Каждое значение проходит через `htmlspecialchars`, прежде чем попасть в 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>
```

Запустите встроенный сервер командой `php -S localhost:8000` и откройте `http://localhost:8000/movie.php?id=603`. URL постера указывает на хост изображений, которому ключ не нужен.

## Что дальше

- Перенесите кэш из временной папки в каталог, который вы контролируете, и удаляйте файлы старше суток заданием cron.
- Добавьте `language=ru-RU` или другой из [14 языков](https://amodb.dev/ru/languages/) и кэшируйте каждый язык отдельно, что хеш URL уже обеспечивает.
- Добавьте `moods` в `append_to_response` и выведите элемент `$movie['moods']['results']` с наибольшим `avg`. См. страницу [настроения](https://amodb.dev/ru/moods/).
- Прочитайте про [лимиты запросов](https://amodb.dev/ru/rate-limits/) или сравните с [руководством по Go](https://amodb.dev/ru/go-movie-api/).

## Вопросы и ответы

### Какие расширения PHP нужны коду?

Только cURL. Поддержка JSON встроена в PHP 8. Выполните php -m и проверьте, что curl есть в списке. Код работает на PHP 8.2 и новее и не имеет зависимостей Composer, поэтому два файла можно положить в любой проект.

### Где файловый кэш хранит данные?

В папке amodb-cache внутри системного временного каталога. Каждому запросу соответствует один файл JSON, названный по хешу его URL. Файл используется повторно, пока его возраст меньше TTL, по умолчанию это 6 часов.

### Как передать ключ под PHP-FPM?

Добавьте env[AMODB_KEY] в конфигурацию пула, потому что воркеры не наследуют окружение вашей оболочки. Со встроенным сервером достаточно переменной, экспортированной перед php -S. Никогда не записывайте ключ в файл внутри корня веб-сервера.

### Что должна делать страница, если amodb возвращает ошибку?

Показать короткое сообщение и отправить статус 502, чтобы ваш мониторинг это заметил. Логируйте status_message на сервере и никогда не выводите ключ. Ответ 401 означает, что все следующие вызовы тоже не пройдут, поэтому исправьте ключ до повтора.

### Безопасно ли ждать при ответе 429 внутри веб-запроса?

Делайте это ненадолго. Запрос, который спит 30 секунд, занимает воркер, поэтому на страницах ограничивайте ожидание небольшим значением или отдавайте устаревшую копию из кэша. При включенном файловом кэше повторяющиеся id редко доходят до 429.

## Связанные страницы

- [API деталей фильма](https://amodb.dev/ru/movies/): API деталей фильма с теми же путями и JSON, что в TMDB v3. Получите фильм по ID, добавьте актеров, рейтинги и настроения через append_to_response, на 14 языках.
- [API изображений и хост изображений](https://amodb.dev/ru/images-api/): API изображений фильмов с путями TMDB v3: списки постеров, фонов, логотипов и кадров для каждого проекта, загрузка с img.amodb.dev в 10 размерах без ключа.
- [Аутентификация](https://amodb.dev/ru/authentication/): Как передать ключ amodb: api_key в запросе, заголовок Authorization Bearer или apikey для вызовов OMDb. Формат ключа, хранение, ротация и ошибки 401.
- [Ошибки](https://amodb.dev/ru/errors/): Все ошибки amodb: HTTP-статус, status_code и сообщение TMDB, а также формат ошибки OMDb. Что означает каждая и что должен делать ваш клиент дальше.
- [Руководство по Python movie API](https://amodb.dev/ru/python-movie-api/): Создайте консольный инструмент на Python: поиск фильма по названию, вывод года, актеров, рейтинга IMDb и постера, повтор при 429 и файловый кэш. Нужен requests.
- [Руководство по JavaScript movie API](https://amodb.dev/ru/javascript-movie-api/): Напишите скрипт Node.js: по списку id фильмов он загружает детали, актеров и рейтинги одним запросом на фильм, с кэшем TTL, повтором при 429, без зависимостей.

Обновлено: 2026-10-18. Source: https://amodb.dev/ru/php-movie-api/
