PHP movie API tutorial
Build one reusable PHP function and a tiny movie page. The function calls amodb with cURL, reads Retry-After on a 429, and caches each reply in a file for 6 hours. The page shows poster, cast and IMDb score. It needs PHP 8.2 or newer with the curl extension and takes about 20 minutes.
You will build two small files. amodb.php holds one function, amodb_get(), that calls the API with cURL and caches replies in files. movie.php is a page that shows a movie by id with poster, cast and IMDb score. You need PHP 8.2 or newer with the curl extension and about 20 minutes.
Step 1: Get an API key
Buy a prepaid period on pricing. There is no free tier, and every plan returns the full dataset at 20, 40 or 120 requests per minute. Create a key in your account dashboard. It is 32 lowercase hex characters. Export it before you start PHP:
The code reads it with getenv('AMODB_KEY'). Under PHP-FPM, workers do not see your shell, so set env[AMODB_KEY] in the pool configuration instead.
export AMODB_KEY="YOUR_API_KEY"Step 2: Prepare the project
Make a folder and check that cURL is available:
The first command should report 8.2 or higher. The second must print curl.
mkdir movie-page && cd movie-page
php -v
php -m | grep -i curlStep 3: Send the first request
A bare cURL call needs four things: the URL, a timeout, the Authorization header and a status check. In PHP 8 the handle frees itself, so there is no curl_close().
<?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;Step 4: Wrap it in a function
Calling cURL from every page repeats the same setup. amodb_ builds the URL with http_build_query, sends the call and returns a decoded array. The movie page passes append_, so cast and scores arrive in the same request. Field details are on movies, and poster sizes are on images.
Step 5: Handle errors and rate limits
The function throws AmodbException for every failure, and the page catches it in one place.
| Status | What the function does |
|---|---|
| 401 | Throws at once. A bad or suspended key cannot be fixed by retrying. |
| 404 | Throws with the status_message, so the page can say the id is unknown. |
| 429 | Reads the Retry-After header, sleeps for that many seconds (1 to 30), then retries up to twice. |
| 5xx | Throws with the status code and message. |
| cURL error | Throws Network error with the text from curl_error(). |
The header arrives through CURLOPT_HEADERFUNCTION, a callback that cURL runs for each response header line. See errors for the full code list.
Step 6: Cache replies in files
The edge keeps details for 6 hours. A local file saves the request itself, and a cache hit costs nothing from your limit. Ages add up, so a file that is 6 hours old can hold data that was already hours old at the edge. Lower the TTL if that matters. The function hashes the final URL with sha1, looks for a file with that name and returns it when its modified time is newer than the TTL. Otherwise it fetches, writes the body with LOCK_EX and returns the data.
Step 7: The complete program
Save the function as amodb.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);
}Then save the page as movie.php. Every value goes through htmlspecialchars before it reaches the HTML.
Start the built-in server with php -S localhost:8000 and open http:/. The poster URL points at the image host, which needs no key.
<?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>Next steps
- Move the cache from the temp folder to a directory you control, and delete files older than a day with a cron job.
- Add
language=ru-RUor another of the 14 languages and cache each language separately, which the URL hash already does. - Add
moodstoappend_to_responseand print the entry of$movie['moods']['results']with the highestavg. See moods. - Read about rate limits, or compare this with the Go tutorial.