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

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