Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Tmdb Laravel Package

astrotomic/laravel-tmdb

Laravel wrapper for The Movie Database (TMDb) API. Fetch movies, TV shows, people, genres, and search results with a fluent, Laravel-friendly client that handles API keys, configuration, and responses for you. Quick to set up and easy to inject or mock in tests.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require astrotomic/laravel-tmdb
php artisan vendor:publish --provider="Astrotomic\Tmdb\TmdbServiceProvider"

Add the key to .env (the published config/tmdb.php reads it):

TMDB_API_KEY=your_v3_api_key

First real use case — look up a title and pull its credits in one round trip:

use Astrotomic\Tmdb\Facades\Tmdb;

$movie = Tmdb::getMoviesApi()->getMovie(550, [
    'append_to_response' => 'credits,videos,images',
]);

$movie['title'];          // "Fight Club"
$movie['credits']['cast'];

Where to look first:

  1. config/tmdb.php — this is the entire configuration surface of the package.
  2. The Twmdb facade proxies php-tmdb/api's Tmdb\Client. The package is a thin Laravel binding (provider + config + facade), so the real API surface is php-tmdb/api, not this repo. Read Tmdb\Client and its getMoviesApi(), getTvApi(), getSearchApi(), getDiscoverApi(), getPeopleApi(), getConfigurationApi() accessors.
  3. Bootstrap a small "metadata" service class on day one so no controller ever touches the facade directly.

Implementation Patterns

Wrap the client in one service. Because the facade returns the underlying client, everything is static-ish. Isolate it:

final class TmdbMetadata
{
    public function movie(int $id, array $append = []): ?array
    {
        try {
            return Cache::remember(
                "tmdb:movie:{$id}:".md5(serialize($append)),
                now()->addDays(7),
                fn () => Tmdb::getMoviesApi()->getMovie($id, ['append_to_response' => implode(',', $append)])
            );
        } catch (TmdbApiException $e) {
            report($e);
            return null;
        }
    }
}

Now you have one place to swap the package out, one place to cache, one place to fake in tests.

Search → details is two calls, not one. Search endpoints return a reduced payload (no runtime, no credits, no full genres). Search to resolve the TMDB id, persist that id on your own model, then fetch details by id:

$results = Tmdb::getSearchApi()->searchMovies($query, ['language' => 'en-US', 'page' => 1]);
$id = $results['results'][0]['id'] ?? null;

Do the search interactively and the detail fetch in a queued job — searches are user-latency-sensitive, detail hydration is not.

Use append_to_response instead of N+1 calls. credits,videos,images,similar,recommendations,keywords,release_dates (comma-separated) is far cheaper than separate calls in a loop. Cap it: TMDB limits the number of appended sub-requests (~20).

Cache aggressively and key on everything that changes the response. Language, region, page, include_adult, and the append list all change payloads. TMDB data changes rarely; a 1–7 day TTL is normal, and their terms expect you to expire cached data rather than mirror the catalogue indefinitely. Never cache a 404 or an exception result.

Store ids and columns you query, not blobs. Keep tmdb_id, media_type, title, release_date locally so you can index and filter; hydrate posters/overviews on read via the cache. Dumping full JSON into a json column makes search and sorting painful later.

Build image URLs from the configuration endpoint, not hard-coded hosts. Cache the result and expose a helper:

$config = Cache::rememberForever('tmdb:configuration', fn () => Tmdb::getConfigurationApi()->getConfiguration());
$base   = $config['images']['secure_base_url'];
$url    = $base.'w500'.$movie['poster_path'];

Validate poster_path/profile_path/backdrop_path are non-null first — they frequently are.

Use discover for feeds, search only for user input. getDiscoverApi()->discoverMovies(['with_genres' => 28, 'sort_by' => 'popularity.desc', 'page' => $page]) is the right primitive for "trending in genre X" pages. Search is for a query string.

Queue bulk syncs. Any loop over more than a handful of ids belongs in a job with retry, backoff, and rateLimit/middleware — TMDB no longer enforces the old hard 40 req/10s limit, but bursts still get you throttled or blocked.

Gotchas and Tips

API key type matters. php-tmdb/api authenticates with the v3 API key passed as a query parameter. The v4 "read access token" (Bearer JWT) is a different credential and will not work here. If you just created a TMDB account, request an API key explicitly — it is not enabled by default.

Http::fake() does not touch this package. It uses its own Guzzle client, not Laravel's HTTP client. In tests, either fake your own TmdbMetadata service (recommended), or bind a Guzzle client configured with MockHandler into the container for the Tmdb\Client instance.

Errors are exceptions, not nulls. Tmdb\Exception\TmdbApiException is thrown for 401 (bad key), 404 (bad id), and 429. Wrap per-item calls in loops so one bad id doesn't abort a whole sync — log the id and continue.

Language and region are per-call, not magic. The wrapper doesn't auto-detect locale. Pass language (en-US, de-DE, …) on every call, or centralise it in your service. Missing translations fall back to English, and fields like overview and release_date can legitimately be empty strings — treat '' as missing, not as a value.

Deep pagination is capped. total_pages will happily report more than you can fetch; TMDB limits how far you can page (and discover caps total_results around 10,000). Don't build infinite scroll against TMDB directly — sync a bounded window and paginate locally.

Config caching bites. After changing TMDB_API_KEY, run php artisan config:clear; if the config is cached, env() won't be re-read. Never call env('TMDB_API_KEY') outside config/ for the same reason.

No retry or circuit breaker is built in. If TMDB is down or slow, your pages are slow. Add a short timeout on the Guzzle request options (via the client/config) and a Cache::remember fallback so a stale poster beats a 500. php-tmdb also supports a Guzzle logger — wire it in when you need to see raw requests and responses.

Respect the terms. Display the "This product uses the TMDB API but is not endorsed or certified by TMDB" attribution, and don't present TMDB ratings as your own.

This package is a thin wrapper (41 stars, small surface). Pin the version in composer.json. The upgrade risk lives almost entirely in php-tmdb/api major versions, where API methods have historically changed return shapes; keeping all calls behind your own service class is the cheapest insurance you can buy.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core