# Падручнік па PHP і 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/be/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/be/movies/), а памеры постэраў на старонцы [выявы](https://amodb.dev/be/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/be/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/be/languages/) і кэшуйце кожную мову асобна, што хэш URL ужо робіць.
- Дадайце `moods` у `append_to_response` і надрукуйце запіс з `$movie['moods']['results']` з найвышэйшым `avg`. Гл. [настрой](https://amodb.dev/be/moods/).
- Прачытайце пра [ліміты запытаў](https://amodb.dev/be/rate-limits/) або параўнайце з [падручнікам па Go](https://amodb.dev/be/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/be/movies/): API дэталяў фільма з тымі ж шляхамі і JSON, што ў TMDB v3. Адзін фільм па id, а акцёры, рэйтынгі і настрой праз append_to_response, на 14 мовах.
- [API выяў і хост выяў](https://amodb.dev/be/images-api/): API выяў фільмаў з шляхамі TMDB v3: спісы постэраў, фонавых выяў, лагатыпаў і кадраў назвы, а потым загрузка з img.amodb.dev у 10 памерах без ключа.
- [Аўтэнтыфікацыя](https://amodb.dev/be/authentication/): Як перадаваць ключ amodb: api_key у запыце, загаловак Authorization Bearer або apikey для выклікаў OMDb. Фармат ключа, захоўванне, замена і памылкі 401.
- [Памылкі](https://amodb.dev/be/errors/): Усе памылкі amodb з HTTP-статусам, TMDB status_code і паведамленнем, а таксама форма памылкі OMDb. Што азначае кожная і што рабіць вашаму кліенту далей.
- [Падручнік па Python і API фільмаў](https://amodb.dev/be/python-movie-api/): Стварыце на Python кансольную ўтыліту, якая шукае фільм і друкуе год, акцёраў, рэйтынг IMDb і постэр, з паўторам пры 429 і файлавым кэшам. Бібліятэка requests.
- [Падручнік па JavaScript і API фільмаў](https://amodb.dev/be/javascript-movie-api/): Напішыце скрыпт Node.js, які загружае дэталі, акцёраў і рэйтынгі для спісу id фільмаў адным запытам на фільм, з TTL-кэшам, паўторам пры 429 і без залежнасцей.

Абноўлена: 2026-10-18. Source: https://amodb.dev/be/php-movie-api/
