# PHP 电影 API 教程

> 构建一个可复用的 PHP 函数和一个小型电影页面。该函数用 cURL 调用 amodb，429 时读取 Retry-After，并把响应缓存 6 小时。页面显示海报、演员和 IMDb 评分。需要 PHP 8.2 或更新版本及 curl 扩展，约 20 分钟。

你将构建两个小文件。`amodb.php` 包含一个函数 `amodb_get()`，用 cURL 调用 API 并把响应缓存到文件。`movie.php` 是一个页面，按 ID 显示电影的海报、演员和 IMDb 评分。你需要带有 `curl` 扩展的 PHP 8.2 或更新版本，以及大约 20 分钟。

## 步骤 1：获取 API 密钥

在[定价](https://amodb.dev/zh/pricing/)页购买一个预付周期。没有免费额度，每个套餐都以每分钟 20、40 或 120 个请求返回完整数据集。在账户后台创建密钥，它是 32 个小写十六进制字符。启动 PHP 之前先导出它：

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

代码用 `getenv('AMODB_KEY')` 读取它。在 PHP-FPM 下，工作进程看不到你的 shell，所以请改为在 pool 配置中设置 `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)` 用 `http_build_query` 构建 URL，发送调用并返回解码后的数组。电影页面传入 `append_to_response=credits,ratings`，所以演员和评分在同一个请求中返回。字段详情见[电影](https://amodb.dev/zh/movies/)，海报尺寸见[图片](https://amodb.dev/zh/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/zh/errors/)。

## 步骤 6：把响应缓存到文件

边缘节点缓存详情 6 小时。本地文件节省的是请求本身，缓存命中不占用你的限额。存活时间会叠加，所以一个已有 6 小时的文件，其数据可能在边缘缓存中就已经有几个小时了。如果这很重要，请降低 TTL。函数用 `sha1` 对最终 URL 取哈希，查找同名文件，并在其修改时间比 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`。每个值在进入 HTML 之前都会经过 `htmlspecialchars`。

```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/zh/languages/)之一，并分别缓存每种语言，URL 哈希已经做到了这一点。
- 在 `append_to_response` 中加入 `moods`，并打印 `$movie['moods']['results']` 中 `avg` 最高的那一项。见[情绪指数](https://amodb.dev/zh/moods/)。
- 阅读[速率限制](https://amodb.dev/zh/rate-limits/)，或与 [Go 教程](https://amodb.dev/zh/go-movie-api/)比较。

## 常见问题

### 代码需要哪些 PHP 扩展？

只需要 cURL。JSON 支持已内置于 PHP 8。运行 php -m 检查 curl 是否在列表中。代码可在 PHP 8.2 及更新版本上运行，没有 Composer 依赖，所以可以把这两个文件放进任何项目。

### 文件缓存的数据存放在哪里？

存放在系统临时目录中名为 amodb-cache 的文件夹里。每个请求对应一个 JSON 文件，文件名取自其 URL 的哈希。文件的存活时间低于 TTL 时就会被复用，TTL 默认为 6 小时。

### 在 PHP-FPM 下如何传入密钥？

在 pool 配置中添加 env[AMODB_KEY]，因为工作进程不会继承你的 shell。使用内置服务器时，在运行 php -S 之前导出的变量就足够了。绝不要把密钥写进 Web 根目录下的文件。

### amodb 返回错误时页面该怎么做？

显示一条简短的消息并发送 502，以便你自己的监控发现问题。在服务器上记录 status_message，绝不要打印密钥。401 表示之后的每次调用也都会失败，所以重试之前请先修复密钥。

### 在 Web 请求中因 429 而休眠安全吗？

要保持很短。休眠 30 秒的请求会占用一个工作进程，所以页面上要把等待上限设得很低，或者返回过期的缓存副本。有了文件缓存，重复的 ID 很少会触发 429。

## 相关页面

- [电影详情 API](https://amodb.dev/zh/movies/): 路径和 JSON 与 TMDB v3 相同的电影详情 API：按 ID 取电影，用 append_to_response 加入演职员和评分，支持 14 种语言。
- [图片 API 与图片主机](https://amodb.dev/zh/images-api/): 采用 TMDB v3 路径的电影图片 API：列出作品的海报、背景图、标志和剧照，再从 img.amodb.dev 以 10 种尺寸无密钥加载。
- [认证](https://amodb.dev/zh/authentication/): 如何发送 amodb 密钥：查询中的 api_key、Bearer 请求头，或 OMDb 的 apikey。含密钥格式、保管和 401 错误。
- [错误](https://amodb.dev/zh/errors/): amodb 的每个错误及其 HTTP 状态、TMDB status_code 和消息，以及 OMDb 错误结构。每个错误的含义和客户端下一步该做什么。
- [Python 电影 API 教程](https://amodb.dev/zh/python-movie-api/): 构建一个 Python 命令行工具：搜索片名并打印年份、演员、IMDb 评分和海报，带 429 重试和文件缓存。使用 requests。
- [JavaScript 电影 API 教程](https://amodb.dev/zh/javascript-movie-api/): 编写 Node.js 脚本，对一组电影 ID 各用一次请求加载详情、演职员和评分，带 TTL 缓存、429 重试且无依赖。

更新于: 2026-10-18. Source: https://amodb.dev/zh/php-movie-api/
